2024-08-08 16:21:19 +05:30
import React from "react"
2024-09-11 19:06:04 +05:30
2024-09-23 18:25:40 +05:30
// import { DndContext } from '@dnd-kit/core'
2024-09-11 19:06:04 +05:30
2024-09-23 22:49:44 +05:30
import { DeleteOutlined , EditOutlined , FileImageOutlined , ReloadOutlined } from "@ant-design/icons"
2024-09-29 17:51:42 +05:30
import { Button , Tooltip , Dropdown , message } from "antd"
2024-08-08 16:21:19 +05:30
2024-09-23 22:49:44 +05:30
import domtoimage from "dom-to-image-more"
import { saveAs } from 'file-saver'
2024-09-23 18:25:40 +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"
2024-09-13 12:28:32 +05:30
import { removeDuplicateObjects } from "../utils/common"
2024-08-08 16:21:19 +05:30
2024-09-14 16:03:26 +05:30
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-23 18:25:40 +05:30
2024-09-24 21:49:26 +05:30
import { Layouts , PosType } from "./constants/layouts"
2024-09-19 19:26:10 +05:30
import WidgetContainer from "./constants/containers"
2024-09-22 15:00:34 +05:30
import { isSubClassOfWidget } from "../utils/widget"
2024-09-23 12:31:01 +05:30
import { ButtonModal } from "../components/modals"
2024-09-24 21:49:26 +05:30
import ResizeWidgetContainer from "./resizeContainer"
2025-03-09 11:07:08 +05:30
import { WidgetContext , WidgetContextProvider } from "./context/widgetContext"
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-29 17:51:42 +05:30
// FIXME: once the items is selected and deleted , the toolbar doesn't disappear
2024-09-10 21:34:05 +05:30
const CanvasModes = {
DEFAULT : 0 ,
PAN : 1 ,
MOVE _WIDGET : 2 // when the mode is move widget
}
2025-03-09 11:07:08 +05:30
// TODO: replace this.widgetRefs with this.widgetRefs.current
2024-09-10 21:34:05 +05:30
2025-03-07 18:29:07 +05:30
const IS _PRODUCTION = process . env . NODE _ENV === "production"
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 )
2024-09-12 19:20:46 +05:30
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
}
2025-03-09 11:07:08 +05:30
this . setWidgets = null // a helper variable to help update widgets from widgetProviderContext
this . widgetRefs = { } // a helper variable to store the widgetRef from widgetProviderContext (doesn't store a copy but just a reference pointer )
this . widgets = [ ] // a helper variable to store the widgets from widgetProviderContext (doesn't store a copy but just a reference pointer )
this . selectedWidget = null // a helper variable that stores the reference to the selected widget fro widgetProvider
this . setSelectedWidget = null // a helper variable that stores reference to setSelected fro widgetProvider context
2024-09-12 22:09:13 +05:30
// this._contextMenuItems = []
2024-08-08 16:21:19 +05:30
this . state = {
2024-09-24 21:49:26 +05:30
isWidgetDragging : false ,
2024-09-18 11:39:07 +05:30
widgetResizing : "" , // set this to "nw", "sw" etc based on the side when widgets resizing handles are selected
2025-03-09 11:07:08 +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 : [ ] ,
2025-03-09 11:07:08 +05:30
// selectedWidget: null,
2024-09-18 22:16:34 +05:30
2024-09-26 13:47:05 +05:30
toolbarOpen : false ,
2024-09-15 12:08:29 +05:30
toolbarAttrs : null
2024-09-13 16:03:58 +05:30
}
2024-09-09 19:06:03 +05:30
2024-09-12 19:20:46 +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
2024-09-09 19:06:03 +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 )
2024-09-09 19:06:03 +05:30
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
2025-03-09 11:07:08 +05:30
// this.setSelectedWidget = this.setSelectedWidget.bind(this)
2024-09-13 12:28:32 +05:30
this . deleteSelectedWidgets = this . deleteSelectedWidgets . bind ( this )
this . removeWidget = this . removeWidget . bind ( this )
2024-09-09 19:06:03 +05:30
this . clearSelections = this . clearSelections . bind ( this )
this . clearCanvas = this . clearCanvas . bind ( this )
2024-08-08 16:21:19 +05:30
2024-09-22 15:00:34 +05:30
this . createWidget = this . createWidget . bind ( this )
2024-09-30 19:13:26 +05:30
this . closeToolBar = this . closeToolBar . bind ( this )
2024-08-08 16:21:19 +05:30
// this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this)
}
componentDidMount ( ) {
this . initEvents ( )
2025-03-07 05:26:36 +05:30
// NOTE: adding the transform will make the inner fixed position to be relative
// NOTE: this is needed to keep the resize widget poition correct in base widget
this . applyTransform ( )
2024-08-08 16:21:19 +05:30
}
componentWillUnmount ( ) {
2025-03-09 11:07:08 +05:30
console . log ( "unmounted" )
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 )
2024-09-09 19:06:03 +05:30
// 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 } ) `
}
2025-03-08 12:02:00 +05:30
closeToolBar ( ) {
2024-09-30 19:13:26 +05:30
this . setState ( {
toolbarAttrs : null ,
toolbarOpen : false
} )
}
2024-09-18 22:16:34 +05:30
/ * *
*
* @ returns { import ( "./widgets/base" ) . Widget [ ] }
* /
getWidgets ( ) {
2025-03-09 11:07:08 +05:30
// return this.widgets
return this . widgets
2024-08-08 16:21:19 +05:30
}
/ * *
2024-09-09 19:06:03 +05:30
* 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 ( ) {
2025-03-09 11:07:08 +05:30
return Object . values ( this . widgetRefs . current ) . filter ( ( widgetRef ) => {
2024-09-09 19:06:03 +05:30
return widgetRef . current ? . isSelected ( )
} )
}
2024-08-08 16:21:19 +05:30
2024-09-09 19:06:03 +05:30
/ * *
* returns the widget that contains the target
* @ param { HTMLElement } target
* @ returns { Widget }
* /
2024-09-18 22:16:34 +05:30
getWidgetFromTarget ( target ) {
2025-03-09 11:07:08 +05:30
// TODO: improve search, currently O(n), but can be improved via this.widgets or something
2024-09-19 19:26:10 +05:30
let innerWidget = null
2025-03-09 11:07:08 +05:30
for ( let [ key , ref ] of Object . entries ( this . widgetRefs . current ) ) {
2024-09-24 21:49:26 +05:30
if ( ref . current === target ) {
2024-09-22 12:39:03 +05:30
innerWidget = ref . current
break
}
2024-09-29 19:17:11 +05:30
// console.log("refs: ", ref)
// TODO: remove the ref.current? if there are bugs it would become hard to debug
if ( ref . current ? . getElement ( ) . contains ( target ) ) {
2024-09-20 15:12:43 +05:30
2024-09-19 19:26:10 +05:30
if ( ! innerWidget ) {
2024-09-20 19:17:29 +05:30
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
2024-09-20 19:17:29 +05:30
innerWidget = ref . current
2024-09-19 19:26:10 +05:30
}
2024-09-09 19:06:03 +05:30
}
}
2024-08-08 16:21:19 +05:30
2024-09-19 19:26:10 +05:30
return innerWidget
2024-09-09 19:06:03 +05:30
}
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-09 19:06:03 +05:30
2024-09-12 22:09:13 +05:30
this . mousePos = { x : event . clientX , y : event . clientY }
2024-09-18 22:16:34 +05:30
2024-09-09 19:06:03 +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 ) {
2024-09-22 12:39:03 +05:30
// console.log("selected widget2: ", selectedWidget.getId(), this.state.selectedWidget?.getId())
2025-03-09 11:07:08 +05:30
this . selectedWidget ? . deSelect ( ) // deselect the previous widget before adding the new one
this . selectedWidget ? . setZIndex ( 0 )
2024-09-22 12:39:03 +05:30
selectedWidget . setZIndex ( 1000 )
selectedWidget . select ( )
2024-09-25 19:20:05 +05:30
// console.log("selected widget", selectedWidget.getToolbarAttrs(), selectedWidget, this.state.selectedWidget)
2024-09-22 12:39:03 +05:30
this . setState ( {
2025-03-09 11:07:08 +05:30
// selectedWidget: selectedWidget,
2024-09-26 13:47:05 +05:30
toolbarAttrs : selectedWidget . getToolbarAttrs ( ) ,
toolbarOpen : true
2024-09-22 12:39:03 +05:30
} )
2025-03-09 11:07:08 +05:30
this . setSelectedWidget ( selectedWidget )
2024-09-24 21:49:26 +05:30
2024-09-22 12:39:03 +05:30
// 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-09 19:06:03 +05:30
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 )
2024-09-22 12:39:03 +05:30
// console.log("clear selection")
2024-09-12 22:09:13 +05:30
}
this . setState ( {
2024-09-13 16:03:58 +05:30
contextMenuItems : [ ] ,
2024-09-26 13:47:05 +05:30
// 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
2025-03-09 11:07:08 +05:30
if ( this . selectedWidget && this . selectedWidget . _ _id !== selectedWidget . _ _id ) {
2024-09-13 12:28:32 +05:30
this . clearSelections ( )
}
2024-09-18 22:16:34 +05:30
if ( selectedWidget ) {
2025-03-09 11:07:08 +05:30
this . setSelectedWidget ( selectedWidget )
2024-09-12 22:09:13 +05:30
this . setState ( {
2025-03-09 11:07:08 +05:30
// selectedWidget: selectedWidget,
2024-09-13 12:28:32 +05:30
contextMenuItems : [
{
key : "rename" ,
label : ( < div onClick = { ( ) => selectedWidget . openRenaming ( ) } > < EditOutlined / > Rename < / d i v > ) ,
icons : < EditOutlined / > ,
} ,
{
key : "delete" ,
label : ( < div onClick = { ( ) => this . deleteSelectedWidgets ( [ selectedWidget ] ) } > < DeleteOutlined / > Delete < / d i v > ) ,
icons : < DeleteOutlined / > ,
danger : true
2024-09-23 22:49:44 +05:30
} ,
{
type : 'divider' ,
} ,
{
key : "snap" ,
label : ( < div onClick = { ( ) => {
2024-09-24 21:49:26 +05:30
domtoimage . toPng ( selectedWidget . getElement ( ) , {
width : selectedWidget . getElement ( ) . offsetWidth * 2 , // Multiply element's width by 2
height : selectedWidget . getElement ( ) . offsetHeight * 2 // Multiply element's height by 2
} ) . then ( ( dataUrl ) => {
saveAs ( dataUrl , 'widget.png' )
} ) . catch ( ( error ) => {
console . error ( 'Error capturing widget as PNg:' , error )
} )
} } >
< FileImageOutlined / > Save as Image < / d i v > ) ,
2024-09-23 22:49:44 +05:30
icons : < FileImageOutlined / > ,
2024-09-13 12:28:32 +05:30
}
]
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-09-09 19:06:03 +05:30
}
}
2024-08-08 16:21:19 +05:30
2024-09-18 22:16:34 +05:30
mouseMoveEvent ( event ) {
2024-09-13 12:28:32 +05:30
2024-09-18 22:16:34 +05:30
if ( this . state . widgetResizing !== "" ) {
2024-09-18 11:39:07 +05:30
// if resizing is taking place don't do anything else
this . handleResize ( event )
return
}
2024-09-09 19:06:03 +05:30
// 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 ) ) {
2024-09-09 19:06:03 +05:30
const deltaX = event . clientX - this . mousePos . x
const deltaY = event . clientY - this . mousePos . y
2024-09-18 22:16:34 +05:30
2025-03-09 11:07:08 +05:30
if ( ! this . 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 {
2024-09-09 19:06:03 +05:30
// 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 ) {
2024-09-09 19:06:03 +05:30
this . mousePressed = false
2024-09-10 21:34:05 +05:30
this . currentMode = CanvasModes . DEFAULT
2024-09-09 19:06:03 +05:30
this . setCursor ( Cursor . DEFAULT )
2024-09-18 11:39:07 +05:30
if ( this . state . widgetResizing ) {
this . setState ( { widgetResizing : "" } )
}
2024-09-20 22:07:22 +05:30
2025-03-09 11:07:08 +05:30
for ( let [ key , widget ] of Object . entries ( this . widgetRefs . current ) ) {
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
2024-09-29 23:34:26 +05:30
widget . current ? . enableDrag ( )
2024-09-20 22:07:22 +05:30
}
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
}
2024-09-18 11:39:07 +05:30
/ * *
* handles widgets resizing
* @ param { MouseEvent } event - mouse move event
* @ returns
* /
handleResize = ( event ) => {
if ( this . state . resizing === "" ) return
2025-03-09 11:07:08 +05:30
const widget = this . selectedWidget
2024-09-18 11:39:07 +05:30
if ( ! widget ) return
2024-09-29 17:51:42 +05:30
2025-03-08 12:02:00 +05:30
if ( widget . state . fitContent ? . width && widget . state . fitContent ? . height ) {
this . setState ( { widgetResizing : "" } ) // Disable resizing if this is true, since the user will have to uncheck fit width and height
2024-09-29 17:51:42 +05:30
message . warning ( "both width and height are set to fit-content, unset it to start resizing" )
return
}
2024-09-18 11:39:07 +05:30
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
}
2024-09-18 11:39:07 +05:30
widget . setResize ( newPos , newSize )
}
2024-09-18 22:16:34 +05:30
getCanvasContainerBoundingRect ( ) {
2024-09-12 19:20:46 +05:30
return this . canvasContainerRef . current . getBoundingClientRect ( )
}
2024-09-18 22:16:34 +05:30
getCanvasBoundingRect ( ) {
2024-09-12 19:20:46 +05:30
return this . canvasRef . current . getBoundingClientRect ( )
}
2024-09-18 22:16:34 +05:30
getCanvasTranslation ( ) {
2024-09-12 19:20:46 +05:30
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-12 19:20:46 +05:30
2024-09-18 22:16:34 +05:30
getZoom ( ) {
2024-09-12 19:20:46 +05:30
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 )
}
2025-03-10 11:58:27 +05:30
setPan = ( x , y ) => {
this . setState ( {
currentTranslate : { x , y }
} )
}
panToWidget = ( widgetId ) => {
const widget = this . getWidgetById ( widgetId ) . current
const canvasBoundingRect = this . getCanvasBoundingRect ( ) ; // Get the canvas dimensions
// Get widget position
const widgetRect = widget . getBoundingRect ( ) ; // Get widget's bounding box
// Calculate widget center position in canvas space
const widgetCenter = {
x : ( widgetRect . left - canvasBoundingRect . left + widgetRect . width / 2 ) / this . state . zoom ,
y : ( widgetRect . top - canvasBoundingRect . top + widgetRect . height / 2 ) / this . state . zoom
}
// Calculate new translation to center the widget in the viewport
const newTranslate = {
x : ( canvasBoundingRect . width / 2 ) / this . state . zoom - widgetCenter . x ,
y : ( canvasBoundingRect . height / 2 ) / this . state . zoom - widgetCenter . y
}
this . setState ( {
currentTranslate : newTranslate
} )
}
2025-03-09 11:07:08 +05:30
// setSelectedWidget(selectedWidget) {
// this.setState({ selectedWidget: [selectedWidget] })
// }
2024-09-13 19:24:03 +05:30
2024-09-18 22:16:34 +05:30
clearSelections ( ) {
2024-09-18 11:39:07 +05:30
2025-03-09 11:07:08 +05:30
if ( ! this . selectedWidget )
2024-09-18 11:39:07 +05:30
return
2024-09-09 19:06:03 +05:30
this . getActiveObjects ( ) . forEach ( widget => {
widget . current ? . deSelect ( )
} )
2024-09-13 16:03:58 +05:30
2024-09-17 09:13:54 +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 ( {
2025-03-09 11:07:08 +05:30
// selectedWidget: null,
2024-09-15 15:31:04 +05:30
toolbarAttrs : null ,
2024-09-26 13:47:05 +05:30
toolbarOpen : false
2024-09-13 16:03:58 +05:30
} )
2025-03-09 11:07:08 +05:30
this . setSelectedWidget ( null )
2024-09-09 19:06:03 +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
2025-03-09 11:07:08 +05:30
for ( let widget of Object . values ( this . widgetRefs . current ) ) {
2024-09-10 21:34:05 +05:30
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
/ * *
2025-03-09 11:07:08 +05:30
* finds widgets from the list of this . widgets , also checks the children to find the widgets
2024-09-18 22:16:34 +05:30
* @ 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
}
2025-03-09 11:07:08 +05:30
return searchWidgetById ( this . widgets , widgetId )
2024-09-18 22:16:34 +05:30
}
/ * *
* 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 ) => {
2024-09-20 15:12:43 +05:30
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
}
2024-09-20 15:12:43 +05:30
// Start the recursive removal from the top level
2025-03-09 11:07:08 +05:30
return recursiveRemove ( this . widgets )
2024-09-20 15:12:43 +05:30
}
2024-09-18 22:16:34 +05:30
2024-09-20 15:12:43 +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
}
2025-03-08 12:02:00 +05:30
/ * *
* Handles drop event to canvas from the sidebar and on canvas widget movement
* @ param { DragEvent } e
* /
handleDropEvent = ( e , draggedElement , widgetClass = null , posMetaData ) => {
2024-09-24 21:49:26 +05:30
e . preventDefault ( )
2024-09-29 19:17:11 +05:30
// console.log("Drop event")
2024-09-24 21:49:26 +05:30
this . setState ( { isWidgetDragging : false } )
if ( ! draggedElement || ! draggedElement . getAttribute ( "data-drag-start-within" ) ) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
const container = draggedElement . getAttribute ( "data-container" )
const canvasRect = this . canvasRef . current . getBoundingClientRect ( )
2025-03-07 17:09:35 +05:30
// const draggedElementRect = draggedElement.getBoundingClientRect()
// const elementWidth = draggedElementRect.width
// const elementHeight = draggedElementRect.height
2024-09-24 21:49:26 +05:30
const { clientX , clientY } = e
let finalPosition = {
x : ( clientX - canvasRect . left ) / this . state . zoom ,
y : ( clientY - canvasRect . top ) / this . state . zoom ,
}
2025-03-10 10:37:57 +05:30
// console.log("Final position1: ", finalPosition, container)
2024-09-24 21:49:26 +05:30
if ( container === WidgetContainer . SIDEBAR ) {
if ( ! widgetClass ) {
throw new Error ( "WidgetClass has to be passed for widgets dropped 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
2025-03-06 14:32:00 +05:30
// 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,
// }
2025-03-08 12:02:00 +05:30
const { dragStartCursorPos , initialPos } = posMetaData
2025-03-06 14:32:00 +05:30
const canvasBoundingRect = this . getCanvasBoundingRect ( )
// calculate the initial offset from the div to the cursor grab
2025-03-09 17:48:23 +05:30
2025-03-06 14:32:00 +05:30
const initialOffset = {
2025-03-10 10:37:57 +05:30
x : ( ( dragStartCursorPos . x - canvasBoundingRect . left ) / this . state . zoom ) - ( initialPos . x / this . state . zoom ) ,
y : ( ( dragStartCursorPos . y - canvasBoundingRect . top ) / this . state . zoom ) - ( initialPos . y / this . state . zoom )
2025-03-06 14:32:00 +05:30
}
2025-03-09 17:48:23 +05:30
finalPosition = {
x : finalPosition . x - initialOffset . x ,
y : finalPosition . y - initialOffset . y
}
2024-09-24 21:49:26 +05:30
let widgetId = draggedElement . getAttribute ( "data-widget-id" )
const widgetObj = this . getWidgetById ( widgetId )
// console.log("WidgetObj: ", widgetObj)
if ( container === WidgetContainer . CANVAS ) {
widgetObj . current . setPos ( finalPosition . x , finalPosition . y )
} else if ( container === WidgetContainer . WIDGET ) {
// if the widget was inside another widget move it outside
let childWidgetObj = this . findWidgetFromListById ( widgetObj . current . getId ( ) )
let parentWidgetObj = this . findWidgetFromListById ( childWidgetObj . parent )
const childData = widgetObj . current . serialize ( ) // save the data and pass it the updated child object
// remove child from current position
const updatedChildWidget = {
... childWidgetObj ,
parent : "" ,
initialData : {
... childData ,
2025-03-07 17:09:35 +05:30
parentWidgetRef : null ,
2024-09-24 21:49:26 +05:30
pos : { x : finalPosition . x , y : finalPosition . y } ,
positionType : PosType . ABSOLUTE , // makes sure that after dropping the position is set to absolute value
2024-09-25 17:27:12 +05:30
parentLayout : null , // reset the parent layout when its put on the canvas
2024-09-24 21:49:26 +05:30
zIndex : 0 ,
widgetContainer : WidgetContainer . CANVAS
}
}
2025-03-08 12:02:00 +05:30
2024-09-24 21:49:26 +05:30
let updatedWidgets = this . removeWidgetFromCurrentList ( widgetObj . current . getId ( ) )
// Create a new copy of the parent widget with the child added
const updatedParentWidget = {
... parentWidgetObj ,
// children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id)
}
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
}
} )
2025-03-09 11:07:08 +05:30
this . setWidgets ( [ ... updatedWidgets , updatedChildWidget ] )
// this.setState({
// widgets: [...updatedWidgets, updatedChildWidget]
// })
2024-09-24 21:49:26 +05:30
}
}
}
2025-03-07 18:29:07 +05:30
/ * *
* Checks if the child fell in the swappable area
*
* /
2025-03-08 12:02:00 +05:30
_ _checkClosestShiftElement = ( { event , parentWidgetId , dragElementID } ) => {
// NOTE: work on this more maybe even check all four corners
2025-03-08 20:11:22 +05:30
const parentWidget = this . findWidgetFromListById ( parentWidgetId )
if ( ! parentWidget ) return
2025-03-08 12:02:00 +05:30
2025-03-08 20:11:22 +05:30
const dropX = event . clientX
const dropY = event . clientY
2025-03-08 12:02:00 +05:30
2025-03-08 20:11:22 +05:30
let closestChild = null
let closestIndex = parentWidget . children . length
let minDistance = Infinity
2025-03-08 12:02:00 +05:30
// Only check the first level of children
parentWidget . children . forEach ( ( child , index ) => {
if ( child . id !== dragElementID ) {
2025-03-09 11:07:08 +05:30
const childElement = this . widgetRefs . current [ child . id ]
2025-03-08 12:02:00 +05:30
2025-03-08 20:11:22 +05:30
if ( ! childElement ) return
2025-03-08 12:02:00 +05:30
const rect = childElement . current . getBoundingRect ( )
const childTopRightX = rect . right
const childTopRightY = rect . top
// Compute Euclidean distance from drop position
2025-03-08 20:11:22 +05:30
const distance = Math . hypot ( childTopRightX - dropX , childTopRightY - dropY )
2025-03-08 12:02:00 +05:30
if ( distance < minDistance ) {
2025-03-08 20:11:22 +05:30
minDistance = distance
closestChild = child
2025-03-08 12:02:00 +05:30
closestIndex = index + 1
}
}
} ) ;
if ( closestChild && closestIndex === 1 ) {
// by default the new index of the closest first element will be one and it will be zero if rect.left > dropX
2025-03-09 11:07:08 +05:30
const childElement = this . widgetRefs . current [ closestChild . id ]
2025-03-08 12:02:00 +05:30
const rect = childElement . current . getBoundingRect ( )
// Closest is first child, check if dropped before or after
2025-03-08 20:11:22 +05:30
closestIndex = dropX < rect . left ? 0 : 1
2025-03-08 12:02:00 +05:30
}
return { closestChild , closestIndex }
}
shiftWidgetPosition = ( parentWidgetId , dragElementID , targetIndex ) => {
const parentWidget = this . findWidgetFromListById ( parentWidgetId ) ;
2025-03-07 18:29:07 +05:30
2025-03-08 12:02:00 +05:30
let childrenCopy = [ ... parentWidget . children ] ;
const dragIndex = childrenCopy . findIndex ( child => child . id === dragElementID )
if ( dragIndex !== - 1 && dragIndex !== targetIndex ) {
const [ draggedItem ] = childrenCopy . splice ( dragIndex , 1 ) // Remove dragged item
childrenCopy . splice ( targetIndex , 0 , draggedItem ) // Insert at the target index
const updatedParent = { ... parentWidget , children : childrenCopy }
2025-03-09 11:07:08 +05:30
// this.setState(prevState => ({
// widgets: this.updateWidgetRecursively(prevState.widgets, updatedParent, {})
// }))
this . setWidgets ( this . updateWidgetRecursively ( this . widgets , updatedParent , { } ) )
2025-03-08 12:02:00 +05:30
}
2025-03-07 18:29:07 +05:30
}
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
* /
2025-03-08 12:02:00 +05:30
handleAddWidgetChild = ( { event , parentWidgetId , dragElementID , posMetaData } ) => {
2024-09-24 21:49:26 +05:30
2024-09-30 22:18:08 +05:30
// console.log("event: ", event)
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 )
2024-09-26 23:16:43 +05:30
// console.log("Drag widget obj: ", dragWidgetObj, dropWidgetObj)
2024-09-22 15:00:34 +05:30
2024-09-21 18:37:28 +05:30
if ( dropWidgetObj && dragWidgetObj ) {
2025-03-09 11:07:08 +05:30
const dragWidget = this . widgetRefs . current [ dragWidgetObj . id ]
2024-09-21 18:37:28 +05:30
const dragData = dragWidget . current . serialize ( )
2024-09-24 21:49:26 +05:30
2025-03-09 11:07:08 +05:30
const parentWidget = this . widgetRefs . current [ parentWidgetId ] . current
2024-09-24 21:49:26 +05:30
const parentRect = parentWidget . getBoundingRect ( )
const { clientX , clientY } = event
2025-03-08 12:02:00 +05:30
// const childRect = dragWidget.current.getBoundingRect()
// con
const { dragStartCursorPos , initialPos } = posMetaData
const canvasBoundingRect = this . getCanvasBoundingRect ( )
2024-09-24 21:49:26 +05:30
let finalPosition = {
x : ( clientX - parentRect . left ) / this . state . zoom ,
y : ( clientY - parentRect . top ) / this . state . zoom ,
}
2025-03-07 17:09:35 +05:30
2025-03-10 10:37:57 +05:30
// FIXME: if the drag is from sidebar in layout absolute the position is wrong
2025-03-09 17:48:23 +05:30
2025-03-07 17:09:35 +05:30
const initialOffset = {
2025-03-10 10:37:57 +05:30
x : ( ( dragStartCursorPos . x - canvasBoundingRect . left ) / this . state . zoom ) - ( initialPos . x / this . state . zoom ) ,
y : ( ( dragStartCursorPos . y - canvasBoundingRect . top ) / this . state . zoom ) - ( initialPos . y / this . state . zoom )
2025-03-07 17:09:35 +05:30
}
2025-03-09 17:48:23 +05:30
finalPosition = {
x : finalPosition . x - initialOffset . x ,
y : finalPosition . y - initialOffset . y
}
2025-03-08 12:02:00 +05:30
let updatedWidgets = this . removeWidgetFromCurrentList ( dragElementID )
2025-03-06 19:20:40 +05:30
2025-03-08 12:02:00 +05:30
const parentLayout = parentWidget . getLayout ( ) ? . layout || null
// FIXME: if the layout is flex or grid the position should be different
dragWidget . current . setPos ( finalPosition . x , finalPosition . y )
const updatedDragWidget = {
... dragWidgetObj ,
parent : dropWidgetObj . id , // Keep the parent reference
initialData : {
... dragData ,
positionType : parentLayout === Layouts . PLACE ? PosType . ABSOLUTE : PosType . NONE ,
parentLayout : parentWidget . getLayout ( ) || null , // pass everything about the parent layout
2025-03-09 11:07:08 +05:30
parentWidgetRef : this . widgetRefs . current [ parentWidgetId ] ,
2025-03-08 12:02:00 +05:30
zIndex : 0 ,
pos : { x : finalPosition . x , y : finalPosition . y } ,
widgetContainer : WidgetContainer . WIDGET
2024-09-20 15:12:43 +05:30
}
2025-03-08 12:02:00 +05:30
}
2024-09-18 22:16:34 +05:30
2025-03-08 12:02:00 +05:30
const updatedDropWidget = {
... dropWidgetObj ,
children : [ ... dropWidgetObj . children , updatedDragWidget ]
}
2024-09-20 15:12:43 +05:30
2024-09-18 22:16:34 +05:30
2025-03-08 12:02:00 +05:30
// Recursively update the widget structure
updatedWidgets = this . updateWidgetRecursively ( updatedWidgets , updatedDropWidget , updatedDragWidget )
2024-09-18 22:16:34 +05:30
2025-03-09 11:07:08 +05:30
this . setWidgets ( updatedWidgets )
setTimeout ( ( ) => {
if ( parentLayout !== Layouts . PLACE ) {
2025-03-08 12:02:00 +05:30
// swap only for grid and flex placements
2025-03-09 11:07:08 +05:30
const swapClosest = this . _ _checkClosestShiftElement ( {
event ,
parentWidgetId ,
dragElementID ,
} )
if ( swapClosest . closestChild ) {
2025-03-08 12:02:00 +05:30
this . shiftWidgetPosition ( parentWidgetId , dragElementID , swapClosest . closestIndex )
}
}
2025-03-09 11:07:08 +05:30
} , 1 )
// Update the state with the new widget hierarchy
// this.setState({
// widgets: updatedWidgets
// }, () => {
// if (parentLayout !== Layouts.PLACE){
// // swap only for grid and flex placements
// const swapClosest = this.__checkClosestShiftElement({ event, parentWidgetId, dragElementID })
// if (swapClosest.closestChild){
// this.shiftWidgetPosition(parentWidgetId, dragElementID, swapClosest.closestIndex)
// }
// }
// })
2025-03-08 12:02:00 +05:30
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 ) {
2024-09-22 15:00:34 +05:30
2024-09-24 21:49:26 +05:30
if ( ! isSubClassOfWidget ( widgetComponentType ) ) {
2024-09-22 15:00:34 +05:30
throw new Error ( "widgetComponentType must be a subclass of Widget class" )
}
2024-09-23 12:31:01 +05:30
// console.log("componete: ", widgetComponentType)
2024-09-22 19:23:06 +05:30
2024-08-08 22:49:14 +05:30
const widgetRef = React . createRef ( )
const id = ` ${ widgetComponentType . widgetType } _ ${ UID ( ) } `
// Store the ref in the instance variable
2025-03-09 11:07:08 +05:30
this . widgetRefs . current [ id ] = widgetRef
// this.setWidgetRefs({...this.widgetRefs.current, [id]: widgetRef})
2024-09-12 19:20:46 +05:30
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)
}
2025-03-09 11:07:08 +05:30
const widgets = [ ... this . widgets , newWidget ] // don't add the widget refs in the state
this . setWidgets ( widgets )
2024-09-18 22:16:34 +05:30
2025-03-09 11:07:08 +05:30
setTimeout ( ( ) => {
2024-09-12 19:20:46 +05:30
if ( callback )
2024-09-18 22:16:34 +05:30
callback ( { id , widgetRef } )
2024-09-12 19:20:46 +05:30
if ( this . _onWidgetListUpdated )
2024-09-17 11:55:21 +05:30
this . _onWidgetListUpdated ( widgets ) // inform the parent container
2025-03-09 11:07:08 +05:30
} , 1 )
// Update the state to include the new widget's type and ID
// this.setState({
// widgets: widgets
// }, () => {
// if (callback)
// callback({ id, widgetRef })
// if (this._onWidgetListUpdated)
// this._onWidgetListUpdated(widgets) // inform the parent container
// })
2024-09-12 19:20:46 +05:30
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 ) {
2025-03-09 11:07:08 +05:30
return this . widgetRefs . current [ id ]
2024-09-17 11:55:21 +05:30
}
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 = [ ] ) {
2025-03-09 11:07:08 +05:30
let activeWidgets = removeDuplicateObjects ( [ ... widgets , this . selectedWidget ] , "__id" )
2025-03-09 17:48:23 +05:30
this . setSelectedWidget ( null )
2024-09-18 22:16:34 +05:30
2024-09-23 12:31:01 +05:30
this . setState ( {
toolbarAttrs : null ,
2024-09-29 23:34:26 +05:30
toolbarOpen : false ,
2025-03-09 11:07:08 +05:30
// selectedWidget: null
2024-09-23 12:31:01 +05:30
} )
2024-09-13 12:28:32 +05:30
const widgetIds = activeWidgets . map ( widget => widget . _ _id )
2024-09-18 22:16:34 +05:30
for ( let widgetId of widgetIds ) {
2024-09-22 12:39:03 +05:30
this . removeWidget ( widgetId )
2024-09-13 12:28:32 +05:30
}
2025-03-08 12:02:00 +05:30
2024-09-13 12:28:32 +05:30
}
2024-09-09 19:06:03 +05:30
/ * *
* removes all the widgets from the canvas
* /
2024-09-18 22:16:34 +05:30
clearCanvas ( ) {
2024-09-09 19:06:03 +05:30
2024-09-13 12:28:32 +05:30
// NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets
2024-09-09 19:06:03 +05:30
2025-03-09 11:07:08 +05:30
this . widgetRefs . current = { }
this . setWidgets ( [ ] )
// this.setState({
// widgets: []
// })
2024-09-13 19:24:03 +05:30
if ( this . _onWidgetListUpdated )
this . _onWidgetListUpdated ( [ ] )
2024-09-09 19:06:03 +05:30
}
2024-09-24 21:49:26 +05:30
getWidgetByIdFromWidgetList = ( widgetId ) => {
function recursiveFind ( objects ) {
for ( const obj of objects ) {
// Check if the current object has the matching ID
if ( obj . id === widgetId ) {
return obj // Return the object if found
}
// Recursively check children if they exist
if ( obj . children && obj . children . length > 0 ) {
const found = recursiveFind ( obj . children )
if ( found ) {
return found // Return the found object from children
}
}
}
return null // Return null if not found
}
2025-03-09 11:07:08 +05:30
return recursiveFind ( this . widgets )
2024-09-24 21:49:26 +05:30
}
2024-09-18 22:16:34 +05:30
removeWidget ( widgetId ) {
2024-09-20 15:12:43 +05:30
2025-03-09 11:07:08 +05:30
delete this . widgetRefs . current [ widgetId ]
2024-09-09 19:06:03 +05:30
2024-09-22 12:39:03 +05:30
const widgets = this . removeWidgetFromCurrentList ( widgetId )
2025-03-09 11:07:08 +05:30
// this.setState({
// widgets: widgets
// })
this . setWidgets ( widgets )
2024-09-13 19:24:03 +05:30
if ( this . _onWidgetListUpdated )
this . _onWidgetListUpdated ( widgets )
2024-09-09 19:06:03 +05:30
}
2024-09-18 22:16:34 +05:30
onActiveWidgetUpdate ( widgetId ) {
2024-09-15 12:08:29 +05:30
2025-03-09 11:07:08 +05:30
if ( ! this . selectedWidget || widgetId !== this . selectedWidget . _ _id )
2024-09-15 12:08:29 +05:30
return
2024-09-18 11:39:07 +05:30
// 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 ( {
2025-03-09 11:07:08 +05:30
toolbarAttrs : this . selectedWidget . getToolbarAttrs ( )
2024-09-15 12:08:29 +05:30
} )
}
2024-09-24 21:49:26 +05:30
2024-09-16 12:23:15 +05:30
/ * *
2024-09-24 21:49:26 +05:30
* informs the child about the parent layout
2024-09-16 12:23:15 +05:30
* /
2025-03-08 12:02:00 +05:30
updateChildLayouts = ( { parentId , parentLayout } ) => {
2024-09-24 21:49:26 +05:30
const parent = this . getWidgetByIdFromWidgetList ( parentId )
2024-09-21 22:47:47 +05:30
2024-09-24 21:49:26 +05:30
if ( ! parent ) return
2024-09-15 19:22:32 +05:30
2025-03-08 12:02:00 +05:30
for ( let child of parent . children ) {
2025-03-09 11:07:08 +05:30
this . widgetRefs . current [ child . id ] . current . setParentLayout ( parentLayout )
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-20 15:12:43 +05:30
2024-09-18 22:16:34 +05:30
renderWidget = ( widget ) => {
2024-09-20 15:12:43 +05:30
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 (
2024-09-24 21:49:26 +05:30
2024-09-18 22:16:34 +05:30
< ComponentType
key = { id }
id = { id }
2025-03-09 11:07:08 +05:30
ref = { this . widgetRefs . current [ id ] }
2024-09-19 19:26:10 +05:30
initialData = { initialData }
2025-03-07 17:09:35 +05:30
parentWidgetRef = { initialData . parentWidgetRef || null }
2024-09-18 22:16:34 +05:30
canvasRef = { this . canvasContainerRef }
2025-03-06 21:41:54 +05:30
canvasInnerContainerRef = { this . canvasRef }
2025-03-06 19:20:40 +05:30
canvasMetaData = { {
zoom : this . state . zoom ,
pan : this . state . currentTranslate
} }
2025-03-10 11:58:27 +05:30
onSelect = { ( widgetId ) => this . setSelectedWidget ( this . getWidgetById ( widgetId ) ? . current ) }
2025-03-09 11:07:08 +05:30
onWidgetDeleteRequest = { this . removeWidget }
2025-03-06 19:20:40 +05:30
2025-03-10 11:58:27 +05:30
onPanToWidget = { this . panToWidget }
2024-09-18 22:16:34 +05:30
onWidgetUpdate = { this . onActiveWidgetUpdate }
onAddChildWidget = { this . handleAddWidgetChild }
2024-09-22 15:00:34 +05:30
onCreateWidgetRequest = { this . createWidget } // create widget when dropped from sidebar
2024-09-18 22:16:34 +05:30
onWidgetResizing = { ( resizeSide ) => this . setState ( { widgetResizing : resizeSide } ) }
2024-09-24 21:49:26 +05:30
// onWidgetDragStart={() => this.setState({isWidgetDragging: true})}
// onWidgetDragEnd={() => this.setState({isWidgetDragging: false})}
onLayoutUpdate = { this . updateChildLayouts }
2024-09-18 22:16:34 +05:30
>
{ /* Render children inside the parent with layout applied */ }
{ renderChildren ( children ) }
< / C o m p o n e n t T y p e >
)
2024-08-08 22:49:14 +05:30
}
2024-08-08 16:21:19 +05:30
render ( ) {
2024-09-24 21:49:26 +05:30
2024-08-08 16:21:19 +05:30
return (
2025-03-09 11:07:08 +05:30
< WidgetContext . Consumer >
{
( { widgets , setWidgets , widgetRefs , activeWidget , setActiveWidget } ) => {
this . widgets = widgets
this . setWidgets = setWidgets
// this.setWidgetRefs = setWidgetRefs
this . widgetRefs = widgetRefs
this . selectedWidget = activeWidget
this . setSelectedWidget = setActiveWidget
return ( < div className = "tw-relative tw-overflow-hidden tw-flex tw-w-full tw-h-full tw-max-h-[100vh]" >
< div className = " tw - absolute tw - p - 2 tw - bg - white tw - z - 10 tw - min - w - [ 100 px ] tw - h - [ 50 px ] tw - gap - 2
tw - top - 4 tw - place - items - center tw - right - 4 tw - shadow - md tw - rounded - md tw - flex " >
< Tooltip title = "Reset viewport" >
< Button icon = { < ReloadOutlined / > } onClick = { this . resetTransforms } / >
< / T o o l t i p >
< 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 / > } / >
< / T o o l t i p >
< / B u t t o n M o d a l >
< / d i v >
{ /* <ActiveWidgetProvider> */ }
< DroppableWrapper id = "canvas-droppable"
droppableTags = { { exclude : [ "image" , "video" ] } }
className = "tw-w-full tw-h-full"
onDrop = { this . handleDropEvent } >
{ /* <DragWidgetProvider> */ }
< 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 */ }
2025-03-10 11:58:27 +05:30
{ /* TODO: add translation in class instead of applyTransform function */ }
< div data - canvas className = { ` tw-w-full tw-h-full tw-absolute ${ ! IS _PRODUCTION ? "tw-bg-red-300" : "tw-bg-transparent" }
tw - top - 0 tw - select - none
` }
style = { {
transform : ` translate( ${ this . state . currentTranslate . x } px, ${ this . state . currentTranslate . y } px) scale( ${ this . state . zoom } ) `
} }
2025-03-09 11:07:08 +05:30
ref = { this . canvasRef } >
< div className = "tw-relative tw-w-full tw-h-full" >
{
this . widgets . map ( this . renderWidget )
}
< / d i v >
< / d i v >
< / d i v >
< / D r o p d o w n >
{ /* </DragWidgetProvider> */ }
< / D r o p p a b l e W r a p p e r >
< CanvasToolBar isOpen = { this . state . toolbarOpen }
widgetType = { this . selectedWidget ? . getWidgetType ( ) || "" }
attrs = { this . state . toolbarAttrs }
/ >
{ /* </ActiveWidgetProvider> */ }
2024-09-11 19:06:04 +05:30
< / d i v >
2025-03-09 11:07:08 +05:30
)
}
}
< / W i d g e t C o n t e x t . C o n s u m e r >
2024-08-08 16:21:19 +05:30
)
}
}
export default Canvas