当前位置: 首页 > news >正文

关于wangEdit如何添加标注

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

image

 


效果如下:

image

具体代码如下:
创建index.ts

import { Boot } from '@wangeditor/editor'
import annotationModule from "./annotation-module"let registered = false
export function registerWangEditorPlugins() {if (registered) returnBoot.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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}/** 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 ,选中文本点击标注添加

image

 







http://www.jsqmd.com/news/439903/

相关文章:

  • 计算机毕业设计springboot基于Android的运动助手 基于SpringBoot与Android技术的智能健身管理平台设计与实现 采用移动端的企业员工运动健康追踪系统研发
  • 拼多多商品券后价API接口使用指南
  • 3月科技企业孵化器公司推荐,看看哪些做得好,科技企业孵化器/企业孵化服务/科技政策申报,科技企业孵化器企业口碑推荐榜 - 品牌推荐师
  • hot100 322.零钱兑换
  • 2026年全屋定制品牌推荐:智能家居趋势评测,涵盖日常与高端场景定制痛点 - 品牌推荐
  • 直接上代码先看效果!咱们先跑个Demo感受下哈里斯鹰优化LSSVM的威力。准备好你的Matlab,把这段代码扔进去运行
  • 2026年国内热门齿轮减速机厂家怎么选?这些要点务必要知道,硬齿面斜齿轮减速机/粮机用减速机,齿轮减速机供应厂家怎么选 - 品牌推荐师
  • 2026年冷水机厂家推荐:聚焦化工制药领域评价,解决定制化与稳定运行痛点 - 品牌推荐
  • 从零到一:现代Web扫雷游戏的全栈开发实践与深度解析
  • 照着用就行:10个降AI率软件降AIGC网站 本科生必看!降AI率测评与推荐
  • 2026 API 中转站怎么选:我更看重成本可控和迁移省事 - 147API
  • .NET SqlSugar多线程下SqlSugarClient 的线程安全陷阱
  • 2026体育场地服务商推荐榜:塑胶跑道厂家/塑胶跑道实力厂家/塑胶跑道施工/塑胶跑道源头厂家/塑胶跑道生产厂家/选择指南 - 优质品牌商家
  • 2026年冷水机厂家推荐:基于多行业应用评价,针对稳定性与能效痛点精准指南 - 品牌推荐
  • 好用还专业!降AIGC软件 千笔 VS 文途AI 专科生首选
  • 2026年上海离婚律师推荐:涉外与本地婚姻法律需求全面评价与排名分析 - 品牌推荐
  • 2026国内靠谱彩色乒乓球企业排行,口碑好的都在这,训练乒乓球/训练球乒乓球/三星乒乓球正品,乒乓球企业推荐榜单 - 品牌推荐师
  • 喷墨印刷流量测量优选:高精准超声波流量传感器品牌推荐 - 品牌2026
  • 2026年上海离婚律师推荐:涉外与财产分割场景评价,解决情绪疏导与证据痛点 - 品牌推荐
  • 音视频开源项目:Seal顶级Android视频与音频下载器(几乎是全平台视频解析开源工具)
  • 适配涂覆工艺流量测量,2026超声波流量传感器品牌推荐 - 品牌2026
  • 2026年工控主板厂家推荐:基于多行业应用实测评价,针对兼容性与耐用性痛点精准指南 - 品牌推荐
  • 2026年冷水机厂家推荐:基于多行业应用评价,针对稳定性与能效痛点精准指南。 - 品牌推荐
  • 工业自动化领域流量测量优选:超声波流量计品牌推荐 - 品牌2026
  • 如何选择上海离婚律师?2026年上海离婚律师推荐与排名,直击谈判效率与成本痛点 - 品牌推荐
  • 2026年广东抗HPV生物蛋白敷料品牌推荐:广东长帆科技“梦之树”系列,专注高危HPV阳性转阴与黏膜修护全周期方案 - 品牌推荐官
  • 2026年冷水机厂家推荐:基于多行业应用评价针对稳定性与能效痛点精准指南。 - 品牌推荐
  • 2026年冷水机厂家推荐:智能制造趋势评测,涵盖化工与食品加工场景核心痛点 - 品牌推荐
  • 从高精度时间基准刚需到稳健增长:全球原子钟2026-2032年CAGR5.3%,2032年达4.86亿美元
  • 如何选择上海离婚律师?2026年上海离婚律师推荐与排名,直击谈判与取证痛点 - 品牌推荐