手摸手使用G6实现(轻)图编辑应用系列-自定义行为

1,627 阅读4分钟

我正在参加「掘金·启航计划」

系列文章:

前言

上一节,自定义了边、箭头,接下来自定义行为

行为分析

  • 操作行为

    • 编辑模式下,从物料区拖拽到画布,创建指定节点
    • 无连线节点鼠标移入点击删除操作图标
    • 点击创建图标创建一个以点击节点为起点,以鼠标位置为终点的连接线
    • 连接线需要线为虚线,箭头为实线,样式不同于其他线样式
    • 在除节点外的地方点击,删除此连接线
    • 点击任意节点指定区域时,建立连接线
    • 用户填入参数,用来描述连接线
    • 根据节点间边数决定边类型,进行更新效果
  • 官方提供create-edge内置行为,没有使用主要有三个原因

    • 边实例在边创建后才会触发事件传递出来,导致连线过程中,鼠标移动到自身节点时,线会变active样式(实际连接线在用户确定前样式不能变)
    • 为了熟悉此扩展机制的使用方式
    • 把从物料区拖拽创建节点以及删除节点也封装到自定义行为内
  • 官方自定义行为方法

    • 行为属于交互,交互起源于用户在系统上的所有事件,是否允许交互发生同事件密切相关;本质就是通过监听事件,对相应情况做出相应
G6.registerBehavior(
    // 自定义行为名称
    'activate-node', 
    {
       //默认配置,会将其返回对象的key挂在到this自身上
        getDefaultCfg(){
            return {};
        }
        // 自定义行为涉及的事件
        getEvents() {
            return {};
        },
    }
)
  • 自定义行为使用
    • 图配置中存在交互模式Mode,模式其实就是行为的集合
    • 将行为名称放入指定模式对应的数组中,图实例切换到此种模式即可生效

行为实现

监听事件

  • 官方支持
    • 全局事件
    • canvas事件
    • 节点、边、combo事件
    • 图形事件
    • 时机事件
    • 自定义事件
  • 实现思路
    • 拖拽节点到画布,监听全局drop事件
    • 删除节点,监听图形click事件
    • 创建连接线,监听节点click事件
    • 跟随鼠标连线,监听全局mousemove事件
    • 删除线,监听线click事件

代码实现

定义行为涉及事件
getEvents() {
    return {
      drop: 'dropNode',
      'text-shape-delete:click': 'deleteClick',
      'node:click': 'onClick',
      mousemove: 'onMousemove',
      'edge:click': 'onEdgeClick',
    };
  },
创建节点
  • 根据拖动时存储在dataTransfer中的数据,以及drop的事件对象的坐标来创建节点
  • 监听不同对象拖拽结束获取的坐标有些区分
    • 监听拖拽元素的拖拽结束事件,获取的坐标是基于浏览器窗口的 clientX/clientY
    • 监听G6全局drop事件,获取的坐标是G6 pointX/pointY 坐标系
  • G6的三种坐标系
    • clientX/clientY
    • canvasX/canvasY
    • pointX/pointY
    • 注:缩放平移改动的都是pointX/pointY,官方提供了各种坐标系间点的转换方法 xy.png
  • 代码实现
addNode (transferData, { x, y }) {
  const nodeData = JSON.parse(transferData);
  this.noConnectNode = this.noConnectNode.filter(item => item.id !== nodeData.id);
  const model = {
    ...nodeData,
    x,
    y,
  };
  const item = graph.addItem('node', model);
  ......
},
dropNode(e){
    const { originalEvent } = e;
    if(originalEvent.dataTransfer) {
      const transferData = originalEvent.dataTransfer.getData('dragComponent');
      if(transferData) {
        addNode(transferData, e);
      }
    }
},
删除节点
deleteClick(ev){
    const node = ev.item;
    this.graph.removeItem(node);
},
节点间连线
  • 连接线(虚线,实线箭头)
connectLine:{
    stroke: '#F6BD16',
    lineWidth: 2,
    lineDash: [5],
    endArrow: {
      path: G6.Arrow.vee(6, 10, 0),
      d: 0,
      fill: '#F6BD16',
      lineDash: [0.000001]
    }
},
  • 线三种情况
    • 创建线,通过点击的目标图形判断是否指定图形
      • 不是 return
      • 是,创建一个起点终点都是点击节点的线
    • 移动线
      • 通过移动事件获取鼠标的pointX/pointY,更新线的目标节点位置
    • 连接线,通过点击的目标图形判断是否指定图形
      • 不是 return
      • 是,更新边的目标节点值,触发连线成功事件(要处理自连情况)
    • 注:过程中需要存储的值都放到instance全局变量上
// 鼠标移动
onMousemove(ev) {
    const self = this;
    // 鼠标位置
    const point = { x: ev.x, y: ev.y };
    if (self.addingEdge && instance.currentEdge) {
      self.graph.updateItem(instance.currentEdge, {
        target: point,
      });
    }
},
onClick(ev) {
    const self = this;
    const targetShape = ev.shape;
    const node = ev.item;
    const graph = self.graph;
    if(!self.addingEdge){
      if (!targetShape || targetShape.get('name') !== 'text-shape-connect') return;
    }else{
      if(!targetShape || !['key-shape', 'center-image'].includes(targetShape.get('name'))) return;
    }
    const model = node.getModel();
    if (self.addingEdge && instance.currentEdge) {
      // 连线
      const defaultModel = {
        target: model.id,
      }
      if(instance.currentModel.id === model.id){
        // 自连情况,不需要处理箭头
        defaultModel.type = 'loop';
      }
      graph.updateItem(instance.currentEdge, defaultModel);
      graph.emit('afterPaintEdge', {edge: instance.currentEdge});
      self.addingEdge = false;
    } else {
      // 创建线
      instance.currentModel = model;
      instance.currentEdge = graph.addItem('edge', {
        source: model.id,
        target: model.id,
        style: Object.assign(
          {}, 
          styleObj.connectLine
        )
      });
      self.addingEdge = true;
    }
},
删除线
  • 鼠标移动过程中,单击任意位置,触发线的点击事件,若处于连线过程中并且点击的是连接线,则删除
onEdgeClick(ev) {
    const self = this;
    const currentEdge = ev.item;
    if (self.addingEdge && instance.currentEdge === currentEdge) {
      self.graph.removeItem(instance.currentEdge);
      instance.currentEdge = null;
      instance.currentModel = null;
      self.addingEdge = false;
    }
},

结尾

截至到此,自定义行为实现完成,下一节会介绍实现demo过程中功能点的优化

希望看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励