You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							494 lines
						
					
					
						
							18 KiB
						
					
					
				
			
		
		
	
	
							494 lines
						
					
					
						
							18 KiB
						
					
					
				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 };
 | 
						|
 |