| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 | import extend from 'extend';import Delta from 'quill-delta';import Parchment from 'parchment';import Quill from '../core/quill';import logger from '../core/logger';import Module from '../core/module';import { AlignAttribute, AlignStyle } from '../formats/align';import { BackgroundStyle } from '../formats/background';import CodeBlock from '../formats/code';import { ColorStyle } from '../formats/color';import { DirectionAttribute, DirectionStyle } from '../formats/direction';import { FontStyle } from '../formats/font';import { SizeStyle } from '../formats/size';let debug = logger('quill:clipboard');const DOM_KEY = '__ql-matcher';const CLIPBOARD_CONFIG = [  [Node.TEXT_NODE, matchText],  [Node.TEXT_NODE, matchNewline],  ['br', matchBreak],  [Node.ELEMENT_NODE, matchNewline],  [Node.ELEMENT_NODE, matchBlot],  [Node.ELEMENT_NODE, matchSpacing],  [Node.ELEMENT_NODE, matchAttributor],  [Node.ELEMENT_NODE, matchStyles],  ['li', matchIndent],  ['b', matchAlias.bind(matchAlias, 'bold')],  ['i', matchAlias.bind(matchAlias, 'italic')],  ['style', matchIgnore]];const ATTRIBUTE_ATTRIBUTORS = [  AlignAttribute,  DirectionAttribute].reduce(function(memo, attr) {  memo[attr.keyName] = attr;  return memo;}, {});const STYLE_ATTRIBUTORS = [  AlignStyle,  BackgroundStyle,  ColorStyle,  DirectionStyle,  FontStyle,  SizeStyle].reduce(function(memo, attr) {  memo[attr.keyName] = attr;  return memo;}, {});class Clipboard extends Module {  constructor(quill, options) {    super(quill, options);    this.quill.root.addEventListener('paste', this.onPaste.bind(this));    this.container = this.quill.addContainer('ql-clipboard');    this.container.setAttribute('contenteditable', true);    this.container.setAttribute('tabindex', -1);    this.matchers = [];    CLIPBOARD_CONFIG.concat(this.options.matchers).forEach(([selector, matcher]) => {      if (!options.matchVisual && matcher === matchSpacing) return;      this.addMatcher(selector, matcher);    });  }  addMatcher(selector, matcher) {    this.matchers.push([selector, matcher]);  }  convert(html) {    if (typeof html === 'string') {      this.container.innerHTML = html.replace(/\>\r?\n +\</g, '><'); // Remove spaces between tags      return this.convert();    }    const formats = this.quill.getFormat(this.quill.selection.savedRange.index);    if (formats[CodeBlock.blotName]) {      const text = this.container.innerText;      this.container.innerHTML = '';      return new Delta().insert(text, { [CodeBlock.blotName]: formats[CodeBlock.blotName] });    }    let [elementMatchers, textMatchers] = this.prepareMatching();    let delta = traverse(this.container, elementMatchers, textMatchers);    // Remove trailing newline    if (deltaEndsWith(delta, '\n') && delta.ops[delta.ops.length - 1].attributes == null) {      delta = delta.compose(new Delta().retain(delta.length() - 1).delete(1));    }    debug.log('convert', this.container.innerHTML, delta);    this.container.innerHTML = '';    return delta;  }  dangerouslyPasteHTML(index, html, source = Quill.sources.API) {    if (typeof index === 'string') {      this.quill.setContents(this.convert(index), html);      this.quill.setSelection(0, Quill.sources.SILENT);    } else {      let paste = this.convert(html);      this.quill.updateContents(new Delta().retain(index).concat(paste), source);      this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);    }  }  onPaste(e) {    if (e.defaultPrevented || !this.quill.isEnabled()) return;    let range = this.quill.getSelection();    let delta = new Delta().retain(range.index);    let scrollTop = this.quill.scrollingContainer.scrollTop;    this.container.focus();    this.quill.selection.update(Quill.sources.SILENT);    setTimeout(() => {      delta = delta.concat(this.convert()).delete(range.length);      this.quill.updateContents(delta, Quill.sources.USER);      // range.length contributes to delta.length()      this.quill.setSelection(delta.length() - range.length, Quill.sources.SILENT);      this.quill.scrollingContainer.scrollTop = scrollTop;      this.quill.focus();    }, 1);  }  prepareMatching() {    let elementMatchers = [], textMatchers = [];    this.matchers.forEach((pair) => {      let [selector, matcher] = pair;      switch (selector) {        case Node.TEXT_NODE:          textMatchers.push(matcher);          break;        case Node.ELEMENT_NODE:          elementMatchers.push(matcher);          break;        default:          [].forEach.call(this.container.querySelectorAll(selector), (node) => {            // TODO use weakmap            node[DOM_KEY] = node[DOM_KEY] || [];            node[DOM_KEY].push(matcher);          });          break;      }    });    return [elementMatchers, textMatchers];  }}Clipboard.DEFAULTS = {  matchers: [],  matchVisual: true};function applyFormat(delta, format, value) {  if (typeof format === 'object') {    return Object.keys(format).reduce(function(delta, key) {      return applyFormat(delta, key, format[key]);    }, delta);  } else {    return delta.reduce(function(delta, op) {      if (op.attributes && op.attributes[format]) {        return delta.push(op);      } else {        return delta.insert(op.insert, extend({}, {[format]: value}, op.attributes));      }    }, new Delta());  }}function computeStyle(node) {  if (node.nodeType !== Node.ELEMENT_NODE) return {};  const DOM_KEY = '__ql-computed-style';  return node[DOM_KEY] || (node[DOM_KEY] = window.getComputedStyle(node));}function deltaEndsWith(delta, text) {  let endText = "";  for (let i = delta.ops.length - 1; i >= 0 && endText.length < text.length; --i) {    let op  = delta.ops[i];    if (typeof op.insert !== 'string') break;    endText = op.insert + endText;  }  return endText.slice(-1*text.length) === text;}function isLine(node) {  if (node.childNodes.length === 0) return false;   // Exclude embed blocks  let style = computeStyle(node);  return ['block', 'list-item'].indexOf(style.display) > -1;}function traverse(node, elementMatchers, textMatchers) {  // Post-order  if (node.nodeType === node.TEXT_NODE) {    return textMatchers.reduce(function(delta, matcher) {      return matcher(node, delta);    }, new Delta());  } else if (node.nodeType === node.ELEMENT_NODE) {    return [].reduce.call(node.childNodes || [], (delta, childNode) => {      let childrenDelta = traverse(childNode, elementMatchers, textMatchers);      if (childNode.nodeType === node.ELEMENT_NODE) {        childrenDelta = elementMatchers.reduce(function(childrenDelta, matcher) {          return matcher(childNode, childrenDelta);        }, childrenDelta);        childrenDelta = (childNode[DOM_KEY] || []).reduce(function(childrenDelta, matcher) {          return matcher(childNode, childrenDelta);        }, childrenDelta);      }      return delta.concat(childrenDelta);    }, new Delta());  } else {    return new Delta();  }}function matchAlias(format, node, delta) {  return applyFormat(delta, format, true);}function matchAttributor(node, delta) {  let attributes = Parchment.Attributor.Attribute.keys(node);  let classes = Parchment.Attributor.Class.keys(node);  let styles = Parchment.Attributor.Style.keys(node);  let formats = {};  attributes.concat(classes).concat(styles).forEach((name) => {    let attr = Parchment.query(name, Parchment.Scope.ATTRIBUTE);    if (attr != null) {      formats[attr.attrName] = attr.value(node);      if (formats[attr.attrName]) return;    }    attr = ATTRIBUTE_ATTRIBUTORS[name];    if (attr != null && (attr.attrName === name || attr.keyName === name)) {      formats[attr.attrName] = attr.value(node) || undefined;    }    attr = STYLE_ATTRIBUTORS[name]    if (attr != null && (attr.attrName === name || attr.keyName === name)) {      attr = STYLE_ATTRIBUTORS[name];      formats[attr.attrName] = attr.value(node) || undefined;    }  });  if (Object.keys(formats).length > 0) {    delta = applyFormat(delta, formats);  }  return delta;}function matchBlot(node, delta) {  let match = Parchment.query(node);  if (match == null) return delta;  if (match.prototype instanceof Parchment.Embed) {    let embed = {};    let value = match.value(node);    if (value != null) {      embed[match.blotName] = value;      delta = new Delta().insert(embed, match.formats(node));    }  } else if (typeof match.formats === 'function') {    delta = applyFormat(delta, match.blotName, match.formats(node));  }  return delta;}function matchBreak(node, delta) {  if (!deltaEndsWith(delta, '\n')) {    delta.insert('\n');  }  return delta;}function matchIgnore() {  return new Delta();}function matchIndent(node, delta) {  let match = Parchment.query(node);  if (match == null || match.blotName !== 'list-item' || !deltaEndsWith(delta, '\n')) {    return delta;  }  let indent = -1, parent = node.parentNode;  while (!parent.classList.contains('ql-clipboard')) {    if ((Parchment.query(parent) || {}).blotName === 'list') {      indent += 1;    }    parent = parent.parentNode;  }  if (indent <= 0) return delta;  return delta.compose(new Delta().retain(delta.length() - 1).retain(1, { indent: indent}));}function matchNewline(node, delta) {  if (!deltaEndsWith(delta, '\n')) {    if (isLine(node) || (delta.length() > 0 && node.nextSibling && isLine(node.nextSibling))) {      delta.insert('\n');    }  }  return delta;}function matchSpacing(node, delta) {  if (isLine(node) && node.nextElementSibling != null && !deltaEndsWith(delta, '\n\n')) {    let nodeHeight = node.offsetHeight + parseFloat(computeStyle(node).marginTop) + parseFloat(computeStyle(node).marginBottom);    if (node.nextElementSibling.offsetTop > node.offsetTop + nodeHeight*1.5) {      delta.insert('\n');    }  }  return delta;}function matchStyles(node, delta) {  let formats = {};  let style = node.style || {};  if (style.fontStyle && computeStyle(node).fontStyle === 'italic') {    formats.italic = true;  }  if (style.fontWeight && (computeStyle(node).fontWeight.startsWith('bold') ||                           parseInt(computeStyle(node).fontWeight) >= 700)) {    formats.bold = true;  }  if (Object.keys(formats).length > 0) {    delta = applyFormat(delta, formats);  }  if (parseFloat(style.textIndent || 0) > 0) {  // Could be 0.5in    delta = new Delta().insert('\t').concat(delta);  }  return delta;}function matchText(node, delta) {  let text = node.data;  // Word represents empty line with <o:p> </o:p>  if (node.parentNode.tagName === 'O:P') {    return delta.insert(text.trim());  }  if (text.trim().length === 0 && node.parentNode.classList.contains('ql-clipboard')) {    return delta;  }  if (!computeStyle(node.parentNode).whiteSpace.startsWith('pre')) {    // eslint-disable-next-line func-style    let replacer = function(collapse, match) {      match = match.replace(/[^\u00a0]/g, '');    // \u00a0 is nbsp;      return match.length < 1 && collapse ? ' ' : match;    };    text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');    text = text.replace(/\s\s+/g, replacer.bind(replacer, true));  // collapse whitespace    if ((node.previousSibling == null && isLine(node.parentNode)) ||        (node.previousSibling != null && isLine(node.previousSibling))) {      text = text.replace(/^\s+/, replacer.bind(replacer, false));    }    if ((node.nextSibling == null && isLine(node.parentNode)) ||        (node.nextSibling != null && isLine(node.nextSibling))) {      text = text.replace(/\s+$/, replacer.bind(replacer, false));    }  }  return delta.insert(text);}export { Clipboard as default, matchAttributor, matchBlot, matchNewline, matchSpacing, matchText };
 |