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

系列文章

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

说明

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

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

实现形变锚点

        形变锚点的添加思想与连接锚点类似,但是是通过动态创建实现(是在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 });// 循环创建 rectpoints.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-ignorerect.style.cursor = cursor;// 添加拖动事件rect.addEventListener("mousedown", () => {console.log("形变锚点事件");});

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

 // 形变事件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();};

临界值优化

 // 临界值处理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;

反方向拖动优化

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

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

实现旋转锚点

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

实现move移动

        移动的核心就是 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();}

实现文本

        使用div创建contenteditable的元素:

// 2. 当前位置创建 contentEditorabel divconst 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; 文本不响应鼠标事件,不然有了文本后,拖拽也会有问题。

右键菜单

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

  // 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) => {// 获取commandi.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();}

实现用户自定义右键

 // 自定义右键菜单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);});});

矫正右键菜单位置 

// 右键菜单唤起事件需要矫正位置private correctContextMenuPosition(div: HTMLDivElement, e: Event) {// 获取父元素的宽高 取 this.rootconst { 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-widthif (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. 创建yuanjianeditor.Rect(200, 200);const editor2 = new SFEditor(".flow-demo2");// 3. 执行动作editor2.command.executeAddGraph({type: "rect",width: 200,height: 200,});

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

         

        防止多实例dom相互影响。

总结 

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/525815.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面向对象——UML图

一、结构图 静态的 &#xff08;一&#xff09;类图 1、图 &#xff08;二&#xff09;对象图 1、如何区分对象和类&#xff1f; &#xff1a;对象 对象名&#xff1a;类 类 &#xff08;三&#xff09;包图*(带*号的是软考从来没考过的&#xff09; &#xff08;四&am…

pytorch 批量归一化BatchNorm的BatchNorm1d和BatchNorm2d理解

BatchNorm即批量归一化&#xff0c;是深度学习中经常用到的加速神经网络训练&#xff0c;加速收敛速度及稳定性的算法&#xff0c;是神经网络训练必不可少的一部分。 BatchNorm作用&#xff1a;在进行批量训练过程中&#xff0c;每个batch具有不同的分布&#xff0c;使数据分布…

光明网发布稿件多少钱?新闻投稿低价渠道推荐,附光明网价格明细表

想要在光明网发稿&#xff1f;不知道费用是多少&#xff1f;媒介多多告诉你答案&#xff01; 在当今数字化时代&#xff0c;媒体平台的重要性日益突出&#xff0c;而光明网作为国内知名的新闻门户网站&#xff0c;吸引了大量的目标受众。许多企业和个人都希望能够在光明网上投…

开源的Java报表库JasperReports介绍

JasperReports 是一个流行的开源 Java 报表库&#xff0c;它允许开发人员创建丰富的、基于 Java 的报表&#xff0c;这些报表可以与多种数据源交互&#xff0c;并且可以很容易地集成到 Java 应用程序中。JasperReports 提供了丰富的功能&#xff0c;包括数据可视化、图表、子报…

<C++>深度剖析菱形继承

​​ 文章目录 什么是菱形继承探究底层 什么是菱形继承 继承关系形如下图的继承即为菱形继承&#xff0c;或者叫钻石继承。 菱形继承的问题&#xff1a;公有继承前提下&#xff0c;如果类A中含有成员变量a&#xff0c;那么类B与类C中都有继承自类A的a&#xff0c;类D中又继承…

膜厚测量仪在半导体应用中及其重要

随着科技的不断发展&#xff0c;半导体行业已成为当今世界的核心产业之一。在这个领域中&#xff0c;半导体膜厚测量仪作为关键设备&#xff0c;其精度和可靠性对于产品质量和生产效率具有至关重要的作用。本文将详细介绍半导体膜厚测量仪的工作原理、应用领域以及其在半导体制…

【Unity】使用ScriptableObject存储数据

1.为什么要用ScriptableObject&#xff1f; 在游戏开发中&#xff0c;有大量的配置数据需要存储&#xff0c;这个时候就需要ScriptableObject来存储数据了。 很多人会说我可以用json、xml、txt&#xff0c;excel等等 但是你们有没有想过&#xff0c;假设你使用的是json&#x…

基于YOLOv8深度学习的葡萄病害智能诊断与防治系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

【蓝桥杯-单片机】基础模块LED和按键

文章目录 【蓝桥杯-单片机】Led、按键等基础模块01 前置准备&#xff08;1&#xff09;新建工程&#xff08;4&#xff09;编写程序 02 基础模块&#xff1a;LED&#xff08;0&#xff09;LED原理图&#xff08;1&#xff09;对P1整体赋值&#xff0c;控制所有的LED灯&#xff…

Unity Shader实现UI流光效果

效果&#xff1a; shader Shader "UI/Unlit/Flowlight" {Properties{[PerRendererData] _MainTex("Sprite Texture", 2D) "white" {}_Color("Tint", Color) (1, 1, 1, 1)[MaterialToggle] PixelSnap("Pixel snap", float…

【react框架】跟我一起速读Next.js官方入门教学课程文档

文章目录 前言目录结构样式方案正常引入样式文件Tailwind方案CSS Modules方案clsx方案 文字和图片优化文字图片 Pages和Layout的机制PagesLayout 通过Link组件改变路由并且拆分打包提供Hooks通过Vercel创建数据未完待续... 前言 对于那些对Next.js一无所知的前端伙伴来说&…

CNC机加工引入复合机器人可以提高生产效率,降低成本

CNC加工企业在过去依赖大量的人工来完成生产线上的各项任务&#xff0c;包括CNC机床的上下料、物料搬运以及部分装配工作。然而&#xff0c;随着产能需求的不断增长和人工成本的持续上升&#xff0c;企业逐渐意识到自动化升级的重要性与迫切性。 面临的挑战与需求&#xff1a; …