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

news2024/11/15 15:43:00

说明

        这项目也是我第一次写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.coloradmin.cn/o/1502564.html

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

相关文章

结构体和malloc学习笔记

结构体学习&#xff1a; 为什么会出现结构体&#xff1a; 为了表示一些复杂的数据&#xff0c;而普通的基本类型变量无法满足要求&#xff1b; 定义&#xff1a; 结构体是用户根据实际需要自己定义的符合数类型&#xff1b; 如何使用结构体&#xff1a; //定义结构体 struc…

2024年新算法||吸引-排斥优化算法(Attraction–Repulsion Algorithm)

本期介绍一种求解约束全局优化问题的元启发式搜索算法——吸引-排斥优化算法Attraction–Repulsion Optimization Algorithm,AROA。该算法模拟自然界中发生的吸引-排斥现象相关的平衡。该成果于2024年2月发表在中科院1区SCI期刊 Swarm and Evolutionary Computation&#xff08…

【鸿蒙 HarmonyOS 4.0】常用组件:List/Grid/Tabs

一、背景 列表页面&#xff1a;List组件和Grid组件&#xff1b; 页签切换&#xff1a;Tabs组件&#xff1b; 二、列表页面 在我们常用的手机应用中&#xff0c;经常会见到一些数据列表&#xff0c;如设置页面、通讯录、商品列表等。下图中两个页面都包含列表&#xff0c;“…

为什么没有做好ETL的BI项目最终都会失败?

随着数字化转型&#xff0c;企业越来越重视数据的价值和利用。商业智能&#xff08;Business Intelligence&#xff0c;BI&#xff09;作为一种数据分析和决策支持的重要工具&#xff0c;被广泛应用于各行各业。然而&#xff0c;对于BI项目的成功实施&#xff0c;ETL&#xff0…

Aop注解+Redis解决SpringBoot接口幂等性(源码自取)

目录 一、什么是幂等性&#xff1f; 二、哪些请求天生就是幂等的&#xff1f; 三、为什么需要幂等 1.超时重试 2.异步回调 3.消息队列 四、实现幂等的关键因素 关键因素1 关键因素2 五、引入幂等性后对系统的影响 六、Restful API 接口的幂等性 实战Aop注解redis解…

单例九品--第五品

单例九品--第五品 上一品引入写在前边代码部分1代码部分2实现方式评注与思考下一品的设计思考 上一品引入 第四品中可能会因为翻译单元的链接先后顺序&#xff0c;造成静态初始化灾难的问题。造成的原因是因为存在调用单例对象前没有完成定义的问题&#xff0c;这一品将着重解…

站长必备溯源教程-绕过CDN查找背后IP的方法手段

绕过CDN查询背后真实IP方法&#xff1a; 方法一 DNS历史解析记录 查询域名的历史解析记录&#xff0c;可能会找到网站使用CDN前的解析记录&#xff0c;从而获取真实IP 相关查询的网站有&#xff1a;iphistory、DNS查询、微步在线、域名查询、DNS历史查询、Netcraft 方法二 …

基于springboot的水果购物商城管理系统(程序+文档+数据库)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 一、研究背景…

如何从 iPhone 恢复永久删除的视频

您来到这里主要是因为您想知道如何从 iPhone 恢复已删除的视频。其实&#xff0c;如果视频是用你的iPhone拍摄的&#xff0c;你可以尝试在相册“最近删除”中找到它。删除后该信息将保留 40 天。如果您清空了相册或者无法从相册中找到已删除的视频&#xff0c;则必须尝试深度数…

第九篇 – 过程发现(Process Discovery)是如何赋能数字化市场营销全过程?- 我为什么要翻译介绍美国人工智能科技巨头IAB公司

IAB平台&#xff0c;使命和功能 IAB成立于1996年&#xff0c;总部位于纽约市。 作为美国的人工智能科技巨头社会媒体和营销专业平台公司&#xff0c;互动广告局&#xff08;IAB- the Interactive Advertising Bureau&#xff09;自1996年成立以来&#xff0c;先后为700多家媒体…

【xv6操作系统】Lab systems calls

一、实验前须知 阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件&#xff1a; 系统调用的用户空间代码在 user/user.h 和 user/usys.pl 中。 内核空间代码在 kernel/syscall.h 和 kernel/syscall.c 中。 与进程相关的代码在 kernel/proc.h 和 kernel/proc.c…

iOS-系统弹窗调用

代码&#xff1a; UIAlertController *alertViewController [UIAlertController alertControllerWithTitle:"请选择方式" message:nil preferredStyle:UIAlertControllerStyleActionSheet];// style 为 sheet UIAlertAction *cancle [UIAlertAction actionWithTit…

GitHub和Gitee的基本使用和在IDEA中的集成

文章目录 【1】GitHub1.创建仓库2.增加和修改文件3.创建分支4.删除仓库5.远程仓库下载到本地 【2】Gitee1.创建仓库2.远程仓库下载到本地. 【3】IDEA集成GitHub【4】IDEA集成Gitee1.在Gitee中修改&#xff0c;同步到本地2.从Gitee中下载项目 【1】GitHub 1.创建仓库 先登陆这…

基于Token的身份验证:安全与效率的结合

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

阿里云服务器“地域”是什么?怎么选择比较好?

阿里云服务器地域选择方法&#xff0c;如何选择速度更快、网络延迟更低的地域节点&#xff0c;地域指云服务器所在的地理位置区域&#xff0c;地域以城市划分&#xff0c;如北京、杭州、深圳及上海等&#xff0c;如何选择地域&#xff1f;建议根据用户所在地区就近选择地域&…

【Multisim】关于导入TI的SPICE模型发生的报错

关于如何在Multisim导入TI提供的SPICE模型&#xff0c;该篇博客描述的很清楚&#xff1a;在Multisim导入TI提供的SPICE模型_multisim如何导入元器件的仿真模型-CSDN博客 但是使用13或14版本的Multisim在操作过程中可能会遇到如下问题&#xff1a; The model contains multiple …

STM32的启动流程分析 和 一些底层控制的原理

阅读引言&#xff1a; 阅读本文之后&#xff0c; 你将对单片机&#xff0c; 甚至是嵌入式系统&#xff0c; 或者是传统的PC机系统的启动流程有一个大致的了解&#xff0c; 本文更加偏向于单片机的启动流程分析。 目录 一、基础知识 1.STM32系列的微控制器&#xff08;mcu&…

PDF控件Spire.PDF for .NET【安全】演示:使用时间戳服务器对 PDF 进行数字签名

Spire.PDF for .NET 是一款独立 PDF 控件&#xff0c;用于 .NET 程序中创建、编辑和操作 PDF 文档。使用 Spire.PDF 类库&#xff0c;开发人员可以新建一个 PDF 文档或者对现有的 PDF 文档进行处理&#xff0c;且无需安装 Adobe Acrobat。 E-iceblue 功能类库Spire 系列文档处…

阿里云服务器买哪个地区比较好?2024阿里云服务器地域怎么选择?

阿里云服务器地域选择方法&#xff0c;如何选择速度更快、网络延迟更低的地域节点&#xff0c;地域指云服务器所在的地理位置区域&#xff0c;地域以城市划分&#xff0c;如北京、杭州、深圳及上海等&#xff0c;如何选择地域&#xff1f;建议根据用户所在地区就近选择地域&…

物联网在智慧城市建设中的关键作用:连接、感知、智能响应

一、引言 随着信息技术的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;技术已经渗透到我们生活的方方面面&#xff0c;特别是在智慧城市建设中发挥着至关重要的作用。智慧城市是指通过运用先进的信息和通信技术&#xff0c;实现城市基础设施、公共服务、交通管理、环…