Skip to content

Commit

Permalink
Avoid generating unsupported formats on paste
Browse files Browse the repository at this point in the history
  • Loading branch information
luin committed Feb 3, 2024
1 parent f8bfde4 commit be77b1a
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 42 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
80 changes: 48 additions & 32 deletions packages/quill/src/modules/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
];

Expand Down Expand Up @@ -254,18 +254,16 @@ Clipboard.DEFAULTS = {
matchers: [],
};

function applyFormat(delta: Delta, formats: Record<string, unknown>): Delta;
function applyFormat(delta: Delta, format: string, value: unknown): Delta;
function applyFormat(
delta: Delta,
format: string | Record<string, unknown>,
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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, unknown> = {};
const style: Partial<CSSStyleDeclaration> = node.style || {};
if (style.fontStyle === 'italic') {
Expand All @@ -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
Expand All @@ -562,16 +573,21 @@ 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
: node.parentElement?.parentElement;
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) {
Expand Down
46 changes: 36 additions & 10 deletions packages/quill/test/unit/modules/clipboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -165,7 +173,7 @@ describe('Clipboard', () => {
});

describe('convert', () => {
const createClipboard = () => {
const createClipboard = (extraFormats: RegistryDefinition[] = []) => {
const container = document.body.appendChild(
document.createElement('div'),
);
Expand All @@ -183,6 +191,7 @@ describe('Clipboard', () => {
Image,
Video,
Link,
...extraFormats,
]);
const quill = new Quill(container, { registry });
quill.setSelection(2, 5);
Expand Down Expand Up @@ -291,15 +300,17 @@ describe('Clipboard', () => {

test('pre', () => {
const html = '<pre> 01 \n 23 </pre>';
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 = '<pre><span> 01 </span>\n<span> 23 </span></pre>';
const delta = createClipboard().convert({ html });
const delta = createClipboard([CodeBlock]).convert({ html });
expect(delta).toEqual(
new Delta().insert(' 01 \n 23 \n', { 'code-block': true }),
);
Expand Down Expand Up @@ -446,17 +457,32 @@ describe('Clipboard', () => {
});

test('attributor and style match', () => {
const delta = createClipboard().convert({
html: '<p style="direction:rtl;">Test</p>',
const html = '<p style="direction:rtl;">Test</p>';
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: '<span style="color: red;"><span style="color: blue;">Test</span></span>',
const html =
'<span style="color: red;"><span style="color: blue;">Test</span></span>';
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', () => {
Expand Down

0 comments on commit be77b1a

Please sign in to comment.