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
2024-09-13 12:28:32 +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"
2024-09-13 12:28:32 +05:30
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"
2024-09-17 11:55:21 +05:30
import { DragWidgetProvider } from "./widgets/draggableWidgetContext"
2024-09-19 19:26:10 +05:30
import { PosType } from "./constants/layouts"
import WidgetContainer from "./constants/containers"
2024-09-22 15:00:34 +05:30
import { isSubClassOfWidget } from "../utils/widget"
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 )
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
}
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 = {
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
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 : [ ] ,
2024-09-18 11:39:07 +05:30
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
}
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
2024-09-13 19:24:03 +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-08-08 16:21:19 +05:30
// this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this)
}
componentDidMount ( ) {
this . initEvents ( )
2024-09-17 21:23:01 +05:30
this . createWidget ( Widget )
2024-08-08 16:21:19 +05:30
}
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 )
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 } ) `
}
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
}
/ * *
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 ( ) {
2024-09-09 19:06:03 +05:30
return Object . values ( this . widgetRefs ) . filter ( ( widgetRef ) => {
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 ) {
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 ) ) {
2024-09-22 12:39:03 +05:30
if ( ref . current === target ) {
innerWidget = ref . current
break
}
2024-09-18 22:16:34 +05:30
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())
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-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 : [ ] ,
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 ) {
2024-09-13 12:28:32 +05:30
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 ( {
2024-09-18 11:39:07 +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-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
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 {
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
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
}
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
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
}
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 )
}
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 ( ) {
2024-09-18 11:39:07 +05:30
if ( ! this . state . selectedWidget )
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 ( {
2024-09-18 11:39:07 +05:30
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-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
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 ) => {
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
return recursiveRemove ( this . state . widgets )
}
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
}
/ * *
* 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
* /
2024-09-22 15:00:34 +05:30
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 )
2024-09-22 15:00:34 +05:30
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 )
2024-09-21 22:47:47 +05:30
// 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-20 15:12:43 +05:30
}
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-20 15:12:43 +05:30
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 ) {
2024-09-22 15:00:34 +05:30
if ( ! isSubClassOfWidget ( widgetComponentType ) ) {
throw new Error ( "widgetComponentType must be a subclass of Widget class" )
}
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-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)
}
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
2024-09-12 19:20:46 +05:30
this . setState ( {
widgets : widgets
} , ( ) => {
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
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 ) {
2024-09-17 11:55:21 +05:30
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 = [ ] ) {
2024-09-18 11:39:07 +05:30
let activeWidgets = removeDuplicateObjects ( [ ... widgets , this . state . selectedWidget ] , "__id" )
2024-09-18 22:16:34 +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
}
}
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
this . widgetRefs = { }
2024-09-15 12:08:29 +05:30
this . setState ( {
2024-09-09 19:06:03 +05:30
widgets : [ ]
2024-09-12 19:20:46 +05:30
} )
2024-09-13 19:24:03 +05:30
if ( this . _onWidgetListUpdated )
this . _onWidgetListUpdated ( [ ] )
2024-09-09 19:06:03 +05:30
}
2024-09-18 22:16:34 +05:30
removeWidget ( widgetId ) {
2024-09-20 15:12:43 +05:30
2024-09-09 19:06:03 +05:30
delete this . widgetRefs [ widgetId ]
2024-09-22 12:39:03 +05:30
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-09 19:06:03 +05:30
}
2024-09-18 22:16:34 +05:30
onActiveWidgetUpdate ( widgetId ) {
2024-09-15 12:08:29 +05:30
2024-09-18 11:39:07 +05:30
if ( ! this . state . selectedWidget || widgetId !== this . state . 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 ( {
2024-09-18 11:39:07 +05:30
toolbarAttrs : this . state . selectedWidget . getToolbarAttrs ( )
2024-09-15 12:08:29 +05:30
} )
}
2024-09-16 12:23:15 +05:30
/ * *
2024-09-17 11:55:21 +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
* /
2024-09-22 12:39:03 +05:30
handleDropEvent = ( e , draggedElement , widgetClass = null ) => {
2024-09-15 19:22:32 +05:30
e . preventDefault ( )
2024-09-17 11:55:21 +05:30
const container = draggedElement . getAttribute ( "data-container" )
2024-09-15 22:54:53 +05:30
const canvasRect = this . canvasRef . current . getBoundingClientRect ( )
2024-09-21 22:47:47 +05:30
const draggedElementRect = draggedElement . getBoundingClientRect ( )
const elementWidth = draggedElementRect . width
const elementHeight = draggedElementRect . height
2024-09-15 19:22:32 +05:30
const { clientX , clientY } = e
2024-09-22 12:39:03 +05:30
let finalPosition = {
x : ( clientX - canvasRect . left ) / this . state . zoom ,
y : ( clientY - canvasRect . top ) / this . state . zoom ,
2024-09-18 22:16:34 +05:30
}
2024-09-21 22:47:47 +05:30
2024-09-22 12:39:03 +05:30
2024-09-20 15:12:43 +05:30
if ( container === WidgetContainer . SIDEBAR ) {
2024-09-22 12:39:03 +05:30
if ( ! widgetClass ) {
throw new Error ( "WidgetClass has to be passed for widgets dropped from sidebar" )
}
2024-09-21 22:47:47 +05:30
// TODO: handle drop from sidebar
2024-09-17 18:32:33 +05:30
// if the widget is being dropped from the sidebar, use the info to create the widget first
2024-09-18 22:16:34 +05:30
this . createWidget ( Widget , ( { id , widgetRef } ) => {
2024-09-17 11:55:21 +05:30
widgetRef . current . setPos ( finalPosition . x , finalPosition . y )
} )
2024-09-20 15:12:43 +05:30
} else if ( [ WidgetContainer . CANVAS , WidgetContainer . WIDGET ] . includes ( container ) ) {
2024-09-17 11:55:21 +05:30
2024-09-22 12:39:03 +05:30
// 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 )
2024-09-17 11:55:21 +05:30
// console.log("WidgetObj: ", widgetObj)
2024-09-20 15:12:43 +05:30
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
2024-09-20 15:12:43 +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
2024-09-20 15:12:43 +05:30
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
2024-09-20 15:12:43 +05:30
// console.log("pre updated widgets: ", updatedWidgets)
2024-09-19 19:26:10 +05:30
const updatedChildWidget = {
... childWidgetObj ,
parent : "" ,
2024-09-20 15:12:43 +05:30
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
}
2024-09-20 15:12:43 +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 ,
2024-09-20 15:12:43 +05:30
// children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id)
2024-09-19 19:26:10 +05:30
}
2024-09-20 15:12:43 +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-20 15:12:43 +05:30
2024-09-19 19:26:10 +05:30
this . setState ( {
2024-09-20 15:12:43 +05:30
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-20 15:12:43 +05:30
2024-09-19 19:26:10 +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 (
< 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 }
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 } ) }
>
{ /* 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 ( ) {
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 - [ 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 " >
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
< / T o o l t i p >
2024-09-13 16:03:58 +05:30
< Tooltip title = "Clear canvas" >
2024-09-14 16:03:26 +05:30
< Button danger icon = { < DeleteOutlined / > } onClick = { this . clearCanvas } / >
2024-09-12 19:20:46 +05:30
< / T o o l t i p >
2024-08-08 16:21:19 +05:30
< / d i v >
2024-09-16 22:04:24 +05:30
{ /* <ActiveWidgetProvider> */ }
2024-09-18 22:16:34 +05:30
< DroppableWrapper id = "canvas-droppable"
className = "tw-w-full tw-h-full"
onDrop = { this . handleDropEvent } >
2024-09-17 11:55:21 +05:30
{ /* <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
< / d i v >
2024-09-13 16:03:58 +05:30
< / d i v >
2024-09-18 22:16:34 +05:30
< / d i v >
< / D r o p d o w n >
2024-09-17 11:55:21 +05:30
{ /* </DragWidgetProvider> */ }
2024-09-15 19:22:32 +05:30
< / D r o p p a b l e W r a p p e r >
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
< / d i v >
)
}
}
export default Canvas