👋一起来给流程图加便签吧,超级简单!

开始前在这里贴一下我们的项目地址和官网,欢迎大家访问并为我们的项目点上star⭐️~

项目地址:github.com/didi/LogicF...

官网地址:site.logic-flow.cn/

引言

便签节点是一种非常实用的辅助节点,用于在流程图中添加额外的说明、注释或备注信息。它们通常以不同于常规节点(矩形、菱形、圆形)的视觉形式呈现,例如云形框、带图标的文本框等,以帮助用户更好地理解流程图的逻辑、功能和背景。

🤔便签都有哪些特征呢?

为了搞清楚这个问题,小编研究了一些自己常用的流程图编辑工具,发现它们的便签节点一般具备以下三个特征:

这里以 ProcessOn 和 FigJam 为例

特点\示例 ProcessOn FigJam
样式仿真便利贴:看起来像真实的便签贴纸,清晰直观。
高度随内容变化:输入内容越多,节点自动扩展,方便展示。
支持富文本编辑:能插入格式化的文字,比如加粗、斜体或链接。

今天,就让我们一起照葫芦画瓢,为 LogicFlow 添加便签节点,让流程图更生动、实用,先看效果:

👨‍💻‍三步实现这个便签

第一步:画出便利贴样式

由于 LogicFlow 的底层基于 SVG 实现,因此在绘制便签样式时,我们有两个选择:

  1. 使用 SVG 的基础图形组合实现样式(如矩形 + 文本)。
  2. 通过 SVG 的 foreignObject 标签嵌套 HTML 元素实现样式。

虽然第二种方式功能强大,但需要额外处理 foreignObject 和 HTML 宽高联动的逻辑,会增加复杂度。

因此,我们选择第一种思路,直接用 SVG 图形绘制便利贴样式。 我们继承 LogicFlow 的矩形节点并进行自定义:

  • 引入依赖:便签节点的基本形状为矩形,因此我们需要引入 LogicFlow 的矩形节点( RectNode 和 RectNodeModel ),并通过继承的方式添加自定义功能。此外,使用 LogicFlow 提供的 h 函数来创建自定义图形节点。
typescript 复制代码
import LogicFlow, { RectNode, RectNodeModel, h } from '@logicflow/core';
  • 定义节点视图:通过继承 RectNode,重写其 getShape 方法,生成一个带折角的便签图形,折角的尺寸可以通过 cornerSize 属性控制。
typescript 复制代码
export class NoteView extends RectNode { // 继承矩形节点的视图类
  getShape() { // 重写getShape方法,返回一个带折角的矩形
    const { model } = this.props;
    const {
            x, // 节点x坐标
            y, // 节点y坐标
            width, // 节点宽度
            height, // 节点高度
            cornerSize // 自定义属性:便签折角尺寸
          } = model;
    const style = model.getNodeStyle();
    const strokeWidth = style.strokeWidth || 1;
    // 起点坐标,因为stroke是图形的外边框,添加后图形会被向右下方挤压
    // 为了能让边框完整的显示出来,所以把起点设置在边框中间
    const startPosition = strokeWidth / 2;
    const noteMainPath = ` // 便签图形
      ${startPosition},${startPosition}
      ${startPosition + width},${startPosition}
      ${startPosition + width},${startPosition + height - cornerSize}
      ${startPosition + width - cornerSize},${startPosition + height}
      ${startPosition},${startPosition + height}`;
    const noteCornerPath = ` // 便签折角图形
      ${width - cornerSize},${startPosition + height - cornerSize}
      ${width},${height - cornerSize}
      ${width - cornerSize},${height}`;
    return h( // 返回便签形状
      'svg',
      {
        ...style,
        x: x - width / 2,
        y: y - height / 2,
        width: width + strokeWidth,
        height: height + strokeWidth,
        viewBox: `-1 -1 ${width + strokeWidth + 2} ${height + strokeWidth + 2}`,
      },
      [
        h('polygon', { // 便签图形
          points: noteMainPath,
          fill: style.fill,
          stroke: style.stroke,
          strokeWidth,
        }),
        h('polygon', { // 便签折角图形
          points: noteCornerPath,
          fill: style.fill,
          stroke: style.stroke,
          strokeWidth,
        }),
      ]
    );
  }
}
  • 定义节点数据模型:继承 RectNodeModel 并重写 initNodeData 和 getNodeStyle 方法,以初始化节点数据和样式。
typescript 复制代码
export class NoteModel extends RectNodeModel {
  initNodeData(data: LogicFlow.NodeConfig<LogicFlow.PropertiesType>) { // 初始化节点数据
    super.initNodeData(data);
    const { properties } = data;
    if (!properties) return;
    // 给节点的宽高、折角尺寸赋值
    const { width, height } = properties;
    this.width = width || this.width;
    this.height = height || this.height;
    this.defaultHeight = height;
    this.cornerSize = Math.min(this.width, this.height) * 0.2;
  }


  getNodeStyle() { // 重写节点样式
    const { cornerSize } = this;
    const style = super.getNodeStyle();
    const { style: { fill, stroke, strokeWidth } = {} } = this.properties;
    style.fill = fill || '#fff';
    style.stroke = stroke || '#2961EF';
    style.strokeWidth = strokeWidth || cornerSize / 20;
    return style;
  }
}

这样,我们可以动态调整便签节点的宽高、折角尺寸及样式,使节点外观更符合需求。

  • 声明节点类型:将定义好的 NoteView 和 NoteModel 注册为 LogicFlow 的自定义节点类型。
typescript 复制代码
export const note = {
  type: 'note',
  view: NoteView,
  model: NoteModel,
}

现在,我们就成功自定义一个带折角的便签节点啦!它具有以下特点:

  1. 灵活的图形设计:支持自定义宽高和折角尺寸。
  2. 完善的样式配置:通过重写 getNodeStyle 方法控制填充色、边框色等样式细节。
  3. 便于扩展:基于 LogicFlow 的节点继承机制,便于进一步增加功能(如富文本编辑、动态调整等)。

可以看到这时的便签节点还只支持基础的文本输入能力,没办法做富文本编辑。 所以接下来,我们来为便签增加富文本文本编辑能力。

第二步:加入富文本编辑能力

在实现富文本编辑功能之前,我们先了解一下流程图框架中文本编辑的常见设计方法。

🧐流程图框架的文本编辑功能是如何实现的?

以 LogicFlow 为例,由于 SVG 本身不支持输入框,LogicFlow 的文本编辑功能通过以下方式实现:

  1. 双层结构:在 SVG 层显示静态文本,在其上方的工具层用 div 实现可编辑的文本框。
  2. 动态切换:双击文本后,工具层的 div 替代静态文本成为编辑态,用户完成输入后再同步回 SVG 层。

这种设计兼顾了文本的展示与编辑需求,也是我们便签节点富文本功能的基础。

要加入富文本编辑能力,首先要考虑的是富文本编辑器如何实现,为了避免重复造轮子,我们选择了开源的富文本编辑器 medium-editor ,并在此基础上封装了一个 Label 插件来实现富文本功能。实现时只需要引入 Label 插件就能增加富文本编辑能力:

typescript 复制代码
const lf = new LogicFlow({
  ...config,
  plugins: [Label, DndPanel],
  pluginsOptions: {
    label: {
      isMultiple: true, // 是否支持一个元素绑定多个label
      maxCount: 3, // 一个元素最多能绑定多少个label
      labelWidth: 80, // label文本框的宽度
      textOverflowMode: 'wrap', // 文本换行模式,支持的选项:'ellipsis' | 'wrap' | 'clip' | 'nowrap' | 'default'
    },
  },
})

这里对 Label 插件的内部实现做简单的介绍,大家可以根据自己的喜好选择用某个现成的库还是自行开发~

Label插件源码

小课堂:Label插件内部逻辑简介

Label插件是参考Text功能的逻辑实现了基础的文本输入和展示能力,在此基础上引入了 medium-editor 作为富文本编辑器,主要有三个核心逻辑。

文本状态变更

LogicFlow 的节点具有两种状态:展示态文本编辑态。当用户双击处于展示态的节点时,节点会切换为文本编辑态,并触发 node:dbclick 事件,通知外部该节点被双击。

Label 插件会监听 node:dbclick 事件并进行处理:当事件触发时,插件会判断双击位置是否包含 Label。

  • 如果存在 Label,则将其状态切换为编辑态,并将节点中存储的 Label 内容回填到编辑框中;
  • 如果不存在,则会在工具层创建一个 contenteditable 属性为 true 的 div,用作新的编辑框,供用户直接输入内容。

添加富文本编辑器

插件内部根据 medium-editor 的语法声明编辑器配置 defaultOptions ,然后在工具层挂载时传入 需要监听的 DOM 的类名 和 编辑器配置 创建编辑器实例,挂到工具层,就能实现选中元素出现编辑器的效果了。

typescript 复制代码
const defaultOptions = { // medium-editor的默认配置
  toolbar: { // 工具栏配置
    allowMultiParagraphSelection: true,
    buttons: [
      'bold',
      'colorpicker',
      'italic',
      'underline',
      'strikethrough',
      'quote',
      'justifyLeft',
      'justifyCenter',
      'justifyRight',
      'justifyFull',
      'superscript',
      'subscript',
      'orderedlist',
      'unorderedlist',
      'pre',
      'removeFormat',
      'outdent',
      'indent',
      'h2',
      'h3',
    ],
    standardizeSelectionStart: false,
    updateOnEmptySelection: false,
  },

  placeholder: {
    text: '请输入内容',
    hideOnClick: true,
  },
  disableEditing: true,
}

// ...

componentDidUpdate() {
    // ...
    this.editor?.destroy() // 为保证编辑器全局唯一,在创建前先销毁一下
    this.editor = new MediumEditor( // 创建编辑器实例,挂到全局
      '.lf-label-editor', // Label插件为内部输入框的类名
      merge(defaultOptions, {
        autoLink: true,
        extensions: {
          colorPicker: new ColorPickerButton(),
        },
      }),
    )
  }

数据的存储

经过富文本编辑器编辑过的内容会是这样的一个 HTML 片段,但最终在数据上我们需要存储一个纯文本数据,再加上数据存储后需要回填,所以小编用两个字段来存储便签内容:

  • content:存储带样式的 HTML 片段
  • text:存储纯文本内容

这里的操作很简单,在文本框失焦时获取 innerText 和 innerHTML 存储到节点里。

typescript 复制代码
    const value = this.textRef.current?.innerText ?? ''
    const content = this.textRef.current?.innerHTML ?? ''

    this.setElementModelLabelInfo({ // 把数据塞到model里
      value,
      content,
    })

第三步:高度与内容联动变化

最后就是高度联动啦,为了让便签节点的高度随内容变化,我们需要监听输入框的高度变化,并实时更新便签节点的外观。

首先我们给输入框 div 加上 onInput 事件

HTML 复制代码
    <div
      ref={this.textRef}
      id={`editor-container-${id}`}
      className={classNames('lf-label-editor', {
        'lf-label-editor-dragging': isDragging,
        'lf-label-editor-editing': isEditing,
        'lf-label-editor-hover': !isEditing && (isHovered || isSelected),
        [`lf-label-editor-${textOverflowMode}`]: !isEditing,
      })}
      onInput={this.handleInput}
      style={{
        maxWidth: `${maxLabelWidth}px`,
        boxSizing: 'border-box',
        display: 'inline-block',
        background:
          isEditing || element.BaseType === 'edge' ? '#fff' : 'transparent',
        ...style,
      }}
      dangerouslySetInnerHTML={{ __html: content }}
    />

根据内容动态调整节点和文本框的高度,使两者保持一致。

typescript 复制代码
// 触发回调后的核心逻辑
const domId = `editor-container-${label.id}`;
// 获取当前输入的label容器元素
const domElement = document.getElementById(domId);
if (!domElement) return;
// 当前输入的label容器元素的内容(因为便签只有一个文本,所以内容元素即第一个子元素)
const contentDom = domElement.children[0] as HTMLElement;
if (!contentDom) return;
const lineHeight = window.getComputedStyle(contentDom).lineHeight
// 设置高度
// 为了保持图形的高度和输入框不贴边,在每次设置图形高度时都在内容高度的基础上加上1行高的高度
if (contentDom.offsetHeight > this.defaultHeight) {
  this.setProperty('height', contentDom.offsetHeight + parseFloat(lineHeight));
} else {
  this.setProperty('height', contentDom.offsetHeight < this.defaultHeight ? this.defaultHeight : contentDom.offsetHeight + parseFloat(lineHeight));
}

this.properties._label?.forEach((label: any) => {
    label.style = {
      ...label.style,
      minHeight: `${height - height * 0.2}px`, // 更新文本框高度
    };
  });
  

通过这个联动机制,无论便签内容多少,节点都会自动适配,保证显示效果。

结语

以上就是流程图加便签的整个实现过程,便签节点作为流程图中的一种辅助元素,不仅能够直观地传递额外信息,还为用户提供了更强的交互体验。

未来,便签节点还有更多可以探索的方向,比如支持插入图片、链接,或者提供个性化的样式和主题选项。这不仅能提升流程图的表现力,也能让用户更高效地传递和处理信息。如果你对流程图工具的开发感兴趣,欢迎你发挥自己的奇思妙想与我们一起共建~

便签Demo源码

如果这篇文章对你有帮助,欢迎关注我们的账号,我们会持续输出干货文章。也希望您能为我们的项目点上 Star,这对我们非常重要,感恩的心~

项目地址传送门:github.com/didi/LogicF...

相关推荐
cnsxjean1 小时前
Vue教程|搭建vue项目|Vue-CLI2.x 模板脚手架
javascript·vue.js·ui·前端框架·npm
小小优化师 anny2 小时前
JS +CSS @keyframes fadeInUp 来定义载入动画
javascript·css·css3
每一天,每一步3 小时前
react antd不在form表单中提交表单数据,而是点查询按钮时才将form表单数据和其他查询条件一起触发一次查询,避免重复触发请求
前端·javascript·react.js
花之亡灵3 小时前
(笔记)vue3引入Element-plus
前端·javascript·vue.js
神秘代码行者5 小时前
Node.js JWT认证教程
javascript·node.js
不做超级小白5 小时前
深入理解 Axios 拦截器的执行链机制
开发语言·前端·javascript
以对_5 小时前
【el-table】表格后端排序
前端·javascript·vue.js
北城笑笑5 小时前
Vue 90 ,Element 13 ,Vue + Element UI 中 el-switch 使用小细节解析,避免入坑(获取后端的数据类型自动转变)
前端·javascript·vue.js·elementui
JEECG低代码平台6 小时前
【免费开源】JeecgBoot单点登录源码全部开源了
低代码·开源