自定义 Combo

阅读时间约 7 分钟

F6 提供了一系列内置 Combo,包括 circlerect。若内置 Combo 无法满足需求,用户还可以通过 F6.registerCombo ('comboName', options, expendedComboName) 进行自定义扩展内置的 Combo,方便用户开发更加定制化的 Combo,包括含有复杂图形的 Combo、复杂交互的 Combo、带有动画的 Combo 等。

在本章中,我们通过两个案例,讲解通过自定义扩展现有 Combo。

Combo 接口

通过 图形 Shape 章节的学习,我们应该已经知道了自定义 Combo 时需要满足以下两点:

  • 控制 Combo 的生命周期;
  • 解析用户输入的数据,在图形上展示。

在自定义扩展内置 'circle' 或 'rect' Combo 时,API 中可以复写的方法如下:

F6.registerCombo(
  'comboName',
  {
    /**
     * 绘制 Combo 中的图形。不需要为默认的 label 增加图形,父类方法会自动增加 label
     * @param  {Object} cfg Combo 的配置项
     * @param  {G.Group} group 图形分组,Combo 中的图形对象的容器
     * @return {G.Shape} 返回一个绘制的图形作为 keyShape,通过 combo.get('keyShape') 可以获取。
     * 关于 keyShape 可参考文档 核心概念-节点/边/Combo-图形 Shape 与 keyShape
     */
    drawShape(cfg, group) {},
    /**
     * 绘制后的附加操作,默认没有任何操作
     * @param  {Object} cfg Combo 的配置项
     * @param  {G.Group} group 图形分组,Combo 中的图形对象的容器
     */
    afterDraw(cfg, group) {},
    /**
     * 更新节点后的操作,新增的图形需要在这里控制其更新逻辑
     * @override
     * @param  {Object} cfg 节点的配置项
     * @param  {Combo} combo 节点
     */
    afterUpdate(cfg, combo) {},
    /**
     * 响应 Combo 的状态变化。
     * 在需要使用动画来响应状态变化时需要被复写,其他样式的响应参见下文提及的 [配置状态样式] 文档
     * @param  {String} name 状态名称
     * @param  {Object} value 状态值
     * @param  {Combo} combo 节点
     */
    setState(name, value, combo) {},
  },
  // 被继承的 Combo 类型名,可选:'circle' 或 'rect'
  extendedComboName,
);

注意事项(必读)

因 Combo 更新逻辑的特殊性(需要根据其子元素信息自动更新自身位置和大小),自定义 Combo 时,与自定义节点/边有所不同:

  1. 不建议“从无到有”地自定义 Combo,推荐使用继承的方式扩展内置的 'circle' 或 'rect' Combo;
  2. drawShape 方法中不需要为 label 增加图形,父类方法将会自动增加默认的 label,可以通过配置的方式指定 label 的位置和样式;
  3. 与自定义节点/边不同,这里不建议复写 updatedraw 方法,否则会使 Combo 根据子元素更新的逻辑异常;
  4. 复写的 drawShape 方法返回值与推荐继承内置的 'circle'、'rect' 的 keyShape 一致。即继承 'circle' 时,drawShape 方法应该返回一个 circle 图形;继承 'rect' 时,drawShape 方法应该返回一个 rect 图形;
  5. 除 keyShape 外,自定义新增的图形需要afterUpdate 中定义其位置更新逻辑
  6. setState 只有在需要使用动画来响应状态变化时需要被复写,一般的样式响应状态变化可以通过 配置状态样式 实现。

1. 自定义扩展内置 Rect Combo

Demo

内置 Rect Combo 位置逻辑详解

首先,我们需要了解内置的 rect 类型的 Combo 内部的位置逻辑:

  • 下图灰色虚线框内部是子元素的分布范围,其宽高分别为 innerWidth 和 innerHeight;
  • 灰色虚线框上下左右可以配置 padding 值,该 Combo 的 keyShape 真实绘制大小 width 与 height 是 innerWidth 和 innerHeight 加上了 padding 后的值;
  • 一个 Combo 内部的图形以自身坐标系为参考,原点 (0, 0) 在灰色虚线框正中心;
  • padding 值的上与下、左与右可能不相等,这就导致了该矩形的左上角坐标不是简单的 (-width / 2, -height / 2),而是通过如图标注的计算获得;
  • rect 类型 Combo 的 label 默认位于矩形内部左上角,上边距为 refY,左边距为 refX。label 的位置(position)、refXrefY 可以在使用该类型 Combo 时配置。
img

Rect Combo 位置说明图

绘制图形

现在,我们自己实现一个如下图所示的 Combo 类型(下图展示空 Combo):

img

根据上述 内置 Rect Combo 位置逻辑详解,在扩展 rect 类型 Combo 时需要注意复写方法中 xywidthheight 的设置

F6.registerCombo(
  'cRect',
  {
    drawShape: function drawShape(cfg, group) {
      const self = this;
      // 获取配置中的 Combo 内边距
      cfg.padding = cfg.padding || [50, 20, 20, 20];
      // 获取样式配置,style.width 与 style.height 对应 rect Combo 位置说明图中的 width 与 height
      const style = self.getShapeStyle(cfg);
      // 绘制一个矩形作为 keyShape,与 'rect' Combo 的 keyShape 一致
      const rect = group.addShape('rect', {
        attrs: {
          ...style,
          x: -style.width / 2 - (cfg.padding[3] - cfg.padding[1]) / 2,
          y: -style.height / 2 - (cfg.padding[0] - cfg.padding[2]) / 2,
          width: style.width,
          height: style.height,
        },
        draggable: true,
        name: 'combo-keyShape',
      });
      // 增加右侧圆
      group.addShape('circle', {
        attrs: {
          ...style,
          fill: '#fff',
          opacity: 1,
          // cfg.style.width 与 cfg.style.heigth 对应 rect Combo 位置说明图中的 innerWdth 与 innerHeight
          x: cfg.style.width / 2 + cfg.padding[1],
          y: (cfg.padding[2] - cfg.padding[0]) / 2,
          r: 5,
        },
        draggable: true,
        name: 'combo-circle-shape',
      });
      return rect;
    },
    // 定义新增的右侧圆的位置更新逻辑
    afterUpdate: function afterUpdate(cfg, combo) {
      const group = combo.get('group');
      // 在该 Combo 的图形分组根据 name 找到右侧圆图形
      const circle = group.find((ele) => ele.get('name') === 'combo-circle-shape');
      // 更新右侧圆位置
      circle.attr({
        // cfg.style.width 与 cfg.style.heigth 对应 rect Combo 位置说明图中的 innerWdth 与 innerHeight
        x: cfg.style.width / 2 + cfg.padding[1],
        y: (cfg.padding[2] - cfg.padding[0]) / 2,
      });
    },
  },
  'rect',
);

值得注意的是,F6 3.3 需要用户为自定义节点中的图形设置 namedraggable。其中,name 可以是不唯一的任意值。draggabletrue 是表示允许该图形响应拖拽事件,只有 draggable: true 时,图上的交互行为 'drag-combo' 才能在该图形上生效。若上面代码仅在 keyShape 上设置了 draggable: true,而右侧圆图形上没有设置,则鼠标拖拽只能在 keyShape 上响应。

使用自定义 Combo

现在,我们使用下面的代码使用 'cRect' 类型的 Combo:

const data = {
  nodes: [
    { id: 'node1', x: 250, y: 100, comboId: 'combo1' },
    { id: 'node2', x: 300, y: 100, comboId: 'combo1' },
  ],
  combos: [
    { id: 'combo1', label: 'Combo 1', parentId: 'combo2' },
    { id: 'combo2', label: 'Combo 2' },
    { id: 'combo3', label: 'Combo 3' },
  ],
};
const graph = new F6.Graph({
  ...
  width: 800,
  height: 800,
  // 全局 Combo 配置
  defaultCombo: {
    // 指定 Combo 类型,也可以将 type 写到 combo 数据中
    type: 'cRect',
    // ... 此处可配置默认 Combo 的其他样式
  },
});
graph.data(data);
graph.render();
img

2. 自定义扩展内置 Circle Combo

Demo

内置 Circle Combo 位置逻辑详解

如下面 Circle Combo 位置说明图所示,circle 类型的 Combo 内部的位置逻辑比 rect 类型简单,其 (x, y) 为圆心,padding 为一个数值:

  • 下图灰色虚线圈内部是子元素的分布范围,其半径为 innerR;
  • 与 rect 不同的是,灰色虚线圈的 padding 是一个数值,即灰色虚线圈外围的 padding 是均匀的,该 Combo 的 keyShape 真实绘制半径 R = innerR + padding;
  • 一个 Combo 内部的图形以自身坐标系为参考,原点 (0, 0) 在灰色虚线框正中心(由于 padding 是均匀的,所以原点也在 keyShape 正中心);
  • circle 图形的 x 与 y 为其圆心 (0, 0);
  • circle 类型 Combo 的 label 默认位于圆形外部正上方,距离圆形上边缘 refY。label 的位置(position)、refXrefY 可以在使用该类型 Combo 时配置。
img

Circle Combo 位置说明图

绘制图形

现在,我们自己实现一个如下图所示的 Combo 类型(下图展示空 Combo):

img
// 定义下面需要使用的 symbol
const collapseIcon = (x, y, r) => {
  return [
    ['M', x - r, y],
    ['a', r, r, 0, 1, 0, r * 2, 0],
    ['a', r, r, 0, 1, 0, -r * 2, 0],
    ['M', x - r + 4, y],
    ['L', x - r + 2 * r - 4, y],
  ];
};
const expandIcon = (x, y, r) => {
  return [
    ['M', x - r, y],
    ['a', r, r, 0, 1, 0, r * 2, 0],
    ['a', r, r, 0, 1, 0, -r * 2, 0],
    ['M', x - r + 4, y],
    ['L', x - r + 2 * r - 4, y],
    ['M', x - r + r, y - r + 4],
    ['L', x, y + r - 4],
  ];
};

F6.registerCombo(
  'cCircle',
  {
    drawShape: function draw(cfg, group) {
      const self = this;
      // 获取样式配置,style.r 是加上了 padding 的半径
      // 对应 Circle Combo 位置说明图中的 R
      const style = self.getShapeStyle(cfg);
      // 绘制一个 circle 作为 keyShape,与 'circle' Combo 的 keyShape 一致
      const circle = group.addShape('circle', {
        attrs: {
          ...style,
          x: 0,
          y: 0,
          r: style.r,
        },
        draggable: true,
        name: 'combo-keyShape',
      });
      // 增加下方 marker
      const marker = group.addShape('marker', {
        attrs: {
          ...style,
          fill: '#fff',
          opacity: 1,
          x: 0,
          y: style.r,
          r: 10,
          symbol: collapseIcon,
        },
        draggable: true,
        name: 'combo-marker-shape',
      });

      return circle;
    },
    // 定义新增的下方 marker 的位置更新逻辑
    afterUpdate: function afterUpdate(cfg, combo) {
      const self = this;
      // 获取样式配置,style.r 是加上了 padding 的半径
      // 对应 Circle Combo 位置说明图中的 R    const style = self.getShapeStyle(cfg);
      const group = combo.get('group');
      // 在该 Combo 的图形分组根据 name 找到下方 marker
      const marker = group.find((ele) => ele.get('name') === 'combo-marker-shape');
      // 更新 marker
      marker.attr({
        x: 0,
        y: style.r,
        // 数据中的 collapsed 代表该 Combo 是否是收缩状态,根据该字段更新 symbol
        symbol: cfg.collapsed ? expandIcon : collapseIcon,
      });
    },
  },
  'circle',
);

值得注意的是,F6 3.3 需要用户为自定义节点中的图形设置 namedraggable。其中,name 可以是不唯一的任意值。draggabletrue 是表示允许该图形响应拖拽事件,只有 draggable: true 时,图上的交互行为 'drag-combo' 才能在该图形上生效。若上面代码仅在 keyShape 上设置了 draggable: true,而右侧圆图形上没有设置,则鼠标拖拽只能在 keyShape 上响应。

使用自定义 Combo

现在,我们使用下面的代码使用 'cCircle' 类型的 Combo:

const data = {
  nodes: [
    { id: 'node1', x: 250, y: 100, comboId: 'combo1' },
    { id: 'node2', x: 300, y: 100, comboId: 'combo1' },
  ],
  combos: [
    { id: 'combo1', label: 'Combo 1', parentId: 'combo2' },
    { id: 'combo2', label: 'Combo 2' },
    { id: 'combo3', label: 'Combo 3' },
  ],
};
const graph = new F6.Graph({
  ...
  width: 800,
  height: 800,
  // 全局 Combo 配置
  defaultCombo: {
    // 指定 Combo 类型,也可以将 type 写到 combo 数据中
    type: 'cCircle',
    labelCfg: {
      refY: 2,
    },
    // ... 此处可配置默认 Combo 的其他样式
  },
  modes: {
    default: [
      // 配置展开/收缩 Combo 交互,双击 Combo 可以触发
      // 将会修改响应 Combo 数据中的 collapsed 字段,从而标识该 Combo 是否处于收缩状态
      'collapse-expand-combo',
    ],
  },
});
graph.data(data);
graph.render();
img

自定义交互

在上面代码中,实例化图时为图配置了 'collapse-expand-combo' 交互,即双击 Combo 可以展开和收起。若我们希望在单击 Combo 下方的 marker 时,展开/收起 Combo,则可以去掉 'collapse-expand-combo' 配置,并添加如下监听代码:

// collapse/expand when click the marker
graph.on('combo:tap', (e) => {
  if (e.target.get('name') === 'combo-marker-shape') {
    // Collapse or expand the combo
    graph.collapseExpandCombo(e.item);

    if (graph.get('layout')) graph.layout();
    // If there is a layout configured on the graph, relayout
    else graph.refreshPositions(); // Refresh positions for items otherwise
  }
});