fix(creator-studio): render math/footnote shortcodes, inline code blocks, block deletion

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-06 23:52:39 +08:00
1 parent 7885f42cff
commit 6928127bfa
11 files changed
+235 -202

No files matched your search

+75 -52
View File
@@ -104,6 +104,76 @@ function initialDraft(): Draft {
};
}
/**
* Backspace / Delete 删除相邻的块级元素。
*
* 表格、代码块、引用、分割线这类内容不是普通文本,TinyMCE 默认不会在
* 空段落里按删除键时主动移除它们,导致「删不掉」。这里在光标处于空段落、
* 且紧邻目标块时直接移除该块。
*/
function removeAdjacentBlock(editor: any, event: any): void {
const key = Number(event.keyCode);
if (key !== 8 && key !== 46) {
return;
}
const selection = editor.selection;
if (!selection || !selection.isCollapsed()) {
return;
}
const body = editor.getBody();
const block = selection.getNode();
const isBlank = (node: any): boolean =>
!!node && node.nodeType === 1 && (node.textContent || '').trim() === '' &&
!editor.dom.getParent(node, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
const isBlockElement = (node: any): boolean =>
!!node && node.nodeType === 1 && !!(node.matches?.('table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder'));
// 光标可能落在空段落里,也可能落在块级元素的边界(TinyMCE 常见行为);
// 两种情况下都优先删除紧邻的块,而不是让按键悄悄失效。
const blankContainer = isBlank(block) ? block : null;
const direct = blankContainer
? (key === 8 ? blankContainer.previousElementSibling : blankContainer.nextElementSibling)
: null;
if (isBlockElement(direct)) {
event.preventDefault();
editor.dom.remove(direct);
editor.nodeChanged();
editor.undoManager.add();
return;
}
// 光标停在块首/块尾时,删除块本身。
const caretBlock = editor.dom.getParent(block, 'table,pre,hr,.quote_q,.jio-math--block,.jio-toc-placeholder', body);
if (!caretBlock) {
return;
}
const caretIsAtEdge = (start: boolean): boolean => {
const probe = editor.dom.createRng();
probe.selectNodeContents(caretBlock);
probe.collapse(start);
const caret = selection.getRng();
return caret && typeof caret.compareBoundaryPoints === 'function'
&& caret.compareBoundaryPoints(start ? 1 : 3, probe) === 0;
};
if (!((key === 8 && caretIsAtEdge(true)) || (key === 46 && caretIsAtEdge(false)))) {
return;
}
event.preventDefault();
const parent = caretBlock.parentElement;
editor.dom.remove(caretBlock);
// 删掉最后一个块后补一个空段落,否则没有可放置光标的位置。
if (parent && parent.children.length === 0) {
editor.setContent('<p></p>');
editor.selection.setCursorLocation(editor.getBody().firstChild, 0);
}
editor.nodeChanged();
editor.undoManager.add();
}
function escapeAttribute(value: string): string {
return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
@@ -145,8 +215,6 @@ function Studio(): JSX.Element {
}));
const [drawerOpen, setDrawerOpen] = useState(false);
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
const [codeDialog, setCodeDialog] = useState<null | { language: string; code: string }>(null);
const codeTargetRef = useRef<any>(null);
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
const dirtyRef = useRef(false);
@@ -416,23 +484,6 @@ function Studio(): JSX.Element {
});
editor.on('NodeChange', () => updateFormatValue(editor));
// 代码块右上角的语言标签:点击整块即可换语言,等价于公众号的「高亮」入口。
editor.on('click', (event: any) => {
const pre = editor.dom.getParent(event.target, 'pre.enlighter-pre');
if (!pre) {
return;
}
// 只响应右上角语言标签那一片,光标点进代码正文时保持正常编辑。
const rect = pre.getBoundingClientRect();
const relativeY = Number(event.clientY) - rect.top;
const relativeX = Number(event.clientX) - rect.left;
if (relativeY > 40 || relativeX < rect.width - 120) {
return;
}
event.preventDefault();
const language = String(pre.getAttribute('data-enlighter-language') || 'generic');
codeTargetRef.current = pre;
setCodeDialog({ language, code: String((pre.textContent || '').trim()) });
});
editor.on('input change Undo Redo AddUndo', () => {
updateContent(editor.getContent());
updateHistoryAvailability(editor);
@@ -441,7 +492,9 @@ function Studio(): JSX.Element {
if ((event.metaKey || event.ctrlKey) && event.keyCode === 83) {
event.preventDefault();
saveActionRef.current();
return;
}
removeAdjacentBlock(editor, event);
});
},
},
@@ -561,7 +614,8 @@ function Studio(): JSX.Element {
return;
}
if (kind === 'code') {
setCodeDialog({ language: 'generic', code: '' });
// 直接插入;语言可稍后点代码块右上角的角标更换。
insertHtml(codeBlockMarkup('generic'));
return;
}
if (kind === 'list') {
@@ -599,10 +653,8 @@ function Studio(): JSX.Element {
insertHtml(tableMarkup(result));
} else if (result.kind === 'math') {
insertHtml(mathMarkup(result));
} else if (result.kind === 'footnote') {
insertHtml(footnoteMarkup(result.note));
} else {
insertHtml(codeBlockMarkup(result.language, result.code));
insertHtml(footnoteMarkup(result.note));
}
}, [insertHtml]);
@@ -759,35 +811,6 @@ function Studio(): JSX.Element {
/>
)}
{codeDialog && (
<BlockDialog
kind="code"
initialLanguage={codeDialog.language}
initialCode={codeDialog.code}
onSubmit={(result) => {
setCodeDialog(null);
if (result.kind !== 'code') {
return;
}
const previous = codeTargetRef.current;
codeTargetRef.current = null;
insertHtml(codeBlockMarkup(result.language, result.code));
if (previous) {
// 换语言时移除旧块,避免留下两份代码。
const editor = editorRef.current;
if (editor && !editor.removed) {
editor.dom.remove(previous);
updateContent(editor.getContent());
}
}
}}
onClose={() => {
setCodeDialog(null);
codeTargetRef.current = null;
}}
/>
)}
{drawerOpen && (
<PublishDrawer
draft={draft}