Create Dynamic Sections with Drag-and-Drop Functionality and Customizable Textboxes

Dinesh Rawat
calendar_month
February 12, 2024
timer
5 min
This is some text inside of a div block.

Fabric.js 5 is a powerful library that makes it easy to create and manipulate graphics and other visual elements on the web. One of the features it offers is the section component, which allows users to add, update, and drag textboxes within a rectangle. This feature is extremely useful for web developers who want to create visually appealing designs and layouts for their applications.

With Fabric.js 5, you can easily create a customizable section component that enhances the user experience and adds an extra layer of interactivity to your web pages. In this post, we'll explore how to create a section component with Fabric.js 5 and delve into the many ways in which it can be used to improve your website's design and functionality.

Fabric.js 5 and the Section Component

Fabric.js 5 is a powerful JavaScript library that provides developers with a wide range of features for building interactive web applications. With Fabric.js 5, developers can create and manipulate graphics, images, and other visual elements on the web page with ease.

One of the key features of Fabric.js 5 is its ability to create custom components that can be used to build complex user interfaces. The Section component is one such custom component that can be created with Fabric.js 5. The Section component is essentially a rectangular container that can hold one or more text boxes, which can be added, updated, and dragged around by the user.

The Section component offers several benefits to web developers. First and foremost, it provides a simple and intuitive way to organize text content on a web page. By using the Section component, developers can easily create visually appealing layouts that are easy to read and navigate.

Additionally, the Section component is highly customizable, allowing developers to easily adjust the size, shape, and appearance of the container to suit their specific needs. This level of flexibility makes it easy to integrate the Section component into a wide range of web applications.

Overall, the Section component is a powerful tool for web developers looking to create dynamic and engaging user interfaces. With its intuitive design and wide range of customization options, the Section component is sure to be a valuable asset for any web development project.

Creating a Rect, Input Field, and Add Button


let rectHeight = 200;
var rect = new fabric.Rect({
  left: 50,
  top: 100,
  fill: "#F8F8F8",
  width: 300,
  height: rectHeight,
  stroke: "black",
  strokeWidth: 1,
  originX: "left",
  originY: "top",
  id: Date.now()
});

var textbox = new fabric.TextboxWithPadding("Input field", {
  left: rect.left + 15,
  top: rect.top + 30,
  width: rect.width - 35,
  height: 30,
  fontSize: 16,
  fill: "black",
  backgroundColor: "#ffffff",
  borderColor: "#9181fc",
  //@ts-ignore
  originalWidth: rect.width - 35,
  originalTop: rect.top + 30,
  padding: 5,
  id: Date.now()
});
canvas.add(rect);
canvas.add(textbox);
textbox.setControlsVisibility({
  mtr: false,
  tr: false,
  tl: false,
  bl: false,
  br: false
});

textbox.set({
  //@ts-ignore
  originalWidth: rect.width,
  originalHeight: 30,
  originalTop: 110
});
var addButton = new fabric.Rect({
  left: rect.left,
  top: rect.top + rect.getScaledHeight() + 30,
  fill: "#E7E7E7",
  width: 60,
  height: 30,
  stroke: "black",
  strokeWidth: 0,
  corneradius: 5,
  className: "addButton"
});

var addText = new fabric.Text("+ Add", {
  left: addButton.left + addButton.getScaledWidth() / 2,
  top: addButton.top + addButton.getScaledHeight() / 2,
  fontSize: 14,
  fill: "#000000",
  originX: "center",
  originY: "center",
  //@ts-ignore
  id: Date.now()
});

var group = new fabric.Group([addButton, addText], {
  left: rect.left,
  top: rect.top + rect.getScaledHeight() + 30,
  originX: "left",
  originY: "center",
  subTargetCheck: true,
  lockMovementX: true,
  lockMovementY: true,
  lockScalingX: true,
  lockScalingY: true,
  hasControls: false,
  hasBorders: false,
  hoverCursor: "pointer",
  //@ts-ignore
  id: Date.now()
});

canvas.add(group);

var lastTextboxY = textbox.top + textbox.getScaledHeight() + 30;

The provided code creates a rectangular fabric object with a textbox, an add button, and a group of fabric objects on a canvas. The properties of each object are defined in the code, including their positioning, size, and colors.

This will make:

The code starts with an event listener that is triggered when the user clicks the mouse on the canvas. The listener checks if the mouse click targeted a specific group and, if so, repositions the textboxes within that group. Next, the code checks if the group contains an object with a specific class name (in this case, an 'addButton'). If it does, the code creates a new textbox with some default settings, such as its position and size, and adds it to the canvas.

After the new textbox is created, the code checks if it overlaps with any existing textboxes in the group. If there is no overlap, the code animates the textbox by setting its opacity to 0 and gradually increasing it to 1 throughout 200 milliseconds. The animation uses an easing function that starts slow, speeds up, and then slows down again. After the animation is complete, the code sets the textbox's coordinates and adjusts the size of the group to accommodate the new textbox.


canvas.on("mouse:down", function (options) {
  if (options.target && options.target === group) {
    repositionSectionTextboxes();

    // Check if the group contains a child with an ID of 'addButton'
    var addButton = group.getObjects().find(function (obj) {
      return obj.className === "addButton";
    });

    if (addButton && options.target === group) {
      var newTextbox = new fabric.TextboxWithPadding("Input field", {
        left: rect.left + 15,
        width: rect.getScaledWidth() - 35,
        top: lastTextboxY,
        height: textbox.getScaledHeight() || 30,
        fontSize: 16,
        // hasControls: false,
        originalTop: lastTextboxY,
        padding: 5,
        fill: "black",
        backgroundColor: "#ffffff",
        borderColor: "#9181fc",
        id: Date.now()
      });
      // Check if the new textbox will overlap with any existing textbox
      var overlapping = group.getObjects().some(function (obj) {
        return obj !== addButton && obj.intersectsWithObject(newTextbox);
      });

      if (!overlapping) {
        canvas.add(newTextbox);
        newTextbox
          .set({
            //@ts-ignore
            originalWidth: rect.width,
            originalHeight: 30,
            originalTop: lastTextboxY,
            opacity: 0 // Set opacity to 0 for fade in effect
          })
          .setCoords();

        // Animate the object to fade in
        newTextbox.animate("opacity", 1, {
          duration: 200, // Set the duration of animation (in milliseconds)
          onChange: canvas.renderAll.bind(canvas), // Re-render the canvas on each frame of animation
          onComplete: function () {
            newTextbox.setCoords(); // Set the final position of the object after animation
          },
          easing: fabric.util.ease.easeInOutCubic
        });

        newTextbox.setControlsVisibility({
          mtr: false,
          tr: false,
          tl: false,
          bl: false,
          br: false
        });
        // Calculate the required height for the rect based on the total height of all the textboxes
        var totalHeight =
          lastTextboxY + newTextbox.getScaledHeight() - rect.top + 30;
        rectHeight = rect.getScaledHeight();
        if (totalHeight > rectHeight) {
          rect
            .set({
              height: totalHeight / rect.scaleY
            })
            .setCoords();

          canvas.renderAll();
        }

        // Set the top position of the group based on the total height of the group
        group.set({
          top: rect.top + rect.getScaledHeight() + 30
        });

        group.setCoords();
        group.setCoords();

        canvas.renderAll();
        lastTextboxY = newTextbox.top + newTextbox.getScaledHeight() + 30;
      }
    }
  }
  if (options.target && options.target.type === "textbox") {
    let showControls = false;

    if (!rect.intersectsWithObject(options.target, true, true)) {
      showControls = true;
      options.target.hasBorders = true;
    }

    options.target.setControlsVisibility({
      mtr: showControls,
      tr: showControls,
      tl: showControls,
      bl: showControls,
      br: showControls
    });
    options.target.setCoords();
    canvas.requestRenderAll();
  }
});

canvas.on("object:scaling", function (options) {
  var scaledObject = options.target;
  scaledObject.setCoords();
  const isText = scaledObject.type === "textbox";
  if (isText || scaledObject.type === "image") {
    canvas.uniformScaling = true;
  } else {
    //@ts-ignore
    if (
      ["bl", "br", "tr", "tl"].indexOf(scaledObject.transform.corner) !== -1
    ) {
      canvas.uniformScaling = false;
    }
  }
  if (scaledObject === rect) {
    let width = rect.getScaledWidth() - 25;
    let totalHeight = 0;
    let count = 0;

    canvas.forEachObject(function (obj) {
      if (obj.type === "textbox") {
        totalHeight += obj.getScaledHeight() + 30;
        ++count;
      }
    });

    let averageHeight = totalHeight / count;
    let index = 0;
    canvas.forEachObject(function (obj) {
      if (obj.type === "textbox") {
        obj
          .set({
            width: width,
            top: rect.top + index * averageHeight + 30
          })
          .setCoords();
        obj.scaleToWidth(width);

        ++index;
      }
    });

    // update the lastTextboxY variable based on the new position of the last textbox
    let lastTextbox = canvas
      .getObjects()
      .filter((obj) => obj.type === "textbox")
      .pop();
    lastTextboxY = lastTextbox.top + lastTextbox.getScaledHeight() + 30;
  }

  repositionSectionTextboxes();

  let newTop = rect.top + rect.getScaledHeight() + 30;
  if (lastTextboxY > newTop) {
    newTop = lastTextboxY + 30;
  }

  // Set the top position of the group based on the total height of the group
  group
    .set({
      top: newTop
    })
    .setCoords();
  canvas.renderAll();
});

canvas.on("object:moving", function (options) {
  var movedObject = options.target;
  movedObject.setCoords();
  var shadow = new fabric.Shadow({
    color: "rgba(0, 0, 0, 0.2)",
    offsetX: 0,
    offsetY: 0.3,
    blur: 3
  });

  // Add shadow to the moving object
  movedObject.set({
    shadow
  });
  if (movedObject === rect) {
    const lastLeft = movedObject.get("lastLeft") || movedObject.left;
    const lastTop = movedObject.get("lastTop") || movedObject.top;

    var deltaX = movedObject.left - lastLeft;
    var deltaY = movedObject.top - lastTop;

    canvas.forEachObject(function (obj) {
      if (
        obj.type === "textbox" &&
        obj.intersectsWithObject(movedObject, true, true)
      ) {
        obj
          .set({
            left: obj.left + deltaX,
            top: obj.top + deltaY
          })
          .setCoords();
      }
    });

    var addButton = group.getObjects().find(function (obj) {
      return obj.className === "addButton";
    });

    if (addButton) {
      group
        .set({
          left: movedObject.left,
          top: movedObject.top + movedObject.getScaledHeight() + 30
        })
        .setCoords();
    }

    movedObject.set({
      lastLeft: movedObject.left,
      lastTop: movedObject.top
    });

    var lastTextbox = canvas
      .getObjects()
      .filter(function (obj) {
        return obj.type === "textbox";
      })
      .pop();

    if (lastTextbox) {
      lastTextboxY = lastTextbox.top + lastTextbox.getScaledHeight() + 30;
    }
  }

  // Check if the moving object is a textbox
  if (movedObject.type === "textbox") {
    var movingBounds = movedObject.getBoundingRect();
    // Loop through all objects on the canvas
    canvas.forEachObject(function (obj) {
      if (obj === movedObject) {
        return;
      }
      // Check if the object is a textbox and if it's overlapping with the moving object
      if (obj.type === "textbox" && movedObject.intersectsWithObject(obj)) {
        // Check if the overlapping object was moved down previously
        var objBounds = obj.getBoundingRect();
        let top = movingBounds.top + objBounds.height + 2;
        if (objBounds.top > movingBounds.top) {
          // Move the overlapping object back up to its original position
          top = movingBounds.top - objBounds.height;
        }
        if (top < rect.top) {
          top = rect.top;
        }
        obj.top = top;

        // Set the new position of the object and re-render the canvas
        obj.setCoords();
        canvas.renderAll();
      }
    });
  }
});

canvas.on("mouse:up", function () {
  repositionSectionTextboxes();
});

canvas.on("text:changed", function (options) {
  var textbox = options.target;
  textbox.bringToFront();
  canvas.renderAll();
});

The code also includes event listeners for scaling and moving objects on the canvas. When an object is scaled, the code sets a flag to indicate whether it is a textbox or an image. If it is a textbox, the code sets the canvas's uniformScaling property to true. If it is an image or any other object, the code sets uniformScaling to false. When an object is moved, the code updates its coordinates and repositions the textboxes in the group.

Copy & paste in the textbox inside the section


let clipboard = [];
let copyIndex = -1;
let copyObject = {};
// Add event listener for copy and paste
document.addEventListener("keydown", function (e) {
  if (e.keyCode === 67 && (e.ctrlKey || e.metaKey)) {
    // Ctrl + C or Command + C pressed
    const activeObject = canvas.getActiveObject();
    if (!activeObject) {
      return true;
    }

    copyIndex = canvas
      .getObjects()
      .findIndex((object) => object === activeObject); // Get the index of obj in getObjects()

    copyObject = activeObject;
    clipboard =
      activeObject && activeObject.getObjects
        ? activeObject.getObjects()
        : [activeObject];
  } else if (e.keyCode === 86 && (e.ctrlKey || e.metaKey)) {
    clipboard.forEach((object) => {
      if (object.type !== "textbox") {
        return true;
      }

      var newTextbox = new fabric.TextboxWithPadding(object.text, {
        left: rect.left + 15,
        width: rect.getScaledWidth() - 35,
        top: copyObject.top + 30,
        height: textbox.getScaledHeight() || 30,
        fontSize: 16,
        // hasControls: false,
        originalTop: lastTextboxY,
        padding: 5,
        fill: "black",
        backgroundColor: "#ffffff",
        borderColor: "#9181fc"
        // id: Date.now()
      });
      // Check if the new textbox will overlap with any existing textbox
      var overlapping = group.getObjects().some(function (obj) {
        return obj !== addButton && obj.intersectsWithObject(newTextbox);
      });

      if (!overlapping) {
        canvas.add(newTextbox);
        // insert new object at index position 2
        canvas.getObjects().splice(copyIndex, 0, newTextbox);

        newTextbox
          .set({
            //@ts-ignore
            originalWidth: rect.width,
            originalHeight: 30,
            originalTop: lastTextboxY,
            opacity: 0 // Set opacity to 0 for fade in effect
          })
          .setCoords();

        // Animate the object to fade in
        newTextbox.animate("opacity", 1, {
          duration: 200, // Set the duration of animation (in milliseconds)
          onChange: canvas.renderAll.bind(canvas), // Re-render the canvas on each frame of animation
          onComplete: function () {
            newTextbox.setCoords(); // Set the final position of the object after animation
          },
          easing: fabric.util.ease.easeInOutCubic
        });
        newTextbox.setControlsVisibility({
          mtr: false,
          tr: false,
          tl: false,
          bl: false,
          br: false
        });

        var topmostTextbox = canvas
          .getObjects()
          .filter(function (obj) {
            return obj.type === "textbox";
          })
          .sort(function (a, b) {
            return b.top - a.top;
          })[0];

        let top = topmostTextbox.top;
        if (!topmostTextbox.id || topmostTextbox.id === copyObject.id) {
          top -= 30;
        }
        newTextbox.set({
          id: Date.now()
        });
        rect
          .set({
            height: top
          })
          .setCoords();

        canvas.renderAll();
        // Set the top position of the group based on the total height of the group
        group.set({
          top: rect.top + rect.getScaledHeight() + 30
        });

        group.setCoords();
        canvas.renderAll();

        canvas.setActiveObject(newTextbox);
      }
      repositionSectionTextboxes();
    });
    clipboard = [];
    copyIndex = -1;
    copyObject = {};
    // update the lastTextboxY variable based on the new position of the last textbox
    let lastTextbox = canvas
      .getObjects()
      .filter((obj) => obj.type === "textbox")
      .pop();
    lastTextboxY = lastTextbox.top + lastTextbox.getScaledHeight();

    canvas.requestRenderAll();
  }
});

Working demo