Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid generating unsupported formats on paste #3988

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading