2024-08-08 16:21:19 +05:30
import React from "react"
import { NotImplementedError } from "../../utils/errors"
import Tools from "../constants/tools"
2024-09-22 22:24:35 +05:30
import { Layouts , PosType } from "../constants/layouts"
2024-08-08 16:21:19 +05:30
import Cursor from "../constants/cursor"
2024-09-09 19:06:03 +05:30
import { toSnakeCase } from "../utils/utils"
import EditableDiv from "../../components/editableDiv"
2024-09-16 22:04:24 +05:30
import { ActiveWidgetContext } from "../activeWidgetContext"
2024-09-17 11:55:21 +05:30
import { DragWidgetProvider } from "./draggableWidgetContext"
import WidgetDraggable from "./widgetDragDrop"
2024-09-19 19:26:10 +05:30
import WidgetContainer from "../constants/containers"
2024-09-21 18:37:28 +05:30
import { DragContext } from "../../components/draggable/draggableContext"
2024-09-19 19:26:10 +05:30
2024-09-24 23:27:52 +05:30
// FIXME: make it possible to have fit-width and height
2024-09-19 19:26:10 +05:30
const ATTRS _KEYS = [ 'value' , 'label' , 'tool' , 'onChange' , 'toolProps' ] // these are attrs keywords, don't use these keywords as keys while defining the attrs property
2024-08-08 16:21:19 +05:30
/ * *
* Base class to be extended
* /
2024-09-15 12:08:29 +05:30
class Widget extends React . Component {
2024-08-08 16:21:19 +05:30
2024-08-08 22:49:14 +05:30
static widgetType = "widget"
2024-08-08 16:21:19 +05:30
2024-09-24 21:49:26 +05:30
static requirements = [ ] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker
static requiredImports = [ ] // import statements
2024-09-16 22:04:24 +05:30
// static contextType = ActiveWidgetContext
2024-09-15 12:08:29 +05:30
constructor ( props ) {
2024-08-08 16:21:19 +05:30
super ( props )
2024-08-08 22:49:14 +05:30
2024-09-15 12:08:29 +05:30
const { id , widgetName , canvasRef } = props
2024-09-12 19:20:46 +05:30
// console.log("Id: ", id)
2024-08-08 16:21:19 +05:30
// this id has to be unique inside the canvas, it will be set automatically and should never be changed
2024-08-08 22:49:14 +05:30
this . _ _id = id
2024-09-13 22:05:38 +05:30
this . canvas = canvasRef ? . current || null // canvasContainerRef, because some events work properly only if attached to the container
2024-09-09 19:06:03 +05:30
// this._selected = false
2024-08-08 16:21:19 +05:30
this . _disableResize = false
this . _disableSelection = false
2024-09-22 22:24:35 +05:30
this . minSize = { width : 10 , height : 10 } // disable resizing below this number
this . maxSize = { width : 2000 , height : 2000 } // disable resizing above this number
2024-09-10 21:34:05 +05:30
2024-08-08 16:21:19 +05:30
this . cursor = Cursor . POINTER
this . icon = "" // antd icon name representing this widget
2024-09-21 18:37:28 +05:30
this . elementRef = React . createRef ( ) // this is the outer ref for draggable area
this . swappableAreaRef = React . createRef ( ) // helps identify if the users intent is to swap or drop inside the widget
this . innerAreaRef = React . createRef ( ) // this is the inner area where swap is prevented and only drop is accepted
2024-08-08 16:21:19 +05:30
this . functions = {
2024-09-15 12:08:29 +05:30
"load" : { "args1" : "number" , "args2" : "string" }
2024-08-08 16:21:19 +05:30
}
2024-09-23 12:31:01 +05:30
// This indicates if the draggable can be dropped on this widget, set this to null to disable drops
this . droppableTags = { }
2024-08-08 16:21:19 +05:30
this . boundingRect = {
x : 0 ,
y : 0 ,
height : 100 ,
width : 100
2024-09-15 12:08:29 +05:30
}
2024-08-08 16:21:19 +05:30
2024-08-08 22:49:14 +05:30
this . state = {
2024-09-09 19:06:03 +05:30
zIndex : 0 ,
selected : false ,
2024-09-15 12:08:29 +05:30
widgetName : widgetName || 'widget' , // this will later be converted to variable name
2024-09-13 12:28:32 +05:30
enableRename : false , // will open the widgets editable div for renaming
2024-09-22 22:24:35 +05:30
2024-09-24 21:49:26 +05:30
parentLayout : null , // depending on the parents layout the child will behave
2024-09-21 18:37:28 +05:30
isDragging : false , // tells if the widget is currently being dragged
2024-09-17 18:32:33 +05:30
dragEnabled : true ,
2024-09-19 19:26:10 +05:30
widgetContainer : WidgetContainer . CANVAS , // what is the parent of the widget
2024-09-17 18:32:33 +05:30
showDroppableStyle : { // shows the droppable indicator
2024-09-22 22:24:35 +05:30
allow : false ,
2024-09-17 18:32:33 +05:30
show : false ,
} ,
2024-09-13 19:24:03 +05:30
2024-09-15 12:08:29 +05:30
pos : { x : 0 , y : 0 } ,
size : { width : 100 , height : 100 } ,
2024-09-19 19:26:10 +05:30
positionType : PosType . ABSOLUTE ,
2024-09-14 16:03:26 +05:30
2024-09-13 22:05:38 +05:30
widgetStyling : {
2024-09-14 16:03:26 +05:30
// use for widget's inner styling
2024-09-20 19:17:29 +05:30
backgroundColor : "#fff" ,
display : "flex" ,
flexDirection : "row" ,
gap : 10 ,
flexWrap : "wrap"
2024-09-13 22:05:38 +05:30
} ,
2024-09-15 12:08:29 +05:30
attrs : {
2024-09-13 19:24:03 +05:30
styling : {
backgroundColor : {
2024-09-14 16:03:26 +05:30
label : "Background Color" ,
2024-09-13 19:24:03 +05:30
tool : Tools . COLOR _PICKER , // the tool to display, can be either HTML ELement or a constant string
2024-09-20 15:12:43 +05:30
value : "#fff" ,
2024-09-16 22:04:24 +05:30
onChange : ( value ) => {
this . setWidgetStyling ( "backgroundColor" , value )
this . setAttrValue ( "styling.backgroundColor" , value )
}
2024-09-13 19:24:03 +05:30
} ,
2024-09-22 22:24:35 +05:30
2024-09-15 12:08:29 +05:30
label : "Styling"
2024-09-14 16:03:26 +05:30
} ,
layout : {
label : "Layout" ,
2024-09-18 22:16:34 +05:30
tool : Tools . LAYOUT _MANAGER , // the tool to display, can be either HTML ELement or a constant string
value : {
layout : "flex" ,
2024-09-20 15:12:43 +05:30
direction : "row" ,
2024-09-18 22:16:34 +05:30
grid : {
rows : 1 ,
cols : 1
2024-09-20 15:12:43 +05:30
} ,
gap : 10 ,
2024-09-18 22:16:34 +05:30
} ,
2024-09-19 19:26:10 +05:30
toolProps : {
options : [
{ value : "flex" , label : "Flex" } ,
{ value : "grid" , label : "Grid" } ,
{ value : "place" , label : "Place" } ,
] ,
} ,
2024-09-20 15:12:43 +05:30
onChange : ( value ) => {
2024-09-24 21:49:26 +05:30
console . log ( "changed: " , value )
2024-09-20 15:12:43 +05:30
// this.setAttrValue("layout", value)
this . setLayout ( value )
}
2024-09-13 19:24:03 +05:30
} ,
2024-09-24 23:27:52 +05:30
// events: {
// event1: {
// tool: Tools.EVENT_HANDLER,
// value: ""
// }
// }
2024-09-13 19:24:03 +05:30
} ,
2024-08-08 22:49:14 +05:30
}
2024-09-09 19:06:03 +05:30
this . getElement = this . getElement . bind ( this )
2024-09-20 15:12:43 +05:30
this . getId = this . getId . bind ( this )
2024-09-15 12:08:29 +05:30
this . getPos = this . getPos . bind ( this )
this . getSize = this . getSize . bind ( this )
2024-09-13 16:03:58 +05:30
this . getWidgetName = this . getWidgetName . bind ( this )
this . getWidgetType = this . getWidgetType . bind ( this )
2024-09-15 12:08:29 +05:30
this . getBoundingRect = this . getBoundingRect . bind ( this )
2024-09-22 22:24:35 +05:30
this . getAttrValue = this . getAttrValue . bind ( this )
2024-09-15 12:08:29 +05:30
this . getToolbarAttrs = this . getToolbarAttrs . bind ( this )
2024-09-13 12:28:32 +05:30
// this.openRenaming = this.openRenaming.bind(this)
2024-09-09 19:06:03 +05:30
this . isSelected = this . isSelected . bind ( this )
this . setPos = this . setPos . bind ( this )
2024-09-15 12:08:29 +05:30
this . setAttrValue = this . setAttrValue . bind ( this )
this . setWidgetName = this . setWidgetName . bind ( this )
2024-09-13 22:05:38 +05:30
this . setWidgetStyling = this . setWidgetStyling . bind ( this )
2024-09-19 19:26:10 +05:30
this . setPosType = this . setPosType . bind ( this )
2024-09-13 22:05:38 +05:30
2024-09-15 12:08:29 +05:30
}
2024-08-08 16:21:19 +05:30
2024-09-15 12:08:29 +05:30
componentDidMount ( ) {
2024-09-19 19:26:10 +05:30
2024-09-20 19:17:29 +05:30
// FIXME: initial layout is not set properly
2024-09-23 12:31:01 +05:30
if ( this . state . attrs . layout ) {
this . setLayout ( this . state . attrs . layout . value )
console . log ( "prior layout: " , this . state . attrs . layout . value )
}
if ( this . state . attrs . styling . backgroundColor )
this . setWidgetStyling ( 'backgroundColor' , this . state . attrs . styling ? . backgroundColor . value || "#fff" )
2024-09-22 22:24:35 +05:30
2024-09-20 15:12:43 +05:30
this . load ( this . props . initialData || { } ) // load the initial data
2024-09-19 19:26:10 +05:30
2024-08-08 16:21:19 +05:30
}
2024-09-15 12:08:29 +05:30
componentWillUnmount ( ) {
2024-08-08 16:21:19 +05:30
}
2024-09-15 12:08:29 +05:30
updateState = ( newState , callback ) => {
2024-09-16 22:04:24 +05:30
2024-09-22 15:00:34 +05:30
// FIXME: maximum recursion error when updating size, color etc
2024-09-15 12:08:29 +05:30
this . setState ( newState , ( ) => {
2024-09-16 22:04:24 +05:30
2024-09-15 12:08:29 +05:30
const { onWidgetUpdate } = this . props
if ( onWidgetUpdate ) {
onWidgetUpdate ( this . _ _id )
}
2024-09-16 22:04:24 +05:30
// const { activeWidgetId, updateToolAttrs } = this.context
// if (activeWidgetId === this.__id)
// updateToolAttrs(this.getToolbarAttrs())
2024-09-15 12:08:29 +05:30
if ( callback ) callback ( )
2024-09-16 22:04:24 +05:30
2024-09-15 12:08:29 +05:30
} )
}
_getWidgetMethods = ( ) => {
return {
rename : this . setWidgetName ,
resize : this . setWidgetSize ,
setWidgetAttrs : this . setAttrValue ,
}
}
2024-09-22 22:24:35 +05:30
getToolbarAttrs ( ) {
2024-09-15 12:08:29 +05:30
return ( {
2024-09-22 22:24:35 +05:30
id : this . _ _id ,
widgetName : {
label : "Widget Name" ,
tool : Tools . INPUT , // the tool to display, can be either HTML ELement or a constant string
toolProps : { placeholder : "Widget name" , maxLength : 40 } ,
value : this . state . widgetName ,
onChange : ( value ) => this . setWidgetName ( value )
} ,
size : {
label : "Size" ,
display : "horizontal" ,
width : {
label : "Width" ,
tool : Tools . NUMBER _INPUT , // the tool to display, can be either HTML ELement or a constant string
toolProps : { placeholder : "width" , max : this . maxSize . width , min : this . minSize . width } ,
value : this . state . size . width || 100 ,
onChange : ( value ) => this . setWidgetSize ( value , null )
2024-09-15 12:08:29 +05:30
} ,
2024-09-22 22:24:35 +05:30
height : {
label : "Height" ,
tool : Tools . NUMBER _INPUT ,
toolProps : { placeholder : "height" , max : this . maxSize . height , min : this . minSize . height } ,
value : this . state . size . height || 100 ,
onChange : ( value ) => this . setWidgetSize ( null , value )
2024-09-15 12:08:29 +05:30
} ,
2024-09-22 22:24:35 +05:30
} ,
... this . state . attrs ,
2024-09-15 12:08:29 +05:30
} )
}
2024-09-09 19:06:03 +05:30
// TODO: add context menu items such as delete, add etc
2024-09-15 12:08:29 +05:30
contextMenu ( ) {
2024-09-09 19:06:03 +05:30
}
2024-09-15 12:08:29 +05:30
getVariableName ( ) {
2024-09-09 19:06:03 +05:30
return toSnakeCase ( this . state . widgetName )
}
2024-09-15 12:08:29 +05:30
getWidgetName ( ) {
2024-09-13 16:03:58 +05:30
return this . state . widgetName
}
2024-09-15 12:08:29 +05:30
getWidgetType ( ) {
2024-09-13 16:03:58 +05:30
return this . constructor . widgetType
}
2024-09-24 21:49:26 +05:30
getRequirements = ( ) => {
return this . constructor . requirements
}
getImports = ( ) => {
return this . constructor . requiredImports
}
getCode = ( ) => {
throw new NotImplementedError ( "Get Code must be implemented by the subclass" )
}
2024-09-15 12:08:29 +05:30
getAttributes ( ) {
2024-09-13 19:24:03 +05:30
return this . state . attrs
}
2024-09-22 22:24:35 +05:30
getId ( ) {
2024-09-20 15:12:43 +05:30
return this . _ _id
}
2024-09-15 12:08:29 +05:30
select ( ) {
2024-09-09 19:24:43 +05:30
this . setState ( {
2024-09-09 19:06:03 +05:30
selected : true
2024-09-09 19:24:43 +05:30
} )
2024-09-15 12:08:29 +05:30
2024-08-08 16:21:19 +05:30
}
2024-09-15 12:08:29 +05:30
deSelect ( ) {
2024-09-09 19:24:43 +05:30
this . setState ( {
2024-09-09 19:06:03 +05:30
selected : false
2024-09-09 19:24:43 +05:30
} )
2024-09-09 19:06:03 +05:30
}
2024-09-15 12:08:29 +05:30
isSelected ( ) {
2024-09-09 19:06:03 +05:30
return this . state . selected
}
2024-09-22 22:24:35 +05:30
setPosType ( positionType ) {
2024-09-19 19:26:10 +05:30
2024-09-22 22:24:35 +05:30
if ( ! Object . values ( PosType ) . includes ( positionType ) ) {
2024-09-19 19:26:10 +05:30
throw Error ( ` The Position type can only be among: ${ Object . values ( PosType ) . join ( ", " ) } ` )
}
this . setState ( {
positionType : positionType
} )
}
2024-09-15 12:08:29 +05:30
setPos ( x , y ) {
2024-09-10 21:34:05 +05:30
2024-09-16 22:04:24 +05:30
this . setState ( {
2024-09-15 12:08:29 +05:30
pos : { x , y }
} )
2024-09-16 22:04:24 +05:30
// this.updateState({
// pos: { x, y }
// })
2024-09-12 22:09:13 +05:30
}
2024-09-09 19:06:03 +05:30
2024-09-15 12:08:29 +05:30
getPos ( ) {
2024-09-14 16:03:26 +05:30
return this . state . pos
2024-08-08 16:21:19 +05:30
}
2024-09-15 12:08:29 +05:30
getProps ( ) {
2024-09-09 19:06:03 +05:30
return this . attrs
2024-08-08 16:21:19 +05:30
}
2024-09-15 12:08:29 +05:30
getBoundingRect ( ) {
2024-09-10 21:34:05 +05:30
return this . elementRef . current ? . getBoundingClientRect ( )
}
2024-09-15 12:08:29 +05:30
getSize ( ) {
return this . state . size
2024-09-10 21:34:05 +05:30
}
2024-09-15 12:08:29 +05:30
getWidgetFunctions ( ) {
2024-08-08 16:21:19 +05:30
return this . functions
}
2024-09-15 12:08:29 +05:30
getId ( ) {
2024-08-08 16:21:19 +05:30
return this . _ _id
}
2024-09-15 12:08:29 +05:30
getElement ( ) {
2024-09-09 19:06:03 +05:30
return this . elementRef . current
}
2024-09-19 19:26:10 +05:30
2024-09-14 16:03:26 +05:30
/ * *
* Given the key as a path , sets the value for the widget attribute
* @ param { string } path - path to the key , eg : styling . backgroundColor
* @ param { any } value
* /
2024-09-15 12:08:29 +05:30
setAttrValue ( path , value ) {
2024-09-22 15:00:34 +05:30
const keys = path . split ( '.' )
const lastKey = keys . pop ( )
// Traverse the state and update the nested value immutably
let newAttrs = { ... this . state . attrs }
let nestedObject = newAttrs
2024-09-22 22:24:35 +05:30
2024-09-22 15:00:34 +05:30
keys . forEach ( key => {
nestedObject [ key ] = { ... nestedObject [ key ] } // Ensure immutability
nestedObject = nestedObject [ key ]
2024-09-14 16:03:26 +05:30
} )
2024-09-22 22:24:35 +05:30
2024-09-22 15:00:34 +05:30
nestedObject [ lastKey ] . value = value
2024-09-22 22:24:35 +05:30
this . updateState ( { attrs : newAttrs } )
}
/ * *
* Given the key as a path , retrieves the value for the widget attribute
* @ param { string } path - path to the key , eg : styling . backgroundColor
* @ returns { any } - the value at the given path
* /
getAttrValue ( path ) {
const keys = path . split ( '.' )
// Traverse the state and get the nested value
let nestedObject = this . state . attrs
for ( const key of keys ) {
if ( nestedObject [ key ] !== undefined ) {
nestedObject = nestedObject [ key ]
} else {
return undefined // Return undefined if the key doesn't exist
}
}
return nestedObject ? . value // Return the value (assuming it has a 'value' field)
2024-09-14 16:03:26 +05:30
}
2024-09-19 19:26:10 +05:30
/ * *
* returns the path from the serialized attrs values ,
* this is a helper function to remove any non - serializable data associated with attrs
* eg : { "styling.backgroundColor" : "#ffff" , "layout" : { layout : "flex" , direction : "" , grid : } }
* /
serializeAttrsValues = ( ) => {
const serializeValues = ( obj , currentPath = "" ) => {
const result = { }
2024-09-22 22:24:35 +05:30
2024-09-19 19:26:10 +05:30
for ( let key in obj ) {
if ( ATTRS _KEYS . includes ( key ) ) continue // don't serialize these as separate keys
if ( typeof obj [ key ] === 'object' && obj [ key ] !== null ) {
// If the key contains a value property
if ( obj [ key ] . hasOwnProperty ( 'value' ) ) {
const path = currentPath ? ` ${ currentPath } . ${ key } ` : key ;
2024-09-22 22:24:35 +05:30
2024-09-19 19:26:10 +05:30
// If the value is an object, retain the entire value object
if ( typeof obj [ key ] . value === 'object' && obj [ key ] . value !== null ) {
result [ path ] = obj [ key ] . value
} else {
result [ ` ${ path } ` ] = obj [ key ] . value
}
}
// Continue recursion for nested objects
Object . assign ( result , serializeValues ( obj [ key ] , currentPath ? ` ${ currentPath } . ${ key } ` : key ) )
}
}
2024-09-22 22:24:35 +05:30
2024-09-19 19:26:10 +05:30
return result
}
return serializeValues ( this . state . attrs )
}
2024-09-15 12:08:29 +05:30
setZIndex ( zIndex ) {
2024-09-13 12:28:32 +05:30
this . setState ( {
zIndex : zIndex
} )
}
2024-09-15 12:08:29 +05:30
setWidgetName ( name ) {
2024-09-14 16:03:26 +05:30
2024-09-15 12:08:29 +05:30
this . updateState ( {
widgetName : name . length > 0 ? name : this . state . widgetName
} )
2024-09-14 16:03:26 +05:30
}
2024-09-24 21:49:26 +05:30
/ * *
*
* @ param { Layouts } layout
* /
setParentLayout = ( layout ) => {
let updates = {
parentLayout : layout ,
}
if ( layout === Layouts . FLEX || layout === Layouts . GRID ) {
updates = {
... updates ,
positionType : PosType . NONE
}
} else if ( layout === Layouts . PLACE ) {
updates = {
... updates ,
positionType : PosType . ABSOLUTE
}
}
console . log ( "Parent layout updated: " , updates )
this . setState ( updates )
}
getLayout = ( ) => {
return this . state ? . attrs ? . layout ? . value || Layouts . FLEX
}
2024-09-22 22:24:35 +05:30
setLayout ( value ) {
2024-09-22 12:39:03 +05:30
// FIXME: when the parent layout is place, the child widgets should have position absolute
2024-09-22 22:24:35 +05:30
const { layout , direction , grid = { rows : 1 , cols : 1 } , gap = 10 } = value
2024-09-20 15:12:43 +05:30
2024-09-24 21:49:26 +05:30
console . log ( "layout value: " , value )
2024-09-20 15:12:43 +05:30
const widgetStyle = {
... this . state . widgetStyling ,
2024-09-24 21:49:26 +05:30
display : layout !== Layouts . PLACE ? layout : "block" ,
2024-09-20 15:12:43 +05:30
flexDirection : direction ,
gap : ` ${ gap } px ` ,
flexWrap : "wrap"
// TODO: add grid rows and cols
}
this . updateState ( {
widgetStyling : widgetStyle
} )
2024-09-24 21:49:26 +05:30
this . setAttrValue ( "layout" , value )
this . props . onLayoutUpdate ( { parentId : this . _ _id , parentLayout : layout } ) // inform children about the layout update
2024-09-20 15:12:43 +05:30
}
2024-09-15 12:08:29 +05:30
/ * *
*
* @ param { string } key - The string in react Style format
* @ param { string } value - Value of the style
* /
2024-09-16 22:04:24 +05:30
setWidgetStyling ( key , value ) {
2024-09-24 21:49:26 +05:30
2024-09-15 12:08:29 +05:30
const widgetStyle = {
... this . state . widgetStyling ,
[ key ] : value
}
this . setState ( {
widgetStyling : widgetStyle
} )
}
/ * *
*
* @ param { number | null } width
* @ param { number | null } height
* /
2024-09-16 22:04:24 +05:30
setWidgetSize ( width , height ) {
2024-09-15 12:08:29 +05:30
const newSize = {
width : Math . max ( this . minSize . width , Math . min ( width || this . state . size . width , this . maxSize . width ) ) ,
height : Math . max ( this . minSize . height , Math . min ( height || this . state . size . height , this . maxSize . height ) ) ,
}
this . updateState ( {
size : newSize
} )
2024-09-14 16:03:26 +05:30
}
2024-09-22 22:24:35 +05:30
setResize ( pos , size ) {
2024-09-18 11:39:07 +05:30
// useful when resizing the widget relative to the canvas, sets all pos, and size
2024-09-15 12:08:29 +05:30
this . updateState ( {
2024-09-18 11:39:07 +05:30
size : size ,
pos : pos
2024-09-15 12:08:29 +05:30
} )
2024-09-10 21:34:05 +05:30
}
2024-09-15 12:08:29 +05:30
openRenaming ( ) {
2024-09-13 12:28:32 +05:30
this . setState ( {
selected : true ,
enableRename : true
} )
}
2024-09-15 12:08:29 +05:30
closeRenaming ( ) {
2024-09-13 12:28:32 +05:30
this . setState ( {
enableRename : false
} )
}
2024-09-20 22:07:22 +05:30
enableDrag = ( ) => {
this . setState ( {
dragEnabled : true
} )
}
disableDrag = ( ) => {
this . setState ( {
dragEnabled : false
} )
}
2024-09-15 15:31:04 +05:30
2024-09-19 19:26:10 +05:30
/ * *
*
* serialize data for saving
* /
serialize = ( ) => {
// NOTE: when serializing make sure, you are only passing serializable objects not functions or other
return ( {
zIndex : this . state . zIndex ,
widgetName : this . state . widgetName ,
pos : this . state . pos ,
size : this . state . size ,
widgetContainer : this . state . widgetContainer ,
widgetStyling : this . state . widgetStyling ,
2024-09-24 21:49:26 +05:30
parentLayout : this . state . parentLayout ,
2024-09-19 19:26:10 +05:30
positionType : this . state . positionType ,
attrs : this . serializeAttrsValues ( ) // makes sure that functions are not serialized
} )
}
/ * *
* loads the data
* @ param { object } data
* /
load = ( data ) => {
2024-09-20 19:17:29 +05:30
if ( Object . keys ( data ) . length === 0 ) return // no data to load
2024-09-23 18:25:40 +05:30
data = { ... data } // create a shallow copy
2024-09-19 19:26:10 +05:30
2024-09-24 21:49:26 +05:30
const { attrs , parentLayout , ... restData } = data
2024-09-19 19:26:10 +05:30
2024-09-23 18:25:40 +05:30
2024-09-24 21:49:26 +05:30
let layoutUpdates = {
parentLayout : parentLayout
}
2024-09-24 23:27:52 +05:30
2024-09-24 21:49:26 +05:30
if ( parentLayout === Layouts . FLEX || parentLayout === Layouts . GRID ) {
layoutUpdates = {
... layoutUpdates ,
positionType : PosType . NONE
}
} else if ( parentLayout === Layouts . PLACE ) {
layoutUpdates = {
... layoutUpdates ,
positionType : PosType . ABSOLUTE
}
}
const newData = {
... restData ,
layoutUpdates
}
console . log ( "loaded layout2: " , newData )
this . setState ( newData , ( ) => {
2024-09-23 18:25:40 +05:30
// UPdates attrs
let newAttrs = { ... this . state . attrs }
// Iterate over each path in the updates object
Object . entries ( attrs ) . forEach ( ( [ path , value ] ) => {
const keys = path . split ( '.' )
const lastKey = keys . pop ( )
// Traverse the nested object within attrs
let nestedObject = newAttrs
keys . forEach ( key => {
nestedObject [ key ] = { ... nestedObject [ key ] } // Ensure immutability for each nested level
nestedObject = nestedObject [ key ]
} )
// Set the value at the last key
nestedObject [ lastKey ] . value = value
} )
this . updateState ( { attrs : newAttrs } )
} )
2024-09-19 19:26:10 +05:30
}
2024-09-21 18:37:28 +05:30
handleDragStart = ( e , callback ) => {
e . stopPropagation ( )
callback ( this . elementRef ? . current || null )
2024-09-24 21:49:26 +05:30
// this.props.onWidgetDragStart(this.elementRef?.current)
2024-09-21 18:37:28 +05:30
// Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
const dragImage = this . elementRef ? . current . cloneNode ( true )
dragImage . style . opacity = '1' // Ensure full opacity
dragImage . style . position = 'absolute'
dragImage . style . top = '-9999px' // Move it out of view
document . body . appendChild ( dragImage )
const rect = this . elementRef ? . current . getBoundingClientRect ( )
2024-09-21 22:47:47 +05:30
// snap to mouse pos
// const offsetX = e.clientX - rect.left
// const offsetY = e.clientY - rect.top
// snap to middle
const offsetX = rect . width / 2
const offsetY = rect . height / 2
2024-09-21 18:37:28 +05:30
// Set the custom drag image with correct offset to avoid snapping to the top-left corner
e . dataTransfer . setDragImage ( dragImage , offsetX , offsetY )
2024-09-21 22:47:47 +05:30
2024-09-21 18:37:28 +05:30
// Remove the custom drag image after some time to avoid leaving it in the DOM
setTimeout ( ( ) => {
document . body . removeChild ( dragImage )
} , 0 )
2024-09-21 22:47:47 +05:30
2024-09-22 12:39:03 +05:30
// NOTE: this line will prevent problem's such as self-drop or dropping inside its own children
setTimeout ( this . disablePointerEvents , 1 )
2024-09-21 22:47:47 +05:30
2024-09-22 22:24:35 +05:30
this . setState ( { isDragging : true } )
2024-09-21 22:47:47 +05:30
2024-09-21 18:37:28 +05:30
}
handleDragEnter = ( e , draggedElement , setOverElement ) => {
2024-09-24 14:33:02 +05:30
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
}
2024-09-21 18:37:28 +05:30
const dragEleType = draggedElement . getAttribute ( "data-draggable-type" )
2024-09-22 12:39:03 +05:30
// console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
2024-09-21 18:37:28 +05:30
// FIXME: the outer widget shouldn't be swallowed by inner widget
2024-09-22 22:24:35 +05:30
if ( draggedElement === this . elementRef . current ) {
2024-09-21 18:37:28 +05:30
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
2024-09-22 22:24:35 +05:30
return
2024-09-21 18:37:28 +05:30
}
setOverElement ( e . currentTarget ) // provide context to the provider
let showDrop = {
2024-09-22 22:24:35 +05:30
allow : true ,
2024-09-21 18:37:28 +05:30
show : true
}
2024-09-23 12:31:01 +05:30
const allowDrop = ( this . droppableTags && this . droppableTags !== null && ( Object . keys ( this . droppableTags ) . length === 0 ||
2024-09-22 22:24:35 +05:30
( this . droppableTags . include ? . length > 0 && this . droppableTags . include ? . includes ( dragEleType ) ) ||
( this . droppableTags . exclude ? . length > 0 && ! this . droppableTags . exclude ? . includes ( dragEleType ) )
) )
2024-09-22 19:23:06 +05:30
if ( allowDrop ) {
2024-09-21 18:37:28 +05:30
showDrop = {
allow : true ,
show : true
}
} else {
showDrop = {
allow : false ,
show : true
}
}
this . setState ( {
showDroppableStyle : showDrop
} )
2024-09-22 22:24:35 +05:30
2024-09-21 18:37:28 +05:30
}
handleDragOver = ( e , draggedElement ) => {
2024-09-24 14:33:02 +05:30
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
}
2024-09-22 22:24:35 +05:30
if ( draggedElement === this . elementRef . current ) {
2024-09-21 18:37:28 +05:30
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
2024-09-22 22:24:35 +05:30
return
2024-09-21 18:37:28 +05:30
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement . getAttribute ( "data-draggable-type" )
2024-09-23 12:31:01 +05:30
const allowDrop = ( this . droppableTags && this . droppableTags !== null && ( Object . keys ( this . droppableTags ) . length === 0 ||
2024-09-22 22:24:35 +05:30
( this . droppableTags . include ? . length > 0 && this . droppableTags . include ? . includes ( dragEleType ) ) ||
( this . droppableTags . exclude ? . length > 0 && ! this . droppableTags . exclude ? . includes ( dragEleType ) )
) )
2024-09-22 19:23:06 +05:30
if ( allowDrop ) {
2024-09-21 18:37:28 +05:30
e . preventDefault ( ) // NOTE: this is necessary to allow drop to take place
}
}
2024-09-22 22:24:35 +05:30
handleDropEvent = ( e , draggedElement , widgetClass = null ) => {
2024-09-24 14:33:02 +05:30
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
}
2024-09-21 18:37:28 +05:30
e . preventDefault ( )
e . stopPropagation ( )
2024-09-24 14:33:02 +05:30
2024-09-22 12:39:03 +05:30
// FIXME: sometimes the elements showDroppableStyle is not gone, when dropping on the same widget
2024-09-21 18:37:28 +05:30
this . setState ( {
showDroppableStyle : {
2024-09-22 22:24:35 +05:30
allow : false ,
show : false
}
2024-09-21 18:37:28 +05:30
} , ( ) => {
console . log ( "droppable cleared: " , this . elementRef . current , this . state . showDroppableStyle )
} )
2024-09-22 22:24:35 +05:30
if ( draggedElement === this . elementRef . current ) {
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
let currentElement = e . currentTarget
while ( currentElement ) {
if ( currentElement === draggedElement ) {
// if the parent is dropped accidentally into the child don't allow drop
// console.log("Dropped into a descendant element, ignoring drop")
return // Exit early to prevent the drop
}
currentElement = currentElement . parentElement // Traverse up to check ancestors
}
2024-09-21 18:37:28 +05:30
const container = draggedElement . getAttribute ( "data-container" )
const thisContainer = this . elementRef . current . getAttribute ( "data-container" )
// console.log("Dropped as swappable: ", e.target, this.swappableAreaRef.current.contains(e.target))
// If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas
2024-09-22 22:24:35 +05:30
const swapArea = ( this . swappableAreaRef . current . contains ( e . target ) && ! this . innerAreaRef . current . contains ( e . target ) && thisContainer === WidgetContainer . WIDGET )
2024-09-21 18:37:28 +05:30
2024-09-23 18:25:40 +05:30
const dragEleType = draggedElement . getAttribute ( "data-draggable-type" )
const allowDrop = ( this . droppableTags && this . droppableTags !== null && ( Object . keys ( this . droppableTags ) . length === 0 ||
( this . droppableTags . include ? . length > 0 && this . droppableTags . include ? . includes ( dragEleType ) ) ||
( this . droppableTags . exclude ? . length > 0 && ! this . droppableTags . exclude ? . includes ( dragEleType ) )
) )
if ( ! allowDrop && ! swapArea ) {
// only if both swap and drop is not allowed return, if swap is allowed continue
return
}
2024-09-21 18:37:28 +05:30
// TODO: check if the drop is allowed
2024-09-22 22:24:35 +05:30
if ( [ WidgetContainer . CANVAS , WidgetContainer . WIDGET ] . includes ( container ) ) {
2024-09-21 18:37:28 +05:30
// console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer)
2024-09-22 22:24:35 +05:30
this . props . onAddChildWidget ( {
2024-09-24 21:49:26 +05:30
event : e ,
2024-09-22 22:24:35 +05:30
parentWidgetId : this . _ _id ,
dragElementID : draggedElement . getAttribute ( "data-widget-id" ) ,
swap : swapArea || false
} )
} else if ( container === WidgetContainer . SIDEBAR ) {
2024-09-21 18:37:28 +05:30
// console.log("Dropped on Sidebar: ", this.__id)
2024-09-22 22:24:35 +05:30
this . props . onCreateWidgetRequest ( widgetClass , ( { id , widgetRef } ) => {
2024-09-24 21:49:26 +05:30
this . props . onAddChildWidget ( {
event : e ,
parentWidgetId : this . _ _id ,
dragElementID : id
} ) // if dragged from the sidebar create the widget first
2024-09-22 15:00:34 +05:30
} )
2024-09-21 18:37:28 +05:30
}
}
handleDragLeave = ( e , draggedElement ) => {
// console.log("Left: ", e.currentTarget, e.relatedTarget, draggedElement)
if ( ! e . currentTarget . contains ( draggedElement ) ) {
this . setState ( {
2024-09-22 22:24:35 +05:30
showDroppableStyle : {
allow : false ,
show : false
}
} )
2024-09-21 18:37:28 +05:30
}
}
handleDragEnd = ( callback ) => {
callback ( )
2024-09-22 22:24:35 +05:30
this . setState ( { isDragging : false } )
2024-09-22 12:39:03 +05:30
this . enablePointerEvents ( )
2024-09-24 21:49:26 +05:30
// this.props.onWidgetDragEnd(this.elementRef?.current)
2024-09-22 12:39:03 +05:30
}
disablePointerEvents = ( ) => {
2024-09-22 22:24:35 +05:30
2024-09-22 12:39:03 +05:30
if ( this . elementRef . current )
this . elementRef . current . style . pointerEvents = "none"
2024-09-21 18:37:28 +05:30
}
2024-09-22 12:39:03 +05:30
enablePointerEvents = ( ) => {
if ( this . elementRef . current )
this . elementRef . current . style . pointerEvents = "auto"
}
2024-09-21 18:37:28 +05:30
2024-09-22 19:23:06 +05:30
/ * *
* Note : you must implement this method in subclass , if you want children make sure to pass
* { this . props . children } , to modify the style add this . state . widgetStyling
* /
2024-09-22 22:24:35 +05:30
renderContent ( ) {
2024-08-08 16:21:19 +05:30
// throw new NotImplementedError("render method has to be implemented")
return (
2024-09-22 19:23:06 +05:30
< div className = "tw-w-full tw-h-full tw-p-2 tw-content-start tw-rounded-md tw-overflow-hidden" style = { this . state . widgetStyling } >
2024-09-19 19:26:10 +05:30
{ this . props . children }
2024-08-08 16:21:19 +05:30
< / d i v >
)
}
2024-09-15 15:31:04 +05:30
2024-08-08 16:21:19 +05:30
/ * *
* This is an internal methods don ' t override
* @ returns { HTMLElement }
* /
2024-09-23 12:31:01 +05:30
render ( ) {
2024-09-15 12:08:29 +05:30
2024-09-14 16:03:26 +05:30
let outerStyle = {
2024-08-08 16:21:19 +05:30
cursor : this . cursor ,
2024-09-13 12:28:32 +05:30
zIndex : this . state . zIndex ,
2024-09-19 19:26:10 +05:30
position : this . state . positionType , // don't change this if it has to be movable on the canvas
2024-09-15 12:08:29 +05:30
top : ` ${ this . state . pos . y } px ` ,
left : ` ${ this . state . pos . x } px ` ,
2024-09-14 16:03:26 +05:30
width : ` ${ this . state . size . width } px ` ,
height : ` ${ this . state . size . height } px ` ,
2024-09-21 22:47:47 +05:30
opacity : this . state . isDragging ? 0.3 : 1 ,
2024-08-08 16:21:19 +05:30
}
2024-09-24 21:49:26 +05:30
// const boundingRect = this.getBoundingRect
2024-09-22 22:24:35 +05:30
// FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden
2024-08-08 16:21:19 +05:30
return (
2024-09-21 18:37:28 +05:30
< DragContext . Consumer >
{
2024-09-22 22:24:35 +05:30
( { draggedElement , widgetClass , onDragStart , onDragEnd , setOverElement } ) => (
< div data - widget - id = { this . _ _id }
ref = { this . elementRef }
className = "tw-shadow-xl tw-w-fit tw-h-fit"
style = { outerStyle }
data - draggable - type = { this . getWidgetType ( ) } // helps with droppable
data - container = { this . state . widgetContainer } // indicates how the canvas should handle dragging, one is sidebar other is canvas
2024-09-24 14:33:02 +05:30
data - drag - start - within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
2024-09-22 22:24:35 +05:30
draggable = { this . state . dragEnabled }
onDragOver = { ( e ) => this . handleDragOver ( e , draggedElement ) }
onDrop = { ( e ) => this . handleDropEvent ( e , draggedElement , widgetClass ) }
onDragEnter = { ( e ) => this . handleDragEnter ( e , draggedElement , setOverElement ) }
onDragLeave = { ( e ) => this . handleDragLeave ( e , draggedElement ) }
onDragStart = { ( e ) => this . handleDragStart ( e , onDragStart ) }
onDragEnd = { ( e ) => this . handleDragEnd ( onDragEnd ) }
>
2024-09-21 18:37:28 +05:30
{ /* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */ }
{ /* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */ }
< div className = "tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
2024-09-22 22:24:35 +05:30
>
2024-09-21 22:47:47 +05:30
< div className = { ` tw-absolute tw-top-[-5px] tw-left-[-5px]
2024-09-21 18:37:28 +05:30
tw - border - 1 tw - opacity - 0 tw - border - solid tw - border - black
2024-09-24 21:49:26 +05:30
tw - w - full tw - h - full tw - bg - red - 400
2024-09-21 18:37:28 +05:30
tw - scale - [ 1.1 ] tw - opacity - 1 tw - z - [ - 1 ] ` }
2024-09-22 22:24:35 +05:30
style = { {
width : "calc(100% + 10px)" ,
height : "calc(100% + 10px)" ,
} }
ref = { this . swappableAreaRef }
2024-09-24 21:49:26 +05:30
// swapable area
2024-09-22 22:24:35 +05:30
>
2024-09-21 18:37:28 +05:30
{ /* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */ }
2024-09-22 22:24:35 +05:30
< / d i v >
2024-09-21 18:37:28 +05:30
< div className = "tw-relative tw-w-full tw-h-full" ref = { this . innerAreaRef } >
{ this . renderContent ( ) }
< / d i v >
{
// show drop style on drag hover
this . state . showDroppableStyle . show &&
2024-09-22 22:24:35 +05:30
< div className = { ` ${ this . state . showDroppableStyle . allow ? "tw-border-blue-600" : "tw-border-red-600" }
2024-09-21 18:37:28 +05:30
tw - absolute tw - top - [ - 5 px ] tw - left - [ - 5 px ] tw - w - full tw - h - full tw - z - [ 2 ]
tw - border - 2 tw - border - dashed tw - rounded - lg tw - pointer - events - none
` }
2024-09-22 22:24:35 +05:30
style = { {
width : "calc(100% + 10px)" ,
height : "calc(100% + 10px)" ,
} }
>
< / d i v >
2024-09-17 18:32:33 +05:30
}
2024-09-24 21:49:26 +05:30
{ /* FIXME: the resize handles get clipped in parent container */ }
< div className = { ` tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw - w - full tw - h - full
2024-09-21 18:37:28 +05:30
$ { this . state . selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden' } ` }
2024-09-22 22:24:35 +05:30
style = { {
width : "calc(100% + 20px)" ,
height : "calc(100% + 20px)" ,
2024-09-24 21:49:26 +05:30
zIndex : - 1 ,
2024-09-22 22:24:35 +05:30
} }
>
2024-09-21 18:37:28 +05:30
2024-09-22 12:39:03 +05:30
< div className = { ` "tw-relative tw-w-full tw-h-full" ` } > { /* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */ }
2024-09-21 18:37:28 +05:30
< EditableDiv value = { this . state . widgetName } onChange = { this . setWidgetName }
maxLength = { 40 }
openEdit = { this . state . enableRename }
className = " tw - text - sm tw - w - fit tw - max - w - [ 160 px ] tw - text - clip tw - min - w - [ 150 px ]
tw - overflow - hidden tw - absolute tw -- top - 6 tw - h - 6 "
/ >
< div
2024-09-22 12:39:03 +05:30
className = "tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
2024-09-21 18:37:28 +05:30
style = { { cursor : Cursor . NW _RESIZE } }
onMouseDown = { ( e ) => {
e . stopPropagation ( )
e . preventDefault ( )
this . props . onWidgetResizing ( "nw" )
2024-09-22 22:24:35 +05:30
this . setState ( { dragEnabled : false } )
2024-09-21 18:37:28 +05:30
} }
2024-09-22 22:24:35 +05:30
onMouseUp = { ( ) => this . setState ( { dragEnabled : true } ) }
2024-09-21 18:37:28 +05:30
/ >
< div
className = "tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style = { { cursor : Cursor . SW _RESIZE } }
onMouseDown = { ( e ) => {
e . stopPropagation ( )
e . preventDefault ( )
this . props . onWidgetResizing ( "ne" )
2024-09-22 22:24:35 +05:30
this . setState ( { dragEnabled : false } )
2024-09-21 18:37:28 +05:30
} }
2024-09-22 22:24:35 +05:30
onMouseUp = { ( ) => this . setState ( { dragEnabled : true } ) }
2024-09-21 18:37:28 +05:30
/ >
< div
className = "tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style = { { cursor : Cursor . SW _RESIZE } }
onMouseDown = { ( e ) => {
e . stopPropagation ( )
e . preventDefault ( )
this . props . onWidgetResizing ( "sw" )
2024-09-22 22:24:35 +05:30
this . setState ( { dragEnabled : false } )
2024-09-21 18:37:28 +05:30
} }
2024-09-22 22:24:35 +05:30
onMouseUp = { ( ) => this . setState ( { dragEnabled : true } ) }
2024-09-21 18:37:28 +05:30
/ >
< div
className = "tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style = { { cursor : Cursor . SE _RESIZE } }
onMouseDown = { ( e ) => {
e . stopPropagation ( )
e . preventDefault ( )
this . props . onWidgetResizing ( "se" )
2024-09-22 22:24:35 +05:30
this . setState ( { dragEnabled : false } )
2024-09-21 18:37:28 +05:30
} }
2024-09-22 22:24:35 +05:30
onMouseUp = { ( ) => this . setState ( { dragEnabled : true } ) }
2024-09-21 18:37:28 +05:30
/ >
< / d i v >
2024-09-19 19:26:10 +05:30
< / d i v >
2024-09-16 22:04:24 +05:30
2024-09-21 18:37:28 +05:30
< / d i v >
2024-09-16 22:04:24 +05:30
< / d i v >
2024-09-21 18:37:28 +05:30
)
}
2024-08-08 16:21:19 +05:30
2024-09-21 18:37:28 +05:30
< / D r a g C o n t e x t . C o n s u m e r >
2024-08-08 16:21:19 +05:30
)
}
2024-09-15 12:08:29 +05:30
}
2024-08-08 16:21:19 +05:30
export default Widget