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