134 lines
2.8 KiB
TypeScript
134 lines
2.8 KiB
TypeScript
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 {};
|
||
},
|
||
});
|