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

news2025/1/24 17:33:06

系列文章

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

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

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

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

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

协同编辑

        对协同这块已经写了很多篇文章了,如果还是不了解,可以看看之前的文章哈,我们还是使用Yjs实现协同的底层支持,Websocket 还是以插件的形式支持:

         这次的协同,并没有直接使用 y-websocket 插件支持,而是自己实现了websocket 相关的连接、异常、重连操作,y-websocket 插件无非就是内部对协同数据做了合并,监听消息后触发 update更新:

         我们手动实现,只需要对协同的数据进行底层的一致性冲突处理、合并就可以达到一样的目的,如下:

        在发送数据之前,需要先获取本地的所有yjs数据状态 state,携带着一起发送给 websocket 服务器,其他客户端收到后,先执行解析合并操作,然后再从最终结果解析数据,以达到数据一致性的目的。下列就是 yjs 的核心方法:

        发送数据之前,进行数据映射:

         此类,我们就可以不基于 y-websocket插件,自身实现websocket服务,也能使用yjs实现协同,保持数据一致性,关键就是使用 encodeStateAsUpdate 进行本地数据获取,applyUpdate 进行应用更新,详细解释:

Document Updates | Yjs DocsHow to sync documents with other peers.icon-default.png?t=N7T8https://docs.yjs.dev/api/document-updates#syncing-clients        效果如下:

搜索替换

        之前我们文本的实现方案是创建 contenteditable,然后移出时,创建了svg text,使得文本能显示在元件上,但是这样有些问题,不能进行搜索替换,因为svg的样式与css样式还不一致,因此在搜索结果的高亮显示上还有些难以实现。

        因此,我们替换方案为直接使用 contenteditable,移出时,控制样式 point-event:none;user-select:none即可,在搜索高亮中,替换字符串为 b 标签,并加上css 控制,即可实现。

        封装搜索替换组件,并绑定快捷键 Ctrl + F

// 可以用 getSelection 获取用户目前选中的文本
const { anchorOffset, focusOffset, baseNode } = window.getSelection() as Selection;

// 搜索的核心就是遍历目前页面上的文本,判定内容是否包含了搜索框文本
editorBox.querySelectorAll(".sf-editor-box-graphs-main-contenteditable").forEach((item) =>{
    // item 是 contenteditableBox 里面的 div 才是内容
    const editor = item.querySelector("div") as HTMLDivElement;
    editor.innerHTML = editor.innerHTML.replace(/<|>|\/|b|span/g, "");
    const findFlag = editor.innerText.includes(this.keyword);
    findFlag &&this.keyword &&this.conformList.push(item as HTMLDivElement);
});

         在 数量上,则是记录全局变量 index all,all是搜索匹配到的所有文本项,index 则是匹配到的当前索引,替换的方案就是直接 replace 即可,实现效果如下:

表格

        本来想用 luckysheet 实现表格的,但是想了想,还是太冗余了,流程图中的表格尽量简单就好了,主要做数据展示,不涉及复杂的计算,因此,还是用原生的table 实现吧。

  this.table = draw.createHTMLElement("table") as HTMLTableElement;
  this.table.classList.add("sf-editor-table");

  // 创建头部 head
  private createHead(draw: Draw) {
    const thead = draw.createHTMLElement("thead");
    const tr = draw.createHTMLElement("tr");
    for (let i = 0; i < this.col; i++) {
      const th = draw.createHTMLElement("th");
      const div = draw.createHTMLElement("div");
      div.innerText = `标题${i + 1}`;
      th.appendChild(div);
      tr.appendChild(th);
    }
    thead.appendChild(tr);
    this.table.appendChild(thead);
  }

  // 创建 tbody
  private createBody(draw: Draw) {
    const body = draw.createHTMLElement("tbody");
    for (let i = 0; i < this.row; i++) {
      const tr = draw.createHTMLElement("tr");
      for (let i = 0; i < this.col; i++) {
        const td = draw.createHTMLElement("td");
        const div = draw.createHTMLElement("div");
        td.appendChild(div);
        tr.appendChild(td);
      }
      body.appendChild(tr);
    }
    this.table.appendChild(body);
  }

        文本编辑上,使用 contenteditable 实现:

// 初始化 双击编辑事件
  private initEvent() {
    const divs = this.table.querySelectorAll("div");
    divs.forEach((item) => {
      item.addEventListener("dblclick", () => {
        item.setAttribute("contenteditable", "true");
        item.focus();
        this.setRange(item);
        item.addEventListener("blur", () =>
          divs.forEach((i) => i.removeAttribute("contenteditable"))
        );
      });
    });
  }

         效果与markdown的表格类似:

图片导出

        导出使用的是html2canva库,在一些细节的处理上,需要看官网的说明,比如处理跨域图片问题,宽高尺寸问题,还有的就是循环遍历导致截图过慢问题等,可以看出,每次使用插件导出图片,都会从 HTML head 开始遍历DOM结构,在我们的项目中影响不大,但是用户的环境,可能有很多的dom,肯定会影响效率,我们导出图片仅需要在 sf-editor-box 中做处理即可,因此,需要使用 ignoreElements 进行元素过滤。

        没有做过滤,整体的时间大概在435毫秒:

const option = {
      ignoreElements: (ele: HTMLElement) => {
        // this.editorBox compareDocumentPosition
        // 1: 没有关系,这两个节点不属于同一个文档
        // 2: 第一节点(P1)位于第二个节点后(P2)
        // 4: 第一节点(P1)定位在第二节点(P2)前
        // 8: 第一节点(P1)位于第二节点内(P2)
        // 16:第二节点(P2)位于第一节点内(P1)
        // 还可能是上诉值的和!返回 20 意味着在 p2 在 p1 内部(16),并且 p1 在 p2 之前(4)
        const box = this.draw.getEditorBox();
        const index = box.compareDocumentPosition(ele);
        if ([1, 2, 4].includes(index)) return false;
      },
    };

        优化后的平均耗时 250毫秒,如果在大体量DOM结构中,这个优化会更加明显。

  /**
   * 利用 html2canvas 截图
   *  1. ignoreElements 处理截图慢问题: (element) => false 与 root 进行位置比较
   *  2. x y width height 处理最佳宽高,不出现大量空白
   *  3. proxy、useCORS、allowTaint 处理跨域图片问题
   *  4. backgroundColor 支持透明、白色背景(设置null为透明)
   * @param filetype 保存的文件类型,支持 png svg jpg json
   */
  public async screenShot(filetype: string) {
    await nextTick();
    const box = this.draw.getEditorBox();
    // const width = box.clientWidth;
    // const height = box.clientHeight;

    this.draw.showLoading();
    // 处理x y height width - 相对于 editor box 的位置关系
    var minx = 0;
    var miny = 0;
    var maxx = 0;
    var maxy = 0;
    // 获取 editor box 的宽高
    const graphlist = this.draw.getGraphEvent().getAllGraphMain();
    if (graphlist.length) {
      const firstGraph = new Graph(
        this.draw,
        graphlist[0].getAttribute("graphid") as string
      );
      minx = firstGraph.getX();
      miny = firstGraph.getY();

      graphlist.forEach((item) => {
        // 需要得到最小和最大位置的graph
        const nodeID = item.getAttribute("graphid") as string;
        const graph = new Graph(this.draw, nodeID);
        minx = Math.min(minx, graph.getX());
        miny = Math.min(miny, graph.getY());
        maxx = Math.max(maxx, graph.getX() + graph.getWidth() + 20);
        maxy = Math.max(maxy, graph.getY() + graph.getHeight() + 20);
      });
    }

    const option = {
      x: minx,
      y: miny,
      width: maxx - minx,
      height: maxy - miny,
      ignoreElements: (ele: HTMLElement) => {
        // this.editorBox compareDocumentPosition
        // 1: 没有关系,这两个节点不属于同一个文档
        // 2: 第一节点(P1)位于第二个节点后(P2)
        // 4: 第一节点(P1)定位在第二节点(P2)前
        // 8: 第一节点(P1)位于第二节点内(P2)
        // 16:第二节点(P2)位于第一节点内(P1)
        // 还可能是上诉值的和!返回 20 意味着在 p2 在 p1 内部(16),并且 p1 在 p2 之前(4)
        const index = box.compareDocumentPosition(ele);
        if ([1, 2, 4].includes(index)) return false;
      },
    };

    // @ts-ignore
    const canvas = await html2canvas(this.draw.getEditorBox(), option);
    // base64 使用服务器存储方案  const base64 = canvas.toDataURL("image/png");

    canvas.toBlob((b: File) => {
      const url = toBlob(b, "image/png") as string;
      const a = this.draw.createHTMLElement("a");
      a.setAttribute("href", url);
      a.setAttribute("download", "测试");
      this.draw.hideLoading();
      window.open(url);
      // a.click(); // 触发下载
      a.remove();
    });
  }

总结

        至此,该实现的功能基本上都已经具备雏形了,后面就不再更新文章咯,但是还是会持续更新这个库,大家有什么想法,需要什么BUG,都可以在git、文章下留言,我会持续关注大家的意见,维护这个库。

        即将发布的 1.0.15 版本,是1.0版本的最后一版,后续的版本将更替为 1.1 ,主要实现协同、相关工具类、以及关键的 history历史记录。目前市面上也有很多成熟的产品,做这个主要不是为了超越他们,而是熟悉流程图的底层实现、TypeScript的应用、以及主要的提升自我能力,望大家理性看待~

        感谢大家的支持与理解!

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

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

相关文章

MySQL高级篇(存储引擎InnoDB、MyISAM、Memory)

目录 1、存储引擎简介 1.1、查询建表语句&#xff0c;默认存储引擎&#xff1a;InnoDB 1.2、查看当前数据库支持的存储引擎 1.3、创建表&#xff0c;并指定存储引擎 2、 存储引擎-InnoDB介绍 2.1、存储引擎特点 3、MyISAM存储引擎 4、Memory存储引擎 5、InnoDB、MyISAM、Memory…

AcWing刷题-公约数

公约数 代码 from math import gcd a, b map(int, input().split()) p int(input()) max_gcd gcd(a, b) res []for i in range(1, int(max_gcd**0.5)1):if max_gcd % i 0:res.append(i) res.append(max_gcd//i) res sorted(set(res))for _ in range(p):l, r map(int,…

【ArcGIS微课1000例】0108:ArcGIS计算归一化差值植被指数

本文讲解ArcGIS中,基于Landsat8数据的NDVI归一化差值植被指数计算。 文章目录 一、加载数据二、归一化植被指数NDVI1. NDVI介绍2. NDVI计算三、注意事项一、加载数据 加载配套数据0108.rar中的Landsat8的8个单波段数据,如下所示: Landsat8波段信息对照表如下表所示: 接下来…

8.排序(直接插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序)的模拟实现

1.排序的概念及其运用 1.1排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录…

怎么保证缓存与数据库的最终一致性?

目录 零.读数据的标准操作 一.Cache aside Patten--旁路模式 二.Read/Write Through Pattern--读写穿透 三.Write Back Pattern--写回 四.运用canal监听mysql的binlog实现缓存同步 零.读数据的标准操作 这里想说的是不管哪种模式读操作都是一样的&#xff0c;这是一种统一…

MongoDB初探:安装与图形化界面保姆级使用指南

文章目录 前言一、MongoDB下载安装下载解压配置环境变量打开mongoDB 二、配置本地MongoDB服务创建文件下载服务测试服务 三、图形化界面Compass GUINavicat GUI 总结 前言 MongoDB是一种流行的开源、面向文档的NoSQL数据库程序。与传统的关系型数据库不同&#xff0c;MongoDB将…

微信小程序备案指南及注意事项

如何备案小程序&#xff1f; 原文可参考&#xff1a; 微信小程序备案指南及注意事项 注意&#xff1a;备案需要提前准备好以下材料&#xff1b; 身份证正反面照片&#xff08;必须&#xff09;&#xff1b;营业执照照片&#xff08;非个人主体需要&#xff09;&#xff1b; 一…

非机构化解析【包含PDF、word、PPT】

此项目是针对PDF、docx、doc、PPT四种非结构化数据进行解析&#xff0c;识别里面的文本和图片。 代码结构 ├── Dockerfile ├── requirements ├── resluts ├── test_data │ ├── 20151202033304658.pdf │ ├── 2020_World_Energy_Data.pdf │ ├── …

AI大模型下的策略模式与模板方法模式对比解析

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL应用》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 &#x1f680; 转载自热榜文章&#xff1a;设计模式深度解析&#xff1a;AI大模型下…

【C++入门】缺省参数、函数重载与引用

&#x1f49e;&#x1f49e; 前言 hello hello~ &#xff0c;这里是大耳朵土土垚~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f4a5;个人主页&#x…

Severt和tomcat的使用(补充)

打包程序 在pom.xml中添加上述代码之后打包时会生成war包并且包的名称是test 默认情况打的是jar包.jar里量但是tomcat要求的是war包. war包Tomcat专属的压缩包. war里面不光有.class还有一些tomcat要求的配置文件(web.xml等)还有前端的一些代码(html, css, js) 点击其右边的m…

WPS二次开发系列:WPS SDK实现文档打印功能

在办公场景或者家教场景中经常碰到需要对文档进行打印的能力&#xff0c;今天给大家带来一篇如何通过WPS SDK调用WPS打印接口实现文档打印能力 最终实现效果图 实现步骤 2.1. 申请集成资料 通过向WPS官方发送邮件申请&#xff0c;审批通过后即可获取集成相关资料&#xff0c;详…

数据备份的演变:数字时代的一个关键方面

微信关注获取更多内容 数据备份至关重要&#xff0c;涵盖了其过去、现在和未来&#xff0c;是数字时代任何企业运营的一个重要方面。 如今&#xff0c;公司运营的几乎每个方面&#xff0c;从客户信息到内部财务数据&#xff0c;都以数字方式存储。 有鉴于此&#xff0c;数据…

视频压缩软件都有哪些?分享4款专业的视频软件!

在数字化时代&#xff0c;视频已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着视频质量的不断提升&#xff0c;其占用的存储空间也在迅速增长。为了解决这个问题&#xff0c;视频压缩软件应运而生。本文将为您介绍几款热门的视频压缩软件&#xff0c;帮助您选择最适…

后端nginx使用set_real_ip_from获取用户真实IP

随着nginx的迅速崛起&#xff0c;越来越多公司将apache更换成nginx. 同时也越来越多人使用nginx作为负载均衡, 并且代理前面可能还加上了CDN加速&#xff0c;但是随之也遇到一个问题&#xff1a;nginx如何获取用户的真实IP地址. 前言&#xff1a;Nginx ngx_http_realip_module…

JVM—对象的创建流程与内存分配

JVM—对象的创建流程与内存分配 创建流程 对象创建的流程图如下&#xff1a; 对象的内存分配方式 内存分配的方式有两种&#xff1a; 指针碰撞&#xff08;Bump the Pointer&#xff09;空闲列表&#xff08;Free List&#xff09; 分配方式说明收集器指针碰撞&#xff08…

flutter升级3.10.6Xcode构建报错

flutter sdk 升级Xcode报错收集&#xff0c;错误信息如下&#xff1a; Error (Xcode): Cycle inside Runner; building could produce unreliable results.没问题版本信息&#xff1a; Xcode&#xff1a;15.3 flutter sdk &#xff1a;3.7.12 dart sdk&#xff1a;2.19.6 …

hadoop分布式计算组件

什么是计算、分布式计算&#xff1f; 计算&#xff1a;对数据进行处理&#xff0c;使用统计分析等手段得到需要的结果 分布式计算&#xff1a;多台服务器协同工作&#xff0c;共同完成一个计算任务 分布式计算常见的2种工作模式 分散->汇总(MapReduce就是这种模式)中心调…

《价值》-张磊-高瓴资本-5-投资是一场修行;坚持第一性原理;避开陷阱;信誉就是生命

第五章&#xff1a;价值投资者的自我修养 价值投资不是投资者之间的零和游戏&#xff0c;而是共同把蛋糕做大的正和游戏。 从事投资的过程中&#xff0c;我渐渐发觉&#xff0c;投资 一方面是对真理的探寻&#xff0c;探索外部世界&#xff1b;另一方面是谋求心灵的宁静&#x…

Java: LinkedList的模拟实现

一、双向链表简介 上一篇文章我介绍了单向链表的实现&#xff0c;单向链表的特点是&#xff1a;可以根据上一个节点访问下一个节点&#xff01;但是&#xff0c;它有个缺点&#xff0c;无法通过下一个节点访问上一个节点&#xff01;这也是它称为单向链表的原因。 那么&#x…