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

+38 -11
View File
@@ -59,11 +59,25 @@ function topLevelBlock(node: Node, body: HTMLElement): Element | null {
return current?.parentElement === body ? current : null;
}
function selectedTopLevelBlock(range: Range, body: HTMLElement): Element | null {
if (range.startContainer !== body || range.endContainer !== body || range.endOffset !== range.startOffset + 1) {
return null;
}
const node = body.childNodes[range.startOffset];
return node?.nodeType === 1 && isStructuralBlock(node as Element) ? node as Element : null;
}
function selectBlock(editor: any, event: any, block: Element): void {
event.preventDefault();
editor.selection.select(block);
editor.nodeChanged();
}
/**
* Backspace / Delete 删除相邻的块级元素。
*
* 光标位于正文块边界时删除相邻的结构块;位于结构块自身的首尾时删除
* 整个块。选区中的文字和块内非边界位置交给 TinyMCE 正常处理。
* 整个块。整块选区直接删除;文字选区和块内非边界位置交给 TinyMCE 处理。
*/
export function removeAdjacentBlock(editor: any, event: any): void {
const key = event.key || (event.keyCode === 8 ? 'Backspace' : event.keyCode === 46 ? 'Delete' : '');
@@ -72,7 +86,7 @@ export function removeAdjacentBlock(editor: any, event: any): void {
return;
}
const selection = editor.selection;
if (!selection || !selection.isCollapsed()) {
if (!selection) {
return;
}
const body = editor.getBody() as HTMLElement;
@@ -80,18 +94,27 @@ export function removeAdjacentBlock(editor: any, event: any): void {
if (!body || !range || !body.contains(range.startContainer)) {
return;
}
const collapsed = selection.isCollapsed();
const selected = collapsed ? null : selectedTopLevelBlock(range, body);
if (!collapsed && !selected) {
return;
}
const backward = key === 'Backspace';
const current = topLevelBlock(range.startContainer, body);
let target: Element | null = null;
const current = selected ? null : topLevelBlock(range.startContainer, body);
let target: Element | null = selected;
if (current) {
if (isBlankParagraph(current)) {
const adjacent = backward ? current.previousElementSibling : current.nextElementSibling;
if (adjacent && isStructuralBlock(adjacent) && isAtEdge(range, current, backward)) {
commitDeletion(editor, event, () => {
const offset = Array.prototype.indexOf.call(body.childNodes, current);
editor.dom.remove(current);
selection.setCursorLocation(body, offset);
});
if (adjacent.matches('.tinymce-hide')) {
selectBlock(editor, event, adjacent);
} else {
commitDeletion(editor, event, () => {
const offset = Array.prototype.indexOf.call(body.childNodes, current);
editor.dom.remove(current);
selection.setCursorLocation(body, offset);
});
}
}
return;
}
@@ -110,7 +133,7 @@ export function removeAdjacentBlock(editor: any, event: any): void {
}
target = isStructuralBlock(current)
? current : backward ? current.previousElementSibling : current.nextElementSibling;
} else if (range.startContainer === body) {
} else if (!selected && range.startContainer === body) {
let offset = range.startOffset + (backward ? -1 : 0);
const step = backward ? -1 : 1;
while (body.childNodes[offset]?.nodeType === 3 && !body.childNodes[offset].textContent?.trim()) {
@@ -121,8 +144,12 @@ export function removeAdjacentBlock(editor: any, event: any): void {
if (!target || !isStructuralBlock(target)) {
return;
}
if (!selected && target.matches('.tinymce-hide')) {
selectBlock(editor, event, target);
return;
}
commitDeletion(editor, event, () => {
if (target === current) {
if (target === current || selected) {
const previous = target.previousElementSibling;
const next = target.nextElementSibling;
const destination = backward
@@ -136,12 +136,12 @@ export const HIDE_BLOCKS: Record<HideKind, { open: string; close: string; label:
*
* .hide-before 里的短代码文本通过 CSS 的 font-size:0 隐藏,只显示 data-jio-hide-label;
* .hide-after 与前台投稿编辑器一致地保持 display:none。两者都留在 DOM 里,保存时
* 不需要额外的序列化转换。
* 不需要额外的序列化转换。外层不可编辑,只有正文子容器可编辑。
*/
export function hiddenContentMarkup(kind: HideKind, innerHtml: string): string {
const def = HIDE_BLOCKS[kind];
return (
`<div class="tinymce-hide" data-jio-hide-type="${kind}">` +
`<div class="tinymce-hide" data-jio-hide-type="${kind}" contenteditable="false">` +
`<p class="hide-before" data-jio-hide-label="${escapeHtml(def.label)}">${escapeHtml(def.open)}</p>` +
`<div contenteditable="true">${normalizeInner(innerHtml)}</div>` +
`<p class="hide-after">${escapeHtml(def.close)}</p>` +
@@ -154,6 +154,10 @@ export function hydrateBlocks(html: string): string {
container.innerHTML = html;
container.querySelectorAll('pre').forEach((pre) => hydrateCodeBlock(pre));
container.querySelectorAll<HTMLElement>('.tinymce-hide').forEach((block) => {
block.setAttribute('contenteditable', 'false');
block.querySelector(':scope > div')?.setAttribute('contenteditable', 'true');
});
container.querySelectorAll('p').forEach((paragraph) => {
const match = paragraph.textContent?.trim().match(/^\[jio_math display="1"\]([\s\S]*?)\[\/jio_math\]$/);
if (!match) {