fix(creator-studio): delete selected hidden blocks reliably

This commit is contained in:
kate522 committed 2026-11-10 20:01:16 +08:00
1 parent 304c59c8f3
commit d391b0fd00
4 files changed
+124 -21

No files matched your search

@@ -20,6 +20,12 @@ const serializeCompiled = ts.transpileModule(serializeSource, {
}).outputText;
const serializer = {};
new Function('exports', serializeCompiled)(serializer);
const markupSource = fs.readFileSync(path.join(__dirname, '../src/utils/block-markup.ts'), 'utf8');
const markupCompiled = ts.transpileModule(markupSource, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const markup = {};
new Function('exports', 'require', markupCompiled)(markup, () => ({ __: (value) => value }));
function fixture(html, nodeSelector, offset = 0) {
const dom = new JSDOM(`<!doctype html><body contenteditable="true">${html}</body>`);
@@ -40,6 +46,10 @@ function fixture(html, nodeSelector, offset = 0) {
range.setStart(target, at);
range.collapse(true);
},
select: (target) => {
range = dom.window.document.createRange();
range.selectNode(target);
},
},
dom: { remove: (target) => target.remove() },
undoManager: { transact: (action) => { events.push('undo'); action(); } },
@@ -128,14 +138,76 @@ test('a real line break inside a paragraph is not a block boundary', () => {
assert.equal(f.body.querySelectorAll('hr').length, 1);
});
test('nested quote and hidden-content editing boundaries remove the whole outer block', () => {
for (const html of [
'<div class="quote_q" contenteditable="false"><div contenteditable="true"><p>quote</p></div></div>',
'<div class="tinymce-hide"><p class="hide-before">[vip]</p><div contenteditable="true"><p>secret</p></div><p class="hide-after">[/vip]</p></div>',
]) {
const f = fixture(`${html}<p>next</p>`, '[contenteditable="true"] p', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
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.innerHTML, '<p>next</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', () => {
const dom = new JSDOM('<!doctype html><body></body>');
dom.window.document.body.innerHTML = markup.hiddenContentMarkup('vip', '<p>secret</p>');
const block = dom.window.document.body.querySelector('.tinymce-hide');
assert.equal(block.getAttribute('contenteditable'), 'false');
assert.equal(block.querySelector(':scope > div').getAttribute('contenteditable'), 'true');
assert.equal(block.querySelector('.hide-before').textContent, '[vip]');
assert.equal(block.querySelector('.hide-after').textContent, '[/vip]');
});
test('Backspace after a hidden block selects it first, then removes it in one more press', () => {
const f = fixture(`${hiddenBlock}<p><br data-mce-bogus="1"></p>`, 'body > p', 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.tinymce-hide').length, 1);
assert.equal(f.editor.selection.getRng().startContainer, f.body);
assert.equal(f.editor.selection.getRng().endOffset, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.querySelectorAll('.tinymce-hide').length, 0);
assert.equal(f.body.firstElementChild.tagName, 'P');
assert.deepEqual(f.events, ['node', 'undo', 'node', 'change']);
});
test('editing hidden text stays native; Backspace at its start selects the whole block', () => {
const f = fixture(`${hiddenBlock}<p>next</p>`, '.tinymce-hide [contenteditable="true"] p', 0);
const text = f.body.querySelector('.tinymce-hide [contenteditable="true"] p').firstChild;
f.editor.selection.setCursorLocation(text, 3);
assert.equal(f.press('Backspace').prevented, false);
assert.equal(f.body.querySelector('.tinymce-hide [contenteditable="true"] p').textContent, 'secret');
f.editor.selection.setCursorLocation(text, 0);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.editor.selection.getRng().endOffset, 1);
assert.equal(f.press('Backspace').prevented, true);
assert.equal(f.body.innerHTML, '<p>next</p>');
});
test('a manually selected hidden block deletes without repeated native key presses', () => {
const f = fixture(hiddenBlock, 'body', 0);
f.editor.selection.select(f.body.firstElementChild);
assert.equal(f.press('Delete').prevented, true);
assert.equal(f.body.querySelector('.tinymce-hide'), null);
assert.equal(f.body.firstElementChild.tagName, 'P');
});
test('selecting only hidden-block text does not delete its container', () => {
const f = fixture(hiddenBlock, '.tinymce-hide [contenteditable="true"] p', 0);
const range = f.editor.selection.getRng();
range.selectNodeContents(f.body.querySelector('.tinymce-hide [contenteditable="true"] p'));
assert.equal(f.press('Backspace').prevented, false);
assert.equal(f.body.querySelectorAll('.tinymce-hide').length, 1);
});
test('hydration restores the hidden block editing boundary', () => {
const dom = new JSDOM('<!doctype html><body></body>');
const previousDocument = global.document;
global.document = dom.window.document;
try {
dom.window.document.body.innerHTML = serializer.hydrateBlocks(hiddenBlock.replace(' contenteditable="false"', '').replace(' contenteditable="true"', ''));
const block = dom.window.document.body.querySelector('.tinymce-hide');
assert.equal(block.getAttribute('contenteditable'), 'false');
assert.equal(block.querySelector(':scope > div').getAttribute('contenteditable'), 'true');
} finally {
global.document = previousDocument;
}
});