From 8fc9bb78cf2083ca0504b60713e6b8aa84967eb1 Mon Sep 17 00:00:00 2001 From: Zihua Li Date: Fri, 2 Feb 2024 22:27:57 +0800 Subject: [PATCH] Avoid generating unsupported formats on paste --- CHANGELOG.md | 1 + packages/quill/src/modules/clipboard.ts | 80 +++++++++++-------- .../quill/test/unit/modules/clipboard.spec.ts | 46 ++++++++--- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b88b10406b..44ed55646e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # [Unreleased] - **Clipboard** Convert newlines between inline elements to a space. +- **Clipboard** Avoid generating unsupported formats on paste. - **Syntax** Support highlight.js v10 and v11. # 2.0.0-beta.2 diff --git a/packages/quill/src/modules/clipboard.ts b/packages/quill/src/modules/clipboard.ts index 50af925a4f..ad21b95f23 100644 --- a/packages/quill/src/modules/clipboard.ts +++ b/packages/quill/src/modules/clipboard.ts @@ -40,9 +40,9 @@ const CLIPBOARD_CONFIG: [Selector, Matcher][] = [ ['ol, ul', matchList], ['pre', matchCodeBlock], ['tr', matchTable], - ['b', matchAlias.bind(matchAlias, 'bold')], - ['i', matchAlias.bind(matchAlias, 'italic')], - ['strike', matchAlias.bind(matchAlias, 'strike')], + ['b', createMatchAlias('bold')], + ['i', createMatchAlias('italic')], + ['strike', createMatchAlias('strike')], ['style', matchIgnore], ]; @@ -254,18 +254,16 @@ Clipboard.DEFAULTS = { matchers: [], }; -function applyFormat(delta: Delta, formats: Record): Delta; -function applyFormat(delta: Delta, format: string, value: unknown): Delta; function applyFormat( delta: Delta, - format: string | Record, - value?: unknown, + format: string, + value: unknown, + scroll: ScrollBlot, ): Delta { - if (typeof format === 'object') { - return Object.keys(format).reduce((newDelta, key) => { - return applyFormat(newDelta, key, format[key]); - }, delta); + if (!scroll.query(format)) { + return delta; } + return delta.reduce((newDelta, op) => { if (op.attributes && op.attributes[format]) { return newDelta.push(op); @@ -392,8 +390,10 @@ function traverse( return new Delta(); } -function matchAlias(format: string, node: Element, delta: Delta) { - return applyFormat(delta, format, true); +function createMatchAlias(format: string) { + return (_node: Element, delta: Delta, scroll: ScrollBlot) => { + return applyFormat(delta, format, true, scroll); + }; } function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { @@ -420,10 +420,11 @@ function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { formats[attr.attrName] = attr.value(node) || undefined; } }); - if (Object.keys(formats).length > 0) { - return applyFormat(delta, formats); - } - return delta; + + return Object.entries(formats).reduce( + (newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll), + delta, + ); } function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) { @@ -445,10 +446,17 @@ function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot) { if (match.prototype instanceof BlockBlot && !deltaEndsWith(delta, '\n')) { delta.insert('\n'); } - // @ts-expect-error - if (typeof match.formats === 'function') { - // @ts-expect-error - return applyFormat(delta, match.blotName, match.formats(node, scroll)); + if ( + 'blotName' in match && + 'formats' in match && + typeof match.formats === 'function' + ) { + return applyFormat( + delta, + match.blotName, + match.formats(node, scroll), + scroll, + ); } } return delta; @@ -463,9 +471,11 @@ function matchBreak(node: Node, delta: Delta) { function matchCodeBlock(node: Node, delta: Delta, scroll: ScrollBlot) { const match = scroll.query('code-block'); - // @ts-expect-error - const language = match ? match.formats(node, scroll) : true; - return applyFormat(delta, 'code-block', language); + const language = + match && 'formats' in match && typeof match.formats === 'function' + ? match.formats(node, scroll) + : true; + return applyFormat(delta, 'code-block', language, scroll); } function matchIgnore() { @@ -501,10 +511,10 @@ function matchIndent(node: Node, delta: Delta, scroll: ScrollBlot) { }, new Delta()); } -function matchList(node: Node, delta: Delta) { +function matchList(node: Node, delta: Delta, scroll: ScrollBlot) { // @ts-expect-error const list = node.tagName === 'OL' ? 'ordered' : 'bullet'; - return applyFormat(delta, 'list', list); + return applyFormat(delta, 'list', list, scroll); } function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) { @@ -532,7 +542,7 @@ function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot) { return delta; } -function matchStyles(node: HTMLElement, delta: Delta) { +function matchStyles(node: HTMLElement, delta: Delta, scroll: ScrollBlot) { const formats: Record = {}; const style: Partial = node.style || {}; if (style.fontStyle === 'italic') { @@ -551,9 +561,10 @@ function matchStyles(node: HTMLElement, delta: Delta) { ) { formats.bold = true; } - if (Object.keys(formats).length > 0) { - delta = applyFormat(delta, formats); - } + delta = Object.entries(formats).reduce( + (newDelta, [name, value]) => applyFormat(newDelta, name, value, scroll), + delta, + ); // @ts-expect-error if (parseFloat(style.textIndent || 0) > 0) { // Could be 0.5in @@ -562,7 +573,11 @@ function matchStyles(node: HTMLElement, delta: Delta) { return delta; } -function matchTable(node: HTMLTableRowElement, delta: Delta) { +function matchTable( + node: HTMLTableRowElement, + delta: Delta, + scroll: ScrollBlot, +) { const table = node.parentElement?.tagName === 'TABLE' ? node.parentElement @@ -570,8 +585,9 @@ function matchTable(node: HTMLTableRowElement, delta: Delta) { if (table != null) { const rows = Array.from(table.querySelectorAll('tr')); const row = rows.indexOf(node) + 1; - return applyFormat(delta, 'table', row); + return applyFormat(delta, 'table', row, scroll); } + return delta; } function matchText(node: HTMLElement, delta: Delta) { diff --git a/packages/quill/test/unit/modules/clipboard.spec.ts b/packages/quill/test/unit/modules/clipboard.spec.ts index 5044d674bd..bc9de3e7f7 100644 --- a/packages/quill/test/unit/modules/clipboard.spec.ts +++ b/packages/quill/test/unit/modules/clipboard.spec.ts @@ -18,6 +18,14 @@ import { import Video from '../../../src/formats/video'; import { createRegistry } from '../__helpers__/factory'; import { sleep } from '../__helpers__/utils'; +import type { RegistryDefinition } from 'parchment'; +import { + DirectionAttribute, + DirectionClass, + DirectionStyle, +} from '../../../src/formats/direction'; +import CodeBlock from '../../../src/formats/code'; +import { ColorClass, ColorStyle } from '../../../src/formats/color'; describe('Clipboard', () => { describe('events', () => { @@ -165,7 +173,7 @@ describe('Clipboard', () => { }); describe('convert', () => { - const createClipboard = () => { + const createClipboard = (extraFormats: RegistryDefinition[] = []) => { const container = document.body.appendChild( document.createElement('div'), ); @@ -183,6 +191,7 @@ describe('Clipboard', () => { Image, Video, Link, + ...extraFormats, ]); const quill = new Quill(container, { registry }); quill.setSelection(2, 5); @@ -291,15 +300,17 @@ describe('Clipboard', () => { test('pre', () => { const html = '
 01 \n 23 
'; - const delta = createClipboard().convert({ html }); - expect(delta).toEqual( + expect(createClipboard([CodeBlock]).convert({ html })).toEqual( new Delta().insert(' 01 \n 23 \n', { 'code-block': true }), ); + expect(createClipboard().convert({ html })).toEqual( + new Delta().insert(' 01 \n 23 '), + ); }); test('pre with \\n node', () => { const html = '
 01 \n 23 
'; - const delta = createClipboard().convert({ html }); + const delta = createClipboard([CodeBlock]).convert({ html }); expect(delta).toEqual( new Delta().insert(' 01 \n 23 \n', { 'code-block': true }), ); @@ -446,17 +457,32 @@ describe('Clipboard', () => { }); test('attributor and style match', () => { - const delta = createClipboard().convert({ - html: '

Test

', + const html = '

Test

'; + const attributors = [DirectionStyle, DirectionClass, DirectionAttribute]; + attributors.forEach((attributor) => { + expect(createClipboard([attributor]).convert({ html })).toEqual( + new Delta().insert('Test\n', { direction: 'rtl' }), + ); }); - expect(delta).toEqual(new Delta().insert('Test\n', { direction: 'rtl' })); + + expect(createClipboard().convert({ html })).toEqual( + new Delta().insert('Test'), + ); }); test('nested styles', () => { - const delta = createClipboard().convert({ - html: 'Test', + const html = + 'Test'; + const attributors = [ColorStyle, ColorClass]; + attributors.forEach((attributor) => { + expect(createClipboard([attributor]).convert({ html })).toEqual( + new Delta().insert('Test', { color: 'blue' }), + ); }); - expect(delta).toEqual(new Delta().insert('Test', { color: 'blue' })); + + expect(createClipboard().convert({ html })).toEqual( + new Delta().insert('Test'), + ); }); test('custom matcher', () => {