fix(creator-studio): unify block and inline marker deletion

This commit is contained in:
kate522 committed 2026-11-10 20:10:48 +08:00
1 parent d391b0fd00
commit 30ceab4615
3 files changed
+193 -47

No files matched your search

@@ -69,12 +69,14 @@ function fixture(html, nodeSelector, offset = 0) {
return { body, editor, events, press };
}
test('Backspace deletes the adjacent table from a paragraph boundary and records one edit', () => {
test('Backspace selects the adjacent table before deleting it as one edit', () => {
const f = fixture('<table><tbody><tr><td>data</td></tr></tbody></table><p><strong>after</strong></p>', 'strong', 0);
const event = f.press('Backspace');
assert.equal(event.prevented, true);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('table').length, 1);
assert.equal(f.editor.selection.getRng().startContainer, f.body);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p><strong>after</strong></p>');
assert.deepEqual(f.events, ['undo', 'node', 'change']);
assert.deepEqual(f.events, ['node', 'undo', 'node', 'change']);
});
test('Delete removes a following list only at the end of ordinary text', () => {
@@ -84,12 +86,16 @@ test('Delete removes a following list only at the end of ordinary text', () => {
assert.equal(f.body.querySelectorAll('ul').length, 1);
f.editor.selection.setCursorLocation(f.body.querySelector('p').firstChild, 6);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.querySelectorAll('ul').length, 1);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.innerHTML, '<p>before</p>');
});
test('a displayed formula is removed with its paragraph wrapper', () => {
const f = fixture('<p><span class="jio-math jio-math--block" contenteditable="false">x</span><br data-mce-bogus="1"></p><p>next</p>', 'p', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.jio-math--block').length, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
assert.equal(f.editor.selection.getRng().startContainer, f.body.querySelector('p'));
});
@@ -97,14 +103,16 @@ test('a displayed formula is removed with its paragraph wrapper', () => {
test('Delete before a displayed formula removes only its paragraph', () => {
const f = fixture('<p>before</p><p><span class="jio-math jio-math--block" contenteditable="false">x</span></p>', 'p:nth-child(2)', 0);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.querySelectorAll('.jio-math--block').length, 1);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.innerHTML, '<p>before</p>');
});
test('Backspace on the line after a formula removes the empty line first', () => {
test('Backspace on the line after a formula selects it without removing the line', () => {
const f = fixture('<p><span class="jio-math jio-math--block" contenteditable="false">x</span></p><p><br data-mce-bogus="1"></p>', 'p:nth-child(2)', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.jio-math--block').length, 1);
assert.equal(f.body.children.length, 1);
assert.equal(f.body.children.length, 2);
assert.equal(f.editor.selection.getRng().startContainer, f.body);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.jio-math--block').length, 0);
@@ -115,7 +123,7 @@ test('Backspace on an empty line after a table preserves the table', () => {
const f = fixture('<table><tbody><tr><td>data</td></tr></tbody></table><p><br></p>', 'p', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('table').length, 1);
assert.equal(f.body.querySelectorAll('p').length, 0);
assert.equal(f.body.querySelectorAll('p').length, 1);
});
test('a saved display formula restores as one paragraph without phantom lines', () => {
@@ -141,9 +149,100 @@ test('a real line break inside a paragraph is not a block boundary', () => {
test('a quote can be removed from its inner editing boundary', () => {
const f = fixture('<div class="quote_q" contenteditable="false"><div contenteditable="true"><p>quote</p></div></div><p>next</p>', '[contenteditable="true"] p', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.quote_q').length, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
});
test('a code block stays editable until its boundary is used to select it', () => {
const f = fixture('<pre>code</pre><p>next</p>', 'pre', 0);
f.editor.selection.setCursorLocation(f.body.querySelector('pre').firstChild, 2);
assert.equal(f.press('Backspace').prevented, false);
assert.equal(f.body.querySelector('pre').textContent, 'code');
f.editor.selection.setCursorLocation(f.body.querySelector('pre').firstChild, 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelector('pre').textContent, 'code');
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
});
test('the placeholder break in an empty code block does not require an extra key press', () => {
const f = fixture('<pre><br></pre><p>next</p>', 'pre', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelector('pre')?.innerHTML, '<br>');
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
});
test('every standalone block selects from the following line before removal', () => {
const blocks = [
markup.dividerMarkup().replace('<p></p>', ''),
markup.quoteMarkup('<p>quote</p>', '').replace(/<p><\/p>$/, ''),
markup.codeBlockMarkup().replace('<p></p>', ''),
markup.tableMarkup({ rows: 1, cols: 1, header: false, striped: false }).replace('<p></p>', ''),
markup.mathMarkup({ formula: 'x^2', display: true }),
];
for (const block of blocks) {
const f = fixture(`${block}<p><br data-mce-bogus="1"></p>`, 'body > p:last-child', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.children.length, 2, block);
assert.equal(f.editor.selection.getRng().startContainer, f.body, block);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.children.length, 1, block);
assert.equal(f.body.firstElementChild.tagName, 'P', block);
}
});
test('every standalone block selects from the preceding line before Delete removes it', () => {
const blocks = [
markup.dividerMarkup().replace('<p></p>', ''),
markup.quoteMarkup('<p>quote</p>', '').replace(/<p><\/p>$/, ''),
markup.codeBlockMarkup().replace('<p></p>', ''),
markup.tableMarkup({ rows: 1, cols: 1, header: false, striped: false }).replace('<p></p>', ''),
markup.mathMarkup({ formula: 'x^2', display: true }),
];
for (const block of blocks) {
const f = fixture(`<p>before</p>${block}`, 'body > p:first-child', 1);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.children.length, 2, block);
assert.equal(f.editor.selection.getRng().startContainer, f.body, block);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.innerHTML, '<p>before</p>', block);
}
});
test('an inline footnote is selected from either side before deletion', () => {
const note = '<sup class="jio-footnote-ref" data-jio-footnote="note" contenteditable="false">note</sup>';
for (const key of ['Delete', 'Backspace']) {
const f = fixture(`<p>before${note}after</p>`, 'p', 0);
const paragraph = f.body.querySelector('p');
f.editor.selection.setCursorLocation(key === 'Delete' ? paragraph.firstChild : paragraph.lastChild, key === 'Delete' ? 6 : 0);
assert.equal(f.press(key).prevented, true);
assert.equal(f.body.querySelectorAll('.jio-footnote-ref').length, 1);
assert.equal(f.press(key).prevented, true);
assert.equal(f.body.innerHTML, '<p>beforeafter</p>');
}
});
test('Delete inside adjacent text does not select the footnote', () => {
const f = fixture('<p>before<sup class="jio-footnote-ref" contenteditable="false">note</sup></p>', 'p', 0);
f.editor.selection.setCursorLocation(f.body.querySelector('p').firstChild, 3);
assert.equal(f.press('Delete').prevented, false);
assert.equal(f.body.querySelectorAll('.jio-footnote-ref').length, 1);
});
test('an inline formula uses the same two-step deletion and is noneditable on insertion', () => {
const html = markup.mathMarkup({ formula: 'x^2', display: false });
const f = fixture(`<p>before${html}after</p>`, 'p', 0);
const formula = f.body.querySelector('.jio-math');
assert.equal(formula.getAttribute('contenteditable'), 'false');
f.editor.selection.setCursorLocation(f.body.querySelector('p').lastChild, 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.jio-math').length, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>beforeafter</p>');
});
const hiddenBlock = '<div class="tinymce-hide" contenteditable="false"><p class="hide-before">[vip]</p><div contenteditable="true"><p>secret</p></div><p class="hide-after">[/vip]</p></div>';
test('new hidden blocks keep shortcodes outside the editable content', () => {
@@ -214,6 +313,8 @@ test('hydration restores the hidden block editing boundary', () => {
test('deleting the last structure creates an editable caret home without resetting content', () => {
const f = fixture('<pre>code</pre>', 'pre', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.firstElementChild.tagName, 'PRE');
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.children.length, 1);
assert.equal(f.body.firstElementChild.tagName, 'P');
assert.equal(f.editor.selection.getRng().startContainer, f.body.firstElementChild);
@@ -222,12 +323,15 @@ test('deleting the last structure creates an editable caret home without resetti
test('body-level caret can delete an adjacent divider', () => {
const f = fixture('<hr><p>after</p>', 'body', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('hr').length, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>after</p>');
});
test('body-level deletion of the only divider leaves a caret home', () => {
const f = fixture('<hr>', 'body', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.firstElementChild.tagName, 'P');
assert.equal(f.editor.selection.getRng().startContainer, f.body.firstElementChild);
});
@@ -235,6 +339,7 @@ test('body-level deletion of the only divider leaves a caret home', () => {
test('body-level deletion places the caret before the next structural block', () => {
const f = fixture('<hr><pre>code</pre>', 'body', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.firstElementChild.tagName, 'P');
assert.equal(f.body.lastElementChild.tagName, 'PRE');
assert.equal(f.editor.selection.getRng().startContainer, f.body.firstElementChild);
@@ -243,6 +348,7 @@ test('body-level deletion places the caret before the next structural block', ()
test('media-only paragraphs are removed with their media', () => {
const f = fixture('<p><img src="example.jpg"><br data-mce-bogus="1"></p><p>next</p>', 'p', 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
});