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 });
    // 循环创建 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("形变锚点事件");
      });

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

 // 形变事件
 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 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; 文本不响应鼠标事件,不然有了文本后,拖拽也会有问题。

右键菜单

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

实现用户自定义右键

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

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

         

        防止多实例dom相互影响。

总结 

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

运动想象 (MI) 迁移学习系列 (3) : MSFT

运动想象迁移学习系列:MSFT 0. 引言1. 主要贡献2. 数据增强方法3. 基于度量的空间滤波转换器3.1 空间过滤3.2 脑电图ViT3.2.1 变压器编码器层3.2.2 基于度量的损失函数 4. 实验结果4.1 消融实验4.2 基线任务对比4.3 跨主题 5. 总结欢迎来稿 论文地址&#xff1a;https://www.s…

深入浅出计算机网络 day.1 概论② 因特网概述

当你回头看的时候&#xff0c;你会发现自己走了一段&#xff0c;自己都没想到的路 —— 24.3.9 内容概述 01.网络、互连&#xff08;联&#xff09;网与因特网的区别与联系 02.因特网简介 一、网络、互连&#xff08;联&#xff09;网与因特网的区别与联系 1.若干节点和链路互连…

Java 客户端向服务端上传文件(TCP通信)

一、实验内容 编写一个客户端向服务端上传文件的程序&#xff0c;要求使用TCP通信的的知识&#xff0c;完成将本地机器输入的路径下的文件上传到D盘中名称为upload的文件夹中。并把客户端的IP地址加上count标识作为上传后文件的文件名&#xff0c;即IP&#xff08;count&#…

excel统计分析——嵌套设计

参考资料&#xff1a;生物统计学&#xff0c;巢式嵌套设计的方差分析 嵌套设计&#xff08;nested design&#xff09;也称为系统分组设计或巢式设计&#xff0c;是把试验空间逐级向低层次划分的试验设计方法。与裂区设计相似&#xff0c;先按一级因素设计试验&#xff0c;然后…

Linux网络套接字之预备知识

(&#xff61;&#xff65;∀&#xff65;)&#xff89;&#xff9e;嗨&#xff01;你好这里是ky233的主页&#xff1a;这里是ky233的主页&#xff0c;欢迎光临~https://blog.csdn.net/ky233?typeblog 点个关注不迷路⌯▾⌯ 目录 一、预备知识 1.理解源IP地址和目的IP地址 …

真实案例分享:MOS管电源开关电路,遇到上电冲击电流超标

做硬件&#xff0c;堆经验。 分享一个案例&#xff1a;MOS管电源开关电路&#xff0c;遇到上电冲击电流超标&#xff0c;怎么解决的呢&#xff1f; 下面是正文部分。 —— 正文 —— 最近有一颗用了挺久的MOSFET发了停产通知&#xff0c;供应链部门找到我们研发部门&#xff0c…

RabbitMQ发布确认高级版

1.前言 在生产环境中由于一些不明原因&#xff0c;导致 RabbitMQ 重启&#xff0c;在 RabbitMQ 重启期间生产者消息投递失败&#xff0c; 导致消息丢失&#xff0c;需要手动处理和恢复。于是&#xff0c;我们开始思考&#xff0c;如何才能进行 RabbitMQ 的消息可靠投递呢&…

C++——string模拟实现

前言&#xff1a;上篇文章我们对string类及其常用的接口方法的使用进行了分享&#xff0c;这篇文章将着重进行对这些常用的接口方法的内部细节进行分享和模拟实现。 目录 一.基础框架 二.遍历字符串 1.[]运算符重载 2.迭代器 3.范围for 三.常用方法 1.增加 2.删除 3.调…

spring boot 2.4.x 之前版本(对应spring-cloud-openfeign 3.0.0之前版本)feign请求异常逻辑

目录 feign SynchronousMethodHandler 第一部分 第二部分 第三部分 spring-cloud-openfeign LoadBalancerFeignClient ribbon AbstractLoadBalancerAwareClient 在之前写的文章配置基础上 https://blog.csdn.net/zlpzlpzyd/article/details/136060312 因为从 spring …

Excel F4键的作用

目录 一. 单元格相对/绝对引用转换二. 重复上一步操作 一. 单元格相对/绝对引用转换 ⏹ 使用F4键 如下图所示&#xff0c;B1单元格引用了A1单元格的内容。此时是使用相对引用&#xff0c;可以按下键盘上的F4键进行相对引用和绝对引用的转换。 二. 重复上一步操作 ⏹添加或删除…

【Python】装饰器函数

专栏文章索引&#xff1a;Python 原文章&#xff1a;装饰器函数基础_装饰函数-CSDN博客 目录 1. 学习装饰器的基础 2.最简单的装饰器 3.闭包函数装饰器 4.装饰器将传入的函数中的值大写 5. 装饰器的好处 6. 多个装饰器的执行顺序 7. 装饰器传递参数 8. 结语 1. 学习装饰…

c++中string的模拟实现(超详细!!!)

1.string的成员变量、&#xff08;拷贝&#xff09;构造、析构函数 1.1.成员变量 private:char* _str;size_t _size; //string中有效字符个数size_t _capacity; //string中能存储有效字符个数的大小 1.2&#xff08;拷贝&#xff09;构造函数 //构造函数string(const char* …

Chain of Verification(验证链、CoVe)—理解与实现

原文地址&#xff1a;Chain of Verification (CoVe) — Understanding & Implementation 2023 年 10 月 9 日 GitHub 存储库 介绍 在处理大型语言模型&#xff08;LLM&#xff09;时&#xff0c;一个重大挑战&#xff0c;特别是在事实问答中&#xff0c;是幻觉问题。当答案…

React-路由导航

1.声明式路由导航 1.1概念 说明&#xff1a;声明式导航是指通过在模版中通过<Link/>组件描述出要跳转到哪里去&#xff0c;比如后台管理系统的左侧菜单通常使用这种方式进行。 import {Link} from "react-router-dom" const Login()>{return (<div>…

资源哟正版无授权模版源码(含搭建教程)

资源哟 v1.3 – 新增两种首页布局 – 新增幻灯片插件 – 优化深色模式颜色效果 – 优化导航页面左侧栏目跳转效果 – 优化后台辅助插件当前页面打开 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88898100 更多资源下载&#xff1a;关注我。

Linux多线程之线程控制

(&#xff61;&#xff65;∀&#xff65;)&#xff89;&#xff9e;嗨&#xff01;你好这里是ky233的主页&#xff1a;这里是ky233的主页&#xff0c;欢迎光临~https://blog.csdn.net/ky233?typeblog 点个关注不迷路⌯▾⌯ 目录 一、pthread_crate 二、pthread_join 三、p…

RAG、数据隐私、攻击方法和安全提示

原文地址&#xff1a;RAG, Data Privacy, Attack Methods & Safe-Prompts 最近的一项研究探讨了 RAG 安全漏洞以及通过检索数据集访问私有数据的方式。还讨论了防御和安全提示工程示例。 介绍 RAG 在构建生成式 AI 应用程序中非常受欢迎。RAG 在生成式 AI 应用中采用的原因…

Elasticsearch架构原理

一. Elasticsearch架构原理 1、Elasticsearch的节点类型 在Elasticsearch主要分成两类节点&#xff0c;一类是Master&#xff0c;一类是DataNode。 1.1 Master节点 在Elasticsearch启动时&#xff0c;会选举出来一个Master节点。当某个节点启动后&#xff0c;然后使用Zen D…

指针数组和数组指针(详细解释)

指针数组 指针数组的作用 指针数组和数组指针是C语言中常用的概念&#xff0c;它们分别有不同的作用和用法。 指针数组&#xff1a; 指针数组是一个数组&#xff0c;其中的每个元素都是指针类型。它可以用来存储多个指针&#xff0c;每个指针可以指向不同的数据类型或者相同…

Pytorch学习 day08(最大池化层、非线性激活层、正则化层、循环层、Transformer层、线性层、Dropout层)

最大池化层 最大池化&#xff0c;也叫上采样&#xff0c;是池化核在输入图像上不断移动&#xff0c;并取对应区域中的最大值&#xff0c;目的是&#xff1a;在保留输入特征的同时&#xff0c;减小输入数据量&#xff0c;加快训练。参数设置如下&#xff1a; kernel_size&#…