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,一经查实,立即删除!

相关文章

【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 语言里面, 类的加载, 连接和初始化过程都是在程序…

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

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

大模型学习过程记录

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

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

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

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

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

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

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

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

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

小程序 van-field label和输入框改成上下布局

在组件上面加个样式就行&#xff1a;custom-style"display:block;" <van-field label"备注说明" type"textarea" clearable title-width"100px" custom-style"display:block;" placeholder"请输入" /> …

大载重无人机基础技术,研发一款50KG负重六旋翼无人机技术及成本分析

六旋翼无人机是一种多旋翼无人机&#xff0c;具有六个旋翼&#xff0c;通常呈“X”形布局。它采用电动串列式结构&#xff0c;具有垂直起降、悬停、前飞、后飞、侧飞、俯仰、翻滚等多种飞行动作的能力。六旋翼无人机通常被用于航拍、农业植保、环境监测、地形测绘等领域。 六旋…

Day34-Linux网络管理4

Day34-Linux网络管理4 1. IP地址分类与子网划分基础1.1 什么是IP地址1.2 十进制与二进制的转换1.3 IP地址的分类1.4 私网地址和局域网地址 2. 通信类型3. 子网划分讲解3.1 为什么要划分子网&#xff1f;3.2 什么是子网划分&#xff1f;3.3 子网划分的作用&#xff1f;3.4 子网划…

云计算项目十一:构建完整的日志分析平台

检查k8s集群环境&#xff0c;master主机操作&#xff0c;确定是ready 启动harbor [rootharbor ~]# cd /usr/local/harbor [rootharbor harbor]# /usr/local/bin/docker-compose up -d 检查head插件是否启动&#xff0c;如果没有&#xff0c;需要启动 [rootes-0001 ~]# system…

如何使用Hexo搭建个人博客

文章目录 如何使用Hexo搭建个人博客环境搭建连接 Github创建 Github Pages 仓库本地安装 Hexo 博客程序安装 HexoHexo 初始化和本地预览 部署 Hexo 到 GitHub Pages开始使用发布文章网站设置更换主题常用命令 插件安装解决成功上传github但是web不更新不想上传文章处理方式链接…

Yolov8模型用torch_pruning剪枝

目录 &#x1f680;&#x1f680;&#x1f680;订阅专栏&#xff0c;更新及时查看不迷路&#x1f680;&#x1f680;&#x1f680; 原理 遍历所有分组 高级剪枝器 &#x1f680;&#x1f680;&#x1f680;订阅专栏&#xff0c;更新及时查看不迷路&#x1f680;&#x1f680…

JVM基本概念、命令、参数、GC日志总结

原文: 赵侠客 一、前言 NPE&#xff08;NullPointerException&#xff09;和OOM&#xff08;OutofMemoryError&#xff09;在JAVA程序员中扮演着重要的角色&#xff0c;它也是很多人始终摆脱不掉的梦魇&#xff0c;与NPE不同的是OOM一旦在生产环境中出现就意味着只靠代码已经无…

java集合题库详解

1. Arraylist与LinkedList区别 可以从它们的底层数据结构、效率、开销进行阐述哈 ArrayList是数组的数据结构&#xff0c;LinkedList是链表的数据结构。 随机访问的时候&#xff0c;ArrayList的效率比较高&#xff0c;因为LinkedList要移动指针&#xff0c;而ArrayList是基于索…