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.mzph.cn/news/733528.shtml

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

相关文章

完全背包问题(一般写法与空间优化写法)

题目 有 N 种物品和一个容量是 V 的背包&#xff0c;每种物品都有无限件可用。 第 i 种物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整…

最多几个直角三角形python

最多几个直角三角形 问题描述思路代码实现 问题描述 最多可以组成几个直角三角形&#xff0c;一个边只能用一次。 输入描述&#xff1a; 第一行输入一个正整数T&#xff08;1<&#xff1d;T<&#xff1d;100&#xff09;&#xff0c;表示有T组测试数据. 对于每组测试数据…

贪心算法: 奶牛做题

5289. 奶牛做题 - AcWing题库 贝茜正在参加一场奶牛智力竞赛。 赛事方给每位选手发放 n 张试卷。 每张试卷包含 k 道题目&#xff0c;编号 1∼k。 已知&#xff0c;不同卷子上的相同编号题目的难度相同&#xff0c;解题时间也相同。 其中&#xff0c;解决第 i 道题&#xff08;…

【C语言】字符指针

在指针的类型中我们知道有一种指针类型为字符指针char* 一般使用&#xff1a; int main() { char ch w; char *pc &ch; *pc w; return 0; } 还有一种使用方式&#xff0c;如下&#xff1a; int main() { const char* pstr "hello bit.";//这⾥是把⼀个字…

plantUML使用指南之序列图

文章目录 前言一、序列图1.1 语法规则1.1.1 参与者1.1.2 生命线1.1.3 消息1.1.4 自动编号1.1.5 注释1.1.6 其它1.1.7 例子 1.2 如何画好 参考 前言 在软件开发、系统设计和架构文档编写过程中&#xff0c;图形化建模工具扮演着重要的角色。而 PlantUML 作为一种强大且简洁的开…

【stm32 外部中断】

中断&#xff1a;在主程序运行过程中&#xff0c;出现了特定的中断触发条件&#xff08;中断源&#xff09;&#xff0c;使得CPU暂停当前正在运行的程序&#xff0c;转而去处理中断程序&#xff0c;处理完成后又返回原来被暂停的位置继续运行 中断优先级&#xff1a;当有多个中…

LoadBalancer (本地负载均衡)

1.loadbalancer本地负载均衡客户端 VS Nginx服务端负载均衡区别 Nginx是服务器负载均衡&#xff0c;客户端所有请求都会交给nginx&#xff0c;然后由nginx实现转发请求&#xff0c;即负载均衡是由服务端实现的。 loadbalancer本地负载均衡&#xff0c;在调用微服务接口时候&a…

考研复习C语言初阶(4)+标记和BFS展开的扫雷游戏

目录 1. 一维数组的创建和初始化。 1.1 数组的创建 1.2 数组的初始化 1.3 一维数组的使用 1.4 一维数组在内存中的存储 2. 二维数组的创建和初始化 2.1 二维数组的创建 2.2 二维数组的初始化 2.3 二维数组的使用 2.4 二维数组在内存中的存储 3. 数组越界 4. 冒泡…

【Java JVM】Class 文件的加载

Java 虚拟机把描述类的数据从 Class 文件加载到内存, 并对数据进行校验, 转换解析和初始化, 最终形成可以被虚拟机直接使用的 Java 类型, 这个过程被称作虚拟机的类加载机制。 与那些在编译时需要进行连接的语言不同, 在 Java 语言里面, 类的加载, 连接和初始化过程都是在程序…

Apache HBase

一、HBase简介 1、HBase定义 Apache HBase™是以hdfs为数据存储的&#xff0c;一种分布式、可扩展的NoSQL数据库。 HBase官网 Welcome to Apache HBase™ Apache HBase™ is the Hadoop database, a distributed, scalable, big data store.Use Apache HBase™ when you nee…

解决阿里云服务器开启frp服务端,内网服务器开启frp客户端却连接不上的问题

解决方法&#xff1a; 把阿里云自带的Alibabxxxxxxxlinux系统 换成centos 7系统&#xff01;&#xff01;&#xff01;&#xff01; 说一下我的过程和问题&#xff1a;由于我们内网的服务器在校外是不能连接的&#xff0c;因此我弄了个阿里云服务器做内网穿透&#xff0c;所谓…

【git】总结

一般提交流程&#xff1a; git add 或者直接加号暂存 git status 检查状态 git commit -m “提交信息” git pull git push origin HEAD:refs/for/分支 git pull 拉代码时有冲突&#xff1a; git add . 暂存所有 git stash 暂存 git pull 解决冲突 git stash apply 放出暂存…

大模型学习过程记录

一、基础知识 自然语言处理&#xff1a;能够让计算理解人类的语言。 检测计算机是否智能化的方法&#xff1a;图灵测试 自然语言处理相关基础点&#xff1a; 基础点1——词表示问题&#xff1a; 1、词表示&#xff1a;把自然语言中最基本的语言单位——词&#xff0c;将它转…

【C语言】C语言编程实战:Base64编解码算法从理论到实现(附完整代码)

&#x1f9d1; 作者简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向的学习指导…

你应该打好你的日志,起码避免被甩锅

大家好&#xff0c;我是蓝胖子,相信大家或多或少都有这样的经历&#xff0c;当你负责的功能出现线上问题时&#xff0c;领导第一时间便是找到你询问原因&#xff0c;然而有时问题的根因或许不在你这儿&#xff0c;只是这个功能或许依赖了第三方或者内部其他部门&#xff0c;这个…

PHP端口批量查询工具单文件

查询速度较慢&#xff0c;建议输入20个以内端口&#xff0c;暂未整理端口服务信息所以暂时不显示。 <?php error_reporting(0); //打开报错&#xff1a;去行首双斜杠 header(Content-type:text/html;charset utf-8); //如果乱码删行首双斜杠 header(Cache-Control: no-ca…

【Unity InputSystem】实用指南:在PC端(鼠标与键盘)、手机端(触摸屏)、主机手柄上同步实现角色移动与跳跃功能

前引 随着Unity的不断发展&#xff0c;开发者对于项目的输入系统要求也日益提高。在进行多平台适配和跨平台移植时&#xff0c;常常需要改变输入系统&#xff0c;这给开发者带来了不少困扰。而Unity官方推出的InputSystem插件&#xff0c;则是为了解决这一问题而推出的全新输入…

Linux内存管理--系列文章壹

一、引子 作者、我在上班闲着没事的时候&#xff0c;看了一些关于Linux内存管理和程序装载、链接的文章&#xff0c;然后自己就总结出了一些东西。 本系列文章一方面将资料中的长篇大论总结到最少、以方便可以直接找到答案&#xff0c;一方面也是方便面试的时候可以吹牛逼。 L…

【快捷键】Mac如何打出数学符号和希腊字母

下面是一些常见的希腊字符及其对应的快捷键&#xff1a; α (Alpha): Option Aβ (Beta): Option Bγ (Gamma): Option Gδ (Delta): Option Dε (Epsilon): Option Eζ (Zeta): Option Zη (Eta): Option Hθ (Theta): Option Qι (Iota): Option Iκ (Kappa): Opti…

【Docker】golang使用DockerFile正确食用指南

【Docker】golang使用DockerFile正确食用指南 大家好 我是寸铁&#x1f44a; 总结了一篇golang使用DockerFile正确食用指南✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 问题背景 今天寸铁想让编写好的go程序在docker上面跑&#xff0c;要想实现这样的效果&#xff0c;就需要用…