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

Handle consecutive spaces when copying and pasting #4502

Merged
merged 4 commits into from
Nov 29, 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
3 changes: 2 additions & 1 deletion packages/quill/src/core/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ function convertHTML(
return blot.html(index, length);
}
if (blot instanceof TextBlot) {
return escapeText(blot.value().slice(index, index + length));
const escapedText = escapeText(blot.value().slice(index, index + length));
return escapedText.replaceAll(' ', ' ');
}
if (blot instanceof ParentBlot) {
// TODO fix API
Expand Down
20 changes: 11 additions & 9 deletions packages/quill/src/modules/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ function matchTable(

function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
// @ts-expect-error
let text = node.data;
let text = node.data as string;
// Word represents empty line with <o:p>&nbsp;</o:p>
if (node.parentElement?.tagName === 'O:P') {
return delta.insert(text.trim());
Expand All @@ -639,29 +639,31 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
) {
return delta;
}
const replacer = (collapse: unknown, match: string) => {
const replaced = match.replace(/[^\u00a0]/g, ''); // \u00a0 is nbsp;
return replaced.length < 1 && collapse ? ' ' : replaced;
};
text = text.replace(/\r\n/g, ' ').replace(/\n/g, ' ');
text = text.replace(/\s\s+/g, replacer.bind(replacer, true)); // collapse whitespace
// convert all non-nbsp whitespace into regular space
text = text.replace(/[^\S\u00a0]/g, ' ');
// collapse consecutive spaces into one
text = text.replace(/ {2,}/g, ' ');
if (
(node.previousSibling == null &&
node.parentElement != null &&
isLine(node.parentElement, scroll)) ||
(node.previousSibling instanceof Element &&
isLine(node.previousSibling, scroll))
) {
text = text.replace(/^\s+/, replacer.bind(replacer, false));
// block structure means we don't need leading space
text = text.replace(/^ /, '');
}
if (
(node.nextSibling == null &&
node.parentElement != null &&
isLine(node.parentElement, scroll)) ||
(node.nextSibling instanceof Element && isLine(node.nextSibling, scroll))
) {
text = text.replace(/\s+$/, replacer.bind(replacer, false));
// block structure means we don't need trailing space
text = text.replace(/ $/, '');
}
// done removing whitespace and can normalize all to regular space
text = text.replaceAll('\u00a0', ' ');
}
return delta.insert(text);
}
Expand Down
28 changes: 26 additions & 2 deletions packages/quill/test/unit/core/editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import { ColorClass } from '../../../src/formats/color.js';
import Quill from '../../../src/core.js';
import { normalizeHTML } from '../__helpers__/utils.js';

const createEditor = (html: string) => {
const createEditor = (htmlOrContents: string | Delta) => {
const container = document.createElement('div');
container.innerHTML = normalizeHTML(html);
if (typeof htmlOrContents === 'string') {
container.innerHTML = normalizeHTML(htmlOrContents);
}
document.body.appendChild(container);
const quill = new Quill(container, {
registry: createRegistry([
Expand All @@ -54,6 +56,9 @@ const createEditor = (html: string) => {
SizeClass,
]),
});
if (typeof htmlOrContents !== 'string') {
quill.setContents(htmlOrContents);
}
return quill.editor;
};

Expand Down Expand Up @@ -1246,6 +1251,25 @@ describe('Editor', () => {
);
});

test('collapsible spaces', () => {
expect(
createEditor('<p><strong>123 </strong>123<em> 123</em></p>').getHTML(
0,
11,
),
).toEqual('<strong>123&nbsp;</strong>123<em>&nbsp;123</em>');

expect(createEditor(new Delta().insert('1 2\n')).getHTML(0, 5)).toEqual(
'1&nbsp;&nbsp;&nbsp;2',
);

expect(
createEditor(
new Delta().insert(' 123', { bold: true }).insert('\n'),
).getHTML(0, 5),
).toEqual('<strong>&nbsp;&nbsp;123</strong>');
});

test('mixed list', () => {
const editor = createEditor(
`
Expand Down
24 changes: 17 additions & 7 deletions packages/quill/test/unit/modules/clipboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ describe('Clipboard', () => {
expect(delta).toEqual(new Delta().insert('0\n1 2 3 4\n5 6 7 8'));
});

test('multiple whitespaces', () => {
const html = '<div>1 2 3</div>';
const delta = createClipboard().convert({ html });
expect(delta).toEqual(new Delta().insert('1 2 3'));
});

test('inline whitespace', () => {
const html = '<p>0 <strong>1</strong> 2</p>';
const delta = createClipboard().convert({ html });
Expand All @@ -256,19 +262,23 @@ describe('Clipboard', () => {
const html = '<span>0&nbsp;<strong>1</strong>&nbsp;2</span>';
const delta = createClipboard().convert({ html });
expect(delta).toEqual(
new Delta()
.insert('0\u00a0')
.insert('1', { bold: true })
.insert('\u00a02'),
new Delta().insert('0 ').insert('1', { bold: true }).insert(' 2'),
);
});

test('consecutive intentional whitespace', () => {
const html = '<strong>&nbsp;&nbsp;1&nbsp;&nbsp;</strong>';
const delta = createClipboard().convert({ html });
expect(delta).toEqual(
new Delta().insert('\u00a0\u00a01\u00a0\u00a0', { bold: true }),
);
expect(delta).toEqual(new Delta().insert(' 1 ', { bold: true }));
});

test('intentional whitespace at line start/end', () => {
expect(
createClipboard().convert({ html: '<p>0 &nbsp;</p><p>&nbsp; 2</p>' }),
).toEqual(new Delta().insert('0 \n 2'));
expect(
createClipboard().convert({ html: '<p>0&nbsp; </p><p> &nbsp;2</p>' }),
).toEqual(new Delta().insert('0 \n 2'));
});

test('newlines between inline elements', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/quill/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"compilerOptions": {
"outDir": "./dist",
"allowSyntheticDefaultImports": true,
"target": "ES2020",
"target": "ES2021",
"sourceMap": true,
"resolveJsonModule": true,
"declaration": false,
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"target": "ES2020",
"target": "ES2021",
"sourceMap": true,
"declaration": true,
"module": "ES2020",
Expand Down
Loading