2025-03-22 21:16:13 +05:30
import { memo , useEffect , useMemo , useRef , useState } from "react"
2024-09-13 19:24:03 +05:30
2024-09-24 23:27:52 +05:30
import {
Checkbox , ColorPicker , Input ,
InputNumber , Select , Collapse
} from "antd"
2024-09-13 19:24:03 +05:30
2024-09-13 16:03:58 +05:30
import { capitalize } from "../utils/common"
2024-09-13 22:05:38 +05:30
import Tools from "./constants/tools.js"
2024-09-16 22:04:24 +05:30
import { useActiveWidget } from "./activeWidgetContext.js"
2024-09-19 19:26:10 +05:30
import { Layouts } from "./constants/layouts.js"
2024-09-23 18:25:40 +05:30
import { DynamicRadioInputList } from "../components/inputs.js"
2024-09-27 11:42:34 +05:30
import { useFileUploadContext } from "../contexts/fileUploadContext.js"
import { AudioOutlined , FileImageOutlined , FileTextOutlined , VideoCameraOutlined } from "@ant-design/icons"
2025-03-16 15:43:41 +05:30
import { useWidgetContext } from "./context/widgetContext.js"
2024-09-13 16:03:58 +05:30
2024-09-13 19:24:03 +05:30
/ * *
*
* @ param { boolean } isOpen
2024-09-15 12:08:29 +05:30
* @ param { string } widgetType
* @ param { object } attrs - widget attributes
2024-09-13 19:24:03 +05:30
* /
2025-03-16 15:43:41 +05:30
const CanvasToolBar = memo ( ( { isOpen , widgetType , } ) => {
2024-09-13 16:03:58 +05:30
2024-09-16 22:04:24 +05:30
// const { activeWidgetAttrs } = useActiveWidget()
2025-03-16 15:43:41 +05:30
const { activeWidget } = useWidgetContext ( )
2024-09-16 22:04:24 +05:30
// console.log("active widget context: ", activeWidgetAttrs)
2024-09-13 16:03:58 +05:30
const [ toolbarOpen , setToolbarOpen ] = useState ( isOpen )
2025-03-16 15:43:41 +05:30
const [ toolbarAttrs , setToolbarAttrs ] = useState ( { } )
2024-09-13 16:03:58 +05:30
2025-03-22 21:16:13 +05:30
const focusedInputRef = useRef ( )
const [ cursorPos , setCursorPos ] = useState ( 0 ) // store cursor position for focused input so during remount if the cursor position goes to end we can use this
const [ activeInputKey , setActiveInputKey ] = useState ( null )
2024-09-27 11:42:34 +05:30
const { uploadedAssets } = useFileUploadContext ( )
2025-03-16 15:43:41 +05:30
useEffect ( ( ) => {
const stateUpdatedCallback = ( ) => {
setToolbarAttrs ( activeWidget . getToolbarAttrs ( ) )
}
if ( activeWidget ) {
activeWidget . stateChangeSubscriberCallback ( stateUpdatedCallback )
// console.log("sytate update: ", activeWidget.getToolbarAttrs())
setToolbarAttrs ( activeWidget . getToolbarAttrs ( ) )
2025-03-22 21:16:13 +05:30
2025-03-16 15:43:41 +05:30
}
2025-03-22 21:16:13 +05:30
} , [ activeWidget , focusedInputRef , cursorPos ] ) // , activeWidget?.state
2025-03-16 15:43:41 +05:30
useEffect ( ( ) => {
setToolbarOpen ( isOpen )
} , [ isOpen ] )
2025-03-22 21:16:13 +05:30
useEffect ( ( ) => {
if ( focusedInputRef . current ? . input && activeInputKey ) {
// this fixes the cursor going to the end issue during remount
focusedInputRef . current . setSelectionRange ( cursorPos , cursorPos )
}
} , [ toolbarAttrs , focusedInputRef , cursorPos ] )
2025-03-16 15:43:41 +05:30
// useEffect(() => {
// setToolbarAttrs(activeWidget.getToolbarAttrs())
// }, [])
2024-09-27 11:42:34 +05:30
const uploadItems = useMemo ( ( ) => {
const returnComponentBasedOnFileType = ( file ) => {
if ( file . fileType === "image" ) {
return (
< div className = "tw-w-flex tw-gap-2" >
< FileImageOutlined / >
< span className = "tw-ml-1" > { file . name } < / s p a n >
< / d i v >
)
} else if ( file . fileType === "video" ) {
return (
< div className = "tw-w-flex tw-gap-2" >
< VideoCameraOutlined / >
< span className = "tw-ml-1" > { file . name } < / s p a n >
< / d i v >
)
} else if ( file . fileType === "audio" ) {
return (
< div className = "tw-w-flex tw-gap-2" >
< AudioOutlined / >
< span className = "tw-ml-1" > { file . name } < / s p a n >
< / d i v >
)
} else {
return (
< div className = "tw-w-flex tw-gap-2" >
< FileTextOutlined / >
< span className = "tw-ml-1" > { file . name } < / s p a n >
< / d i v >
)
}
}
const uploadList = uploadedAssets . map ( ( file , idx ) => ( {
value : file . name ,
label : returnComponentBasedOnFileType ( file ) ,
fileType : file . fileType ,
type : file . type ,
// previewUrl: file.previewUrl,
} ) )
return uploadList
} , [ uploadedAssets ] )
2024-09-20 15:12:43 +05:30
2025-03-22 21:16:13 +05:30
const handleInputFocus = ( key , event ) => {
const { value , target } = event
setActiveInputKey ( key )
setCursorPos ( target . selectionStart )
focusedInputRef . current = event . target
}
const handleInputBlur = ( ) => {
setActiveInputKey ( null ) ;
focusedInputRef . current = null ;
}
2024-09-15 12:08:29 +05:30
const handleChange = ( value , callback ) => {
if ( callback ) {
2024-09-14 16:03:26 +05:30
callback ( value )
}
2025-03-22 21:16:13 +05:30
if ( focusedInputRef . current ? . input ) {
2025-03-25 05:24:37 +05:30
setCursorPos ( focusedInputRef . current . input . selectionStart )
2025-03-22 21:16:13 +05:30
} else {
setCursorPos ( 0 )
}
2024-09-14 16:03:26 +05:30
}
2024-09-28 21:50:30 +05:30
function getUploadFileFromName ( name ) {
return uploadedAssets . find ( val => val . name === name )
}
2024-09-18 22:16:34 +05:30
2024-09-27 11:42:34 +05:30
const renderUploadDropDown = ( val , filter ) => {
let uploadOptions = [ ... uploadItems ]
if ( filter ) {
uploadOptions = uploadOptions . filter ( ( value , idx ) => filter . includes ( value . type ) )
}
return (
< div className = "tw-flex tw-w-full" >
< Select
options = { uploadOptions }
size = "large"
placeholder = "select content"
showSearch
className = "tw-w-full"
2025-03-25 19:11:37 +05:30
notFoundContent = "Start uploading from uploads tab"
2024-09-28 21:50:30 +05:30
value = { val . value ? . name || "" }
onChange = { ( value ) => {
const file = getUploadFileFromName ( value )
handleChange ( { name : value , previewUrl : file . previewUrl } , val . onChange )
} }
2024-09-27 11:42:34 +05:30
filterOption = { ( input , option ) =>
( option ? . value ? ? '' ) . toLowerCase ( ) . includes ( input . toLowerCase ( ) )
}
/ >
< / d i v >
)
}
2024-09-18 22:16:34 +05:30
const renderLayoutManager = ( val ) => {
return (
< div className = "tw-flex tw-flex-col tw-gap-2" >
< Select
options = { [
{ value : Layouts . FLEX , label : "Flex" } ,
{ value : Layouts . GRID , label : "Grid" } ,
{ value : Layouts . PLACE , label : "Place" } ,
] }
showSearch
value = { val . value ? . layout || "" }
placeholder = { ` ${ val . label } ` }
size = "medium"
2024-09-20 15:12:43 +05:30
onChange = { ( value ) => handleChange ( { ... val . value , layout : value } , val . onChange ) }
2024-09-18 22:16:34 +05:30
/ >
< div className = "tw-flex tw-flex-col tw-gap-1" >
< span className = "tw-text-sm" > Direction < / s p a n >
< Select
options = { [
2024-09-20 15:12:43 +05:30
{ value : "column" , label : "Vertical" } ,
{ value : "row" , label : "Horizontal" } ,
2024-09-18 22:16:34 +05:30
] }
showSearch
2024-09-20 15:12:43 +05:30
value = { val . value ? . direction || "row" }
2024-09-18 22:16:34 +05:30
placeholder = { ` ${ val . label } ` }
2024-09-20 15:12:43 +05:30
onChange = { ( value ) => handleChange ( { ... val . value , direction : value } , val . onChange ) }
/ >
< / d i v >
2025-03-16 10:01:35 +05:30
{ / * < d i v c l a s s N a m e = " t w - f l e x t w - f l e x - c o l t w - g a p - 1 " >
2024-09-30 15:30:46 +05:30
< span className = "tw-text-sm" > Align items < / s p a n >
< Select
options = { [
{ value : "start" , label : "Start" } ,
{ value : "center" , label : "Center" } ,
{ value : "end" , label : "End" } ,
] }
showSearch
value = { val . value ? . align || "start" }
placeholder = { ` ${ val . label } ` }
onChange = { ( value ) => handleChange ( { ... val . value , align : value } , val . onChange ) }
/ >
2025-03-16 10:01:35 +05:30
< /div> */ }
2024-09-20 15:12:43 +05:30
< div className = "tw-flex tw-flex-col" >
< span className = "tw-text-sm" > Gap < / s p a n >
< InputNumber
max = { 500 }
min = { 1 }
value = { val . value ? . gap || 10 }
size = "small"
2025-03-22 21:16:13 +05:30
onFocus = { ( e ) => handleInputFocus ( val . label , e ) }
onBlur = { handleInputBlur }
ref = { activeInputKey === val . label ? focusedInputRef : null }
2024-09-20 15:12:43 +05:30
onChange = { ( value ) => {
handleChange ( { ... val . value , gap : value } , val . onChange )
} }
2024-09-18 22:16:34 +05:30
/ >
< / d i v >
2024-09-25 17:27:12 +05:30
{ / * < d i v c l a s s N a m e = " t w - f l e x t w - f l e x - c o l " >
2024-09-18 22:16:34 +05:30
< span className = "tw-text-sm tw-font-medium" > Grids < / s p a n >
< div className = "tw-flex tw-gap-2" >
< div className = "tw-flex tw-flex-col" >
< span className = "tw-text-sm" > Rows < / s p a n >
< InputNumber
max = { 12 }
min = { 1 }
value = { val . value ? . grid . rows || 1 }
size = "small"
2024-09-20 15:12:43 +05:30
onChange = { ( value ) => {
let newGrid = {
rows : value ,
cols : val . value ? . grid . cols
}
handleChange ( { ... val . value , grid : newGrid } , val . onChange )
} }
2024-09-18 22:16:34 +05:30
/ >
< / d i v >
< div className = "tw-flex tw-flex-col" >
< span className = "tw-text-sm" > Columns < / s p a n >
< InputNumber
max = { 12 }
min = { 1 }
value = { val . value ? . grid . cols || 1 }
size = "small"
2024-09-20 15:12:43 +05:30
onChange = { ( value ) => {
let newGrid = {
rows : val . value ? . grid . cols ,
cols : value
}
handleChange ( { ... val . value , grid : newGrid } , val . onChange )
} }
2024-09-18 22:16:34 +05:30
/ >
< / d i v >
< / d i v >
2024-09-25 17:27:12 +05:30
< /div> */ }
2024-09-18 22:16:34 +05:30
< / d i v >
)
}
2025-03-24 15:50:46 +05:30
const renderCustomTool = ( val ) => {
return (
// NOTE: custom components must accept value and onChange
< val . Component
{ ... val . toolProps }
value = { val . value }
onChange = { ( value ) => handleChange ( value , val . onChange ) } >
< / v a l . C o m p o n e n t >
)
}
2024-09-24 23:27:52 +05:30
const renderTool = ( keyName , val ) => {
return (
< >
{ val . tool === Tools . INPUT && (
< Input
{ ... val . toolProps }
2025-03-22 21:16:13 +05:30
// value={val.value}
onFocus = { ( event ) => handleInputFocus ( val . label , event ) }
onBlur = { handleInputBlur }
ref = { activeInputKey === val . label ? focusedInputRef : null }
2024-09-24 23:27:52 +05:30
value = { val . value }
onChange = { ( e ) => handleChange ( e . target . value , val . onChange ) }
/ >
) }
{ val . tool === Tools . NUMBER _INPUT && (
< InputNumber
{ ... val . toolProps }
value = { val . value || 0 }
size = "small"
2025-03-22 21:16:13 +05:30
onFocus = { ( event ) => handleInputFocus ( val . label , event ) }
onBlur = { handleInputBlur }
ref = { activeInputKey === val . label ? focusedInputRef : null }
2024-09-24 23:27:52 +05:30
onChange = { ( value ) => handleChange ( value , val . onChange ) }
/ >
) }
{ val . tool === Tools . COLOR _PICKER && (
< ColorPicker
// defaultValue={val.value || "#fff"}
value = { val . value || "#fff" }
disabledAlpha
arrow = { false }
size = "middle"
showText
format = "hex"
placement = "bottomRight"
className = "tw-w-fit !tw-min-w-[110px]"
onChange = { ( value ) => handleChange ( value . toHexString ( ) , val . onChange ) }
/ >
) }
{ val . tool === Tools . SELECT _DROPDOWN && (
< Select
options = { val . options }
showSearch
value = { val . value || "" }
placeholder = { ` ${ val . label } ` }
2025-03-23 19:02:54 +05:30
className = "tw-w-full tw-min-w-[80px]"
2024-09-27 16:04:03 +05:30
filterOption = { ( input , option ) =>
( option ? . label ? ? '' ) . toLowerCase ( ) . includes ( input . toLowerCase ( ) )
}
2024-09-24 23:27:52 +05:30
onChange = { ( value ) => handleChange ( value , val . onChange ) }
/ >
) }
{ val . tool === Tools . CHECK _BUTTON && (
< Checkbox
checked = { val . value }
defaultChecked = { val . value }
onChange = { ( e ) => handleChange ( e . target . checked , val . onChange ) }
> { val . label } < / C h e c k b o x >
) }
{ val . tool === Tools . INPUT _RADIO _LIST && (
2025-03-24 15:50:46 +05:30
// FIXME: problem with maximum recursion error
2024-09-24 23:27:52 +05:30
< DynamicRadioInputList
defaultInputs = { val . value . inputs }
defaultSelected = { val . value . selectedRadio }
onChange = { ( { inputs , selectedRadio } ) => handleChange ( { inputs , selectedRadio } , val . onChange ) }
/ >
) }
{
val . tool === Tools . LAYOUT _MANAGER && (
renderLayoutManager ( val )
)
}
2024-09-27 11:42:34 +05:30
{
val . tool === Tools . UPLOADED _LIST && (
2024-09-28 17:49:54 +05:30
renderUploadDropDown ( val , val ? . toolProps ? . filterOptions || null )
2024-09-27 11:42:34 +05:30
)
}
2024-09-24 23:27:52 +05:30
2025-03-24 15:50:46 +05:30
{
val . tool === Tools . CUSTOM && (
renderCustomTool ( val )
)
}
2024-09-24 23:27:52 +05:30
< / >
)
}
const renderToolbar = ( obj , parentKey = "" , toolCount = 0 ) => {
2025-03-18 18:08:25 +05:30
// console.log("obj: ", obj)
2024-09-24 23:27:52 +05:30
const keys = [ ]
2024-09-14 16:03:26 +05:30
return Object . entries ( obj ) . map ( ( [ key , val ] , i ) => {
2024-09-15 12:08:29 +05:30
const keyName = parentKey ? ` ${ parentKey } . ${ key } ` : key
2025-03-18 18:08:25 +05:30
// console.log("obj2: ", key, val)
2024-09-15 12:08:29 +05:30
// Highlight outer labels in blue for first-level keys
const isFirstLevel = parentKey === ""
const outerLabelClass = isFirstLevel
2024-09-24 23:27:52 +05:30
? "tw-text-sm tw-text-black tw-font-medium"
: "tw-text-sm"
2024-09-15 12:08:29 +05:30
2025-03-18 18:08:25 +05:30
if ( ! val ? . label ) {
return null
}
2024-09-15 12:08:29 +05:30
// Render tool widgets
2024-09-14 16:03:26 +05:30
if ( typeof val === "object" && val . tool ) {
2024-09-24 23:27:52 +05:30
if ( isFirstLevel && keys . length < 3 ) keys . push ( keyName )
if ( isFirstLevel ) {
return (
2025-03-11 14:00:09 +05:30
< Collapse key = { keyName } defaultActiveKey = { keys } >
2024-09-24 23:27:52 +05:30
< Collapse . Panel header = { val . label } key = { keyName } >
{ renderTool ( keyName , val ) }
< / C o l l a p s e . P a n e l >
< / C o l l a p s e >
)
}
else
return (
< div key = { keyName } className = "tw-flex tw-flex-col tw-gap-2" >
< div className = { ` ${ isFirstLevel ? outerLabelClass : "tw-text-sm" } ` } > { val . label } < / d i v >
{ renderTool ( keyName , val ) }
< / d i v >
)
2024-09-14 16:03:26 +05:30
}
2024-09-15 12:08:29 +05:30
// Handle nested objects and horizontal display for inner elements
2024-09-14 16:03:26 +05:30
if ( typeof val === "object" ) {
2024-09-15 12:08:29 +05:30
const containerClass = val . display === "horizontal"
2024-09-23 18:25:40 +05:30
? "tw-flex tw-flex-row tw-flex-wrap tw-content-start tw-gap-4"
2024-09-15 12:08:29 +05:30
: "tw-flex tw-flex-col tw-gap-2"
2024-09-24 23:27:52 +05:30
if ( isFirstLevel && keys . length < 3 ) keys . push ( keyName )
if ( isFirstLevel ) {
return (
2025-03-11 14:00:09 +05:30
< Collapse key = { keyName } defaultActiveKey = { keys } >
2024-09-24 23:27:52 +05:30
< Collapse . Panel header = { val . label } key = { keyName } >
< div className = { ` ${ containerClass } tw-px-2 ` } >
{ renderToolbar ( val , keyName , toolCount + 1 ) }
< / d i v >
< / C o l l a p s e . P a n e l >
< / C o l l a p s e >
)
} else {
return (
< div key = { keyName } className = "tw-flex tw-flex-col tw-gap-2" >
{ /* Outer label highlighted in blue for first-level */ }
< div className = { outerLabelClass } > { val . label } < / d i v >
< div className = { ` ${ containerClass } tw-px-2 ` } >
{ renderToolbar ( val , keyName , toolCount + 1 ) }
< / d i v >
< / d i v >
)
}
2024-09-14 16:03:26 +05:30
}
2024-09-15 12:08:29 +05:30
return null
2024-09-14 16:03:26 +05:30
} )
}
2024-09-13 16:03:58 +05:30
return (
2024-09-15 12:08:29 +05:30
< div
2025-03-14 16:42:27 +05:30
className = { ` tw-absolute tw-top-10 tw-right-5 tw-bg-white
2024-09-26 13:47:05 +05:30
$ { toolbarOpen ? "tw-translate-x-0" : "tw-translate-x-full" }
2025-03-14 16:42:27 +05:30
tw - w - [ 280 px ] tw - px - 3 tw - p - 2 tw - h - [ 80 vh ] tw - rounded - md tw - z - [ 1000 ] tw - shadow - lg
2024-09-26 13:47:05 +05:30
tw - transition - transform tw - duration - [ 0.3 s ] tw - overflow - x - hidden
2025-03-11 14:00:09 +05:30
tw - flex tw - flex - col tw - gap - 2 tw - overflow - y - auto tw - border - [ 2 px ]
tw - border - solid tw - border - gray - 300 ` }
2024-09-26 13:47:05 +05:30
style = { {
transform : toolbarOpen ? "translateX(0)" : "translateX(calc(100% + 50px))"
} }
2024-09-15 12:08:29 +05:30
>
2025-03-11 14:00:09 +05:30
< h3 className = " tw - text - lg tw - text - center tw - bg - [ # FAFAFA ] tw - border - [ 1 px ] tw - border - solid tw - border - [ # D9D9D9 ]
tw - p - 1 tw - px - 2 tw - rounded - md tw - font - medium " >
2025-03-16 15:43:41 +05:30
{ capitalize ( ` ${ activeWidget ? . getDisplayName ( ) || "" } ` ) . replace ( /_/g , " " ) }
2024-09-13 16:03:58 +05:30
< / h 3 >
2024-09-24 23:27:52 +05:30
< div className = "tw-flex tw-flex-col tw-gap-2" >
{ renderToolbar ( toolbarAttrs || { } ) }
< / d i v >
2024-09-13 16:03:58 +05:30
< / d i v >
)
2024-09-15 15:31:04 +05:30
} )
2024-09-13 16:03:58 +05:30
export default CanvasToolBar