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

系列文章

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.https://docs.yjs.dev/api/document-updates#syncing-clients 效果如下:

搜索替换

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

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

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

javascript 复制代码
// 可以用 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 实现吧。

javascript 复制代码
  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 实现:

javascript 复制代码
// 初始化 双击编辑事件
  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毫秒:

javascript 复制代码
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结构中,这个优化会更加明显。

javascript 复制代码
  /**
   * 利用 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的应用、以及主要的提升自我能力,望大家理性看待~

感谢大家的支持与理解!

相关推荐
nbsaas-boot32 分钟前
如何利用ChatGPT加速开发与学习:以BPMN编辑器为例
学习·chatgpt·编辑器
一棵开花的树,枝芽无限靠近你2 小时前
【PPTist】添加PPT模版
前端·学习·编辑器·html
热爱生活的五柒2 小时前
vscode利用ofExtensions插件可以调试单进程Openfoam,但是不能调试mpi多进程案例
ide·vscode·编辑器
为什么每天的风都这么大12 小时前
Vscode/Code-server无网环境安装通义灵码
ide·vscode·阿里云·编辑器·ai编程·code-server
陌上阳光21 小时前
vscode连接远程开发机报错
ide·vscode·编辑器
羊子雄起1 天前
CKEditor前端样式和编辑器的样式不一致的问题
前端·编辑器
界面开发小八哥1 天前
「Java EE开发指南」如何使用Visual JSF编辑器设计JSP?(一)
java·ide·java-ee·编辑器·myeclipse
Bio Coder1 天前
vim 一次注释多行 的几种方法
linux·编辑器·vim·注释·快捷键·方法·取消注释
luckilyil2 天前
前端—Cursor编辑器
前端·编辑器
一棵开花的树,枝芽无限靠近你2 天前
【PPTist】开源PPT编辑器初体验
编辑器·powerpoint