Skip to content

Commit

Permalink
Handle consecutive spaces when copying and pasting
Browse files Browse the repository at this point in the history
Co-authored-by: Zihua Li <635902+luin@users.noreply.github.com>
  • Loading branch information
jhchen and luin authored Nov 29, 2024
1 parent b213e10 commit 07b68c9
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 21 deletions.
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(' ', '&nbsp;');

This comment has been minimized.

Copy link
@hypertyper84

hypertyper84 Dec 18, 2024

Why did you make this? This will result that no word breaks are possible. Bug!

}
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

0 comments on commit 07b68c9

Please sign in to comment.