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

效果展示

项目概述

        svg flow editor 是一款流程图编辑器,提供了一系列流程图交互、编辑所必需的功能,支持前端研发自定义开发各种逻辑编排场景,如流程图、ER 图、BPMN 流程等。

        目前也有比较好的流程图设计框架,但是还是难满足项目个性化定制,BMPN.js、Jsplumb 的拓展能力不足,自定义节点支持成本很高。

技术选型

        本项目使用typescript与svg、canvas等技术进行搭建,脱离vue、react等框架的限制,使得用户更快、更轻松融合到自己的项目中,在底层结合typescript,使得数据类型得到更加健壮、完整的支持,对图形元组使用 svg 技术进行绘制,使得用户操作、底层实现更加轻松,同时对其他模块(背景网格、水印)使用了canvas技术进行绘制。

功能规划

        本项目大体功能模块如下:

background 背景

        背景模块支持网格绘制、水印的绘制、水印定制化配置等

graph 

        graph 是系统交互的核心元素,支持Rect(矩形)、Circle(圆形)、Ellipse(椭圆)、Polygon(多边形)、Diamond(菱形)、Triangle(三角形)、Text(文本)、HTML(HTML元素)、Image(图片)、Line(线)等多种类型,后期会考虑慢慢完善元件库

websocket

        websocket 是用于处理用户协同的模块

graphData

        graph Data 是双向绑定的数据管理模块

tools

        工具模块,包含图片导出、一键美化、层级处理、布局方式、元件组合、辅助线等

apis

        API 是外部访问内部实现执行动作、获取数据的窗口,并在设计上提供了command、adapt 两个类,在command中隔离内部对象,通过调用adapt实现数据的处理,放置用户通过command对象对内部对象进行风险操作

event

        提供统一的事件处理机制,支持对内部事件的监听、外部事件的注册等,同时,还对graph元件的统一事件进行处理,例如元件的点击事件、双击事件等

history

        历史记录管理模块,支持 redo undo version 等历史相关操作

项目架构

        项目对外暴露基础操作,例如: svg 构造器、command api操作、event事件中心以及全局api,通过暴露对象 sfEditor,实现对内部的数据访问、对象操作等。在核心模块中,需要考虑用户的使用习惯,封装完整的工具类,实现流程图的基本操作、拓展功能。底层依赖了svg对项目元件库的基础元件进行创作,同时使用了canvas对背景网格、水印等进行绘制,使用html进行页面布局,并且提供了typescript的全类型支持。

        在API设计的设计上,采取了Command CommandAdapt 两个类实现,Command中不进行用户方法的直接处理,增加adapt类进行方法中转,防止用户通过API直接操作核心类。Command调用 adapt 的实例方法,在adapt 中获取draw、svg 等核心类进行用户的响应。

        未来的功能模块规划中,还是以协同为核心重点。

项目结构说明

        如上图,核心类在 core 中,index.ts 向外暴露了API,main.ts 则是测试结果的入口文件,interface是类型文件,命名上基本上都是按功能模块走的。 

Graph 实体类

构建 svg 对象

export class SVG {
  private xmlns!: string;
  private svg: Element;
  private svgID!: string;
  private draw: Draw; // 绘制实例
  private graphOption: IGraphOption | undefined;
  constructor(graphOption?: IGraphOption) {
    this.draw = new Draw();
    this.svgID = getNanoid();
    this.graphOption = graphOption;

    //SVG命名空间
    this.xmlns = graphOption?.xmlns || "http://www.w3.org/2000/svg";

    // 1. 判断是否存在当前命名空间的svg
    const svgElement = this.draw.getSvg(this.xmlns);

    // 2. 如果存在 则保存
    if (svgElement) throw new Error(messageInfo.isHaveSvgElement); // 如果已经存在相同xmlns属性的svg 则报错

    // 3. 不存在 则创建新的 svg
    this.svg = this.draw.createSvg(this.xmlns, this.svgID);
  }

  // 将当前创建的svg添加到html DOM 的节点上
  public addTo(container: string | Element) {
    this.draw.addTo(container, this.svg); // 添加到指定容器

    this.size(); // 设置默认大小

    const { gridLines, waterMark, waterMarkText } = this.graphOption || {};

    if (gridLines !== false) this.draw.gridLines(); // 绘制网格

    if (waterMark !== false) this.draw.waterMark(waterMarkText); // 绘制水印

    return this; // 返回 this 供链式调用
  }

  // 设置当前 svg 的大小
  public size(width?: number, height?: number) {
    this.svg.setAttribute("width", width?.toString() || "100%");
    this.svg.setAttribute("height", height?.toString() || "100%");
    return this;
  }

        相关的draw方法:

import { messageInfo } from "../Message";

// 绘制、DOM 操作的核心类 尽量将所有的DOM操作都汇集在该类中,防止多处操作DOM引起的其他问题
export class Draw {
  constructor() {}

  // 通过指定的 xmlns 获取 svg
  public getSvg(xmlns: string) {
    return document.querySelector(`svg[xmlns="${xmlns}"]`);
  }

  //   创建 svg
  public createSvg(xmlns: string, svgID: string) {
    const svg = document.createElementNS(xmlns, "svg");
    svg.setAttribute("ID", svgID);
    svg.setAttribute("xmlns", xmlns);
    svg.setAttribute("version", "1.1");
    svg.setAttribute("baseProfile", "full");
    return svg;
  }

  // 将创建 svg 添加到指定容器
  public addTo(container: string | Element, svg: Element) {
    const type = typeof container === "string";
    // 判断传入参数是选择器还是dom
    let dom = type ? document.querySelector(container) : container;
    dom?.appendChild(svg);
  }

  // 绘制网格线
  public gridLines() {
    console.log("gridLines");
  }

  // 绘制水印
  public waterMark(waterMarkText?: string) {
    const text = waterMarkText || messageInfo.waterMarkText;
  }

  // 清除网格线
  public clearGridLines() {}

  // 清除水印
  public clearWaterMark() {}
}

构建 Rect 类

import { Common } from "./Common";
import { SVG } from "./index";

// 矩形类
export class Rect extends Common {
  private svg: SVG; // 根元素 svg
  private rect: Element;

  constructor(svg: SVG, width: number, height: number) {
    super();
    this.svg = svg;
    this.rect = super.getDraw().createRect(svg.getSvgXmlns());

    // 设置宽高
    this.setAttribute(width, height);

    // 将当前创建的元件添加到 svg 下
    super.addToSvg(this);
  }

  // 独有属性设置
  private setAttribute(width: number, height: number) {
    this.rect.setAttribute("width", width.toString());
    this.rect.setAttribute("height", height.toString());
  }

  //   获取基本Element
  public getElement() {
    return this.rect;
  }

  // 获取 xmlns
  public getXmlns() {
    return this.svg.getSvgXmlns();
  }
}

抽离公共类

        svg 元件具有的公共方法,例如 设置位置信息、设置宽高、设置样式等,还有事件处理机制,都是每一个元件都拥有的方法属性,因此,抽离为独立的类,实现 元件集成即可。

// svg 元件公共类

import { IGraphAttributes } from "../../interface/Graph";
import { Draw } from "../Draw";
import { Rect } from "./Rect";

// 定义元件类型
type IGraph = Rect;

export class Common {
  private draw: Draw;

  constructor() {
    this.draw = new Draw();
  }

  // 设置元件ID
  public setID() {}

  // 获取ID
  public getID() {
    const element = (this as unknown as IGraph).getElement();
    return this.draw.getID(element);
  }

  // 将创建的元件 添加到 svg 下
  protected addToSvg(graph: IGraph) {
    // 创建了基本元件后,需要构建 g 分组,方便处理 hover 及 click 的锚点
    const xmlns = graph.getXmlns();
    const element = graph.getElement();
    const nodeID = graph.getID() as string;

    // 1. 获取分组
    const group = this.draw.createGroup(element, xmlns, nodeID);

    // 2. 获取当前的 svg 根元素
    const svg = this.draw.getSvg(xmlns);

    // 3. 初始化默认属性
    this.attr.call(graph, {});

    // 3. 将当前分组添加到根元素上
    this.draw.addTo(svg as Element, group);
  }

  // 设置位置
  public position(x: number, y: number) {
    const graph = this as unknown as IGraph;
    const element = graph.getElement();
    // 因为设置位置属性的时候,不同的元素不一致,因此需要建立 原型与属性的映射
    const { tagName } = element;
    const attrMap: { [key: string]: string[] } = {
      rect: ["x", "y"],
      circle: ["cx", "cy"],
      ellipse: ["cx", "cy"],
    };
    element.setAttribute(attrMap[tagName][0], x.toString());
    element.setAttribute(attrMap[tagName][1], y.toString());
    // 重新渲染
    this.draw.updateLinkAnchorPoint(
      graph.getID() as string,
      element,
      graph.getXmlns()
    );
    return this;
  }

  //  设置属性
  public attr({ stroke, fill }: IGraphAttributes) {
    // 设置样式
    const graph = this as unknown as IGraph;
    const element = graph.getElement();
    element.setAttribute("stroke", stroke || "black");
    element.setAttribute("fill", fill || "#F2F2F2");
    return this;
  }

  // 获取 draw 操作对象
  protected getDraw() {
    return this.draw;
  }
}

实现效果

 公共事件处理机制

  Common.ts
  // 为所有的子类构造事件
  public click!: (_fun: Function) => IGraph;
  public dblclick!: (_fun: Function) => IGraph;
  public mousedown!: (_fun: Function) => IGraph;
  public mousemove!: (_fun: Function) => IGraph;
  public mouseup!: (_fun: Function) => IGraph;
  public mouseover!: (_fun: Function) => IGraph;
  public mouseout!: (_fun: Function) => IGraph;

  // 初始化公共事件
  private initCommonEvent(graph: IGraph) {
    /**
     * 事件处理机制: 不管用户有没有添加 click ,都需要实现 addEventListener
     */

    const eventList: IEventList = {
      click: (e: Event, graph: IGraph) => this.commonEvent.click(e, graph),
    };
    const element = graph.getElement();
    Object.keys(eventList).forEach((eventname) => {
      let userfun: null | Function;
      // @ts-ignore 用户自定义事件
      graph[eventname] = (_fun: Function | null) => {
        userfun = _fun;
        return graph;
      };

      // 给元素添加事件
      element.addEventListener(eventname, (e) => {
        // 1. 先执行默认事件
        eventList[eventname](e, graph);
        // 在这里处理用户自定义的事件
        userfun && userfun(e);
        // 阻止事件冒泡
        e.preventDefault();
      });
    });
  }

全局指令

// 暴露对外操作API 需要经过 Command Adapt的中转,防止用户直接通过 Command 获取到内部对象
import { Draw } from "../Draw";
import { CommandAdapt } from "./CommandAdapt";

export class Command {
  // 测试设置水印
  public executeWatermark: CommandAdapt["watermark"];

  constructor(draw: Draw) {
    const adapt = new CommandAdapt(draw);
    this.executeWatermark = adapt.watermark.bind(adapt);
  }
}
import { Draw } from "../Draw";

// Command Adapt API 操作核心库
export class CommandAdapt {
  private draw: Draw;
  constructor(draw: Draw) {
    this.draw = draw;
  }

  public watermark() {
    console.log("watermark");
  }
}

事件机制

        事件处理中主要使用event Bus 实现:

export class EventBus<EventMap> {
  private eventHub: Map<string, Set<Function>>

  constructor() {
    this.eventHub = new Map()
  }

  public on<K extends string & keyof EventMap>(
    eventName: K,
    callback: EventMap[K]
  ) {
    if (!eventName || typeof callback !== 'function') return
    const eventSet = this.eventHub.get(eventName) || new Set()
    eventSet.add(callback)
    this.eventHub.set(eventName, eventSet)
  }

  public emit<K extends string & keyof EventMap>(
    eventName: K,
    payload?: EventMap[K] extends (payload: infer P) => void ? P : never
  ) {
    if (!eventName) return
    const callBackSet = this.eventHub.get(eventName)
    if (!callBackSet) return
    if (callBackSet.size === 1) {
      const callBack = [...callBackSet]
      return callBack[0](payload)
    }
    callBackSet.forEach(callBack => callBack(payload))
  }

  public off<K extends string & keyof EventMap>(
    eventName: K,
    callback: EventMap[K]
  ) {
    if (!eventName || typeof callback !== 'function') return
    const callBackSet = this.eventHub.get(eventName)
    if (!callBackSet) return
    callBackSet.delete(callback)
  }

  public isSubscribe<K extends string & keyof EventMap>(eventName: K): boolean {
    const eventSet = this.eventHub.get(eventName)
    return !!eventSet && eventSet.size > 0
  }
}

总结

        至此,整体项目的框架已经跑通了,包括API的封装(command adapt)、事件处理机制、svg元件构建,本文先处理这么多事情。

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

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

相关文章

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:隐私遮罩)

用于对组件内容进行隐私遮罩处理。 说明&#xff1a; 从API Version 10开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 obscured obscured(reasons: Array<ObscuredReasons>) 设置组件内容的遮罩类型。 系统能力&#xff1a; Sys…

拓尔微代理商 TMI3252T 600kHz 18V 2A同步COT降压转换器

TMI3252/S/T是高效率600kHz&#xff0c;恒定导通时间 &#xff08;COT&#xff09; 控制同步模式降压型DC-DC转换器&#xff0c;能够提供高达2A电流。TMI3252/S/T集成主要具有极低 RDS&#xff08;ON&#xff09; 的开关和同步开关以尽量减少传导损耗。低输出电压纹波和小尺寸的…

E8-写了一个方法,处理一个表单里有多组需要实现单选或复选的复选框

起因 今天同事发来需求&#xff0c;要做一个工作流&#xff0c;其中表单里有几组选项。在纸质单上是留出位置画勾选择的。简单的聊了一下对填报的要求&#xff0c;要求有的组要控制单选&#xff0c;有的组还不需要制多选。用文字来描述很晦涩&#xff0c;看到表单估计小伙伴们…

上位机图像处理和嵌入式模块部署(qmacvisual入门)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 虽然我们前面学习了很多的知识点&#xff0c;比如说在windows这边&#xff0c;用qt写界面&#xff0c;用opencv写图像处理代码&#xff1b;在linux…

Jmeter 性能 —— 50TPS与秒杀分析!

1、50tps——5tps分析 50tps基本上已经满足了大部分中小型企业要求了 需求&#xff1a;期望我项目的接口&#xff0c;都要能满足50tps&#xff1f; 算 50tps&#xff1a;50 个事务每秒50 t/s 1分钟&#xff1a;50\*60s 3000 事务1小时 3000 \* 60 180000 事务 1小时要处理…

基于Golang客户端实现Nacos服务注册发现和配置管理

基于Golang客户端实现Nacos服务注册发现和配置管理 背景 最近需要把Golang实现的一个web项目集成到基于Spring Cloud Alibaba的微服务体系中&#xff0c;走Spring Cloud Gateway网关路由实现统一的鉴权入口。 软件版本 组件名称组件版本Nacos2.2.0Go1.21.0Ginv1.9.1Nacos-s…

WhatsApp API号注册平台价格对比:帮你选择性价比最高的服务

WhatsApp作为全球使用人数众多的即时通讯工具&#xff0c;推出的API服务为企业提供了强大的客户互动能力。然而面对众多提供WhatsApp API号注册的平台&#xff0c;企业在选择时很容易感到困惑。这篇文章将会对目前市面上比较主流的WhatsApp API号注册平台进行价格对比&#xff…

三、加载路径对桁架的影响

加载路径&#xff1a; \color{green}加载路径&#xff1a; 加载路径&#xff1a; 材料在受力时所经历的应力和应变变化路径。在塑性力学中&#xff0c;加载路径对材料的变形行为和力学性能有着重要影响。 案例分析 \color{green}案例分析 案例分析 如图所示&#xff0c;理想弹…

Linux——基础IO

目录 前言 C语言文件操作 stdin & stdout & stderr 系统文件IO open close write read 文件描述符fd 重定向 dup2 Linux下一切皆文件 缓冲区 简易缓冲区 文件系统 磁盘 创建文件 删除文件 查看文件 软硬链接 软链接 硬链接 动静态库 静态库 …

40. 【Linux教程】文件系统介绍

从本小节我们将要开始 Linux 中的文件系统&#xff0c;在 Linux 系统中&#xff0c;文件系统都是基于硬盘创建的&#xff0c;Linux 系统支持多种不同类型的文件系统管理文件和目录&#xff0c;本小节介绍几种基本常见的文件系统。 Linux 文件系统中的文件是数据的集合&#xf…

算法沉淀——动态规划之两个数组的 dp(上)(leetcode真题剖析)

算法沉淀——动态规划之两个数组的 dp 01.最长公共子序列02.不相交的线03.不同的子序列04.通配符匹配 01.最长公共子序列 题目链接&#xff1a;https://leetcode.cn/problems/longest-common-subsequence/ 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 …

医院床旁交互系统概述 -智慧护理-全视通

全视通床旁交互系统是一种先进的医疗信息技术解决方案&#xff0c;旨在改善病患与医疗团队之间的沟通与交流。该系统通过集成多种高科技设备&#xff0c;为病患在病床边提供了一站式的信息交互平台&#xff0c;从而优化了医疗服务流程&#xff0c;提升了医疗体验。 首先&#x…

C++引入

引用不是新定义一个变量&#xff0c;而是给已经存在的变量取一个别名&#xff0c;编译器不会为了引用变量开辟内存空间&#xff0c;它和它引用的变量公用同一块内存空间。如李白被称为诗仙。李白和诗仙都是同一个人。 语法: 类型& 引用变量名(对象名)引用实体; 特性: 引用在…

coppeliasi4.0版本中使用循迹小车跟随路径时问题汇总

加入循迹小车&#xff0c;设置好路径后运行 发现报错&#xff0c;小车直线行驶不跟随设置好的路径移动 观察仿真中可知小车左右中传感器并不工作全是黑色&#xff0c;观察报错语句 Lua runtime error: [string "CUSTOMIZATION SCRIPT LeftSensor"]:12: Invalid hand…

基于SpringBoot的医护人员排班系统(代码+数据库+文档)

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

Vue 项目重复点击菜单刷新当前页面

需求&#xff1a;“在当前页面点击当前页面对应的菜单时&#xff0c;也能刷新页面。” 由于 Vue 项目的路由机制是路由不变的情况下&#xff0c;对应的组件是不重新渲染的。所以重复点击菜单不会改变路由&#xff0c;然后页面就无法刷新了。 方案一 在vue项目中&#xff0c;…

探索Python编程世界:从入门到精通

一.Python 从入门到精通 随着计算机科学的发展&#xff0c;编程已经成为了一种必备的技能。而 Python 作为一种简单易学、功能强大的编程语言&#xff0c;越来越受到人们的喜爱。本文将为初学者介绍 Python 编程的基础知识&#xff0c;帮助他们踏入 Python 编程的大门&#xf…

网络安全: Kali Linux 使用 MSF 渗透测试

目录 一、实验 1.环境 2.登录MSF&#xff08;Metasploit Framework&#xff09;控制台 3.MSF初始化 4.MSF 管理工作区 5.Kali Linux (2024.1) 对Windows server 进行网址目录扫描 6.Kali Linux (2022.4) 对Ubuntu进行网址目录扫描 7.Kali Linux (2024.1) 对Windows ser…

Java 的七种垃圾收集器

了解 Java 中的内存管理。 用 C 或 C 这样的编程语言写一个应用时&#xff0c;需要编写代码来销毁内存中不再需要的对象。当应用程序扩展得越来越复杂时&#xff0c;未使用对象被忽略释放的可能性就越大。这会导致内存泄露&#xff0c;最终内存耗尽&#xff0c;在某个时刻将没有…

Matlab/Simulink验证MAB建模规范

前言 为什么MAB&#xff1f; MathWorks Advisory Board&#xff08;MAB&#xff09;是由MathWorks公司设立的一个咨询委员会&#xff0c;旨在提供有关MathWorks产品和服务的反馈、建议和意见。MAB成员通常是来自学术界、工业界和其他领域的专业人士&#xff0c;他们在各自领域…