Search code examples
javascriptreactjscanvasfabricjs

Editable table with fabricjs


I'm trying to create a table with TextBox or IText which can be edited with fabricjs. I was able to create a table view by combining multiple Textboxes with background colours. according to the design, table should have rounded corners. Any Idea how to add those rounded corners? enter image description here


Solution

  • using custom classes (Rect + TextBox) was able to find a solution for this.

    const canvas = new fabric.Canvas('c')
    
    fabric.RectWithText = fabric.util.createClass(fabric.Rect, {
        type: 'rectWithText',
        text: null,
        textOffsetLeft: 0,
        textOffsetTop: 0,
        _prevObjectStacking: null,
        _prevAngle: 0,
        
        type: "roundedRect",
      topLeft: this.topLeft || [0,0],
      topRight: this.topRight  || [0,0],
      bottomLeft: this.bottomLeft  || [0,0],
      bottomRight: this.bottomRight  || [0,0],
    
      _render: function(ctx) {
        var w = this.width,
          h = this.height,
          x = -this.width / 2,
          y = -this.height / 2,
          /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */
          k = 1 - 0.5522847498;
        ctx.beginPath();
    
        // top left
        ctx.moveTo(x + this.topLeft[0], y);
    
        // line to top right
        ctx.lineTo(x + w - this.topRight[0], y);
        ctx.bezierCurveTo(x + w - k * this.topRight[0], y, x + w, y + k * this.topRight[1], x + w, y + this.topRight[1]);
    
        // line to bottom right
        ctx.lineTo(x + w, y + h - this.bottomRight[1]);
        ctx.bezierCurveTo(x + w, y + h - k * this.bottomRight[1], x + w - k * this.bottomRight[0], y + h, x + w - this.bottomRight[0], y + h);
    
        // line to bottom left
        ctx.lineTo(x + this.bottomLeft[0], y + h);
        ctx.bezierCurveTo(x + k * this.bottomLeft[0], y + h, x, y + h - k * this.bottomLeft[1], x, y + h - this.bottomLeft[1]);
    
        // line to top left
        ctx.lineTo(x, y + this.topLeft[1]);
        ctx.bezierCurveTo(x, y + k * this.topLeft[1], x + k * this.topLeft[0], y, x + this.topLeft[0], y);
    
        ctx.closePath();
    
        this._renderPaintInOrder(ctx);
      },
      
        recalcTextPosition: function () {
          const sin = Math.sin(fabric.util.degreesToRadians(this.angle))
          const cos = Math.cos(fabric.util.degreesToRadians(this.angle))
          const newTop = sin * this.textOffsetLeft + cos * this.textOffsetTop
          const newLeft = cos * this.textOffsetLeft - sin * this.textOffsetTop
          const rectLeftTop = this.getPointByOrigin('left', 'top')
          this.text.set('left', rectLeftTop.x + newLeft)
          this.text.set('top', rectLeftTop.y + newTop)
        },
        
        initialize: function (rectOptions, textOptions, text) {
          this.callSuper('initialize', rectOptions)
          this.text = new fabric.Textbox(text, {
            ...textOptions,
            selectable: false,
            evented: false,
          })
          this.textOffsetLeft = this.text.left - this.left
          this.textOffsetTop = this.text.top - this.top
          this.on('moving', () => {
            this.recalcTextPosition()
          })
          this.on('rotating', () => {
            this.text.rotate(this.text.angle + this.angle - this._prevAngle)
            this.recalcTextPosition()
            this._prevAngle = this.angle
          })
          this.on('scaling', (e) => {
            this.recalcTextPosition()
          })
          this.on('added', () => {
            this.canvas.add(this.text)
          })
          this.on('removed', () => {
            this.canvas.remove(this.text)
          })
          this.on('mousedown:before', () => {
            this._prevObjectStacking = this.canvas.preserveObjectStacking
            this.canvas.preserveObjectStacking = true
          })
          this.on('mousedblclick', () => {
            this.text.selectable = true
            this.text.evented = true
            this.canvas.setActiveObject(this.text)
            this.text.enterEditing()
            this.selectable = false
          })
          this.on('deselected', () => {
            this.canvas.preserveObjectStacking = this._prevObjectStacking
          })
          this.text.on('editing:exited', () => {
            this.text.selectable = false
            this.text.evented = false
            this.selectable = true
          })
        }
    })
    
    const rectOptions = {
      left: 10,
      topLeft: [20,20],
      top: 10,
      width: 200,
      height: 75,
      fill: 'rgba(30, 30, 30, 0.3)',
      rx: 10
    }
    const textOptions = {
      left: 35,
      top: 30,
      width: 150,
      fill: 'white',
      shadow: new fabric.Shadow({
        color: 'rgba(34, 34, 100, 0.4)',
        blur: 2,
        offsetX: -2,
        offsetY: 2
      }),
      fontSize: 30,
      selectable: true
    }
    const rectWithText = new fabric.RectWithText(rectOptions, textOptions, 'Some text')
    canvas.add(rectWithText)
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.2.4/fabric.min.js"></script>
    <canvas id="c" width="600" height="600"></canvas>