Fabric.js 实现表格 table

分类:JavaScript     发布时间:2023-09-20     最后更新:2023-09-20     浏览数:840
最近接到一个项目,需要基于 Fabric.js 实现一个表格,百度、Bing、谷歌搜索了一番,没有找到合适的,无法直接白嫖,那只能自己撸起袖子干。

起因

最近接到一个项目,需要基于 Fabric.js 实现一个表格,百度、Bing、谷歌搜索了一番,没有找到合适的,无法直接白嫖,那只能自己撸起袖子干。

原理分析

一个表格由一个个单元格组成,如下图。

image1

从第二列开始,单元格的左边和前一列的单元右边重合; 从第二行开始,单元格的上边和前一行的单元格的底边重合; 那么一个障眼法的表格就出来了。

任务拆解

实现单元格

在 Fabric.js 内,单元格可以用一个 Group 包住 RectTextbox 来实现。其中单元格的边框由Rect 的边框实现,单元格里面文本由 Textbox 实现。当双击单元格时,使 Textbox 进入编辑模式。

// table.js
import { fabric } from 'fabric'

// 定义自定义的单元格类
fabric.TableCell = fabric.util.createClass(fabric.Group, {
  type: 'tableCell',
  text: null,
  rect: null,
  initialize: function (rectOptions, textOptions, text) {
    this.rect = new fabric.Rect(rectOptions)
    this.text = new fabric.Textbox(text, {
      ...textOptions,
      selectable: false,
    })
    this.on('mousedblclick', () => {
      this.selectable = false
      this.text.selectable = true
      this.canvas.setActiveObject(this.text)
      this.text.enterEditing()
    })
    this.on('mousedown', () => {
      // this.rect.set('fill', 'green')
      // this.canvas.requestRenderAll()
    })
    this.text.on('editing:exited', () => {
      this.text.selectable = false
      this.selectable = true
    })

    this.callSuper('initialize', [this.rect, this.text], {
      subTargetCheck: true,
      objectCaching: false
    })
  },
  toObject: function () {
    return fabric.util.object.extend(this.callSuper('toObject'), {
      text: this.text.get('text')
    })
  }
})

实现表格

一个表格由一个 Group 包裹 rowHeights.length * columnWidths.lengthTableCell 组成。 由于每一列的宽度会不一样,所以要算出下一列的 left 值;同理每一行的高度也会不一样,同样需要算出下一行所有单元格的 top

由于会有单元格合并的情况存在,所以用 ignore 记住哪些单元格是不用渲染的。

// table.js
fabric.Table = fabric.util.createClass(fabric.Group, {
  type: 'table',
  borderWidth: 4, // 单元格边框宽度
  rowHeights: [], // 行高,每一行的高度可以自由配置
  columnWidths: [], // 列宽,每一列的宽度可以自由配置
  cells: [], // 每一个单元格。比较重要的字段有 row, col 分别表示行、列的索引,从 0 开始,rowSpan 代表单元格横跨的行数,colSpan 代表单元格横跨的列数,content 代表单元格内的文字
  initialize: function (tableOptions) {
    const { left, top, borderWidth, rowHeights, columnWidths, cells } =
      tableOptions

    const rectOptions = {
      rx: 0,
      stroke: '#000',
      fill: 'transparent',
      shadow: 0,
      strokeWidth: +borderWidth,
      strokeUniform: true
    }

    const textOptions = {
      lineHeight: 1,
      fontSize: 20,
      stroke: '#000',
      fill: '#000',
      selectable: false,
      textAlign: 'center',
      editingBorderColor: '#FF9002'
    }

    const ignore = []
    const cellCmps = []
    let totalH = 0
    for (let i = 0; i < rowHeights.length; i++) {
      let totalW = 0
      for (let j = 0; j < columnWidths.length; j++) {
        let {
          rowSpan = 0,
          colSpan = 0,
          content
        } = cells.find((cell) => cell.row == i && cell.col == j) || {}
        const height = rowHeights[i]
        const width = columnWidths[j]
        rowSpan = Math.min(rowHeights.length - 1, rowSpan)
        colSpan = Math.min(columnWidths.length - 1, colSpan)

        let w = width
        let h = height
        let rowSpanHeight = 0
        let colSpanHeight = 0

        //@TODO 以下 3 个 for 有很大的优化空间。
        for (let rs = 1; rs <= rowSpan; rs++) {
          rowSpanHeight += rowHeights[i + rs]
          ignore.push(`${i + rs}:${j}`)
        }

        for (let rs = 1; rs <= colSpan; rs++) {
          colSpanHeight += columnWidths[j + rs]
          ignore.push(`${i}:${j + rs}`)
        }

        if (rowSpan > 0 && colSpan > 0) {
          for (let r = 1; r <= rowSpan; r++) {
            for (let c = 1; c <= colSpan; c++) {
              ignore.push(`${i + r}:${j + c}`)
            }
          }
        }

        h += rowSpanHeight
        w += colSpanHeight

        if (ignore.includes(`${i}:${j}`)) {
          totalW += width
          continue
        }

        const cell = new fabric.TableCell(
          {
            ...rectOptions,
            left: totalW + 1,
            top: totalH,
            width: w,
            height: h
          },
          {
            ...textOptions,
            left: totalW + 1 + rectOptions.strokeWidth,
            top: totalH + rectOptions.strokeWidth,
            width: w,
            height: h
          },
          content || `${i}-${j}`
        )
        cellCmps.push(cell)
        totalW += width
      }
      totalH += rowHeights[i]
    }

    this.borderWidth = borderWidth
    this.rowHeights = rowHeights
    this.columnWidths = columnWidths
    this.cells = cells
    this.callSuper('initialize', cellCmps, {
      subTargetCheck: true,
      objectCaching: false,
      left,
      top
    })
  },
  toObject: function () {
    return fabric.util.object.extend(this.callSuper('toObject'), {
      borderWidth: this.get('borderWidth'),
      rowHeights: this.get('rowHeights'),
      columnWidths: this.get('columnWidths'),
      cells: this.get('cells')
    })
  }
})

fabric.Table.fromObject = function (o, callback) {
  const options = {
    left: o.left,
    top: o.top,
    rowHeights: o.rowHeights,
    columnWidths: o.columnWidths,
    borderWidth: o.borderWidth,
    cells: o.cells,
  }
  const newTable = new fabric.Table(options)
  callback(newTable)
}

Demo

import { fabric } from 'fabric'

class PcEditor {
  canvas;

  constructor(canvasId) {
    this.init(canvasId);
  }

  /**
   * 初始化画板
   * @param {string} canvasId
   */
  init(canvasId) {
    const canvas = new fabric.Canvas(canvasId, {
      stopContextMenu: true,
      controlsAboveOverlay: true,
      preserveObjectStacking: true,
      altSelectionKey: "altKey",
    });
    this.canvas = canvas;

    this.addTable()
  }

  addTable() {
    const options = {
      left: 20,
      top: 20,
      borderWidth: 4,
      rowHeights: [80, 120],
      columnWidths: [160, 100, 100],
      cells: [
        {
          row: 0,
          col: 0,
          rowSpan: 0,
          colSpan: 2,
          content: "遥遥领先"
        },
        {
          row: 0,
          col: 1,
          rowSpan: 0,
          colSpan: 0,
          content: ""
        },
        {
          row: 0,
          col: 2,
          rowSpan: 0,
          colSpan: 0,
          content: ""
        },
        {
          row: 1,
          col: 0,
          rowSpan: 0,
          colSpan: 0,
          content: ""
        },
        {
          row: 1,
          col: 1,
          rowSpan: 0,
          colSpan: 0,
          content: ""
        },
        {
          row: 1,
          col: 2,
          rowSpan: 0,
          colSpan: 0,
          content: ""
        }
      ]
    };
    const canvas = this.canvas;
    const center = canvas.getCenter();
    const table = new fabric.Table(options);
    table.set("left", center.left - table.width / 2);
    table.set("top", center.top - table.height / 2);
    canvas.add(table);
  }
}

new PcEditor("canvasBox");

image2

扩展阅读

上一篇: 网页 JavaScript 如何编码 gb2312 下一篇: macOS 查看已连接过的 wifi 密码