| import clone from 'clone'; | 
| import equal from 'deep-equal'; | 
| import extend from 'extend'; | 
| import Delta from 'quill-delta'; | 
| import DeltaOp from 'quill-delta/lib/op'; | 
| import Parchment from 'parchment'; | 
| import Quill from '../core/quill'; | 
| import logger from '../core/logger'; | 
| import Module from '../core/module'; | 
|   | 
| let debug = logger('quill:keyboard'); | 
|   | 
| const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey'; | 
|   | 
|   | 
| class Keyboard extends Module { | 
|   static match(evt, binding) { | 
|     binding = normalize(binding); | 
|     if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(function(key) { | 
|       return (!!binding[key] !== evt[key] && binding[key] !== null); | 
|     })) { | 
|       return false; | 
|     } | 
|     return binding.key === (evt.which || evt.keyCode); | 
|   } | 
|   | 
|   constructor(quill, options) { | 
|     super(quill, options); | 
|     this.bindings = {}; | 
|     Object.keys(this.options.bindings).forEach((name) => { | 
|       if (name === 'list autofill' && | 
|           quill.scroll.whitelist != null && | 
|           !quill.scroll.whitelist['list']) { | 
|         return; | 
|       } | 
|       if (this.options.bindings[name]) { | 
|         this.addBinding(this.options.bindings[name]); | 
|       } | 
|     }); | 
|     this.addBinding({ key: Keyboard.keys.ENTER, shiftKey: null }, handleEnter); | 
|     this.addBinding({ key: Keyboard.keys.ENTER, metaKey: null, ctrlKey: null, altKey: null }, function() {}); | 
|     if (/Firefox/i.test(navigator.userAgent)) { | 
|       // Need to handle delete and backspace for Firefox in the general case #1171 | 
|       this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true }, handleBackspace); | 
|       this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true }, handleDelete); | 
|     } else { | 
|       this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true, prefix: /^.?$/ }, handleBackspace); | 
|       this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true, suffix: /^.?$/ }, handleDelete); | 
|     } | 
|     this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: false }, handleDeleteRange); | 
|     this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: false }, handleDeleteRange); | 
|     this.addBinding({ key: Keyboard.keys.BACKSPACE, altKey: null, ctrlKey: null, metaKey: null, shiftKey: null }, | 
|                     { collapsed: true, offset: 0 }, | 
|                     handleBackspace); | 
|     this.listen(); | 
|   } | 
|   | 
|   addBinding(key, context = {}, handler = {}) { | 
|     let binding = normalize(key); | 
|     if (binding == null || binding.key == null) { | 
|       return debug.warn('Attempted to add invalid keyboard binding', binding); | 
|     } | 
|     if (typeof context === 'function') { | 
|       context = { handler: context }; | 
|     } | 
|     if (typeof handler === 'function') { | 
|       handler = { handler: handler }; | 
|     } | 
|     binding = extend(binding, context, handler); | 
|     this.bindings[binding.key] = this.bindings[binding.key] || []; | 
|     this.bindings[binding.key].push(binding); | 
|   } | 
|   | 
|   listen() { | 
|     this.quill.root.addEventListener('keydown', (evt) => { | 
|       if (evt.defaultPrevented) return; | 
|       let which = evt.which || evt.keyCode; | 
|       let bindings = (this.bindings[which] || []).filter(function(binding) { | 
|         return Keyboard.match(evt, binding); | 
|       }); | 
|       if (bindings.length === 0) return; | 
|       let range = this.quill.getSelection(); | 
|       if (range == null || !this.quill.hasFocus()) return; | 
|       let [line, offset] = this.quill.getLine(range.index); | 
|       let [leafStart, offsetStart] = this.quill.getLeaf(range.index); | 
|       let [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length); | 
|       let prefixText = leafStart instanceof Parchment.Text ? leafStart.value().slice(0, offsetStart) : ''; | 
|       let suffixText = leafEnd instanceof Parchment.Text ? leafEnd.value().slice(offsetEnd) : ''; | 
|       let curContext = { | 
|         collapsed: range.length === 0, | 
|         empty: range.length === 0 && line.length() <= 1, | 
|         format: this.quill.getFormat(range), | 
|         offset: offset, | 
|         prefix: prefixText, | 
|         suffix: suffixText | 
|       }; | 
|       let prevented = bindings.some((binding) => { | 
|         if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) return false; | 
|         if (binding.empty != null && binding.empty !== curContext.empty) return false; | 
|         if (binding.offset != null && binding.offset !== curContext.offset) return false; | 
|         if (Array.isArray(binding.format)) { | 
|           // any format is present | 
|           if (binding.format.every(function(name) { | 
|             return curContext.format[name] == null; | 
|           })) { | 
|             return false; | 
|           } | 
|         } else if (typeof binding.format === 'object') { | 
|           // all formats must match | 
|           if (!Object.keys(binding.format).every(function(name) { | 
|             if (binding.format[name] === true) return curContext.format[name] != null; | 
|             if (binding.format[name] === false) return curContext.format[name] == null; | 
|             return equal(binding.format[name], curContext.format[name]); | 
|           })) { | 
|             return false; | 
|           } | 
|         } | 
|         if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) return false; | 
|         if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) return false; | 
|         return binding.handler.call(this, range, curContext) !== true; | 
|       }); | 
|       if (prevented) { | 
|         evt.preventDefault(); | 
|       } | 
|     }); | 
|   } | 
| } | 
|   | 
| Keyboard.keys = { | 
|   BACKSPACE: 8, | 
|   TAB: 9, | 
|   ENTER: 13, | 
|   ESCAPE: 27, | 
|   LEFT: 37, | 
|   UP: 38, | 
|   RIGHT: 39, | 
|   DOWN: 40, | 
|   DELETE: 46 | 
| }; | 
|   | 
| Keyboard.DEFAULTS = { | 
|   bindings: { | 
|     'bold'      : makeFormatHandler('bold'), | 
|     'italic'    : makeFormatHandler('italic'), | 
|     'underline' : makeFormatHandler('underline'), | 
|     'indent': { | 
|       // highlight tab or tab at beginning of list, indent or blockquote | 
|       key: Keyboard.keys.TAB, | 
|       format: ['blockquote', 'indent', 'list'], | 
|       handler: function(range, context) { | 
|         if (context.collapsed && context.offset !== 0) return true; | 
|         this.quill.format('indent', '+1', Quill.sources.USER); | 
|       } | 
|     }, | 
|     'outdent': { | 
|       key: Keyboard.keys.TAB, | 
|       shiftKey: true, | 
|       format: ['blockquote', 'indent', 'list'], | 
|       // highlight tab or tab at beginning of list, indent or blockquote | 
|       handler: function(range, context) { | 
|         if (context.collapsed && context.offset !== 0) return true; | 
|         this.quill.format('indent', '-1', Quill.sources.USER); | 
|       } | 
|     }, | 
|     'outdent backspace': { | 
|       key: Keyboard.keys.BACKSPACE, | 
|       collapsed: true, | 
|       shiftKey: null, | 
|       metaKey: null, | 
|       ctrlKey: null, | 
|       altKey: null, | 
|       format: ['indent', 'list'], | 
|       offset: 0, | 
|       handler: function(range, context) { | 
|         if (context.format.indent != null) { | 
|           this.quill.format('indent', '-1', Quill.sources.USER); | 
|         } else if (context.format.list != null) { | 
|           this.quill.format('list', false, Quill.sources.USER); | 
|         } | 
|       } | 
|     }, | 
|     'indent code-block': makeCodeBlockHandler(true), | 
|     'outdent code-block': makeCodeBlockHandler(false), | 
|     'remove tab': { | 
|       key: Keyboard.keys.TAB, | 
|       shiftKey: true, | 
|       collapsed: true, | 
|       prefix: /\t$/, | 
|       handler: function(range) { | 
|         this.quill.deleteText(range.index - 1, 1, Quill.sources.USER); | 
|       } | 
|     }, | 
|     'tab': { | 
|       key: Keyboard.keys.TAB, | 
|       handler: function(range) { | 
|         this.quill.history.cutoff(); | 
|         let delta = new Delta().retain(range.index) | 
|                                .delete(range.length) | 
|                                .insert('\t'); | 
|         this.quill.updateContents(delta, Quill.sources.USER); | 
|         this.quill.history.cutoff(); | 
|         this.quill.setSelection(range.index + 1, Quill.sources.SILENT); | 
|       } | 
|     }, | 
|     'list empty enter': { | 
|       key: Keyboard.keys.ENTER, | 
|       collapsed: true, | 
|       format: ['list'], | 
|       empty: true, | 
|       handler: function(range, context) { | 
|         this.quill.format('list', false, Quill.sources.USER); | 
|         if (context.format.indent) { | 
|           this.quill.format('indent', false, Quill.sources.USER); | 
|         } | 
|       } | 
|     }, | 
|     'checklist enter': { | 
|       key: Keyboard.keys.ENTER, | 
|       collapsed: true, | 
|       format: { list: 'checked' }, | 
|       handler: function(range) { | 
|         let [line, offset] = this.quill.getLine(range.index); | 
|         let formats = extend({}, line.formats(), { list: 'checked' }); | 
|         let delta = new Delta().retain(range.index) | 
|                                .insert('\n', formats) | 
|                                .retain(line.length() - offset - 1) | 
|                                .retain(1, { list: 'unchecked' }); | 
|         this.quill.updateContents(delta, Quill.sources.USER); | 
|         this.quill.setSelection(range.index + 1, Quill.sources.SILENT); | 
|         this.quill.scrollIntoView(); | 
|       } | 
|     }, | 
|     'header enter': { | 
|       key: Keyboard.keys.ENTER, | 
|       collapsed: true, | 
|       format: ['header'], | 
|       suffix: /^$/, | 
|       handler: function(range, context) { | 
|         let [line, offset] = this.quill.getLine(range.index); | 
|         let delta = new Delta().retain(range.index) | 
|                                .insert('\n', context.format) | 
|                                .retain(line.length() - offset - 1) | 
|                                .retain(1, { header: null }); | 
|         this.quill.updateContents(delta, Quill.sources.USER); | 
|         this.quill.setSelection(range.index + 1, Quill.sources.SILENT); | 
|         this.quill.scrollIntoView(); | 
|       } | 
|     }, | 
|     'list autofill': { | 
|       key: ' ', | 
|       collapsed: true, | 
|       format: { list: false }, | 
|       prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/, | 
|       handler: function(range, context) { | 
|         let length = context.prefix.length; | 
|         let [line, offset] = this.quill.getLine(range.index); | 
|         if (offset > length) return true; | 
|         let value; | 
|         switch (context.prefix.trim()) { | 
|           case '[]': case '[ ]': | 
|             value = 'unchecked'; | 
|             break; | 
|           case '[x]': | 
|             value = 'checked'; | 
|             break; | 
|           case '-': case '*': | 
|             value = 'bullet'; | 
|             break; | 
|           default: | 
|             value = 'ordered'; | 
|         } | 
|         this.quill.insertText(range.index, ' ', Quill.sources.USER); | 
|         this.quill.history.cutoff(); | 
|         let delta = new Delta().retain(range.index - offset) | 
|                                .delete(length + 1) | 
|                                .retain(line.length() - 2 - offset) | 
|                                .retain(1, { list: value }); | 
|         this.quill.updateContents(delta, Quill.sources.USER); | 
|         this.quill.history.cutoff(); | 
|         this.quill.setSelection(range.index - length, Quill.sources.SILENT); | 
|       } | 
|     }, | 
|     'code exit': { | 
|       key: Keyboard.keys.ENTER, | 
|       collapsed: true, | 
|       format: ['code-block'], | 
|       prefix: /\n\n$/, | 
|       suffix: /^\s+$/, | 
|       handler: function(range) { | 
|         const [line, offset] = this.quill.getLine(range.index); | 
|         const delta = new Delta() | 
|           .retain(range.index + line.length() - offset - 2) | 
|           .retain(1, { 'code-block': null }) | 
|           .delete(1); | 
|         this.quill.updateContents(delta, Quill.sources.USER); | 
|       } | 
|     }, | 
|     'embed left': makeEmbedArrowHandler(Keyboard.keys.LEFT, false), | 
|     'embed left shift': makeEmbedArrowHandler(Keyboard.keys.LEFT, true), | 
|     'embed right': makeEmbedArrowHandler(Keyboard.keys.RIGHT, false), | 
|     'embed right shift': makeEmbedArrowHandler(Keyboard.keys.RIGHT, true) | 
|   } | 
| }; | 
|   | 
| function makeEmbedArrowHandler(key, shiftKey) { | 
|   const where = key === Keyboard.keys.LEFT ? 'prefix' : 'suffix'; | 
|   return { | 
|     key, | 
|     shiftKey, | 
|     altKey: null, | 
|     [where]: /^$/, | 
|     handler: function(range) { | 
|       let index = range.index; | 
|       if (key === Keyboard.keys.RIGHT) { | 
|         index += (range.length + 1); | 
|       } | 
|       const [leaf, ] = this.quill.getLeaf(index); | 
|       if (!(leaf instanceof Parchment.Embed)) return true; | 
|       if (key === Keyboard.keys.LEFT) { | 
|         if (shiftKey) { | 
|           this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER); | 
|         } else { | 
|           this.quill.setSelection(range.index - 1, Quill.sources.USER); | 
|         } | 
|       } else { | 
|         if (shiftKey) { | 
|           this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER); | 
|         } else { | 
|           this.quill.setSelection(range.index + range.length + 1, Quill.sources.USER); | 
|         } | 
|       } | 
|       return false; | 
|     } | 
|   }; | 
| } | 
|   | 
|   | 
| function handleBackspace(range, context) { | 
|   if (range.index === 0 || this.quill.getLength() <= 1) return; | 
|   let [line, ] = this.quill.getLine(range.index); | 
|   let formats = {}; | 
|   if (context.offset === 0) { | 
|     let [prev, ] = this.quill.getLine(range.index - 1); | 
|     if (prev != null && prev.length() > 1) { | 
|       let curFormats = line.formats(); | 
|       let prevFormats = this.quill.getFormat(range.index-1, 1); | 
|       formats = DeltaOp.attributes.diff(curFormats, prevFormats) || {}; | 
|     } | 
|   } | 
|   // Check for astral symbols | 
|   let length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1; | 
|   this.quill.deleteText(range.index-length, length, Quill.sources.USER); | 
|   if (Object.keys(formats).length > 0) { | 
|     this.quill.formatLine(range.index-length, length, formats, Quill.sources.USER); | 
|   } | 
|   this.quill.focus(); | 
| } | 
|   | 
| function handleDelete(range, context) { | 
|   // Check for astral symbols | 
|   let length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1; | 
|   if (range.index >= this.quill.getLength() - length) return; | 
|   let formats = {}, nextLength = 0; | 
|   let [line, ] = this.quill.getLine(range.index); | 
|   if (context.offset >= line.length() - 1) { | 
|     let [next, ] = this.quill.getLine(range.index + 1); | 
|     if (next) { | 
|       let curFormats = line.formats(); | 
|       let nextFormats = this.quill.getFormat(range.index, 1); | 
|       formats = DeltaOp.attributes.diff(curFormats, nextFormats) || {}; | 
|       nextLength = next.length(); | 
|     } | 
|   } | 
|   this.quill.deleteText(range.index, length, Quill.sources.USER); | 
|   if (Object.keys(formats).length > 0) { | 
|     this.quill.formatLine(range.index + nextLength - 1, length, formats, Quill.sources.USER); | 
|   } | 
| } | 
|   | 
| function handleDeleteRange(range) { | 
|   let lines = this.quill.getLines(range); | 
|   let formats = {}; | 
|   if (lines.length > 1) { | 
|     let firstFormats = lines[0].formats(); | 
|     let lastFormats = lines[lines.length - 1].formats(); | 
|     formats = DeltaOp.attributes.diff(lastFormats, firstFormats) || {}; | 
|   } | 
|   this.quill.deleteText(range, Quill.sources.USER); | 
|   if (Object.keys(formats).length > 0) { | 
|     this.quill.formatLine(range.index, 1, formats, Quill.sources.USER); | 
|   } | 
|   this.quill.setSelection(range.index, Quill.sources.SILENT); | 
|   this.quill.focus(); | 
| } | 
|   | 
| function handleEnter(range, context) { | 
|   if (range.length > 0) { | 
|     this.quill.scroll.deleteAt(range.index, range.length);  // So we do not trigger text-change | 
|   } | 
|   let lineFormats = Object.keys(context.format).reduce(function(lineFormats, format) { | 
|     if (Parchment.query(format, Parchment.Scope.BLOCK) && !Array.isArray(context.format[format])) { | 
|       lineFormats[format] = context.format[format]; | 
|     } | 
|     return lineFormats; | 
|   }, {}); | 
|   this.quill.insertText(range.index, '\n', lineFormats, Quill.sources.USER); | 
|   // Earlier scroll.deleteAt might have messed up our selection, | 
|   // so insertText's built in selection preservation is not reliable | 
|   this.quill.setSelection(range.index + 1, Quill.sources.SILENT); | 
|   this.quill.focus(); | 
|   Object.keys(context.format).forEach((name) => { | 
|     if (lineFormats[name] != null) return; | 
|     if (Array.isArray(context.format[name])) return; | 
|     if (name === 'link') return; | 
|     this.quill.format(name, context.format[name], Quill.sources.USER); | 
|   }); | 
| } | 
|   | 
| function makeCodeBlockHandler(indent) { | 
|   return { | 
|     key: Keyboard.keys.TAB, | 
|     shiftKey: !indent, | 
|     format: {'code-block': true }, | 
|     handler: function(range) { | 
|       let CodeBlock = Parchment.query('code-block'); | 
|       let index = range.index, length = range.length; | 
|       let [block, offset] = this.quill.scroll.descendant(CodeBlock, index); | 
|       if (block == null) return; | 
|       let scrollIndex = this.quill.getIndex(block); | 
|       let start = block.newlineIndex(offset, true) + 1; | 
|       let end = block.newlineIndex(scrollIndex + offset + length); | 
|       let lines = block.domNode.textContent.slice(start, end).split('\n'); | 
|       offset = 0; | 
|       lines.forEach((line, i) => { | 
|         if (indent) { | 
|           block.insertAt(start + offset, CodeBlock.TAB); | 
|           offset += CodeBlock.TAB.length; | 
|           if (i === 0) { | 
|             index += CodeBlock.TAB.length; | 
|           } else { | 
|             length += CodeBlock.TAB.length; | 
|           } | 
|         } else if (line.startsWith(CodeBlock.TAB)) { | 
|           block.deleteAt(start + offset, CodeBlock.TAB.length); | 
|           offset -= CodeBlock.TAB.length; | 
|           if (i === 0) { | 
|             index -= CodeBlock.TAB.length; | 
|           } else { | 
|             length -= CodeBlock.TAB.length; | 
|           } | 
|         } | 
|         offset += line.length + 1; | 
|       }); | 
|       this.quill.update(Quill.sources.USER); | 
|       this.quill.setSelection(index, length, Quill.sources.SILENT); | 
|     } | 
|   }; | 
| } | 
|   | 
| function makeFormatHandler(format) { | 
|   return { | 
|     key: format[0].toUpperCase(), | 
|     shortKey: true, | 
|     handler: function(range, context) { | 
|       this.quill.format(format, !context.format[format], Quill.sources.USER); | 
|     } | 
|   }; | 
| } | 
|   | 
| function normalize(binding) { | 
|   if (typeof binding === 'string' || typeof binding === 'number') { | 
|     return normalize({ key: binding }); | 
|   } | 
|   if (typeof binding === 'object') { | 
|     binding = clone(binding, false); | 
|   } | 
|   if (typeof binding.key === 'string') { | 
|     if (Keyboard.keys[binding.key.toUpperCase()] != null) { | 
|       binding.key = Keyboard.keys[binding.key.toUpperCase()]; | 
|     } else if (binding.key.length === 1) { | 
|       binding.key = binding.key.toUpperCase().charCodeAt(0); | 
|     } else { | 
|       return null; | 
|     } | 
|   } | 
|   if (binding.shortKey) { | 
|     binding[SHORTKEY] = binding.shortKey; | 
|     delete binding.shortKey; | 
|   } | 
|   return binding; | 
| } | 
|   | 
|   | 
| export { Keyboard as default, SHORTKEY }; |