From ccf6c03096261a290712ab3f40cf2c0ea034c0db Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:24:04 +0800 Subject: [PATCH] fix(exams): preserve selected text when wrapIn fails in isolating nodes When wrapIn fails inside isolating nodes (e.g. composite question block), the previous fallback used insertContent which replaced the entire selection with an empty questionBlock, causing other sub-questions to disappear and content to be cleared. New approach: when wrapIn fails, extract the selected text, delete the selection, then insert a new node (questionBlock/groupBlock/sectionBlock) containing the selected text as paragraphs. This preserves the content and converts it into the desired structure. Applied to all three wrap operations: - insertQuestion (questionBlock) - insertGroup (groupBlock) - insertSection (sectionBlock) --- .../exams/editor/selection-toolbar.tsx | 100 ++++++++++++++---- 1 file changed, 80 insertions(+), 20 deletions(-) diff --git a/src/modules/exams/editor/selection-toolbar.tsx b/src/modules/exams/editor/selection-toolbar.tsx index 18cda46..746eb66 100644 --- a/src/modules/exams/editor/selection-toolbar.tsx +++ b/src/modules/exams/editor/selection-toolbar.tsx @@ -149,37 +149,97 @@ export function SelectionToolbar({ const hasTextSelection = !empty && from !== to const insertQuestion = (type: QuestionBlockType) => { - const chain = editor.chain().focus() + const score = type === "composite" ? 5 : 2 if (hasTextSelection) { - // 选中文本时:包裹为题目块 - // 注意:在 isolating 节点(如复合题)内,wrapIn 可能失败, - // 此时降级为插入空题目块 - const success = chain.wrapInQuestion({ type, score: type === "composite" ? 5 : 2 }).run() - if (!success) { - chain.insertQuestion({ type, score: type === "composite" ? 5 : 2 }).run() + // 先尝试 wrapIn(普通情况下可用) + const wrapped = editor.commands.wrapInQuestion({ type, score }) + if (!wrapped) { + // wrapIn 失败(通常在 isolating 节点如复合题块内): + // 获取选中文本,删除选区,插入包含选中文本的 questionBlock + // 这样不会清空内容,而是把选中文本转为新的子题 + const { from, to } = editor.state.selection + const selectedText = editor.state.doc.textBetween(from, to, "\n") + const lines = selectedText + .split("\n") + .filter((l) => l.trim().length > 0) + const content = lines.length > 0 + ? lines.map((line) => ({ + type: "paragraph", + content: [{ type: "text", text: line }], + })) + : [{ type: "paragraph", content: [{ type: "text", text: " " }] }] + + editor.chain() + .focus() + .deleteSelection() + .insertContent({ + type: "questionBlock", + attrs: { type, score, questionId: "" }, + content, + }) + .run() } } else { // 未选中文本时:插入空题目块 - chain.insertQuestion({ type, score: type === "composite" ? 5 : 2 }).run() + editor.chain().focus().insertQuestion({ type, score }).run() + } + } + + /** 通用降级包裹:wrapIn 失败时,把选中文本转为指定节点 */ + const wrapOrInsert = ( + wrapFn: () => boolean, + insertFn: () => void, + nodeType: "groupBlock" | "sectionBlock", + attrs: Record, + defaultTitle: string + ) => { + if (!hasTextSelection) { + insertFn() + return + } + const wrapped = wrapFn() + if (!wrapped) { + // wrapIn 失败:获取选中文本,删除选区,插入包含选中文本的节点 + const { from, to } = editor.state.selection + const selectedText = editor.state.doc.textBetween(from, to, "\n") + const lines = selectedText.split("\n").filter((l) => l.trim().length > 0) + const content = lines.length > 0 + ? lines.map((line) => ({ + type: "paragraph", + content: [{ type: "text", text: line }], + })) + : [{ type: "paragraph", content: [{ type: "text", text: " " }] }] + + editor.chain() + .focus() + .deleteSelection() + .insertContent({ + type: nodeType, + attrs: { ...attrs, title: defaultTitle }, + content, + }) + .run() } } const insertGroup = () => { - const chain = editor.chain().focus() - if (hasTextSelection) { - chain.wrapInGroup("一、选择题", "").run() - } else { - chain.insertGroup("一、选择题", "").run() - } + wrapOrInsert( + () => editor.commands.wrapInGroup("一、选择题", ""), + () => editor.chain().focus().insertGroup("一、选择题", "").run(), + "groupBlock", + { instruction: "" }, + "一、选择题" + ) } const insertSection = () => { - const chain = editor.chain().focus() - if (hasTextSelection) { - chain.wrapInSection("第Ⅰ卷 选择题", 1).run() - } else { - chain.insertSection("第Ⅰ卷 选择题", 1).run() - } + wrapOrInsert( + () => editor.commands.wrapInSection("第Ⅰ卷 选择题", 1), + () => editor.chain().focus().insertSection("第Ⅰ卷 选择题", 1).run(), + "sectionBlock", + { level: 1 }, + "第Ⅰ卷 选择题" + ) } const toggleDotted = () => {