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.
		
		
		
		
		
			
		
			
				
					
					
						
							506 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
	
	
							506 lines
						
					
					
						
							15 KiB
						
					
					
				import './polyfill'; | 
						|
import Delta from 'quill-delta'; | 
						|
import Editor from './editor'; | 
						|
import Emitter from './emitter'; | 
						|
import Module from './module'; | 
						|
import Parchment from 'parchment'; | 
						|
import Selection, { Range } from './selection'; | 
						|
import extend from 'extend'; | 
						|
import logger from './logger'; | 
						|
import Theme from './theme'; | 
						|
 | 
						|
let debug = logger('quill'); | 
						|
 | 
						|
 | 
						|
class Quill { | 
						|
  static debug(limit) { | 
						|
    if (limit === true) { | 
						|
      limit = 'log'; | 
						|
    } | 
						|
    logger.level(limit); | 
						|
  } | 
						|
 | 
						|
  static find(node) { | 
						|
    return node.__quill || Parchment.find(node); | 
						|
  } | 
						|
 | 
						|
  static import(name) { | 
						|
    if (this.imports[name] == null) { | 
						|
      debug.error(`Cannot import ${name}. Are you sure it was registered?`); | 
						|
    } | 
						|
    return this.imports[name]; | 
						|
  } | 
						|
 | 
						|
  static register(path, target, overwrite = false) { | 
						|
    if (typeof path !== 'string') { | 
						|
      let name = path.attrName || path.blotName; | 
						|
      if (typeof name === 'string') { | 
						|
        // register(Blot | Attributor, overwrite) | 
						|
        this.register('formats/' + name, path, target); | 
						|
      } else { | 
						|
        Object.keys(path).forEach((key) => { | 
						|
          this.register(key, path[key], target); | 
						|
        }); | 
						|
      } | 
						|
    } else { | 
						|
      if (this.imports[path] != null && !overwrite) { | 
						|
        debug.warn(`Overwriting ${path} with`, target); | 
						|
      } | 
						|
      this.imports[path] = target; | 
						|
      if ((path.startsWith('blots/') || path.startsWith('formats/')) && | 
						|
          target.blotName !== 'abstract') { | 
						|
        Parchment.register(target); | 
						|
      } else if (path.startsWith('modules') && typeof target.register === 'function') { | 
						|
        target.register(); | 
						|
      } | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  constructor(container, options = {}) { | 
						|
    this.options = expandConfig(container, options); | 
						|
    this.container = this.options.container; | 
						|
    if (this.container == null) { | 
						|
      return debug.error('Invalid Quill container', container); | 
						|
    } | 
						|
    if (this.options.debug) { | 
						|
      Quill.debug(this.options.debug); | 
						|
    } | 
						|
    let html = this.container.innerHTML.trim(); | 
						|
    this.container.classList.add('ql-container'); | 
						|
    this.container.innerHTML = ''; | 
						|
    this.container.__quill = this; | 
						|
    this.root = this.addContainer('ql-editor'); | 
						|
    this.root.classList.add('ql-blank'); | 
						|
    this.root.setAttribute('data-gramm', false); | 
						|
    this.scrollingContainer = this.options.scrollingContainer || this.root; | 
						|
    this.emitter = new Emitter(); | 
						|
    this.scroll = Parchment.create(this.root, { | 
						|
      emitter: this.emitter, | 
						|
      whitelist: this.options.formats | 
						|
    }); | 
						|
    this.editor = new Editor(this.scroll); | 
						|
    this.selection = new Selection(this.scroll, this.emitter); | 
						|
    this.theme = new this.options.theme(this, this.options); | 
						|
    this.keyboard = this.theme.addModule('keyboard'); | 
						|
    this.clipboard = this.theme.addModule('clipboard'); | 
						|
    this.history = this.theme.addModule('history'); | 
						|
    this.theme.init(); | 
						|
    this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => { | 
						|
      if (type === Emitter.events.TEXT_CHANGE) { | 
						|
        this.root.classList.toggle('ql-blank', this.editor.isBlank()); | 
						|
      } | 
						|
    }); | 
						|
    this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => { | 
						|
      let range = this.selection.lastRange; | 
						|
      let index = range && range.length === 0 ? range.index : undefined; | 
						|
      modify.call(this, () => { | 
						|
        return this.editor.update(null, mutations, index); | 
						|
      }, source); | 
						|
    }); | 
						|
    let contents = this.clipboard.convert(`<div class='ql-editor' style="white-space: normal;">${html}<p><br></p></div>`); | 
						|
    this.setContents(contents); | 
						|
    this.history.clear(); | 
						|
    if (this.options.placeholder) { | 
						|
      this.root.setAttribute('data-placeholder', this.options.placeholder); | 
						|
    } | 
						|
    if (this.options.readOnly) { | 
						|
      this.disable(); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  addContainer(container, refNode = null) { | 
						|
    if (typeof container === 'string') { | 
						|
      let className = container; | 
						|
      container = document.createElement('div'); | 
						|
      container.classList.add(className); | 
						|
    } | 
						|
    this.container.insertBefore(container, refNode); | 
						|
    return container; | 
						|
  } | 
						|
 | 
						|
  blur() { | 
						|
    this.selection.setRange(null); | 
						|
  } | 
						|
 | 
						|
  deleteText(index, length, source) { | 
						|
    [index, length, , source] = overload(index, length, source); | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.deleteText(index, length); | 
						|
    }, source, index, -1*length); | 
						|
  } | 
						|
 | 
						|
  disable() { | 
						|
    this.enable(false); | 
						|
  } | 
						|
 | 
						|
  enable(enabled = true) { | 
						|
    this.scroll.enable(enabled); | 
						|
    this.container.classList.toggle('ql-disabled', !enabled); | 
						|
  } | 
						|
 | 
						|
  focus() { | 
						|
    let scrollTop = this.scrollingContainer.scrollTop; | 
						|
    this.selection.focus(); | 
						|
    this.scrollingContainer.scrollTop = scrollTop; | 
						|
    this.scrollIntoView(); | 
						|
  } | 
						|
 | 
						|
  format(name, value, source = Emitter.sources.API) { | 
						|
    return modify.call(this, () => { | 
						|
      let range = this.getSelection(true); | 
						|
      let change = new Delta(); | 
						|
      if (range == null) { | 
						|
        return change; | 
						|
      } else if (Parchment.query(name, Parchment.Scope.BLOCK)) { | 
						|
        change = this.editor.formatLine(range.index, range.length, { [name]: value }); | 
						|
      } else if (range.length === 0) { | 
						|
        this.selection.format(name, value); | 
						|
        return change; | 
						|
      } else { | 
						|
        change = this.editor.formatText(range.index, range.length, { [name]: value }); | 
						|
      } | 
						|
      this.setSelection(range, Emitter.sources.SILENT); | 
						|
      return change; | 
						|
    }, source); | 
						|
  } | 
						|
 | 
						|
  formatLine(index, length, name, value, source) { | 
						|
    let formats; | 
						|
    [index, length, formats, source] = overload(index, length, name, value, source); | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.formatLine(index, length, formats); | 
						|
    }, source, index, 0); | 
						|
  } | 
						|
 | 
						|
  formatText(index, length, name, value, source) { | 
						|
    let formats; | 
						|
    [index, length, formats, source] = overload(index, length, name, value, source); | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.formatText(index, length, formats); | 
						|
    }, source, index, 0); | 
						|
  } | 
						|
 | 
						|
  getBounds(index, length = 0) { | 
						|
    let bounds; | 
						|
    if (typeof index === 'number') { | 
						|
      bounds = this.selection.getBounds(index, length); | 
						|
    } else { | 
						|
      bounds = this.selection.getBounds(index.index, index.length); | 
						|
    } | 
						|
    let containerBounds = this.container.getBoundingClientRect(); | 
						|
    return { | 
						|
      bottom: bounds.bottom - containerBounds.top, | 
						|
      height: bounds.height, | 
						|
      left: bounds.left - containerBounds.left, | 
						|
      right: bounds.right - containerBounds.left, | 
						|
      top: bounds.top - containerBounds.top, | 
						|
      width: bounds.width | 
						|
    }; | 
						|
  } | 
						|
 | 
						|
  getContents(index = 0, length = this.getLength() - index) { | 
						|
    [index, length] = overload(index, length); | 
						|
    return this.editor.getContents(index, length); | 
						|
  } | 
						|
 | 
						|
  getFormat(index = this.getSelection(true), length = 0) { | 
						|
    if (typeof index === 'number') { | 
						|
      return this.editor.getFormat(index, length); | 
						|
    } else { | 
						|
      return this.editor.getFormat(index.index, index.length); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  getIndex(blot) { | 
						|
    return blot.offset(this.scroll); | 
						|
  } | 
						|
 | 
						|
  getLength() { | 
						|
    return this.scroll.length(); | 
						|
  } | 
						|
 | 
						|
  getLeaf(index) { | 
						|
    return this.scroll.leaf(index); | 
						|
  } | 
						|
 | 
						|
  getLine(index) { | 
						|
    return this.scroll.line(index); | 
						|
  } | 
						|
 | 
						|
  getLines(index = 0, length = Number.MAX_VALUE) { | 
						|
    if (typeof index !== 'number') { | 
						|
      return this.scroll.lines(index.index, index.length); | 
						|
    } else { | 
						|
      return this.scroll.lines(index, length); | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  getModule(name) { | 
						|
    return this.theme.modules[name]; | 
						|
  } | 
						|
 | 
						|
  getSelection(focus = false) { | 
						|
    if (focus) this.focus(); | 
						|
    this.update();  // Make sure we access getRange with editor in consistent state | 
						|
    return this.selection.getRange()[0]; | 
						|
  } | 
						|
 | 
						|
  getText(index = 0, length = this.getLength() - index) { | 
						|
    [index, length] = overload(index, length); | 
						|
    return this.editor.getText(index, length); | 
						|
  } | 
						|
 | 
						|
  hasFocus() { | 
						|
    return this.selection.hasFocus(); | 
						|
  } | 
						|
 | 
						|
  insertEmbed(index, embed, value, source = Quill.sources.API) { | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.insertEmbed(index, embed, value); | 
						|
    }, source, index); | 
						|
  } | 
						|
 | 
						|
  insertText(index, text, name, value, source) { | 
						|
    let formats; | 
						|
    [index, , formats, source] = overload(index, 0, name, value, source); | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.insertText(index, text, formats); | 
						|
    }, source, index, text.length); | 
						|
  } | 
						|
 | 
						|
  isEnabled() { | 
						|
    return !this.container.classList.contains('ql-disabled'); | 
						|
  } | 
						|
 | 
						|
  off() { | 
						|
    return this.emitter.off.apply(this.emitter, arguments); | 
						|
  } | 
						|
 | 
						|
  on() { | 
						|
    return this.emitter.on.apply(this.emitter, arguments); | 
						|
  } | 
						|
 | 
						|
  once() { | 
						|
    return this.emitter.once.apply(this.emitter, arguments); | 
						|
  } | 
						|
 | 
						|
  pasteHTML(index, html, source) { | 
						|
    this.clipboard.dangerouslyPasteHTML(index, html, source); | 
						|
  } | 
						|
 | 
						|
  removeFormat(index, length, source) { | 
						|
    [index, length, , source] = overload(index, length, source); | 
						|
    return modify.call(this, () => { | 
						|
      return this.editor.removeFormat(index, length); | 
						|
    }, source, index); | 
						|
  } | 
						|
 | 
						|
  scrollIntoView() { | 
						|
    this.selection.scrollIntoView(this.scrollingContainer); | 
						|
  } | 
						|
 | 
						|
  setContents(delta, source = Emitter.sources.API) { | 
						|
    return modify.call(this, () => { | 
						|
      delta = new Delta(delta); | 
						|
      let length = this.getLength(); | 
						|
      let deleted = this.editor.deleteText(0, length); | 
						|
      let applied = this.editor.applyDelta(delta); | 
						|
      let lastOp = applied.ops[applied.ops.length - 1]; | 
						|
      if (lastOp != null && typeof(lastOp.insert) === 'string' && lastOp.insert[lastOp.insert.length-1] === '\n') { | 
						|
        this.editor.deleteText(this.getLength() - 1, 1); | 
						|
        applied.delete(1); | 
						|
      } | 
						|
      let ret = deleted.compose(applied); | 
						|
      return ret; | 
						|
    }, source); | 
						|
  } | 
						|
 | 
						|
  setSelection(index, length, source) { | 
						|
    if (index == null) { | 
						|
      this.selection.setRange(null, length || Quill.sources.API); | 
						|
    } else { | 
						|
      [index, length, , source] = overload(index, length, source); | 
						|
      this.selection.setRange(new Range(index, length), source); | 
						|
      if (source !== Emitter.sources.SILENT) { | 
						|
        this.selection.scrollIntoView(this.scrollingContainer); | 
						|
      } | 
						|
    } | 
						|
  } | 
						|
 | 
						|
  setText(text, source = Emitter.sources.API) { | 
						|
    let delta = new Delta().insert(text); | 
						|
    return this.setContents(delta, source); | 
						|
  } | 
						|
 | 
						|
  update(source = Emitter.sources.USER) { | 
						|
    let change = this.scroll.update(source);   // Will update selection before selection.update() does if text changes | 
						|
    this.selection.update(source); | 
						|
    return change; | 
						|
  } | 
						|
 | 
						|
  updateContents(delta, source = Emitter.sources.API) { | 
						|
    return modify.call(this, () => { | 
						|
      delta = new Delta(delta); | 
						|
      return this.editor.applyDelta(delta, source); | 
						|
    }, source, true); | 
						|
  } | 
						|
} | 
						|
Quill.DEFAULTS = { | 
						|
  bounds: null, | 
						|
  formats: null, | 
						|
  modules: {}, | 
						|
  placeholder: '', | 
						|
  readOnly: false, | 
						|
  scrollingContainer: null, | 
						|
  strict: true, | 
						|
  theme: 'default' | 
						|
}; | 
						|
Quill.events = Emitter.events; | 
						|
Quill.sources = Emitter.sources; | 
						|
// eslint-disable-next-line no-undef | 
						|
Quill.version = typeof(QUILL_VERSION) === 'undefined' ? 'dev' : QUILL_VERSION; | 
						|
 | 
						|
Quill.imports = { | 
						|
  'delta'       : Delta, | 
						|
  'parchment'   : Parchment, | 
						|
  'core/module' : Module, | 
						|
  'core/theme'  : Theme | 
						|
}; | 
						|
 | 
						|
 | 
						|
function expandConfig(container, userConfig) { | 
						|
  userConfig = extend(true, { | 
						|
    container: container, | 
						|
    modules: { | 
						|
      clipboard: true, | 
						|
      keyboard: true, | 
						|
      history: true | 
						|
    } | 
						|
  }, userConfig); | 
						|
  if (!userConfig.theme || userConfig.theme === Quill.DEFAULTS.theme) { | 
						|
    userConfig.theme = Theme; | 
						|
  } else { | 
						|
    userConfig.theme = Quill.import(`themes/${userConfig.theme}`); | 
						|
    if (userConfig.theme == null) { | 
						|
      throw new Error(`Invalid theme ${userConfig.theme}. Did you register it?`); | 
						|
    } | 
						|
  } | 
						|
  let themeConfig = extend(true, {}, userConfig.theme.DEFAULTS); | 
						|
  [themeConfig, userConfig].forEach(function(config) { | 
						|
    config.modules = config.modules || {}; | 
						|
    Object.keys(config.modules).forEach(function(module) { | 
						|
      if (config.modules[module] === true) { | 
						|
        config.modules[module] = {}; | 
						|
      } | 
						|
    }); | 
						|
  }); | 
						|
  let moduleNames = Object.keys(themeConfig.modules).concat(Object.keys(userConfig.modules)); | 
						|
  let moduleConfig = moduleNames.reduce(function(config, name) { | 
						|
    let moduleClass = Quill.import(`modules/${name}`); | 
						|
    if (moduleClass == null) { | 
						|
      debug.error(`Cannot load ${name} module. Are you sure you registered it?`); | 
						|
    } else { | 
						|
      config[name] = moduleClass.DEFAULTS || {}; | 
						|
    } | 
						|
    return config; | 
						|
  }, {}); | 
						|
  // Special case toolbar shorthand | 
						|
  if (userConfig.modules != null && userConfig.modules.toolbar && | 
						|
      userConfig.modules.toolbar.constructor !== Object) { | 
						|
    userConfig.modules.toolbar = { | 
						|
      container: userConfig.modules.toolbar | 
						|
    }; | 
						|
  } | 
						|
  userConfig = extend(true, {}, Quill.DEFAULTS, { modules: moduleConfig }, themeConfig, userConfig); | 
						|
  ['bounds', 'container', 'scrollingContainer'].forEach(function(key) { | 
						|
    if (typeof userConfig[key] === 'string') { | 
						|
      userConfig[key] = document.querySelector(userConfig[key]); | 
						|
    } | 
						|
  }); | 
						|
  userConfig.modules = Object.keys(userConfig.modules).reduce(function(config, name) { | 
						|
    if (userConfig.modules[name]) { | 
						|
      config[name] = userConfig.modules[name]; | 
						|
    } | 
						|
    return config; | 
						|
  }, {}); | 
						|
  return userConfig; | 
						|
} | 
						|
 | 
						|
// Handle selection preservation and TEXT_CHANGE emission | 
						|
// common to modification APIs | 
						|
function modify(modifier, source, index, shift) { | 
						|
  if (this.options.strict && !this.isEnabled() && source === Emitter.sources.USER) { | 
						|
    return new Delta(); | 
						|
  } | 
						|
  let range = index == null ? null : this.getSelection(); | 
						|
  let oldDelta = this.editor.delta; | 
						|
  let change = modifier(); | 
						|
  if (range != null) { | 
						|
    if (index === true) index = range.index; | 
						|
    if (shift == null) { | 
						|
      range = shiftRange(range, change, source); | 
						|
    } else if (shift !== 0) { | 
						|
      range = shiftRange(range, index, shift, source); | 
						|
    } | 
						|
    this.setSelection(range, Emitter.sources.SILENT); | 
						|
  } | 
						|
  if (change.length() > 0) { | 
						|
    let args = [Emitter.events.TEXT_CHANGE, change, oldDelta, source]; | 
						|
    this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args); | 
						|
    if (source !== Emitter.sources.SILENT) { | 
						|
      this.emitter.emit(...args); | 
						|
    } | 
						|
  } | 
						|
  return change; | 
						|
} | 
						|
 | 
						|
function overload(index, length, name, value, source) { | 
						|
  let formats = {}; | 
						|
  if (typeof index.index === 'number' && typeof index.length === 'number') { | 
						|
    // Allow for throwaway end (used by insertText/insertEmbed) | 
						|
    if (typeof length !== 'number') { | 
						|
      source = value, value = name, name = length, length = index.length, index = index.index; | 
						|
    } else { | 
						|
      length = index.length, index = index.index; | 
						|
    } | 
						|
  } else if (typeof length !== 'number') { | 
						|
    source = value, value = name, name = length, length = 0; | 
						|
  } | 
						|
  // Handle format being object, two format name/value strings or excluded | 
						|
  if (typeof name === 'object') { | 
						|
    formats = name; | 
						|
    source = value; | 
						|
  } else if (typeof name === 'string') { | 
						|
    if (value != null) { | 
						|
      formats[name] = value; | 
						|
    } else { | 
						|
      source = name; | 
						|
    } | 
						|
  } | 
						|
  // Handle optional source | 
						|
  source = source || Emitter.sources.API; | 
						|
  return [index, length, formats, source]; | 
						|
} | 
						|
 | 
						|
function shiftRange(range, index, length, source) { | 
						|
  if (range == null) return null; | 
						|
  let start, end; | 
						|
  if (index instanceof Delta) { | 
						|
    [start, end] = [range.index, range.index + range.length].map(function(pos) { | 
						|
      return index.transformPosition(pos, source !== Emitter.sources.USER); | 
						|
    }); | 
						|
  } else { | 
						|
    [start, end] = [range.index, range.index + range.length].map(function(pos) { | 
						|
      if (pos < index || (pos === index && source === Emitter.sources.USER)) return pos; | 
						|
      if (length >= 0) { | 
						|
        return pos + length; | 
						|
      } else { | 
						|
        return Math.max(index, pos + length); | 
						|
      } | 
						|
    }); | 
						|
  } | 
						|
  return new Range(start, end - start); | 
						|
} | 
						|
 | 
						|
 | 
						|
export { expandConfig, overload, Quill as default };
 | 
						|
 |