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