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 = () => {