关于wangEdit如何添加标注
项目需要使用富文本编辑器实现标注效果,查询资料之后,没找到相关资料,然后就让ai实现,也是碰了很多次壁之后才简单实现标注,鼠标移入展示title而已,
配置如下:

效果如下:

具体代码如下:
创建index.ts
import { Boot } from '@wangeditor/editor'
import annotationModule from "./annotation-module"
let registered = false
export function registerWangEditorPlugins() {
if (registered) return
Boot.registerModule(annotationModule);
registered = true;
}
然后创建annotation-module.ts 具体代码如下:
import { Boot, DomEditor, IDomEditor, IModalMenu, IButtonMenu, IModuleConf, SlateDescendant, SlateElement } from "@wangeditor/editor";
import { h, type VNode } from "snabbdom";
import { Editor as SlateEditor, Element as SlateSlateElement, Transforms } from "slate";
export const ANNOTATION_TYPE = "annotation";
type AnnotationElement = SlateElement & {
type: typeof ANNOTATION_TYPE;
value?: string; // 标注说明
children: SlateDescendant[];
};
function escapeAttr(s: string) {
return (s || "").replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
}
/** inline */
function withAnnotation<T extends IDomEditor>(editor: T): T {
const { isInline } = editor;
editor.isInline = (elem: any) => {
const type = DomEditor.getNodeType(elem);
if (type === ANNOTATION_TYPE) return true;
return isInline(elem);
};
return editor;
}
/** render in editor */
function renderAnnotation(elem: any, children: VNode[] | null) {
const note = elem.value || "";
return h(
"span",
{
props: { title: note },
attrs: { "data-note": note },
class: { "w-e-annotation": true },
style: {
backgroundColor: "rgba(255,231,150,0.65)",
borderBottom: "1px dashed rgba(200,140,0,0.9)",
padding: "0 2px",
borderRadius: "2px",
cursor: "help",
},
},
children ?? [],
);
}
const renderElemConf = { type: ANNOTATION_TYPE, renderElem: renderAnnotation };
/** to html */
function annotationToHtml(elem: any, childrenHtml: string) {
const note = escapeAttr(elem.value || "");
return `<span data-w-e-type="${ANNOTATION_TYPE}" data-w-e-is-inline data-value="${note}" title="${note}" class="w-e-annotation">${childrenHtml}</span>`;
}
const elemToHtmlConf = { type: ANNOTATION_TYPE, elemToHtml: annotationToHtml };
/** parse html */
const parseElemHtmlConf = {
selector: `span[data-w-e-type="${ANNOTATION_TYPE}"]`,
parseElemHtml: (domElem: Element, children: SlateDescendant[]) => {
const note = domElem.getAttribute("data-value") || "";
return {
type: ANNOTATION_TYPE,
value: note,
children: children?.length ? children : ([{ text: "" }] as any),
} as AnnotationElement;
},
};
function getSelectedAnnotationEntry(editor: IDomEditor) {
const [entry] = SlateEditor.nodes(editor as any, {
match: (n) => !SlateEditor.isEditor(n) && SlateSlateElement.isElement(n) && (n as any).type === ANNOTATION_TYPE,
});
return entry as [AnnotationElement, number[]] | undefined;
}
/** 菜单:标注(像插入链接:选中文字=显示文本,填写“标注说明”) */
class AnnotateMenu implements IModalMenu {
readonly key: string = 'annotate'
readonly title: string = '标注'
readonly tag = "button";
readonly showModal = true;
readonly modalWidth = 420;
isActive(editor: IDomEditor) {
return !!getSelectedAnnotationEntry(editor);
}
getValue(editor: IDomEditor) {
return getSelectedAnnotationEntry(editor)?.[0]?.value || "";
}
isDisabled() {
return false;
}
exec() {}
getModalPositionNode() {
return null;
}
getModalContentElem(editor: IDomEditor): HTMLElement {
// 在打开 modal 的瞬间先拿到选中文本(更像“链接显示文本”)
let selectedTextAtOpen = (editor.getSelectionText && editor.getSelectionText()) || "";
const entry = getSelectedAnnotationEntry(editor);
const oldNote = entry?.[0]?.value || "";
// 修复:编辑已有标注时,从标注节点的children中提取文本内容
if (entry) {
const [annotationNode] = entry;
// 从标注节点的children中提取文本
selectedTextAtOpen = annotationNode.children
.map(child => (child as any).text || "")
.join("");
}
const hasSelection = !!selectedTextAtOpen && selectedTextAtOpen.trim().length > 0;
const wrap = document.createElement("div");
wrap.style.padding = "8px";
const info = document.createElement("div");
info.style.fontSize = "12px";
info.style.marginBottom = "8px";
info.innerHTML = hasSelection ? `标注文字:<b>${selectedTextAtOpen}</b>` : `请先选中文本再添加标注`;
wrap.appendChild(info);
const row = document.createElement("div");
row.style.marginBottom = "8px";
row.innerHTML = `<div style="font-size:12px;margin-bottom:4px;">标注说明</div>`;
const noteArea = document.createElement("textarea");
noteArea.style.width = "100%";
noteArea.style.height = "80px";
noteArea.style.boxSizing = "border-box";
noteArea.style.resize = "none";
noteArea.placeholder = "例如:解释/翻译/备注...";
noteArea.value = oldNote;
row.appendChild(noteArea);
wrap.appendChild(row);
const btnRow = document.createElement("div");
btnRow.style.display = "flex";
btnRow.style.justifyContent = "flex-end";
btnRow.style.gap = "8px";
const cancelBtn = document.createElement("button");
cancelBtn.textContent = "取消";
cancelBtn.onclick = () => editor.focus();
const okBtn = document.createElement("button");
okBtn.textContent = entry ? "保存" : "确定";
okBtn.style.background = "#1677ff";
okBtn.style.color = "#fff";
okBtn.style.border = "none";
okBtn.style.padding = "6px 12px";
okBtn.style.borderRadius = "4px";
okBtn.style.cursor = "pointer";
okBtn.onclick = () => {
const note: any = (noteArea.value || "").trim();
if (!note) return alert("标注说明不能为空");
editor.focus();
// 关键:恢复打开 modal 前的选区,否则就会插到段首
editor.restoreSelection(); // 官方 selection API
// 如果此时仍然没有 selection,就直接拦住(避免插入到最前面)
if (!editor.selection) return alert("未获取到选区,请重新选中文本再试");
const selectedTextNow = (editor.getSelectionText && editor.getSelectionText()) || selectedTextAtOpen;
if (!selectedTextNow) return alert("请先选中文本再添加标注");
// 编辑已有标注:只改说明,不动文字
const cur = getSelectedAnnotationEntry(editor);
if (cur) {
const [, path] = cur;
Transforms.setNodes<AnnotationElement>(editor as any, { value: note }, { at: path });
editor.focus();
return;
}
// 用选中文字作为显示文本,替换原位置内容
editor.deleteFragment(); // 删除选中内容
const node: AnnotationElement = {
type: ANNOTATION_TYPE,
value: note,
children: [{ text: selectedTextNow } as any],
} as any;
editor.insertNode(node); // 插入到“当前选区”位置
// editor.insertText(' ') // 可选:插入空格让继续输入更自然
editor.hidePanelOrModal();
};
btnRow.appendChild(cancelBtn);
btnRow.appendChild(okBtn);
wrap.appendChild(btnRow);
setTimeout(() => noteArea.focus(), 0);
return wrap;
}
}
// hoverbar:编辑标注(复用同一个 modal)
class EditAnnotationMenu extends AnnotateMenu {
readonly key = "editAnnotation";
readonly title = "编辑标注";
}
// 删除标注:保留文字(去壳 unwrap)
class RemoveAnnotationMenu implements IButtonMenu {
readonly key = "removeAnnotation";
readonly title = "删除标注";
readonly tag = "button";
isActive() {
return false;
}
getValue() {
return "";
}
isDisabled(editor: IDomEditor) {
return !getSelectedAnnotationEntry(editor);
}
exec(editor: IDomEditor) {
editor.focus();
// 去壳保留文字
Transforms.unwrapNodes(editor as any, {
match: (n) => SlateSlateElement.isElement(n) && (n as any).type === ANNOTATION_TYPE,
split: true,
});
}
}
const annotationModule: Partial<IModuleConf> = {
menus: [
{ key: "annotate", factory: () => new AnnotateMenu() },
{ key: "editAnnotation", factory: () => new EditAnnotationMenu() },
{ key: "removeAnnotation", factory: () => new RemoveAnnotationMenu() },
],
editorPlugin: withAnnotation,
renderElems: [renderElemConf],
elemsToHtml: [elemToHtmlConf],
parseElemsHtml: [parseElemHtmlConf],
};
export default annotationModule;
最后就是在main.ts中引用使用了
import { registerWangEditorPlugins } from '@/plugins/index';
registerWangEditorPlugins();
然后就是在编辑器组件里面菜单中toolbarKeys添加 annotate ,选中文本点击标注添加

浙公网安备 33010602011771号