feat(lesson-preparation): Tiptap AnchorMark + AnchorPoint 扩展
This commit is contained in:
133
src/modules/lesson-preparation/lib/anchor-mark.ts
Normal file
133
src/modules/lesson-preparation/lib/anchor-mark.ts
Normal file
@@ -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 {};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user