73cfb9e35371f65aea1b0ae90e2a0d1d3c33280d408a163d62988106dafd07320a5dd462ea5d3be3ea310df9d6f768114ace6e9a67a9d4a867127c1c3f1eff 17 KB

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