fixed nested children rendering problem

This commit is contained in:
paul
2024-09-20 15:12:43 +05:30
parent 59bf55e175
commit 474601ad12
6 changed files with 177 additions and 109 deletions

View File

@@ -9,6 +9,7 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme.
- [ ] Allow swappable layout - (swappy/react-dnd-kit)
- [ ] UI fixes and enhancement
- [ ] Add text editor to help with event handlers
- [ ] Duplicate widgets
### 1.5.0
- [ ] Add canvas support (try fabricjs)

View File

@@ -181,7 +181,7 @@ class Canvas extends React.Component {
for (let [key, ref] of Object.entries(this.widgetRefs)) {
if (ref.current.getElement().contains(target)) {
if (!innerWidget) {
innerWidget = ref.current;
} else if (innerWidget.getElement().contains(ref.current.getElement())) {
@@ -199,16 +199,16 @@ class Canvas extends React.Component {
// }
// }
// const returnTargetWidget = (widgets) => {
// for (let x of widgets) {
// const widget = this.widgetRefs[x.id]
// // Check if the widget contains the target
// if (widget && widget.current.getElement().contains(target)) {
// // If it has children, continue checking the children for the innermost match
// const childWidget = returnTargetWidget(x.children)
// // Return the innermost child widget if found, otherwise return the current widget
// return childWidget || widget.current
// }
@@ -252,7 +252,7 @@ class Canvas extends React.Component {
if (!selectedWidget._disableSelection) {
// console.log("selected widget: ", selectedWidget)
if (!this.state.selectedWidget || (selectedWidget.__id !== this.state.selectedWidget?.__id)) {
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)
@@ -263,7 +263,6 @@ class Canvas extends React.Component {
selectedWidget: selectedWidget,
toolbarAttrs: selectedWidget.getToolbarAttrs()
})
// this.context.updateActiveWidget(selectedWidget.__id)
// this.context.updateToolAttrs(selectedWidget.getToolbarAttrs())
// this.props.updateActiveWidget(selectedWidget)
@@ -590,38 +589,46 @@ class Canvas extends React.Component {
* @returns {Array} - The updated widgets list
*/
removeWidgetFromCurrentList = (widgetId) => {
// Helper function to recursively remove widget
const removeWidget = (widgets, widgetId) => {
// Process each widget
return widgets.reduce((acc, widget) => {
// If the widget is found at the top level, skip it
if (widget.id === widgetId) {
return acc
}
// Process children recursively
const updatedChildren = removeWidget(widget.children.map(childId =>
widgets.find(w => w.id === childId)
), widgetId)
// If the widget has children and the widgetId is not found, include it in the results
if (widget.children.length > 0) {
const updatedWidget = {
...widget,
children: updatedChildren.map(child => child.id) // Flatten children IDs
};
return [...acc, updatedWidget]
}
return [...acc, widget]
}, [])
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
}
// Perform the removal operation
return removeWidget(this.state.widgets, widgetId)
// Start the recursive removal from the top level
return recursiveRemove(this.state.widgets)
}
// 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
}
})
}
/**
* Adds the child into the children attribute inside the this.widgets list of objects
* // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
@@ -630,64 +637,46 @@ class Canvas extends React.Component {
* @param {boolean} create - if create is set to true the widget will be created before adding to the child tree
*/
handleAddWidgetChild = (parentWidgetId, dragElementID, create = false) => {
// FIXME: creation of nested child widgets
// TODO: creation of the child widget if its not created
// widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
const parentWidgetObj = this.findWidgetFromListById(parentWidgetId)
let childWidgetObj = this.findWidgetFromListById(dragElementID)
// console.log("WIdgets: ", parentWidgetObj, childWidgetObj)
if (parentWidgetObj && childWidgetObj) {
const childWidget = this.widgetRefs[childWidgetObj.id]
// childWidget.current.setPosType(PosType.RELATIVE) // set state needs to rerender so serialize will return absolute always
const childData = childWidget.current.serialize() // save the data and pass it the updated child object
// remove child from current position
let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID)
console.log("pre updated widgets: ", updatedWidgets)
const childData = childWidget.current.serialize()
// Update the child widget's properties (position type, zIndex, etc.)
const updatedChildWidget = {
...childWidgetObj,
parent: parentWidgetId,
initialData: {...childData, positionType: PosType.NONE, zIndex: 0, widgetContainer: WidgetContainer.WIDGET} // makes sure that after dropping the position is set to non absolute value
initialData: {
...childData,
positionType: PosType.NONE,
zIndex: 0,
widgetContainer: WidgetContainer.WIDGET
}
}
// Create a new copy of the parent widget with the child added
// Remove the child from its previous location
let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID)
// Add the child widget to the new parent's children
const updatedParentWidget = {
...parentWidgetObj,
children: [...parentWidgetObj.children, updatedChildWidget]
}
// Recursively update the widget structure with the new parent and child
updatedWidgets = this.updateWidgetRecursively(updatedWidgets, updatedParentWidget, updatedChildWidget)
// add parent id to the child widget
updatedWidgets = updatedWidgets.map(widget => {
if (widget.id === parentWidgetId) {
return updatedParentWidget // Update the parent widget
} else if (widget.id === updatedChildWidget.id) {
return updatedChildWidget // Update the child widget
} else {
return widget // Leave other widgets unchanged
}
})
console.log("updated widgets: ", updatedWidgets)
// once its mutated the original widget ref is lost so attach the new one
// Update the state with the new widget hierarchy
this.setState({
widgets: updatedWidgets
}, () => {
// this.widgetRefs[dragElementID] = React.createRef()
// // Optionally, force React to update and re-render the refs
// this.forceUpdate()
})
}
}
@@ -784,7 +773,7 @@ class Canvas extends React.Component {
// FIXME: need to delete the child widgets
// IDEA: find the widget first, check for the parent, if parent exist remove it from the parents children list
// this.widgetRefs[widgetId]?.current.remove()
//this.removeWidgetFromCurrentList(widgetID) <--- use this
delete this.widgetRefs[widgetId]
@@ -823,6 +812,7 @@ class Canvas extends React.Component {
const container = draggedElement.getAttribute("data-container")
console.log("Handle drop: ", container)
// console.log("Dropped on canvas",)
// const canvasContainerRect = this.getCanvasContainerBoundingRect()
@@ -834,56 +824,58 @@ class Canvas extends React.Component {
y: (clientY - canvasRect.top) / this.state.zoom,
}
if (container === "sidebar") {
if (container === WidgetContainer.SIDEBAR) {
// if the widget is being dropped from the sidebar, use the info to create the widget first
this.createWidget(Widget, ({ id, widgetRef }) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
})
} else if (container === "canvas") {
} else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) {
let widgetId = draggedElement.getAttribute("data-widget-id")
let widgetContainer = draggedElement.getAttribute("data-container")
const widgetObj = this.getWidgetById(widgetId)
// console.log("WidgetObj: ", widgetObj)
if (widgetContainer === WidgetContainer.CANVAS){
if (container === WidgetContainer.CANVAS) {
widgetObj.current.setPos(finalPosition.x, finalPosition.y)
}else if (widgetContainer === WidgetContainer.WIDGET){
} else if (container === WidgetContainer.WIDGET) {
// FIXME: move the widget out of the widget
// if the widget was inside another widget move it outside
let childWidgetObj = this.findWidgetFromListById(widgetObj.id)
let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId())
let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent)
const childData = widgetObj.current.serialize() // save the data and pass it the updated child object
// remove child from current position
console.log("pre updated widgets: ", updatedWidgets)
// console.log("pre updated widgets: ", updatedWidgets)
const updatedChildWidget = {
...childWidgetObj,
parent: "",
initialData: {...childData,
positionType: PosType.ABSOLUTE, // makes sure that after dropping the position is set to absolute value
zIndex: 0,
widgetContainer: WidgetContainer.CANVAS
}
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
}
}
let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.id)
let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.current.getId())
// Create a new copy of the parent widget with the child added
const updatedParentWidget = {
...parentWidgetObj,
children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id)
// children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id)
}
updatedWidgets = updatedWidgets.map(widget => {
if (widget.id === parentWidgetObj.id) {
return updatedParentWidget // Update the parent widget with the child removed
@@ -892,8 +884,9 @@ class Canvas extends React.Component {
}
})
this.setState({
widgets: updatedWidgets
widgets: [...updatedWidgets, updatedChildWidget]
})
}
@@ -901,12 +894,12 @@ class Canvas extends React.Component {
}
renderWidget = (widget) => {
// FIXME: the child elements are being recreated instead of using the same object
const { id, widgetType: ComponentType, children = [], parent, initialData={} } = widget
const { id, widgetType: ComponentType, children = [], parent, initialData = {} } = widget
const renderChildren = (childrenData) => {

View File

@@ -24,6 +24,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const [toolbarOpen, setToolbarOpen] = useState(isOpen)
const [toolbarAttrs, setToolbarAttrs] = useState(attrs)
useEffect(() => {
setToolbarOpen(isOpen)
}, [isOpen])
@@ -34,7 +35,6 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const handleChange = (value, callback) => {
console.log("changed...")
if (callback) {
callback(value)
}
@@ -55,20 +55,32 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
value={val.value?.layout || ""}
placeholder={`${val.label}`}
size="medium"
onChange={(value) => handleChange(value, val.onChange)}
onChange={(value) => handleChange({ ...val.value, layout: value }, val.onChange)}
/>
<div className="tw-flex tw-flex-col tw-gap-1">
<span className="tw-text-sm">Direction</span>
<Select
options={[
{ value: "vertical", label: "Vertical" },
{ value: "horizontal", label: "Horizontal" },
{ value: "column", label: "Vertical" },
{ value: "row", label: "Horizontal" },
]}
showSearch
value={val.value?.direction || ""}
value={val.value?.direction || "row"}
placeholder={`${val.label}`}
onChange={(value) => handleChange(value, val.onChange)}
onChange={(value) => handleChange({ ...val.value, direction: value }, val.onChange)}
/>
</div>
<div className="tw-flex tw-flex-col">
<span className="tw-text-sm">Gap</span>
<InputNumber
max={500}
min={1}
value={val.value?.gap || 10}
size="small"
onChange={(value) => {
handleChange({ ...val.value, gap: value }, val.onChange)
}}
/>
</div>
<div className="tw-flex tw-flex-col">
@@ -81,7 +93,13 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
min={1}
value={val.value?.grid.rows || 1}
size="small"
onChange={(value) => handleChange(value, val.onChange)}
onChange={(value) => {
let newGrid = {
rows: value,
cols: val.value?.grid.cols
}
handleChange({ ...val.value, grid: newGrid }, val.onChange)
}}
/>
</div>
<div className="tw-flex tw-flex-col">
@@ -91,7 +109,13 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
min={1}
value={val.value?.grid.cols || 1}
size="small"
onChange={(value) => handleChange(value, val.onChange)}
onChange={(value) => {
let newGrid = {
rows: val.value?.grid.cols,
cols: value
}
handleChange({ ...val.value, grid: newGrid }, val.onChange)
}}
/>
</div>
</div>
@@ -104,6 +128,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const renderWidgets = (obj, parentKey = "") => {
// FIXME: The color widget is not being updated on selection change
return Object.entries(obj).map(([key, val], i) => {
const keyName = parentKey ? `${parentKey}.${key}` : key

View File

@@ -90,7 +90,7 @@ class Widget extends React.Component {
backgroundColor: {
label: "Background Color",
tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string
value: "",
value: "#fff",
onChange: (value) => {
this.setWidgetStyling("backgroundColor", value)
this.setAttrValue("styling.backgroundColor", value)
@@ -108,11 +108,12 @@ class Widget extends React.Component {
tool: Tools.LAYOUT_MANAGER, // the tool to display, can be either HTML ELement or a constant string
value: {
layout: "flex",
direction: "vertical",
direction: "row",
grid: {
rows: 1,
cols: 1
}
},
gap: 10,
},
toolProps: {
options: [
@@ -121,7 +122,10 @@ class Widget extends React.Component {
{ value: "place", label: "Place" },
],
},
onChange: (value) => this.setWidgetStyling("backgroundColor", value)
onChange: (value) => {
// this.setAttrValue("layout", value)
this.setLayout(value)
}
},
events: {
event1: {
@@ -135,6 +139,8 @@ class Widget extends React.Component {
this.mousePress = this.mousePress.bind(this)
this.getElement = this.getElement.bind(this)
this.getId = this.getId.bind(this)
this.getPos = this.getPos.bind(this)
this.getSize = this.getSize.bind(this)
this.getWidgetName = this.getWidgetName.bind(this)
@@ -158,7 +164,11 @@ class Widget extends React.Component {
componentDidMount() {
this.elementRef.current?.addEventListener("click", this.mousePress)
this.load(this.props.initialData || {})
this.setLayout(this.state.attrs.layout.value)
this.setWidgetStyling('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff")
this.load(this.props.initialData || {}) // load the initial data
}
@@ -207,6 +217,7 @@ class Widget extends React.Component {
getToolbarAttrs(){
return ({
id: this.__id,
widgetName: {
label: "Widget Name",
tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string
@@ -259,6 +270,10 @@ class Widget extends React.Component {
return this.state.attrs
}
getId(){
return this.__id
}
mousePress(event) {
// event.preventDefault()
if (!this._disableSelection) {
@@ -423,6 +438,26 @@ class Widget extends React.Component {
})
}
setLayout(value){
const {layout, direction, grid={rows: 1, cols: 1}, gap=10} = value
const widgetStyle = {
...this.state.widgetStyling,
display: layout,
flexDirection: direction,
gap: `${gap}px`,
flexWrap: "wrap"
// TODO: add grid rows and cols
}
this.setAttrValue("layout", value)
this.updateState({
widgetStyling: widgetStyle
})
}
/**
*
* @param {string} key - The string in react Style format
@@ -523,6 +558,20 @@ class Widget extends React.Component {
for (let [key, value] of Object.entries(data.attrs|{}))
this.setAttrValue(key, value)
if (data.attrs){
if ('layout' in data.attrs){
this.setLayout(data.attrs.layout.value || {})
}
if ('backgroundColor' in data.attrs){
this.setWidgetStyling('backgroundColor', data.attrs.styling.backgroundColor)
}
if ('foregroundColor' in data.attrs){
this.setWidgetStyling('foregroundColor', data.attrs.styling.backgroundColor)
}
}
delete data.attrs // think of immutable way to modify
/**
@@ -537,10 +586,10 @@ class Widget extends React.Component {
// FIXME: children outside the bounding box
renderContent() {
// console.log("Children: ", this.props.children)
// throw new NotImplementedError("render method has to be implemented")
return (
<div className="tw-w-full tw-h-full tw-p-2 tw-rounded-md tw-bg-red-500" style={this.state.widgetStyling}>
<div className="tw-w-full tw-h-full tw-p-2 tw-rounded-md" style={this.state.widgetStyling}>
{this.props.children}
</div>
)

View File

@@ -18,7 +18,7 @@ function Premium({ children, className = "" }) {
setPremiumModalOpen(false)
}
// FIXME: the pricing section is not responsive
return (
<div onClick={onClick} className={`${className}`}>
{children}

View File

@@ -38,7 +38,7 @@ function Share({children, className=""}){
return (
<div onClick={onClick} className={className}>
{children}
<Modal title={<h3 className="tw-text-xl tw-font-medium">Share FontTester</h3>}
<Modal title={<h3 className="tw-text-xl tw-font-medium">Share PyUI Builder with others</h3>}
styles={{wrapper: {zIndex: 14000, gap: "10px"}}}
onCancel={onClose}
onOk={onClose}