Files
PyUIBuilder/src/canvas/canvas.js

1033 lines
37 KiB
JavaScript
Raw Normal View History

2024-08-08 16:21:19 +05:30
import React from "react"
2024-09-11 19:06:04 +05:30
2024-09-18 22:16:34 +05:30
import { DndContext } from '@dnd-kit/core'
2024-09-11 19:06:04 +05:30
import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons"
2024-09-12 22:09:13 +05:30
import { Button, Tooltip, Dropdown } from "antd"
2024-08-08 16:21:19 +05:30
2024-09-15 19:22:32 +05:30
import Droppable from "../components/utils/droppableDnd"
2024-08-08 16:21:19 +05:30
import Widget from "./widgets/base"
import Cursor from "./constants/cursor"
2024-09-14 16:03:26 +05:30
import CanvasToolBar from "./toolbar"
2024-08-08 22:49:14 +05:30
import { UID } from "../utils/uid"
import { removeDuplicateObjects } from "../utils/common"
2024-08-08 16:21:19 +05:30
2024-09-15 12:08:29 +05:30
import { WidgetContext } from './context/widgetContext'
2024-09-14 16:03:26 +05:30
// import {ReactComponent as DotsBackground} from "../assets/background/dots.svg"
2024-09-17 21:23:01 +05:30
// import DotsBackground from "../assets/background/dots.svg"
2024-09-18 22:16:34 +05:30
import { ReactComponent as DotsBackground } from "../assets/background/dots.svg"
2024-09-17 21:23:01 +05:30
2024-09-16 12:23:15 +05:30
import DroppableWrapper from "../components/draggable/droppable"
2024-09-16 22:04:24 +05:30
import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext"
import { DragWidgetProvider } from "./widgets/draggableWidgetContext"
2024-09-19 19:26:10 +05:30
import { PosType } from "./constants/layouts"
import WidgetContainer from "./constants/containers"
import { isSubClassOfWidget } from "../utils/widget"
import { ButtonModal } from "../components/modals"
2024-09-14 16:03:26 +05:30
// const DotsBackground = require("../assets/background/dots.svg")
2024-08-08 16:21:19 +05:30
2024-09-17 21:23:01 +05:30
2024-09-10 21:34:05 +05:30
const CanvasModes = {
DEFAULT: 0,
PAN: 1,
MOVE_WIDGET: 2 // when the mode is move widget
}
2024-08-08 16:21:19 +05:30
class Canvas extends React.Component {
2024-09-16 22:04:24 +05:30
// static contextType = ActiveWidgetContext
2024-08-08 16:21:19 +05:30
constructor(props) {
super(props)
const { canvasWidgets, onWidgetListUpdated } = props
2024-09-18 22:16:34 +05:30
this.canvasRef = React.createRef()
2024-08-08 16:21:19 +05:30
this.canvasContainerRef = React.createRef()
2024-09-18 22:16:34 +05:30
2024-09-10 21:34:05 +05:30
this.currentMode = CanvasModes.DEFAULT
2024-09-18 22:16:34 +05:30
this.minCanvasSize = { width: 500, height: 500 }
2024-08-08 16:21:19 +05:30
this.mousePressed = false
this.mousePos = {
x: 0,
y: 0
}
2024-09-12 22:09:13 +05:30
// this._contextMenuItems = []
2024-09-17 21:23:01 +05:30
this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...}
2024-09-12 22:09:13 +05:30
2024-08-08 16:21:19 +05:30
this.state = {
widgetResizing: "", // set this to "nw", "sw" etc based on the side when widgets resizing handles are selected
2024-09-19 19:26:10 +05:30
widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", initialData: {}}]
2024-08-08 16:21:19 +05:30
zoom: 1,
isPanning: false,
currentTranslate: { x: 0, y: 0 },
2024-09-18 22:16:34 +05:30
canvasSize: { width: 500, height: 500 },
2024-09-13 16:03:58 +05:30
contextMenuItems: [],
selectedWidget: null,
2024-09-18 22:16:34 +05:30
toolbarOpen: true,
2024-09-15 12:08:29 +05:30
toolbarAttrs: null
2024-09-13 16:03:58 +05:30
}
this._onWidgetListUpdated = onWidgetListUpdated // a function callback when the widget is added to the canvas
2024-08-08 16:21:19 +05:30
this.resetTransforms = this.resetTransforms.bind(this)
2024-08-08 22:49:14 +05:30
this.renderWidget = this.renderWidget.bind(this)
2024-09-18 22:16:34 +05:30
this.mouseDownEvent = this.mouseDownEvent.bind(this)
this.mouseMoveEvent = this.mouseMoveEvent.bind(this)
this.mouseUpEvent = this.mouseUpEvent.bind(this)
2024-09-16 22:04:24 +05:30
this.keyDownEvent = this.keyDownEvent.bind(this)
this.wheelZoom = this.wheelZoom.bind(this)
2024-09-15 12:08:29 +05:30
this.onActiveWidgetUpdate = this.onActiveWidgetUpdate.bind(this)
this.getWidgets = this.getWidgets.bind(this)
this.getActiveObjects = this.getActiveObjects.bind(this)
this.getWidgetFromTarget = this.getWidgetFromTarget.bind(this)
2024-09-10 21:34:05 +05:30
this.getCanvasObjectsBoundingBox = this.getCanvasObjectsBoundingBox.bind(this)
this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this)
2024-09-15 22:54:53 +05:30
this.getCanvasContainerBoundingRect = this.getCanvasContainerBoundingRect.bind(this)
this.getCanvasBoundingRect = this.getCanvasBoundingRect.bind(this)
2024-09-10 21:34:05 +05:30
2024-09-13 19:24:03 +05:30
this.setSelectedWidget = this.setSelectedWidget.bind(this)
this.deleteSelectedWidgets = this.deleteSelectedWidgets.bind(this)
this.removeWidget = this.removeWidget.bind(this)
this.clearSelections = this.clearSelections.bind(this)
this.clearCanvas = this.clearCanvas.bind(this)
2024-08-08 16:21:19 +05:30
this.createWidget = this.createWidget.bind(this)
2024-08-08 16:21:19 +05:30
// this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this)
}
componentDidMount() {
this.initEvents()
}
componentWillUnmount() {
2024-09-18 22:16:34 +05:30
2024-09-16 22:04:24 +05:30
this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent)
this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent)
this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent)
this.canvasContainerRef.current.removeEventListener("wheel", this.wheelZoom)
this.canvasContainerRef.current.removeEventListener("keydown", this.keyDownEvent)
// NOTE: this will clear the canvas
this.clearCanvas()
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
initEvents() {
2024-09-16 22:04:24 +05:30
this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent)
this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent)
this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent)
this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom)
2024-09-18 22:16:34 +05:30
2024-09-16 22:04:24 +05:30
this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true)
// window.addEventListener("keydown", this.keyDownEvent, true)
2024-09-18 22:16:34 +05:30
2024-09-16 22:04:24 +05:30
}
2024-09-18 22:16:34 +05:30
applyTransform() {
2024-09-16 22:04:24 +05:30
const { currentTranslate, zoom } = this.state
this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})`
}
2024-09-18 22:16:34 +05:30
/**
*
* @returns {import("./widgets/base").Widget[]}
*/
getWidgets() {
2024-08-08 16:21:19 +05:30
return this.state.widgets
}
/**
* returns list of active objects / selected objects on the canvas
2024-08-08 16:21:19 +05:30
* @returns Widget[]
*/
2024-09-18 22:16:34 +05:30
getActiveObjects() {
return Object.values(this.widgetRefs).filter((widgetRef) => {
return widgetRef.current?.isSelected()
})
}
2024-08-08 16:21:19 +05:30
/**
* returns the widget that contains the target
* @param {HTMLElement} target
* @returns {Widget}
*/
2024-09-18 22:16:34 +05:30
getWidgetFromTarget(target) {
2024-09-19 19:26:10 +05:30
// TODO: improve search, currently O(n), but can be improved via this.state.widgets or something
let innerWidget = null
2024-09-18 22:16:34 +05:30
for (let [key, ref] of Object.entries(this.widgetRefs)) {
if (ref.current === target){
innerWidget = ref.current
break
}
2024-09-18 22:16:34 +05:30
if (ref.current.getElement().contains(target)) {
2024-09-19 19:26:10 +05:30
if (!innerWidget) {
innerWidget = ref.current
2024-09-19 19:26:10 +05:30
} else if (innerWidget.getElement().contains(ref.current.getElement())) {
// If the current widget is deeper than the existing innermost widget, update innerWidget
innerWidget = ref.current
2024-09-19 19:26:10 +05:30
}
}
}
2024-08-08 16:21:19 +05:30
2024-09-19 19:26:10 +05:30
return innerWidget
}
2024-08-08 16:21:19 +05:30
2024-09-18 22:16:34 +05:30
keyDownEvent(event) {
2024-09-16 22:04:24 +05:30
2024-09-18 22:16:34 +05:30
if (event.key === "Delete") {
2024-09-16 22:04:24 +05:30
this.deleteSelectedWidgets()
}
2024-09-18 22:16:34 +05:30
if (event.key === "+") {
2024-09-16 22:04:24 +05:30
this.setZoom(this.state.zoom + 0.1)
}
2024-09-18 22:16:34 +05:30
if (event.key === "-") {
2024-09-16 22:04:24 +05:30
this.setZoom(this.state.zoom - 0.1)
}
}
2024-08-08 16:21:19 +05:30
2024-09-18 22:16:34 +05:30
mouseDownEvent(event) {
2024-09-12 22:09:13 +05:30
this.mousePos = { x: event.clientX, y: event.clientY }
2024-09-18 22:16:34 +05:30
let selectedWidget = this.getWidgetFromTarget(event.target)
2024-09-19 19:26:10 +05:30
// console.log("selected widget: ", selectedWidget)
2024-09-18 22:16:34 +05:30
if (event.button === 0) {
2024-09-12 22:09:13 +05:30
this.mousePressed = true
2024-09-18 22:16:34 +05:30
if (selectedWidget) {
2024-09-12 22:09:13 +05:30
// if the widget is selected don't pan, instead move the widget
2024-09-18 22:16:34 +05:30
if (!selectedWidget._disableSelection) {
// console.log("selected widget2: ", selectedWidget.getId(), this.state.selectedWidget?.getId())
this.state.selectedWidget?.deSelect() // deselect the previous widget before adding the new one
this.state.selectedWidget?.setZIndex(0)
selectedWidget.setZIndex(1000)
selectedWidget.select()
this.setState({
selectedWidget: selectedWidget,
toolbarAttrs: selectedWidget.getToolbarAttrs()
})
// if (!this.state.selectedWidget || (selectedWidget.getId() !== this.state.selectedWidget?.getId())) {
// this.state.selectedWidget?.deSelect() // deselect the previous widget before adding the new one
// this.state.selectedWidget?.setZIndex(0)
// console.log("working: ", this.state.selectedWidget)
// selectedWidget.setZIndex(1000)
// selectedWidget.select()
// console.log("widget selected")
// this.setState({
// selectedWidget: selectedWidget,
// toolbarAttrs: selectedWidget.getToolbarAttrs()
// })
// }
2024-09-12 22:09:13 +05:30
this.currentMode = CanvasModes.MOVE_WIDGET
}
2024-08-08 16:21:19 +05:30
2024-09-12 22:09:13 +05:30
this.currentMode = CanvasModes.PAN
2024-09-18 22:16:34 +05:30
} else if (!selectedWidget) {
2024-09-12 22:09:13 +05:30
// get the canvas ready to pan, if there are widgets on the canvas
this.clearSelections()
this.currentMode = CanvasModes.PAN
this.setCursor(Cursor.GRAB)
// console.log("clear selection")
2024-09-12 22:09:13 +05:30
}
this.setState({
2024-09-13 16:03:58 +05:30
contextMenuItems: [],
toolbarOpen: true
2024-09-12 22:09:13 +05:30
})
// this.setState({
// showContextMenu: false
// })
2024-09-18 22:16:34 +05:30
} else if (event.button === 2) {
2024-09-12 22:09:13 +05:30
//right click
2024-09-18 22:16:34 +05:30
if (this.state.selectedWidget && this.state.selectedWidget.__id !== selectedWidget.__id) {
this.clearSelections()
}
2024-09-18 22:16:34 +05:30
if (selectedWidget) {
2024-09-13 16:03:58 +05:30
2024-09-12 22:09:13 +05:30
this.setState({
selectedWidget: selectedWidget,
contextMenuItems: [
{
key: "rename",
label: (<div onClick={() => selectedWidget.openRenaming()}><EditOutlined /> Rename</div>),
icons: <EditOutlined />,
},
{
key: "delete",
label: (<div onClick={() => this.deleteSelectedWidgets([selectedWidget])}><DeleteOutlined /> Delete</div>),
icons: <DeleteOutlined />,
danger: true
}
]
2024-09-12 22:09:13 +05:30
})
2024-09-18 22:16:34 +05:30
2024-09-14 16:03:26 +05:30
}
2024-09-12 22:09:13 +05:30
}
}
2024-08-08 16:21:19 +05:30
2024-09-18 22:16:34 +05:30
mouseMoveEvent(event) {
2024-09-18 22:16:34 +05:30
if (this.state.widgetResizing !== "") {
// if resizing is taking place don't do anything else
this.handleResize(event)
return
}
// console.log("mode: ", this.currentMode, this.getActiveObjects())
2024-09-10 21:34:05 +05:30
if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) {
const deltaX = event.clientX - this.mousePos.x
const deltaY = event.clientY - this.mousePos.y
2024-09-18 22:16:34 +05:30
if (!this.state.selectedWidget) {
2024-09-10 21:34:05 +05:30
// if there aren't any selected widgets, then pan the canvas
2024-08-08 16:21:19 +05:30
this.setState(prevState => ({
currentTranslate: {
x: prevState.currentTranslate.x + deltaX,
y: prevState.currentTranslate.y + deltaY,
}
}), this.applyTransform)
2024-09-10 21:34:05 +05:30
2024-09-18 22:16:34 +05:30
} else {
// update the widgets position
2024-09-17 21:23:01 +05:30
// this.state.selectedWidgets.forEach(widget => {
// const {x, y} = widget.getPos()
2024-08-08 16:21:19 +05:30
2024-09-17 21:23:01 +05:30
// const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas
// const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas
// widget.setPos(newPosX, newPosY)
// })
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
this.mousePos = { x: event.clientX, y: event.clientY }
2024-08-08 16:21:19 +05:30
2024-09-18 22:16:34 +05:30
this.setCursor(Cursor.GRAB)
}
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
mouseUpEvent(event) {
this.mousePressed = false
2024-09-10 21:34:05 +05:30
this.currentMode = CanvasModes.DEFAULT
this.setCursor(Cursor.DEFAULT)
if (this.state.widgetResizing) {
this.setState({ widgetResizing: "" })
}
2024-09-20 22:07:22 +05:30
2024-09-21 18:37:28 +05:30
for (let [key, widget] of Object.entries(this.widgetRefs)) {
2024-09-20 22:07:22 +05:30
// since the mouseUp event is not triggered inside the widget once its outside,
// we'll need a global mouse up event to re-enable drag
widget.current.enableDrag()
}
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
wheelZoom(event) {
2024-08-08 16:21:19 +05:30
let delta = event.deltaY
let zoom = this.state.zoom * 0.999 ** delta
2024-09-18 22:16:34 +05:30
this.setZoom(zoom, { x: event.offsetX, y: event.offsetY })
2024-09-10 21:34:05 +05:30
}
/**
* handles widgets resizing
* @param {MouseEvent} event - mouse move event
* @returns
*/
handleResize = (event) => {
if (this.state.resizing === "") return
const widget = this.state.selectedWidget
if (!widget) return
const resizeCorner = this.state.widgetResizing
const size = widget.getSize()
const pos = widget.getPos()
const deltaX = event.movementX
const deltaY = event.movementY
let newSize = { ...size }
let newPos = { ...pos }
const { width: minWidth, height: minHeight } = widget.minSize
const { width: maxWidth, height: maxHeight } = widget.maxSize
// console.log("resizing: ", deltaX, deltaY, event)
switch (resizeCorner) {
case "nw":
newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX))
newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY))
newPos.x += (newSize.width !== size.width) ? deltaX : 0
newPos.y += (newSize.height !== size.height) ? deltaY : 0
break
case "ne":
newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX))
newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY))
newPos.y += (newSize.height !== size.height) ? deltaY : 0
break
case "sw":
newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX))
newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY))
newPos.x += (newSize.width !== size.width) ? deltaX : 0
break
case "se":
newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX))
newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY))
break
default:
break
2024-09-18 22:16:34 +05:30
}
widget.setResize(newPos, newSize)
}
2024-09-18 22:16:34 +05:30
getCanvasContainerBoundingRect() {
return this.canvasContainerRef.current.getBoundingClientRect()
}
2024-09-18 22:16:34 +05:30
getCanvasBoundingRect() {
return this.canvasRef.current.getBoundingClientRect()
}
2024-09-18 22:16:34 +05:30
getCanvasTranslation() {
return this.state.currentTranslate
}
2024-08-08 16:21:19 +05:30
/**
* fits the canvas size to fit the widgets bounding box
*/
2024-09-18 22:16:34 +05:30
fitCanvasToBoundingBox(padding = 0) {
2024-09-10 21:34:05 +05:30
const { top, left, right, bottom } = this.getCanvasObjectsBoundingBox()
const width = right - left
const height = bottom - top
const newWidth = Math.max(width + padding, this.minCanvasSize.width)
const newHeight = Math.max(height + padding, this.minCanvasSize.height)
const canvasStyle = this.canvasRef.current.style
// Adjust the canvas dimensions
canvasStyle.width = `${newWidth}px`
canvasStyle.height = `${newHeight}px`
// Adjust the canvas position if needed
canvasStyle.left = `${left - padding}px`
canvasStyle.top = `${top - padding}px`
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
setCursor(cursor) {
2024-08-08 16:21:19 +05:30
this.canvasContainerRef.current.style.cursor = cursor
}
2024-09-18 22:16:34 +05:30
setZoom(zoom, pos) {
2024-08-08 16:21:19 +05:30
const { currentTranslate } = this.state
2024-09-16 22:04:24 +05:30
let newTranslate = currentTranslate
2024-09-18 22:16:34 +05:30
if (pos) {
2024-09-16 22:04:24 +05:30
// Calculate the new translation to zoom into the mouse position
const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x)
const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y)
2024-09-18 22:16:34 +05:30
2024-09-16 22:04:24 +05:30
const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom)
const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom)
newTranslate = {
2024-08-08 16:21:19 +05:30
x: newTranslateX,
y: newTranslateY
}
2024-09-16 22:04:24 +05:30
}
this.setState({
zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5
currentTranslate: newTranslate
2024-08-08 16:21:19 +05:30
}, this.applyTransform)
2024-09-11 19:06:04 +05:30
2024-08-08 16:21:19 +05:30
}
2024-09-18 22:16:34 +05:30
getZoom() {
return this.state.zoom
}
2024-09-18 22:16:34 +05:30
2024-08-08 16:21:19 +05:30
resetTransforms() {
this.setState({
zoom: 1,
currentTranslate: { x: 0, y: 0 }
}, this.applyTransform)
}
2024-09-18 22:16:34 +05:30
setSelectedWidget(selectedWidget) {
2024-09-13 19:24:03 +05:30
this.setState({ selectedWidget: [selectedWidget] })
}
2024-09-18 22:16:34 +05:30
clearSelections() {
if (!this.state.selectedWidget)
return
this.getActiveObjects().forEach(widget => {
widget.current?.deSelect()
})
2024-09-13 16:03:58 +05:30
// this.context?.updateActiveWidget("")
// this.context.updateToolAttrs({})
2024-09-18 22:16:34 +05:30
2024-09-13 16:03:58 +05:30
this.setState({
selectedWidget: null,
2024-09-15 15:31:04 +05:30
toolbarAttrs: null,
2024-09-15 12:08:29 +05:30
// toolbarOpen:
2024-09-13 16:03:58 +05:30
})
}
2024-09-10 21:34:05 +05:30
/**
* returns tha combined bounding rect of all the widgets on the canvas
*
*/
2024-09-18 22:16:34 +05:30
getCanvasObjectsBoundingBox() {
2024-09-10 21:34:05 +05:30
// Initialize coordinates to opposite extremes
let top = Number.POSITIVE_INFINITY
let left = Number.POSITIVE_INFINITY
let right = Number.NEGATIVE_INFINITY
let bottom = Number.NEGATIVE_INFINITY
for (let widget of Object.values(this.widgetRefs)) {
const rect = widget.current.getBoundingRect()
// Update the top, left, right, and bottom coordinates
if (rect.top < top) top = rect.top
if (rect.left < left) left = rect.left
if (rect.right > right) right = rect.right
if (rect.bottom > bottom) bottom = rect.bottom
}
2024-09-18 22:16:34 +05:30
2024-09-10 21:34:05 +05:30
return { top, left, right, bottom }
}
2024-09-18 22:16:34 +05:30
/**
* finds widgets from the list of this.state.widgets, also checks the children to find the widgets
* @param {string} widgetId
* @returns
*/
findWidgetFromListById = (widgetId) => {
const searchWidgetById = (widgets, widgetId) => {
for (let widget of widgets) {
if (widget.id === widgetId) {
return widget
}
// Recursively search in children
if (widget.children.length > 0) {
const foundInChildren = searchWidgetById(widget.children, widgetId)
if (foundInChildren) {
return foundInChildren // Found in children
}
}
}
return null // Widget not found
}
return searchWidgetById(this.state.widgets, widgetId)
}
/**
* Finds the widget from the list and removes it from its current position, even if the widget is in the child position
* @param {Array} widgets - The current list of widgets
* @param {string} widgetId - The ID of the widget to remove
* @returns {Array} - The updated widgets list
*/
removeWidgetFromCurrentList = (widgetId) => {
function recursiveRemove(objects) {
return objects
.map(obj => {
if (obj.id === widgetId) {
return null // Remove the object
}
// Recursively process children
if (obj.children && obj.children.length > 0) {
obj.children = recursiveRemove(obj.children).filter(Boolean)
}
return obj
})
.filter(Boolean) // Remove any nulls from the array
2024-09-18 22:16:34 +05:30
}
// Start the recursive removal from the top level
return recursiveRemove(this.state.widgets)
}
2024-09-18 22:16:34 +05:30
// Helper function for recursive update
updateWidgetRecursively = (widgets, updatedParentWidget, updatedChildWidget) => {
return widgets.map(widget => {
if (widget.id === updatedParentWidget.id) {
return updatedParentWidget // Update the parent widget
} else if (widget.id === updatedChildWidget.id) {
return updatedChildWidget // Update the child widget
} else if (widget.children && widget.children.length > 0) {
// Recursively update the children if they exist
return {
...widget,
children: this.updateWidgetRecursively(widget.children, updatedParentWidget, updatedChildWidget)
}
} else {
return widget // Leave other widgets unchanged
}
})
2024-09-18 22:16:34 +05:30
}
/**
* Adds the child into the children attribute inside the this.widgets list of objects
* // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
* @param {string} parentWidgetId
* @param {object} dragElement
* @param {boolean} create - if create is set to true the widget will be created before adding to the child tree
*/
handleAddWidgetChild = ({ parentWidgetId, dragElementID, swap = false }) => {
2024-09-19 19:26:10 +05:30
// TODO: creation of the child widget if its not created
2024-09-18 22:16:34 +05:30
// widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
2024-09-21 18:37:28 +05:30
const dropWidgetObj = this.findWidgetFromListById(parentWidgetId)
// Find the dragged widget object
let dragWidgetObj = this.findWidgetFromListById(dragElementID)
// console.log("Drag widget obj: ", dragWidgetObj)
2024-09-21 18:37:28 +05:30
if (dropWidgetObj && dragWidgetObj) {
const dragWidget = this.widgetRefs[dragWidgetObj.id]
const dragData = dragWidget.current.serialize()
if (swap) {
// If swapping, we need to find the common parent
const grandParentWidgetObj = this.findWidgetFromListById(dropWidgetObj.parent)
// console.log("parent widget: ", grandParentWidgetObj, dropWidgetObj, this.state.widgets)
2024-09-21 18:37:28 +05:30
if (grandParentWidgetObj) {
// Find the indices of the dragged and drop widgets in the grandparent's children array
const dragIndex = grandParentWidgetObj.children.findIndex(child => child.id === dragElementID)
const dropIndex = grandParentWidgetObj.children.findIndex(child => child.id === parentWidgetId)
if (dragIndex !== -1 && dropIndex !== -1) {
// Swap their positions
let childrenCopy = [...grandParentWidgetObj.children]
const temp = childrenCopy[dragIndex]
childrenCopy[dragIndex] = childrenCopy[dropIndex]
childrenCopy[dropIndex] = temp
// Update the grandparent with the swapped children
const updatedGrandParentWidget = {
...grandParentWidgetObj,
children: childrenCopy
}
// Update the state with the new widget hierarchy
this.setState((prevState) => ({
widgets: this.updateWidgetRecursively(prevState.widgets, updatedGrandParentWidget)
}))
}
}
} else {
// Non-swap mode: Add the dragged widget as a child of the drop widget
let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID)
const updatedDragWidget = {
...dragWidgetObj,
parent: dropWidgetObj.id, // Keep the parent reference
initialData: {
...dragData,
positionType: PosType.NONE,
zIndex: 0,
widgetContainer: WidgetContainer.WIDGET
}
}
2024-09-18 22:16:34 +05:30
2024-09-21 18:37:28 +05:30
const updatedDropWidget = {
...dropWidgetObj,
children: [...dropWidgetObj.children, updatedDragWidget]
}
2024-09-18 22:16:34 +05:30
2024-09-21 18:37:28 +05:30
// Recursively update the widget structure
updatedWidgets = this.updateWidgetRecursively(updatedWidgets, updatedDropWidget, updatedDragWidget)
2024-09-18 22:16:34 +05:30
2024-09-21 18:37:28 +05:30
// Update the state with the new widget hierarchy
this.setState({
widgets: updatedWidgets
})
}
2024-09-18 22:16:34 +05:30
}
}
2024-08-08 22:49:14 +05:30
/**
*
2024-09-17 21:23:01 +05:30
* @param {Widget} widgetComponentType - don't pass <Widget /> instead pass Widget object/class
2024-08-08 22:49:14 +05:30
*/
2024-09-18 22:16:34 +05:30
createWidget(widgetComponentType, callback) {
if (!isSubClassOfWidget(widgetComponentType)){
throw new Error("widgetComponentType must be a subclass of Widget class")
}
// console.log("componete: ", widgetComponentType)
2024-08-08 22:49:14 +05:30
const widgetRef = React.createRef()
const id = `${widgetComponentType.widgetType}_${UID()}`
// Store the ref in the instance variable
this.widgetRefs[id] = widgetRef
2024-09-19 19:26:10 +05:30
const newWidget = {
id,
widgetType: widgetComponentType,
children: [],
parent: "",
initialData: {} // useful for serializing and deserializing (aka, saving and loading)
}
const widgets = [...this.state.widgets, newWidget] // don't add the widget refs in the state
2024-09-18 22:16:34 +05:30
2024-08-08 22:49:14 +05:30
// Update the state to include the new widget's type and ID
this.setState({
widgets: widgets
}, () => {
if (callback)
2024-09-18 22:16:34 +05:30
callback({ id, widgetRef })
if (this._onWidgetListUpdated)
this._onWidgetListUpdated(widgets) // inform the parent container
})
2024-09-18 22:16:34 +05:30
return { id, widgetRef }
2024-08-08 22:49:14 +05:30
}
2024-09-18 22:16:34 +05:30
getWidgetById(id) {
return this.widgetRefs[id]
}
2024-09-16 22:04:24 +05:30
/**
* delete's the selected widgets from the canvas
* @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets
*/
2024-09-18 22:16:34 +05:30
deleteSelectedWidgets(widgets = []) {
let activeWidgets = removeDuplicateObjects([...widgets, this.state.selectedWidget], "__id")
2024-09-18 22:16:34 +05:30
this.setState({
toolbarAttrs: null,
selectedWidget: null
})
const widgetIds = activeWidgets.map(widget => widget.__id)
2024-09-18 22:16:34 +05:30
for (let widgetId of widgetIds) {
this.removeWidget(widgetId)
}
}
/**
* removes all the widgets from the canvas
*/
2024-09-18 22:16:34 +05:30
clearCanvas() {
// NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets
this.widgetRefs = {}
2024-09-15 12:08:29 +05:30
this.setState({
widgets: []
})
2024-09-13 19:24:03 +05:30
if (this._onWidgetListUpdated)
this._onWidgetListUpdated([])
}
2024-09-18 22:16:34 +05:30
removeWidget(widgetId) {
delete this.widgetRefs[widgetId]
const widgets = this.removeWidgetFromCurrentList(widgetId)
2024-09-13 19:24:03 +05:30
this.setState({
widgets: widgets
})
if (this._onWidgetListUpdated)
this._onWidgetListUpdated(widgets)
}
2024-09-18 22:16:34 +05:30
onActiveWidgetUpdate(widgetId) {
2024-09-15 12:08:29 +05:30
if (!this.state.selectedWidget || widgetId !== this.state.selectedWidget.__id)
2024-09-15 12:08:29 +05:30
return
// console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidget.getToolbarAttrs())
2024-09-16 22:04:24 +05:30
// console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs())
2024-09-15 15:31:04 +05:30
2024-09-15 12:08:29 +05:30
this.setState({
toolbarAttrs: this.state.selectedWidget.getToolbarAttrs()
2024-09-15 12:08:29 +05:30
})
}
2024-09-16 12:23:15 +05:30
/**
* Handles drop event to canvas from the sidebar and on canvas widget movement
2024-09-16 12:23:15 +05:30
* @param {DragEvent} e
*/
handleDropEvent = (e, draggedElement, widgetClass=null) => {
2024-09-15 19:22:32 +05:30
e.preventDefault()
const container = draggedElement.getAttribute("data-container")
2024-09-15 22:54:53 +05:30
const canvasRect = this.canvasRef.current.getBoundingClientRect()
const draggedElementRect = draggedElement.getBoundingClientRect()
const elementWidth = draggedElementRect.width
const elementHeight = draggedElementRect.height
2024-09-15 19:22:32 +05:30
const { clientX, clientY } = e
let finalPosition = {
x: (clientX - canvasRect.left) / this.state.zoom,
y: (clientY - canvasRect.top) / this.state.zoom,
2024-09-18 22:16:34 +05:30
}
if (container === WidgetContainer.SIDEBAR) {
if (!widgetClass){
throw new Error("WidgetClass has to be passed for widgets dropped from sidebar")
}
// TODO: handle drop from sidebar
// if the widget is being dropped from the sidebar, use the info to create the widget first
this.createWidget(widgetClass, ({ id, widgetRef }) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
})
} else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) {
// snaps to center
finalPosition = {
x: (clientX - canvasRect.left) / this.state.zoom - (elementWidth / 2) / this.state.zoom,
y: (clientY - canvasRect.top) / this.state.zoom - (elementHeight / 2) / this.state.zoom,
}
2024-09-19 19:26:10 +05:30
let widgetId = draggedElement.getAttribute("data-widget-id")
const widgetObj = this.getWidgetById(widgetId)
// console.log("WidgetObj: ", widgetObj)
if (container === WidgetContainer.CANVAS) {
2024-09-19 19:26:10 +05:30
widgetObj.current.setPos(finalPosition.x, finalPosition.y)
2024-09-15 19:22:32 +05:30
} else if (container === WidgetContainer.WIDGET) {
2024-09-15 19:22:32 +05:30
2024-09-19 19:26:10 +05:30
// if the widget was inside another widget move it outside
let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId())
2024-09-19 19:26:10 +05:30
let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent)
2024-09-18 22:16:34 +05:30
2024-09-19 19:26:10 +05:30
const childData = widgetObj.current.serialize() // save the data and pass it the updated child object
// remove child from current position
// console.log("pre updated widgets: ", updatedWidgets)
2024-09-19 19:26:10 +05:30
const updatedChildWidget = {
...childWidgetObj,
parent: "",
initialData: {
...childData,
pos: { x: finalPosition.x, y: finalPosition.y },
positionType: PosType.ABSOLUTE, // makes sure that after dropping the position is set to absolute value
zIndex: 0,
widgetContainer: WidgetContainer.CANVAS
}
2024-09-19 19:26:10 +05:30
}
let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.current.getId())
2024-09-19 19:26:10 +05:30
// Create a new copy of the parent widget with the child added
const updatedParentWidget = {
...parentWidgetObj,
// children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id)
2024-09-19 19:26:10 +05:30
}
2024-09-19 19:26:10 +05:30
updatedWidgets = updatedWidgets.map(widget => {
if (widget.id === parentWidgetObj.id) {
return updatedParentWidget // Update the parent widget with the child removed
} else {
return widget // Leave other widgets unchanged
}
})
2024-09-19 19:26:10 +05:30
this.setState({
widgets: [...updatedWidgets, updatedChildWidget]
2024-09-19 19:26:10 +05:30
})
}
2024-09-18 22:16:34 +05:30
}
2024-09-19 19:26:10 +05:30
2024-09-18 22:16:34 +05:30
}
2024-09-19 19:26:10 +05:30
2024-09-18 22:16:34 +05:30
renderWidget = (widget) => {
const { id, widgetType: ComponentType, children = [], parent, initialData = {} } = widget
2024-09-18 22:16:34 +05:30
2024-09-19 19:26:10 +05:30
const renderChildren = (childrenData) => {
// recursively render the child elements
return childrenData.map((child) => {
2024-09-18 22:16:34 +05:30
const childWidget = this.findWidgetFromListById(child.id)
if (childWidget) {
return this.renderWidget(childWidget) // Recursively render child widgets
}
return null
})
}
return (
<ComponentType
key={id}
id={id}
ref={this.widgetRefs[id]}
2024-09-19 19:26:10 +05:30
initialData={initialData}
2024-09-18 22:16:34 +05:30
canvasRef={this.canvasContainerRef}
onWidgetUpdate={this.onActiveWidgetUpdate}
onAddChildWidget={this.handleAddWidgetChild}
onCreateWidgetRequest={this.createWidget} // create widget when dropped from sidebar
2024-09-18 22:16:34 +05:30
onWidgetResizing={(resizeSide) => this.setState({ widgetResizing: resizeSide })}
>
{/* Render children inside the parent with layout applied */}
{renderChildren(children)}
</ComponentType>
)
2024-08-08 22:49:14 +05:30
}
2024-08-08 16:21:19 +05:30
render() {
return (
<div className="tw-relative tw-flex tw-w-full tw-h-full tw-max-h-[100vh]">
2024-09-18 22:16:34 +05:30
2024-08-08 16:21:19 +05:30
<div className="tw-absolute tw-p-2 tw-bg-white tw-z-10 tw-min-w-[100px] tw-h-[50px] tw-gap-2
tw-top-4 tw-place-items-center tw-right-4 tw-shadow-md tw-rounded-md tw-flex">
2024-09-18 22:16:34 +05:30
2024-08-08 16:21:19 +05:30
<Tooltip title="Reset viewport">
2024-09-18 22:16:34 +05:30
<Button icon={<ReloadOutlined />} onClick={this.resetTransforms} />
2024-08-08 16:21:19 +05:30
</Tooltip>
<ButtonModal
message={"Are you sure you want to clear the canvas? This cannot be undone."}
title={"Clear canvas"}
onOk={this.clearCanvas}
okText="Yes"
okButtonType="danger"
>
<Tooltip title="Clear canvas">
<Button danger icon={<DeleteOutlined />} />
</Tooltip>
</ButtonModal>
2024-08-08 16:21:19 +05:30
</div>
2024-09-16 22:04:24 +05:30
{/* <ActiveWidgetProvider> */}
2024-09-18 22:16:34 +05:30
<DroppableWrapper id="canvas-droppable"
droppableTags={{exclude: ["image", "video"]}}
2024-09-18 22:16:34 +05:30
className="tw-w-full tw-h-full"
onDrop={this.handleDropEvent}>
{/* <DragWidgetProvider> */}
2024-09-18 22:16:34 +05:30
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{ items: this.state.contextMenuItems, }}>
<div className="tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url(${DotsBackground})`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
tabIndex={0} // allow focus
>
<DotsBackground
style={{
width: '100%',
height: '100%',
backgroundSize: 'cover'
}}
/>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{
this.state.widgets.map(this.renderWidget)
}
2024-09-11 19:06:04 +05:30
</div>
2024-09-13 16:03:58 +05:30
</div>
2024-09-18 22:16:34 +05:30
</div>
</Dropdown>
{/* </DragWidgetProvider> */}
2024-09-15 19:22:32 +05:30
</DroppableWrapper>
2024-09-13 16:03:58 +05:30
2024-09-18 22:16:34 +05:30
<CanvasToolBar isOpen={this.state.toolbarOpen}
widgetType={this.state.selectedWidget?.getWidgetType() || ""}
attrs={this.state.toolbarAttrs}
/>
2024-09-16 22:04:24 +05:30
{/* </ActiveWidgetProvider> */}
2024-08-08 16:21:19 +05:30
</div>
)
}
}
export default Canvas