| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 | 
							- 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 };
 
 
  |