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的应用、以及主要的提升自我能力,望大家理性看待~

感谢大家的支持与理解!

相关推荐
Mortal_hhh33 分钟前
VScode的C/C++点击转到定义,不是跳转定义而是跳转声明怎么办?(内附详细做法)
ide·vscode·stm32·编辑器
小阮的学习笔记5 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
电子云与长程纠缠10 小时前
UE5.3中通过编辑器工具创建大纲菜单文件夹
java·ue5·编辑器
lucky九年11 小时前
vscode翻译插件
ide·vscode·编辑器
真·Wild·攻城狮12 小时前
【码农日常】Vscode Clangd初始化失败(Win10)
ide·vscode·编辑器
七灵微12 小时前
【测试】【Debug】vscode中同一个测试用例出现重复
ide·vscode·编辑器
4U2471 天前
Linux入门之vim
linux·编辑器·vim·命令模式·底行模式
Liquor14191 天前
vim 编辑器
java·linux·c语言·开发语言·python·编辑器·vim
skywalk81631 天前
三周精通FastAPI:33 在编辑器中调试
python·编辑器·fastapi
宝子向前冲2 天前
纯前端生成PDF(jsPDF)并下载保存或上传到OSS
前端·pdf·html2canvas·oss·jspdf