From 5c4db2fedced4831295b52405c343a82e12eef39 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Sat, 4 Jul 2026 11:18:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(lesson-preparation):=20Tiptap=20AnchorMark?= =?UTF-8?q?=20+=20AnchorPoint=20=E6=89=A9=E5=B1=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lesson-preparation/lib/anchor-mark.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 src/modules/lesson-preparation/lib/anchor-mark.ts diff --git a/src/modules/lesson-preparation/lib/anchor-mark.ts b/src/modules/lesson-preparation/lib/anchor-mark.ts new file mode 100644 index 0000000..691c72e --- /dev/null +++ b/src/modules/lesson-preparation/lib/anchor-mark.ts @@ -0,0 +1,133 @@ +import { Mark, mergeAttributes } from "@tiptap/core"; +import { Node } from "@tiptap/core"; + +/** + * V4 锚点系统:用 Tiptap Mark 内嵌锚点,替代 v3 的字符串偏移。 + * + * - AnchorMark(range 锚点):包裹选中文本,渲染为高亮 + 底部细线 + 行内标签 + * - AnchorPoint(point 锚点):独立 Node,渲染为黑色小圆圈带数字 + * + * 锚点数据存储在 Mark/Node 的 attributes 中,编辑时自动跟随文本移动。 + */ + +export interface AnchorMarkAttributes { + anchorId: string; + nodeId: string; + type: "range"; +} + +export const AnchorMark = Mark.create({ + name: "anchor", + + addAttributes() { + return { + anchorId: { + default: null, + parseHTML: (el) => el.getAttribute("data-anchor-id"), + renderHTML: (attrs) => ({ + "data-anchor-id": attrs.anchorId, + }), + }, + nodeId: { + default: null, + parseHTML: (el) => el.getAttribute("data-node-id"), + renderHTML: (attrs) => ({ + "data-node-id": attrs.nodeId, + }), + }, + type: { + default: "range", + parseHTML: (el) => el.getAttribute("data-anchor-type") ?? "range", + renderHTML: (attrs) => ({ + "data-anchor-type": attrs.type, + }), + }, + }; + }, + + parseHTML() { + return [ + { + tag: "span[data-anchor-id]", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "span", + mergeAttributes(HTMLAttributes, { + class: "lp-anchor-range", + }), + ]; + }, + + // 不自动添加任何快捷键 + addKeyboardShortcuts() { + return {}; + }, +}); + +/** + * AnchorPoint:点锚点,作为独立 Node 插入文本流。 + * 渲染为带数字的黑色小圆圈。 + */ +export const AnchorPoint = Node.create({ + name: "anchorPoint", + + group: "inline", + + inline: true, + + atom: true, + + addAttributes() { + return { + anchorId: { + default: null, + parseHTML: (el) => el.getAttribute("data-anchor-id"), + renderHTML: (attrs) => ({ + "data-anchor-id": attrs.anchorId, + }), + }, + nodeId: { + default: null, + parseHTML: (el) => el.getAttribute("data-node-id"), + renderHTML: (attrs) => ({ + "data-node-id": attrs.nodeId, + }), + }, + label: { + default: "•", + parseHTML: (el) => el.getAttribute("data-label") ?? "•", + renderHTML: (attrs) => ({ + "data-label": attrs.label, + }), + }, + }; + }, + + parseHTML() { + return [ + { + tag: "span[data-anchor-point]", + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "span", + mergeAttributes(HTMLAttributes, { + "data-anchor-point": "true", + class: "lp-anchor-point", + contenteditable: "false", + }), + HTMLAttributes.label ?? "•", + ]; + }, + + addKeyboardShortcuts() { + return {}; + }, +});