Handle consecutive spaces when copying and pasting
Co-authored-by: Zihua Li <635902+luin@users.noreply.github.com>
This commit is contained in:
parent
b213e1073b
commit
07b68c98b3
@ -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
|
||||
|
||||
@ -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> </o:p>
|
||||
if (node.parentElement?.tagName === 'O:P') {
|
||||
return delta.insert(text.trim());
|
||||
@ -639,12 +639,10 @@ 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 &&
|
||||
@ -652,7 +650,8 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
|
||||
(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 &&
|
||||
@ -660,8 +659,11 @@ function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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([
|
||||
@ -54,6 +56,9 @@ const createEditor = (html: string) => {
|
||||
SizeClass,
|
||||
]),
|
||||
});
|
||||
if (typeof htmlOrContents !== 'string') {
|
||||
quill.setContents(htmlOrContents);
|
||||
}
|
||||
return quill.editor;
|
||||
};
|
||||
|
||||
@ -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 </strong>123<em> 123</em>');
|
||||
|
||||
expect(createEditor(new Delta().insert('1 2\n')).getHTML(0, 5)).toEqual(
|
||||
'1 2',
|
||||
);
|
||||
|
||||
expect(
|
||||
createEditor(
|
||||
new Delta().insert(' 123', { bold: true }).insert('\n'),
|
||||
).getHTML(0, 5),
|
||||
).toEqual('<strong> 123</strong>');
|
||||
});
|
||||
|
||||
test('mixed list', () => {
|
||||
const editor = createEditor(
|
||||
`
|
||||
|
||||
@ -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 });
|
||||
@ -256,19 +262,23 @@ describe('Clipboard', () => {
|
||||
const html = '<span>0 <strong>1</strong> 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> 1 </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 </p><p> 2</p>' }),
|
||||
).toEqual(new Delta().insert('0 \n 2'));
|
||||
expect(
|
||||
createClipboard().convert({ html: '<p>0 </p><p> 2</p>' }),
|
||||
).toEqual(new Delta().insert('0 \n 2'));
|
||||
});
|
||||
|
||||
test('newlines between inline elements', () => {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
},
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2020",
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"module": "ES2020",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user