Svg Flow Editor 原生svg流程图编辑器(二)

Svg Flow Editor 原生svg流程图编辑器(二)本文介绍了 SvgFlowEdito 项目中 SVG 流程图编辑器的开发进度 包括元件操作 创建 移动 形变 commandAPI 事件监听 自定义右键菜单 多实例化以及文本创建等功能

大家好,欢迎来到IT知识分享网。

系列文章

Svg Flow Editor 原生svg流程图编辑器(一)

Svg Flow Editor 原生svg流程图编辑器(二)

Svg Flow Editor 原生svg流程图编辑器(三)

Svg Flow Editor 原生svg流程图编辑器(四)​​​​​​​

Svg Flow Editor 原生svg流程图编辑器(五)​​​​​​​

说明

        这项目也是我第一次写TS代码哈,现在还被绕在类型中头昏脑胀,更新可能会慢点,大家见谅~

        目前实现的功能:1. 元件的创建、移动、形变;2. command API;3. eventBus listener 事件监听;4. register 自定义右键菜单; 5. 多实例化; 6. 文本创建与跟随。

Svg Flow Editor 原生svg流程图编辑器(二)

实现形变锚点

        形变锚点的添加思想与连接锚点类似,但是是通过动态创建实现(是在commonEvent中处理哈,因为每一个创建的svg元组都需要实现该效果):

 // click 需要添加形变锚点 public click(e: Event, graph: IGraph) { const nodeID = graph.getID(); // 1. 先看是否目前选中的就是当前节点,是的话,直接返回,防止频繁点击元素 执行dom操作 const selectedID = this.getCurrentSelectedNodeID(); if (selectedID && selectedID === nodeID) return; // 2. 创建形变锚点 this.draw.createFormatAnchorPoint(e, graph); }
核心方法: const points = []; / * 顺序如下 * 1 2 3 * 8 4 * 7 6 5 */ points.push({ cursor: "nwse-resize", x, y }); points.push({ cursor: "ns-resize", x: x + width / 2, y: y }); points.push({ cursor: "nesw-resize", x: x + width, y: y }); points.push({ cursor: "ew-resize", x: x + width, y: y + height / 2 }); points.push({ cursor: "nwse-resize", x: x + width, y: y + height }); points.push({ cursor: "ns-resize", x: x + width / 2, y: y + height }); points.push({ cursor: "nesw-resize", x: x, y: y + height }); points.push({ cursor: "ew-resize", x: x, y: y + height / 2 }); // 循环创建 rect points.forEach(({ x, y, cursor }) => { const rect = document.createElementNS(xmlns, "rect"); rect.setAttribute("x", (x - 4).toString()); rect.setAttribute("y", (y - 4).toString()); rect.setAttribute("width", "8"); rect.setAttribute("height", "8"); rect.setAttribute("fill", "red"); // @ts-ignore rect.style.cursor = cursor; // 添加拖动事件 rect.addEventListener("mousedown", () => { console.log("形变锚点事件"); });

Svg Flow Editor 原生svg流程图编辑器(二)

         而形变事件则是通过创建的锚点事件实现:

 // 形变事件 rect.addEventListener("mousedown", () => this.handleFormatMousedown()); rect.addEventListener("mouseup", () => this.handleFormatMouseup());

元件太小拖动不流畅优化

        正常情况下,通过 mousedown、mousemove、mouseup 三个事件监听的移动拖拽事件,会导致元件太小失焦,从而不能实现流畅的拖拽,因此不适用该思路实现!!!

        实现思路:通过监听down 事件,使得根元素监听move事件,因为根元素的move是不会收到元件大小的影响,可以实现流畅拖动。

// 形变事件处理 private handleFormatMousedown(_e: Event, rect: Element, graph: IGraph) { const svg = this.getSvg(this.getGraph().getSvgXmlns()); const element = graph.getElement(); const nodeID = graph.getID(); const xmlns = graph.getXmlns(); const { offsetX, offsetY } = _e as MouseEvent; const startX = offsetX; // 初始位置 const startY = offsetY; // 初始位置 var width = 0; // 初始宽度 var height = 0; // 初始高度 // 记录初始位置(这恶鬼也要根据targetName动态获取) switch (element.tagName) { case "rect": width = Number(element.getAttribute("width")); height = Number(element.getAttribute("height")); break; case "circle": width = Number(element.getAttribute("r")) * 2; height = width; break; case "ellipse": width = Number(element.getAttribute("rx")) * 2; height = Number(element.getAttribute("ry")) * 2; break; default: break; } // @ts-ignore pointer-events: none; 在拖动过程中,使得 rect 不能响应事件,才能往回托 element.style["pointer-events"] = "none"; // 实现内部函数,才能获取参数 const handleMousedown = (e: Event) => { / * 同时这个的宽高变化还要根据是从哪一个边拖拽,进行不同的宽高变化 */ const { offsetX, offsetY } = e as MouseEvent; // 设置 element 的宽高 const diffX = offsetX - startX; const diffY = offsetY - startY; // @ts-ignore 获取变化方向 const cursor = rect.style.cursor; switch (cursor) { case "ns-resize": // 只进行上下高度调整 element.setAttribute("height", (height + diffY).toString()); break; case "ew-resize": // 只进行左右宽度调整 element.setAttribute("width", (width + diffX).toString()); break; default: // 其他四个方向宽高都调整 element.setAttribute("width", (width + diffX).toString()); element.setAttribute("height", (height + diffY).toString()); break; } // 更新所有锚点 this.updateFormatAnchorPoint(); this.updateLinkAnchorPoint(nodeID, element, xmlns); e.preventDefault(); e.stopPropagation(); };

Svg Flow Editor 原生svg流程图编辑器(二)

临界值优化

 // 临界值处理 if (resultX < MIN_WIDTH) width = MIN_WIDTH; if (resultX > MAX_WIDTH) width = MAX_WIDTH; if (resultY < MIN_HEIGHT) height = MIN_HEIGHT; if (resultY > MAX_HEIGHT) height = MAX_HEIGHT;

反方向拖动优化

        反向拖动的核心,就是处理定位坐标及宽高的关系

Svg Flow Editor 原生svg流程图编辑器(二)

        还有圆形椭圆的圆心坐标目前没有想到好的实现思路,如果大家有想法可以留言交流~

实现旋转锚点

         旋转这块还有些技术问题还没攻克哈,特别是旋转了之后的移动,点线的创建都是问题,大家有思路可以留言讨论。

实现move移动

Svg Flow Editor 原生svg流程图编辑器(二)

        移动的核心就是 mousedown 记录点击位置,在move中,起始点移动了多少位置,元件的中心页移动多少位置即可!特别注意,rect 的定位是左上角,circle的定位是圆心,因此,不能直接将move的坐标直接赋给元件。【包括元件的移动,太快也会导致失焦,也可以考虑使用根元素move方法实现

 核心方法:

// dowm 记录初始位置 public mousedown(e: MouseEvent, graph: IGraph) { const { offsetX, offsetY } = e; const { x, y } = this.getElementPosition(graph.getElement()); this.startX = offsetX; this.startY = offsetY; this.graphX = x; this.graphY = y; this.move = true; } // 移动更新位置 public mousemove(e: MouseEvent, graph: IGraph) { if (!this.move) return; // 这个是新的 offset,直接与旧的 offset 进行运算即可得到差值,与当前位置做计算即可 const { offsetX, offsetY } = e; // 计算差值 const diffX = offsetX - this.startX; const diffY = offsetY - this.startY; graph.position.call(graph, this.graphX + diffX, this.graphY + diffY); } // 弹起重置参数 public mouseup(e: Event, graph: IGraph) { this.resetDefault(); }

Svg Flow Editor 原生svg流程图编辑器(二)

实现文本

        使用div创建contenteditable的元素:

// 2. 当前位置创建 contentEditorabel div const element = graph.getElement(); // 获取当前宽度 高度 位置坐标 const width = graph.getWidth(); const height = graph.getHeight(); const x = graph.getX(); const y = graph.getY(); const left = element.tagName === "rect" ? x + "px" : x - width / 2 + "px"; const top = element.tagName === "rect" ? y + "px" : y - height / 2 + "px"; const div = this.draw.getHTMLElement("div"); div.classList.add("svg-flow-contenteditable"); div.style.width = width + "px"; div.style.height = height + "px"; div.style.left = left; div.style.top = top; // 内部创建div实现编辑,才能实现 const t = this.draw.getHTMLElement("div"); t.setAttribute("contenteditable", "true"); t.style.width = width + "px"; div.appendChild(t); // 添加到根元素 this.draw.addTo(this.draw.getRootElement(), div); // 自动获取焦点 t.focus();

        并且绑定失焦事件:

 // 失去焦点事件 t.addEventListener("blur", () => { // 获取用户输入 const div = document.querySelector( 'div[class="svg-flow-contenteditable"]' ) as HTMLDivElement; const text = div.innerText; // 将内容添加到 graph 元素上 // 清空内容 this.clearContenteditable(); }); // 添加enter事件 t.addEventListener("keydown", (e: KeyboardEvent) => { if (e.code !== "Enter") return; // 执行 enter 结束 t.blur(); });

 跟随移动:

 // 重新渲染文本位置 public updateTextPosition(graph: IGraph) { const element = graph.getElement(); const x = graph.getX(); const y = graph.getY(); // 获取文本节点 const textNode = element.parentNode?.parentNode?.querySelector("text"); textNode?.setAttribute("x", x.toString()); textNode?.setAttribute("y", (y + 5).toString()); }

          user-select: none;记得添加上这个属性哈,不然在移动过程中,会选中文字,导致拖动卡顿异常;pointer-events: none; 文本不响应鼠标事件,不然有了文本后,拖拽也会有问题。

Svg Flow Editor 原生svg流程图编辑器(二)

右键菜单

        在template 中定义好html结构,使用innerHTML添加到div 中,再将div添加到根元素上:

Svg Flow Editor 原生svg流程图编辑器(二)

 // svg 右键事件 public handleSvgContextmenu(e: Event) { const { offsetX, offsetY } = e as PointerEvent; // 先清空右键菜单 const menu = this.getContextmenu(); if (menu) { (menu as HTMLDivElement).style.left = offsetX + "px"; (menu as HTMLDivElement).style.top = offsetY + "px"; e.stopPropagation(); e.preventDefault(); return; } // 不存在则 创建svg右键菜单 const div = document.createElement("div"); div.classList.add("contextmenu-box"); div.style.left = offsetX + "px"; div.style.top = offsetY + "px"; div.innerHTML = contextmenu; // 添加事件!! div .querySelectorAll('div[class="svg-flow-contextmenu-item"]') .forEach((i) => { // 获取command i.addEventListener("click", () => this.handleContextmenu(i.getAttribute("command") as string) ); }); // 右键的右键不影响事件 div.addEventListener("contextmenu", (e) => { e.stopPropagation(); e.preventDefault(); }); setTimeout(() => this.root.appendChild(div)); e.stopPropagation(); e.preventDefault(); }

Svg Flow Editor 原生svg流程图编辑器(二)

实现用户自定义右键

 // 自定义右键菜单 SFEditor.register.contextMenuList = [ { title: "测试右键菜单", callback: () => { console.log("点击了自定义菜单"); }, }, ];
// 判断用户的自定义事件 nextTick(() => { const { contextMenuList } = this.register; if (!contextMenuList.length) return; // 将用户的自定义事件添加到 菜单中 contextMenuList.forEach(({ title, callback }) => { const d = document.createElement("div"); d.classList.add("svg-flow-contextmenu-item"); const spanIcon = document.createElement("span"); spanIcon.innerText = title as string; d.appendChild(spanIcon); d.addEventListener("click", (e: Event) => { callback && callback(e); }); div.querySelector(".svg-flow-contextmenu-svg")?.appendChild(d); }); });

Svg Flow Editor 原生svg流程图编辑器(二)

矫正右键菜单位置 

// 右键菜单唤起事件需要矫正位置 private correctContextMenuPosition(div: HTMLDivElement, e: Event) { // 获取父元素的宽高 取 this.root const { clientHeight, clientWidth } = this.root; // 获取自身的宽高 const width = div.clientWidth; const height = div.clientHeight; const { offsetX, offsetY } = e as PointerEvent; var left = offsetX; var top = offsetY; // 如果 offsetX + width 超过父元素的宽度,则令left = offsetX-width if (offsetX + width > clientWidth) left = offsetX - width; if (offsetY + height > clientHeight) top = offsetY - height; div.style.left = left + "px"; div.style.top = top + "px"; }

实现多实例化

        多实例的核心是创建新对象:

 // 1. 一定要基于创建的 构建的实例对象进行操作 const editor = new SFEditor(".flow-box"); Reflect.set(window, "editor", editor); // 这个是外部调用的关键 // 2. 创建yuanjian editor.Rect(200, 200); const editor2 = new SFEditor(".flow-demo2"); // 3. 执行动作 editor2.command.executeAddGraph({ type: "rect", width: 200, height: 200, });

Svg Flow Editor 原生svg流程图编辑器(二)

        在每次创建实例时,都会生成新的div根节点、svg根节点,并且要求在操作dom时,都需要加上限制,不允许直接使用 document.querySelector 应该限制在当前节点下进行dom操作:

Svg Flow Editor 原生svg流程图编辑器(二)         

        防止多实例dom相互影响。

Svg Flow Editor 原生svg流程图编辑器(二)

总结 

        目前已经可以进行元件的基本操作,实现通过API调用实现响应功能、并且支持事件监听、用户事件注册等;但是还是少了些东西。例如线条、旋转、辅助线等,本来想一起放在本章节写的,但是有些技术难点还是没有想到实现方式,就留着下一节吧。

        ts写起来确实要繁琐些,在项目构建之初,我将 svg 创建的元素都设置为 Element 类型,后来在设置属性、进行事件响应的时候总是有问题,后面又修改了属性类型为SVGSVGElement;项目初期,也没考虑多实例化,后面又改动了项目index的结构;同时,也为了实现项目事件监听回调,在多处进行事件埋点,整体的工作量也是挺大的,所以更新慢了些,大家见谅哈~

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/139958.html

(0)
上一篇 2025-05-31 19:20
下一篇 2025-05-31 19:26

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信