diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index bbdf0a9..ca9af32 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -23,6 +23,8 @@ import { ReactComponent as DotsBackground } from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" import { DragWidgetProvider } from "./widgets/draggableWidgetContext" +import { PosType } from "./constants/layouts" +import WidgetContainer from "./constants/containers" // const DotsBackground = require("../assets/background/dots.svg") @@ -62,7 +64,7 @@ class Canvas extends React.Component { this.state = { widgetResizing: "", // set this to "nw", "sw" etc based on the side when widgets resizing handles are selected - widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", layoutType: "flex"}] + widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", initialData: {}}] zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, @@ -173,14 +175,51 @@ class Canvas extends React.Component { * @returns {Widget} */ getWidgetFromTarget(target) { + // TODO: improve search, currently O(n), but can be improved via this.state.widgets or something + + let innerWidget = null for (let [key, ref] of Object.entries(this.widgetRefs)) { - console.log("ref: ", ref, key) if (ref.current.getElement().contains(target)) { - return ref.current + + if (!innerWidget) { + innerWidget = ref.current; + } else if (innerWidget.getElement().contains(ref.current.getElement())) { + // If the current widget is deeper than the existing innermost widget, update innerWidget + innerWidget = ref.current; + } } } + return innerWidget + // for (let [key, ref] of Object.entries(this.widgetRefs)) { + // console.log("ref: ", ref, key) + // if (ref.current.getElement().contains(target)) { + // return ref.current + // } + // } + + + // 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 + // } + // } + // // If no matching widget is found, return null + // return null + // } + + + // return returnTargetWidget(this.state.widgets) + } keyDownEvent(event) { @@ -204,6 +243,7 @@ class Canvas extends React.Component { this.mousePos = { x: event.clientX, y: event.clientY } let selectedWidget = this.getWidgetFromTarget(event.target) + // console.log("selected widget: ", selectedWidget) if (event.button === 0) { this.mousePressed = true @@ -590,23 +630,28 @@ 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) => { - + // 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) + // console.log("WIdgets: ", parentWidgetObj, childWidgetObj) if (parentWidgetObj && childWidgetObj) { - // remove child from current postion + 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 updatedChildWidget = { ...childWidgetObj, - parent: parentWidgetId + 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 } // Create a new copy of the parent widget with the child added @@ -637,10 +682,10 @@ class Canvas extends React.Component { widgets: updatedWidgets }, () => { - this.widgetRefs[dragElementID] = React.createRef() + // this.widgetRefs[dragElementID] = React.createRef() - // Optionally, force React to update and re-render the refs - this.forceUpdate() + // // Optionally, force React to update and re-render the refs + // this.forceUpdate() }) } @@ -659,7 +704,15 @@ class Canvas extends React.Component { // Store the ref in the instance variable this.widgetRefs[id] = widgetRef - const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType, children: [], parent: "", layoutType: "flex" }] // don't add the widget refs in the state + 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 // Update the state to include the new widget's type and ID this.setState({ @@ -730,8 +783,9 @@ 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] const widgets = this.state.widgets.filter(widget => widget.id !== widgetId) @@ -787,66 +841,97 @@ class Canvas extends React.Component { }) } else if (container === "canvas") { - const widgetObj = this.getWidgetById(draggedElement.getAttribute("data-widget-id")) + let widgetId = draggedElement.getAttribute("data-widget-id") + let widgetContainer = draggedElement.getAttribute("data-container") + + + const widgetObj = this.getWidgetById(widgetId) // console.log("WidgetObj: ", widgetObj) - widgetObj.current.setPos(finalPosition.x, finalPosition.y) + if (widgetContainer === WidgetContainer.CANVAS){ + + widgetObj.current.setPos(finalPosition.x, finalPosition.y) + + }else if (widgetContainer === 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 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) + + 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 + } + } + + let updatedWidgets = this.removeWidgetFromCurrentList(widgetObj.id) + + + // Create a new copy of the parent widget with the child added + const updatedParentWidget = { + ...parentWidgetObj, + children: parentWidgetObj.children.filter(child => child.id !== childWidgetObj.id) + } + + + updatedWidgets = updatedWidgets.map(widget => { + if (widget.id === parentWidgetObj.id) { + return updatedParentWidget // Update the parent widget with the child removed + } else { + return widget // Leave other widgets unchanged + } + }) + + this.setState({ + widgets: updatedWidgets + }) + + } } } - getLayoutStyleForWidget = (widget) => { - const { layoutType } = widget // e.g., 'grid', 'flex', 'absolute' - - switch (layoutType) { - case 'grid': - return { display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '10px' } - case 'flex': - return { display: 'flex', flexDirection: 'row', justifyContent: 'space-around' } - case 'absolute': - return { position: 'absolute', left: widget.left, top: widget.top } // Custom positioning - default: - return {} - } - } + renderWidget = (widget) => { - // FIXME: the child elements ref is not correct when drag and dropped into another - const { id, widgetType: ComponentType, children = [], parent } = widget + // FIXME: the child elements are being recreated instead of using the same object + const { id, widgetType: ComponentType, children = [], parent, initialData={} } = widget - console.log("rendering: ", widget, id) - // Layout management for children inside the parent - const renderChildren = (childWidgets) => { - console.log("Found the child : ", childWidgets) - return childWidgets.map((child) => { + const renderChildren = (childrenData) => { + // recursively render the child elements + return childrenData.map((child) => { const childWidget = this.findWidgetFromListById(child.id) // console.log("Found the child : ", childWidget) if (childWidget) { - console.log("rendering the child", childWidget) return this.renderWidget(childWidget) // Recursively render child widgets } return null }) } - // Example of handling layout within the parent widget - const layoutStyle = this.getLayoutStyleForWidget(widget) - - console.log("widget ref id: ", this.widgetRefs[id], this.widgetRefs) return ( this.setState({ widgetResizing: resizeSide })} - style={layoutStyle} // Apply layout style (for position, size, etc.) > {/* Render children inside the parent with layout applied */} {renderChildren(children)} diff --git a/src/canvas/constants/containers.js b/src/canvas/constants/containers.js new file mode 100644 index 0000000..0f89230 --- /dev/null +++ b/src/canvas/constants/containers.js @@ -0,0 +1,12 @@ + + +const WidgetContainer = { + + CANVAS: "canvas", // widget is on the canvas + SIDEBAR: "sidebar", // widget is contained inside sidebar + WIDGET: "widget", // widget is contained inside another widget + +} + + +export default WidgetContainer \ No newline at end of file diff --git a/src/canvas/constants/layouts.js b/src/canvas/constants/layouts.js index 0b97546..a93a1da 100644 --- a/src/canvas/constants/layouts.js +++ b/src/canvas/constants/layouts.js @@ -1,7 +1,12 @@ -const Layouts = { +export const Layouts = { FLEX: "flex", GRID: "grid", PLACE: "absolute" } -export default Layouts \ No newline at end of file + +export const PosType = { + ABSOLUTE: "absolute", + RELATIVE: "relative", + NONE: "unset" +} diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index 0f26853..c3b64c8 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -5,7 +5,7 @@ import { ColorPicker, Input, InputNumber, Select } from "antd" import { capitalize } from "../utils/common" import Tools from "./constants/tools.js" import { useActiveWidget } from "./activeWidgetContext.js" -import Layouts from "./constants/layouts.js" +import { Layouts } from "./constants/layouts.js" // FIXME: Maximum recursion error diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 34a5fbe..1b70472 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -2,7 +2,7 @@ import React from "react" import { NotImplementedError } from "../../utils/errors" import Tools from "../constants/tools" -import Layouts from "../constants/layouts" +import { Layouts, PosType} from "../constants/layouts" import Cursor from "../constants/cursor" import { toSnakeCase } from "../utils/utils" import EditableDiv from "../../components/editableDiv" @@ -12,6 +12,11 @@ import DroppableWrapper from "../../components/draggable/droppable" import { ActiveWidgetContext } from "../activeWidgetContext" import { DragWidgetProvider } from "./draggableWidgetContext" import WidgetDraggable from "./widgetDragDrop" +import WidgetContainer from "../constants/containers" + + + +const ATTRS_KEYS = ['value', 'label', 'tool', 'onChange', 'toolProps'] // these are attrs keywords, don't use these keywords as keys while defining the attrs property /** @@ -36,9 +41,6 @@ class Widget extends React.Component { this._disableResize = false this._disableSelection = false - this._parent = "" // id of the parent widget, default empty string - this._children = [] // id's of all the child widgets - this.minSize = { width: 50, height: 50 } // disable resizing below this number this.maxSize = { width: 500, height: 500 } // disable resizing above this number @@ -68,6 +70,8 @@ class Widget extends React.Component { enableRename: false, // will open the widgets editable div for renaming dragEnabled: true, + widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget + showDroppableStyle: { // shows the droppable indicator allow: false, show: false, @@ -75,7 +79,7 @@ class Widget extends React.Component { pos: { x: 0, y: 0 }, size: { width: 100, height: 100 }, - position: "absolute", + positionType: PosType.ABSOLUTE, widgetStyling: { // use for widget's inner styling @@ -95,7 +99,7 @@ class Widget extends React.Component { foregroundColor: { label: "Foreground Color", tool: Tools.COLOR_PICKER, - value: "", + value: "#000", }, label: "Styling" }, @@ -110,11 +114,13 @@ class Widget extends React.Component { cols: 1 } }, - options: [ - { value: "flex", label: "Flex" }, - { value: "grid", label: "Grid" }, - { value: "place", label: "Place" }, - ], + toolProps: { + options: [ + { value: "flex", label: "Flex" }, + { value: "grid", label: "Grid" }, + { value: "place", label: "Place" }, + ], + }, onChange: (value) => this.setWidgetStyling("backgroundColor", value) }, events: { @@ -145,11 +151,15 @@ class Widget extends React.Component { this.setAttrValue = this.setAttrValue.bind(this) this.setWidgetName = this.setWidgetName.bind(this) this.setWidgetStyling = this.setWidgetStyling.bind(this) + this.setPosType = this.setPosType.bind(this) } componentDidMount() { this.elementRef.current?.addEventListener("click", this.mousePress) + + this.load(this.props.initialData || {}) + } componentWillUnmount() { @@ -249,13 +259,6 @@ class Widget extends React.Component { return this.state.attrs } - /** - * removes the element/widget - */ - remove() { - this.canvas.removeWidget(this.__id) - } - mousePress(event) { // event.preventDefault() if (!this._disableSelection) { @@ -279,6 +282,18 @@ class Widget extends React.Component { return this.state.selected } + setPosType(positionType){ + + if (!Object.values(PosType).includes(positionType)){ + throw Error(`The Position type can only be among: ${Object.values(PosType).join(", ")}`) + } + + this.setState({ + positionType: positionType + }) + + } + setPos(x, y) { this.setState({ @@ -319,6 +334,20 @@ class Widget extends React.Component { return this.elementRef.current } + getLayoutStyleForWidget = () => { + + switch (this.state.attrs.layout) { + case 'grid': + return { display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '10px' } + case 'flex': + return { display: 'flex', flexDirection: 'row', justifyContent: 'space-around' } + case 'absolute': + return { position: 'absolute', left: "0", top: "0" } // Custom positioning + default: + return {} + } + } + /** * Given the key as a path, sets the value for the widget attribute * @param {string} path - path to the key, eg: styling.backgroundColor @@ -344,6 +373,43 @@ class Widget extends React.Component { }) } + /** + * 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 = {} + + 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; + + // 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)) + } + } + + return result + } + + return serializeValues(this.state.attrs) + } + setZIndex(zIndex) { this.setState({ zIndex: zIndex @@ -412,22 +478,6 @@ class Widget extends React.Component { }) } - setParent(parentId) { - this._parent = parentId - } - - addChild(childWidget) { - - childWidget.setParent(this.__id) - this._children.push(childWidget) - } - - removeChild(childId) { - this._children = this._children.filter(function (item) { - return item !== childId - }) - } - handleDrop = (event, dragElement) => { console.log("dragging event: ", event, dragElement) @@ -445,11 +495,53 @@ class Widget extends React.Component { } + /** + * + * 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, + positionType: this.state.positionType, + attrs: this.serializeAttrsValues() // makes sure that functions are not serialized + }) + + } + + /** + * loads the data + * @param {object} data + */ + load = (data) => { + + for (let [key, value] of Object.entries(data.attrs|{})) + this.setAttrValue(key, value) + + delete data.attrs // think of immutable way to modify + + /** + * const obj = { a: 1, b: 2, c: 3 } + * const { b, ...newObj } = obj + * console.log(newObj) // { a: 1, c: 3 } + */ + + this.setState(data) + + } + + // FIXME: children outside the bounding box renderContent() { + // console.log("Children: ", this.props.children) // throw new NotImplementedError("render method has to be implemented") return ( -
- {/* {this.props.children} */} +
+ {this.props.children}
) } @@ -464,7 +556,7 @@ class Widget extends React.Component { let outerStyle = { cursor: this.cursor, zIndex: this.state.zIndex, - position: "absolute", // don't change this if it has to be movable on the canvas + position: this.state.positionType, // don't change this if it has to be movable on the canvas top: `${this.state.pos.y}px`, left: `${this.state.pos.x}px`, width: `${this.state.size.width}px`, @@ -499,81 +591,83 @@ class Widget extends React.Component { className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit" style={outerStyle} data-draggable-type={this.getWidgetType()} // helps with droppable - data-container={"canvas"} // indicates how the canvas should handle dragging, one is sidebar other is canvas + data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas > - {this.renderContent()} +
- { - // show drop style on drag hover - this.state.showDroppableStyle.show && -
+ > +
+ } + +
+ +
+ + +
{ + this.props.onWidgetResizing("nw") + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} + /> +
{ + this.props.onWidgetResizing("ne") + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} + /> +
{ + this.props.onWidgetResizing("sw") + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} + /> +
{ + this.props.onWidgetResizing("se") + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} + /> +
- } - -
- -
- - -
{ - this.props.onWidgetResizing("nw") - this.setState({dragEnabled: false}) - }} - onMouseLeave={() => this.setState({dragEnabled: true})} - /> -
{ - this.props.onWidgetResizing("ne") - this.setState({dragEnabled: false}) - }} - onMouseLeave={() => this.setState({dragEnabled: true})} - /> -
{ - this.props.onWidgetResizing("sw") - this.setState({dragEnabled: false}) - }} - onMouseLeave={() => this.setState({dragEnabled: true})} - /> -
{ - this.props.onWidgetResizing("se") - this.setState({dragEnabled: false}) - }} - onMouseLeave={() => this.setState({dragEnabled: true})} - />
- + {this.renderContent()}
diff --git a/src/canvas/widgets/widgetDragDrop.js b/src/canvas/widgets/widgetDragDrop.js index 3db98c6..42ed6b2 100644 --- a/src/canvas/widgets/widgetDragDrop.js +++ b/src/canvas/widgets/widgetDragDrop.js @@ -1,4 +1,4 @@ -import { memo, useEffect, useState } from "react" +import { memo, useEffect, useRef, useState } from "react" import { useDragWidgetContext } from "./draggableWidgetContext" import { useDragContext } from "../../components/draggable/draggableContext" @@ -29,10 +29,13 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid }) const handleDragStart = (e) => { + e.stopPropagation() setIsDragging(true) onDragStart(widgetRef?.current || null) + console.log("Drag start: ", widgetRef.current) + // Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas const dragImage = widgetRef?.current.cloneNode(true) dragImage.style.opacity = '1' // Ensure full opacity @@ -58,6 +61,11 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid const dragEleType = draggedElement.getAttribute("data-draggable-type") // console.log("Drag entering...", overElement === e.currentTarget) + // FIXME: the outer widget shouldn't be swallowed by inner widget + if (draggedElement === widgetRef.current){ + // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself + return + } setOverElement(e.currentTarget) @@ -86,6 +94,10 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid } const handleDragOver = (e) => { + if (draggedElement === widgetRef.current){ + // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself + return + } // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) const dragEleType = draggedElement.getAttribute("data-draggable-type") @@ -98,7 +110,21 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid const handleDropEvent = (e) => { e.preventDefault() e.stopPropagation() - // console.log("Dropped") + console.log("Dropped: ", draggedElement, props.children) + + if (draggedElement === widgetRef.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) { + console.log("Dropped into a descendant element, ignoring drop") + return // Exit early to prevent the drop + } + currentElement = currentElement.parentElement // Traverse up to check ancestors + } setShowDroppable({ allow: false, @@ -130,7 +156,7 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid } return ( -