not really known
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.

524 lines
14 KiB

  1. import extend from 'extend';
  2. import Delta from 'quill-delta';
  3. import {
  4. Attributor,
  5. ClassAttributor,
  6. EmbedBlot,
  7. Scope,
  8. StyleAttributor,
  9. BlockBlot,
  10. } from 'parchment';
  11. import { BlockEmbed } from '../blots/block';
  12. import Quill from '../core/quill';
  13. import logger from '../core/logger';
  14. import Module from '../core/module';
  15. import { AlignAttribute, AlignStyle } from '../formats/align';
  16. import { BackgroundStyle } from '../formats/background';
  17. import CodeBlock from '../formats/code';
  18. import { ColorStyle } from '../formats/color';
  19. import { DirectionAttribute, DirectionStyle } from '../formats/direction';
  20. import { FontStyle } from '../formats/font';
  21. import { SizeStyle } from '../formats/size';
  22. const debug = logger('quill:clipboard');
  23. const CLIPBOARD_CONFIG = [
  24. [Node.TEXT_NODE, matchText],
  25. [Node.TEXT_NODE, matchNewline],
  26. ['br', matchBreak],
  27. [Node.ELEMENT_NODE, matchNewline],
  28. [Node.ELEMENT_NODE, matchBlot],
  29. [Node.ELEMENT_NODE, matchAttributor],
  30. [Node.ELEMENT_NODE, matchStyles],
  31. ['li', matchIndent],
  32. ['ol, ul', matchList],
  33. ['pre', matchCodeBlock],
  34. ['tr', matchTable],
  35. ['b', matchAlias.bind(matchAlias, 'bold')],
  36. ['i', matchAlias.bind(matchAlias, 'italic')],
  37. ['style', matchIgnore],
  38. ];
  39. const ATTRIBUTE_ATTRIBUTORS = [AlignAttribute, DirectionAttribute].reduce(
  40. (memo, attr) => {
  41. memo[attr.keyName] = attr;
  42. return memo;
  43. },
  44. {},
  45. );
  46. const STYLE_ATTRIBUTORS = [
  47. AlignStyle,
  48. BackgroundStyle,
  49. ColorStyle,
  50. DirectionStyle,
  51. FontStyle,
  52. SizeStyle,
  53. ].reduce((memo, attr) => {
  54. memo[attr.keyName] = attr;
  55. return memo;
  56. }, {});
  57. class Clipboard extends Module {
  58. constructor(quill, options) {
  59. super(quill, options);
  60. this.quill.root.addEventListener('copy', e => this.onCaptureCopy(e, false));
  61. this.quill.root.addEventListener('cut', e => this.onCaptureCopy(e, true));
  62. this.quill.root.addEventListener('paste', this.onCapturePaste.bind(this));
  63. this.matchers = [];
  64. CLIPBOARD_CONFIG.concat(this.options.matchers).forEach(
  65. ([selector, matcher]) => {
  66. this.addMatcher(selector, matcher);
  67. },
  68. );
  69. }
  70. addMatcher(selector, matcher) {
  71. this.matchers.push([selector, matcher]);
  72. }
  73. convert({ html, text }, formats = {}) {
  74. if (formats[CodeBlock.blotName]) {
  75. return new Delta().insert(text, {
  76. [CodeBlock.blotName]: formats[CodeBlock.blotName],
  77. });
  78. }
  79. if (!html) {
  80. return new Delta().insert(text || '');
  81. }
  82. const doc = new DOMParser().parseFromString(html, 'text/html');
  83. const container = doc.body;
  84. const nodeMatches = new WeakMap();
  85. const [elementMatchers, textMatchers] = this.prepareMatching(
  86. container,
  87. nodeMatches,
  88. );
  89. const delta = traverse(
  90. this.quill.scroll,
  91. container,
  92. elementMatchers,
  93. textMatchers,
  94. nodeMatches,
  95. );
  96. // Remove trailing newline
  97. if (
  98. deltaEndsWith(delta, '\n') &&
  99. (delta.ops[delta.ops.length - 1].attributes == null || formats.table)
  100. ) {
  101. return delta.compose(new Delta().retain(delta.length() - 1).delete(1));
  102. }
  103. return delta;
  104. }
  105. dangerouslyPasteHTML(index, html, source = Quill.sources.API) {
  106. if (typeof index === 'string') {
  107. const delta = this.convert({ html: index, text: '' });
  108. this.quill.setContents(delta, html);
  109. this.quill.setSelection(0, Quill.sources.SILENT);
  110. } else {
  111. const paste = this.convert({ html, text: '' });
  112. this.quill.updateContents(
  113. new Delta().retain(index).concat(paste),
  114. source,
  115. );
  116. this.quill.setSelection(index + paste.length(), Quill.sources.SILENT);
  117. }
  118. }
  119. onCaptureCopy(e, isCut = false) {
  120. if (e.defaultPrevented) return;
  121. e.preventDefault();
  122. const [range] = this.quill.selection.getRange();
  123. if (range == null) return;
  124. const { html, text } = this.onCopy(range, isCut);
  125. e.clipboardData.setData('text/plain', text);
  126. e.clipboardData.setData('text/html', html);
  127. if (isCut) {
  128. this.quill.deleteText(range, Quill.sources.USER);
  129. }
  130. }
  131. onCapturePaste(e) {
  132. if (e.defaultPrevented || !this.quill.isEnabled()) return;
  133. e.preventDefault();
  134. const range = this.quill.getSelection(true);
  135. if (range == null) return;
  136. const html = e.clipboardData.getData('text/html');
  137. const text = e.clipboardData.getData('text/plain');
  138. const files = Array.from(e.clipboardData.files || []);
  139. if (!html && files.length > 0) {
  140. this.quill.uploader.upload(range, files);
  141. } else {
  142. this.onPaste(range, { html, text });
  143. }
  144. }
  145. onCopy(range) {
  146. const text = this.quill.getText(range);
  147. const html = this.quill.getSemanticHTML(range);
  148. return { html, text };
  149. }
  150. onPaste(range, { text, html }) {
  151. const formats = this.quill.getFormat(range.index);
  152. const pastedDelta = this.convert({ text, html }, formats);
  153. debug.log('onPaste', pastedDelta, { text, html });
  154. const delta = new Delta()
  155. .retain(range.index)
  156. .delete(range.length)
  157. .concat(pastedDelta);
  158. this.quill.updateContents(delta, Quill.sources.USER);
  159. // range.length contributes to delta.length()
  160. this.quill.setSelection(
  161. delta.length() - range.length,
  162. Quill.sources.SILENT,
  163. );
  164. this.quill.scrollIntoView();
  165. }
  166. prepareMatching(container, nodeMatches) {
  167. const elementMatchers = [];
  168. const textMatchers = [];
  169. this.matchers.forEach(pair => {
  170. const [selector, matcher] = pair;
  171. switch (selector) {
  172. case Node.TEXT_NODE:
  173. textMatchers.push(matcher);
  174. break;
  175. case Node.ELEMENT_NODE:
  176. elementMatchers.push(matcher);
  177. break;
  178. default:
  179. Array.from(container.querySelectorAll(selector)).forEach(node => {
  180. if (nodeMatches.has(node)) {
  181. const matches = nodeMatches.get(node);
  182. matches.push(matcher);
  183. } else {
  184. nodeMatches.set(node, [matcher]);
  185. }
  186. });
  187. break;
  188. }
  189. });
  190. return [elementMatchers, textMatchers];
  191. }
  192. }
  193. Clipboard.DEFAULTS = {
  194. matchers: [],
  195. };
  196. function applyFormat(delta, format, value) {
  197. if (typeof format === 'object') {
  198. return Object.keys(format).reduce((newDelta, key) => {
  199. return applyFormat(newDelta, key, format[key]);
  200. }, delta);
  201. }
  202. return delta.reduce((newDelta, op) => {
  203. if (op.attributes && op.attributes[format]) {
  204. return newDelta.push(op);
  205. }
  206. return newDelta.insert(
  207. op.insert,
  208. extend({}, { [format]: value }, op.attributes),
  209. );
  210. }, new Delta());
  211. }
  212. function deltaEndsWith(delta, text) {
  213. let endText = '';
  214. for (
  215. let i = delta.ops.length - 1;
  216. i >= 0 && endText.length < text.length;
  217. --i // eslint-disable-line no-plusplus
  218. ) {
  219. const op = delta.ops[i];
  220. if (typeof op.insert !== 'string') break;
  221. endText = op.insert + endText;
  222. }
  223. return endText.slice(-1 * text.length) === text;
  224. }
  225. function isLine(node) {
  226. if (node.childNodes.length === 0) return false; // Exclude embed blocks
  227. return [
  228. 'address',
  229. 'article',
  230. 'blockquote',
  231. 'canvas',
  232. 'dd',
  233. 'div',
  234. 'dl',
  235. 'dt',
  236. 'fieldset',
  237. 'figcaption',
  238. 'figure',
  239. 'footer',
  240. 'form',
  241. 'h1',
  242. 'h2',
  243. 'h3',
  244. 'h4',
  245. 'h5',
  246. 'h6',
  247. 'header',
  248. 'iframe',
  249. 'li',
  250. 'main',
  251. 'nav',
  252. 'ol',
  253. 'output',
  254. 'p',
  255. 'pre',
  256. 'section',
  257. 'table',
  258. 'td',
  259. 'tr',
  260. 'ul',
  261. 'video',
  262. ].includes(node.tagName.toLowerCase());
  263. }
  264. const preNodes = new WeakMap();
  265. function isPre(node) {
  266. if (node == null) return false;
  267. if (!preNodes.has(node)) {
  268. if (node.tagName === 'PRE') {
  269. preNodes.set(node, true);
  270. } else {
  271. preNodes.set(node, isPre(node.parentNode));
  272. }
  273. }
  274. return preNodes.get(node);
  275. }
  276. function traverse(scroll, node, elementMatchers, textMatchers, nodeMatches) {
  277. // Post-order
  278. if (node.nodeType === node.TEXT_NODE) {
  279. return textMatchers.reduce((delta, matcher) => {
  280. return matcher(node, delta, scroll);
  281. }, new Delta());
  282. }
  283. if (node.nodeType === node.ELEMENT_NODE) {
  284. return Array.from(node.childNodes || []).reduce((delta, childNode) => {
  285. let childrenDelta = traverse(
  286. scroll,
  287. childNode,
  288. elementMatchers,
  289. textMatchers,
  290. nodeMatches,
  291. );
  292. if (childNode.nodeType === node.ELEMENT_NODE) {
  293. childrenDelta = elementMatchers.reduce((reducedDelta, matcher) => {
  294. return matcher(childNode, reducedDelta, scroll);
  295. }, childrenDelta);
  296. childrenDelta = (nodeMatches.get(childNode) || []).reduce(
  297. (reducedDelta, matcher) => {
  298. return matcher(childNode, reducedDelta, scroll);
  299. },
  300. childrenDelta,
  301. );
  302. }
  303. return delta.concat(childrenDelta);
  304. }, new Delta());
  305. }
  306. return new Delta();
  307. }
  308. function matchAlias(format, node, delta) {
  309. return applyFormat(delta, format, true);
  310. }
  311. function matchAttributor(node, delta, scroll) {
  312. const attributes = Attributor.keys(node);
  313. const classes = ClassAttributor.keys(node);
  314. const styles = StyleAttributor.keys(node);
  315. const formats = {};
  316. attributes
  317. .concat(classes)
  318. .concat(styles)
  319. .forEach(name => {
  320. let attr = scroll.query(name, Scope.ATTRIBUTE);
  321. if (attr != null) {
  322. formats[attr.attrName] = attr.value(node);
  323. if (formats[attr.attrName]) return;
  324. }
  325. attr = ATTRIBUTE_ATTRIBUTORS[name];
  326. if (attr != null && (attr.attrName === name || attr.keyName === name)) {
  327. formats[attr.attrName] = attr.value(node) || undefined;
  328. }
  329. attr = STYLE_ATTRIBUTORS[name];
  330. if (attr != null && (attr.attrName === name || attr.keyName === name)) {
  331. attr = STYLE_ATTRIBUTORS[name];
  332. formats[attr.attrName] = attr.value(node) || undefined;
  333. }
  334. });
  335. if (Object.keys(formats).length > 0) {
  336. return applyFormat(delta, formats);
  337. }
  338. return delta;
  339. }
  340. function matchBlot(node, delta, scroll) {
  341. const match = scroll.query(node);
  342. if (match == null) return delta;
  343. if (match.prototype instanceof EmbedBlot) {
  344. const embed = {};
  345. const value = match.value(node);
  346. if (value != null) {
  347. embed[match.blotName] = value;
  348. return new Delta().insert(embed, match.formats(node, scroll));
  349. }
  350. } else {
  351. if (match.prototype instanceof BlockBlot && !deltaEndsWith(delta, '\n')) {
  352. delta.insert('\n');
  353. }
  354. if (typeof match.formats === 'function') {
  355. return applyFormat(delta, match.blotName, match.formats(node, scroll));
  356. }
  357. }
  358. return delta;
  359. }
  360. function matchBreak(node, delta) {
  361. if (!deltaEndsWith(delta, '\n')) {
  362. delta.insert('\n');
  363. }
  364. return delta;
  365. }
  366. function matchCodeBlock(node, delta, scroll) {
  367. const match = scroll.query('code-block');
  368. const language = match ? match.formats(node, scroll) : true;
  369. return applyFormat(delta, 'code-block', language);
  370. }
  371. function matchIgnore() {
  372. return new Delta();
  373. }
  374. function matchIndent(node, delta, scroll) {
  375. const match = scroll.query(node);
  376. if (
  377. match == null ||
  378. match.blotName !== 'list' ||
  379. !deltaEndsWith(delta, '\n')
  380. ) {
  381. return delta;
  382. }
  383. let indent = -1;
  384. let parent = node.parentNode;
  385. while (parent != null) {
  386. if (['OL', 'UL'].includes(parent.tagName)) {
  387. indent += 1;
  388. }
  389. parent = parent.parentNode;
  390. }
  391. if (indent <= 0) return delta;
  392. return delta.reduce((composed, op) => {
  393. if (op.attributes && op.attributes.list) {
  394. return composed.push(op);
  395. }
  396. return composed.insert(op.insert, { indent, ...(op.attributes || {}) });
  397. }, new Delta());
  398. }
  399. function matchList(node, delta) {
  400. const list = node.tagName === 'OL' ? 'ordered' : 'bullet';
  401. return applyFormat(delta, 'list', list);
  402. }
  403. function matchNewline(node, delta, scroll) {
  404. if (!deltaEndsWith(delta, '\n')) {
  405. if (isLine(node)) {
  406. return delta.insert('\n');
  407. }
  408. if (delta.length() > 0 && node.nextSibling) {
  409. let { nextSibling } = node;
  410. while (nextSibling != null) {
  411. if (isLine(nextSibling)) {
  412. return delta.insert('\n');
  413. }
  414. const match = scroll.query(nextSibling);
  415. if (match && match.prototype instanceof BlockEmbed) {
  416. return delta.insert('\n');
  417. }
  418. nextSibling = nextSibling.firstChild;
  419. }
  420. }
  421. }
  422. return delta;
  423. }
  424. function matchStyles(node, delta) {
  425. const formats = {};
  426. const style = node.style || {};
  427. if (style.fontStyle === 'italic') {
  428. formats.italic = true;
  429. }
  430. if (
  431. style.fontWeight.startsWith('bold') ||
  432. parseInt(style.fontWeight, 10) >= 700
  433. ) {
  434. formats.bold = true;
  435. }
  436. if (Object.keys(formats).length > 0) {
  437. delta = applyFormat(delta, formats);
  438. }
  439. if (parseFloat(style.textIndent || 0) > 0) {
  440. // Could be 0.5in
  441. return new Delta().insert('\t').concat(delta);
  442. }
  443. return delta;
  444. }
  445. function matchTable(node, delta) {
  446. const table =
  447. node.parentNode.tagName === 'TABLE'
  448. ? node.parentNode
  449. : node.parentNode.parentNode;
  450. const rows = Array.from(table.querySelectorAll('tr'));
  451. const row = rows.indexOf(node) + 1;
  452. return applyFormat(delta, 'table', row);
  453. }
  454. function matchText(node, delta) {
  455. let text = node.data;
  456. // Word represents empty line with <o:p>&nbsp;</o:p>
  457. if (node.parentNode.tagName === 'O:P') {
  458. return delta.insert(text.trim());
  459. }
  460. if (text.trim().length === 0 && text.includes('\n')) {
  461. return delta;
  462. }
  463. if (!isPre(node)) {
  464. const replacer = (collapse, match) => {
  465. const replaced = match.replace(/[^\u00a0]/g, ''); // \u00a0 is nbsp;
  466. return replaced.length < 1 && collapse ? ' ' : replaced;
  467. };
  468. text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
  469. text = text.replace(/\s\s+/g, replacer.bind(replacer, true)); // collapse whitespace
  470. if (
  471. (node.previousSibling == null && isLine(node.parentNode)) ||
  472. (node.previousSibling != null && isLine(node.previousSibling))
  473. ) {
  474. text = text.replace(/^\s+/, replacer.bind(replacer, false));
  475. }
  476. if (
  477. (node.nextSibling == null && isLine(node.parentNode)) ||
  478. (node.nextSibling != null && isLine(node.nextSibling))
  479. ) {
  480. text = text.replace(/\s+$/, replacer.bind(replacer, false));
  481. }
  482. }
  483. return delta.insert(text);
  484. }
  485. export {
  486. Clipboard as default,
  487. matchAttributor,
  488. matchBlot,
  489. matchNewline,
  490. matchText,
  491. traverse,
  492. };