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.
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();
}
});