diff --git a/roadmap.md b/roadmap.md index 1c3cfec..b422298 100644 --- a/roadmap.md +++ b/roadmap.md @@ -5,14 +5,14 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme. ### 1.0.0 - [x] Create the initial version for UI builder -### 1.1.0 -- [ ] Allow swappable layout - (swappy/react-dnd-kit) +### 1.2.0 - [ ] UI fixes and enhancement -- [ ] Add text editor to help with event handlers +- [ ] Add text editor to support event handlers +- [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable) - [ ] Duplicate widgets ### 1.5.0 -- [ ] Add canvas support (try fabricjs) +- [ ] Add canvas support tools (lines, rect etc) (try fabricjs) - [ ] Initial version for Electron App 👑 - [ ] Save files locally 👑 - [ ] Load UI files 👑 @@ -21,6 +21,7 @@ Any feature that has 👑 beside it, is meant only for [premium users](./readme. ### 2.0.0 - [ ] Support for more third party plugins +- [ ] Support more templates - [ ] Support for Kivy - [ ] Sharable Templates - [ ] Dark theme 👑 diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 300f071..580568a 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -368,7 +368,7 @@ class Canvas extends React.Component { this.setState({ widgetResizing: "" }) } - for (let [key, widget] of Object.entries(this.widgetRefs)){ + for (let [key, widget] of Object.entries(this.widgetRefs)) { // since the mouseUp event is not triggered inside the widget once its outside, // we'll need a global mouse up event to re-enable drag widget.current.enableDrag() @@ -643,46 +643,76 @@ class Canvas extends React.Component { * @param {object} dragElement * @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) => { + handleAddWidgetChild = ({ parentWidgetId, dragElementID, create = false, swap = 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) + const dropWidgetObj = this.findWidgetFromListById(parentWidgetId) + // Find the dragged widget object + let dragWidgetObj = this.findWidgetFromListById(dragElementID) - if (parentWidgetObj && childWidgetObj) { + if (dropWidgetObj && dragWidgetObj) { + const dragWidget = this.widgetRefs[dragWidgetObj.id] + const dragData = dragWidget.current.serialize() - const childWidget = this.widgetRefs[childWidgetObj.id] - 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 + if (swap) { + // If swapping, we need to find the common parent + const grandParentWidgetObj = this.findWidgetFromListById(dropWidgetObj.parent) + console.log("parent widget: ", grandParentWidgetObj, dropWidgetObj, this.state.widgets) + if (grandParentWidgetObj) { + // Find the indices of the dragged and drop widgets in the grandparent's children array + const dragIndex = grandParentWidgetObj.children.findIndex(child => child.id === dragElementID) + const dropIndex = grandParentWidgetObj.children.findIndex(child => child.id === parentWidgetId) + + if (dragIndex !== -1 && dropIndex !== -1) { + // Swap their positions + let childrenCopy = [...grandParentWidgetObj.children] + const temp = childrenCopy[dragIndex] + childrenCopy[dragIndex] = childrenCopy[dropIndex] + childrenCopy[dropIndex] = temp + + // Update the grandparent with the swapped children + const updatedGrandParentWidget = { + ...grandParentWidgetObj, + children: childrenCopy + } + + // Update the state with the new widget hierarchy + this.setState((prevState) => ({ + widgets: this.updateWidgetRecursively(prevState.widgets, updatedGrandParentWidget) + })) + } } + } else { + // Non-swap mode: Add the dragged widget as a child of the drop widget + let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID) + + const updatedDragWidget = { + ...dragWidgetObj, + parent: dropWidgetObj.id, // Keep the parent reference + initialData: { + ...dragData, + positionType: PosType.NONE, + zIndex: 0, + widgetContainer: WidgetContainer.WIDGET + } + } + + const updatedDropWidget = { + ...dropWidgetObj, + children: [...dropWidgetObj.children, updatedDragWidget] + } + + + // Recursively update the widget structure + updatedWidgets = this.updateWidgetRecursively(updatedWidgets, updatedDropWidget, updatedDragWidget) + + // Update the state with the new widget hierarchy + this.setState({ + widgets: updatedWidgets + }) } - - // 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) - - // Update the state with the new widget hierarchy - this.setState({ - widgets: updatedWidgets - }) } } diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 544ec22..ef8a0c1 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -13,6 +13,7 @@ import { ActiveWidgetContext } from "../activeWidgetContext" import { DragWidgetProvider } from "./draggableWidgetContext" import WidgetDraggable from "./widgetDragDrop" import WidgetContainer from "../constants/containers" +import { DragContext } from "../../components/draggable/draggableContext" @@ -48,14 +49,15 @@ class Widget extends React.Component { this.icon = "" // antd icon name representing this widget - this.elementRef = React.createRef() + 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 this.functions = { "load": { "args1": "number", "args2": "string" } } - - this.layout = Layouts.FLEX + this.droppableTags = ["widget"] // This indicates if the draggable can be dropped on this widget this.boundingRect = { x: 0, y: 0, @@ -68,6 +70,8 @@ class Widget extends React.Component { selected: false, widgetName: widgetName || 'widget', // this will later be converted to variable name enableRename: false, // will open the widgets editable div for renaming + + isDragging: false, // tells if the widget is currently being dragged dragEnabled: true, widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget @@ -532,22 +536,6 @@ class Widget extends React.Component { }) } - handleDrop = (event, dragElement) => { - console.log("dragging event: ", event, dragElement) - - const container = dragElement.getAttribute("data-container") - // TODO: check if the drop is allowed - if (container === "canvas"){ - - this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id")) - - }else if (container === "sidebar"){ - - this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first - - } - - } /** * @@ -591,7 +579,190 @@ class Widget extends React.Component { } - // FIXME: children outside the bounding box + /** + * + * @depreciated - This function is depreciated in favour of handleDropEvent() + */ + handleDrop = (event, dragElement) => { + // THIS function is depreciated in favour of handleDropEvent() + console.log("dragging event: ", event, dragElement) + const container = dragElement.getAttribute("data-container") + // TODO: check if the drop is allowed + if (container === "canvas"){ + + this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id")) + + }else if (container === "sidebar"){ + + this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first + + } + + } + + handleDragStart = (e, callback) => { + e.stopPropagation() + this.setState({isDragging: true}) + + callback(this.elementRef?.current || null) + + console.log("Drag start: ", this.elementRef) + + // 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() + const offsetX = e.clientX - rect.left + const offsetY = e.clientY - rect.top + + // Set the custom drag image with correct offset to avoid snapping to the top-left corner + e.dataTransfer.setDragImage(dragImage, offsetX, offsetY) + + // Remove the custom drag image after some time to avoid leaving it in the DOM + setTimeout(() => { + document.body.removeChild(dragImage) + }, 0) + } + + handleDragEnter = (e, draggedElement, setOverElement) => { + + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags) + // FIXME: the outer widget shouldn't be swallowed by inner widget + 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 + } + + setOverElement(e.currentTarget) // provide context to the provider + + let showDrop = { + allow: true, + show: true + } + + if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) { + showDrop = { + allow: true, + show: true + } + + } else { + showDrop = { + allow: false, + show: true + } + } + + this.setState({ + showDroppableStyle: showDrop + }) + + } + + handleDragOver = (e, draggedElement) => { + 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 + } + + // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) { + e.preventDefault() // NOTE: this is necessary to allow drop to take place + } + + } + + handleDropEvent = (e, draggedElement) => { + e.preventDefault() + e.stopPropagation() + // FIXME: sometimes the elements shoDroppableStyle is not gone, when dropping on the same widget + this.setState({ + showDroppableStyle: { + allow: false, + show: false + } + }, () => { + console.log("droppable cleared: ", this.elementRef.current, this.state.showDroppableStyle) + }) + + + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + if (this.droppableTags.length > 0 && !this.droppableTags.includes(dragEleType)) { + return // prevent drop if the draggable element doesn't match + } + + 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) { + console.log("Dropped into a descendant element, ignoring drop") + return // Exit early to prevent the drop + } + currentElement = currentElement.parentElement // Traverse up to check ancestors + } + + 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 + const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET) + + // TODO: check if the drop is allowed + if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)){ + // console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer) + + this.props.onAddChildWidget({parentWidgetId: this.__id, + dragElementID: draggedElement.getAttribute("data-widget-id"), + swap: swapArea || false + }) + + }else if (container === WidgetContainer.SIDEBAR){ + + // console.log("Dropped on Sidebar: ", this.__id) + this.props.onAddChildWidget({parentWidgetId: this.__id, create: true}) // if dragged from the sidebar create the widget first + + } + + } + + + handleDragLeave = (e, draggedElement) => { + + // console.log("Left: ", e.currentTarget, e.relatedTarget, draggedElement) + + if (!e.currentTarget.contains(draggedElement)) { + this.setState({ + showDroppableStyle: { + allow: false, + show: false + } + }) + + } + } + + handleDragEnd = (callback) => { + callback() + this.setState({isDragging: false}) + } + + + // FIXME: children outside the bounding box, add tw-overflow-hidden renderContent() { // throw new NotImplementedError("render method has to be implemented") return ( @@ -616,125 +787,165 @@ class Widget extends React.Component { left: `${this.state.pos.x}px`, width: `${this.state.size.width}px`, height: `${this.state.size.height}px`, + opacity: this.state.isDragging ? 0.3 : 1 } // console.log("selected: ", this.state.dragEnabled) return ( - { - this.setState({ - showDroppableStyle: showDrop - }) - } - } - onDragLeave={ () => { - this.setState({ - showDroppableStyle: { - allow: false, - show: false - } - }) - } - } - > -
+ -
- {this.renderContent()} + { + ({draggedElement, onDragStart, onDragEnd, setOverElement}) => ( - { - // show drop style on drag hover - this.state.showDroppableStyle.show && -
this.handleDragOver(e, draggedElement)} + onDrop={(e) => this.handleDropEvent(e, draggedElement)} - `} - style={ - { - width: "calc(100% + 10px)", - height: "calc(100% + 10px)", - } - } - > + onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)} + onDragLeave={(e) => this.handleDragLeave(e, draggedElement)} + + onDragStart={(e) => this.handleDragStart(e, onDragStart)} + onDragEnd={(e) => this.handleDragEnd(onDragEnd)} + > + {/* 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 */} +
+
+ {/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */} +
+
+ {this.renderContent()}
- } - -
+ { + // show drop style on drag hover + this.state.showDroppableStyle.show && +
- + `} + style={{ + width: "calc(100% + 10px)", + height: "calc(100% + 10px)", + }} + > +
+ } + +
+ +
+ + +
{ + e.stopPropagation() + e.preventDefault() + this.props.onWidgetResizing("nw") + this.setState({dragEnabled: false}) + }} + onMouseUp={() => this.setState({dragEnabled: true})} + /> +
{ + e.stopPropagation() + e.preventDefault() + this.props.onWidgetResizing("ne") + this.setState({dragEnabled: false}) + }} + onMouseUp={() => this.setState({dragEnabled: true})} + /> +
{ + e.stopPropagation() + e.preventDefault() + this.props.onWidgetResizing("sw") + this.setState({dragEnabled: false}) + }} + onMouseUp={() => this.setState({dragEnabled: true})} + /> +
{ + e.stopPropagation() + e.preventDefault() + this.props.onWidgetResizing("se") + this.setState({dragEnabled: false}) + }} + onMouseUp={() => this.setState({dragEnabled: true})} + /> + +
+ +
-
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("nw") - this.setState({dragEnabled: false}) - }} - onMouseUp={() => this.setState({dragEnabled: true})} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("ne") - this.setState({dragEnabled: false}) - }} - onMouseUp={() => this.setState({dragEnabled: true})} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("sw") - this.setState({dragEnabled: false}) - }} - onMouseUp={() => this.setState({dragEnabled: true})} - /> -
{ - e.stopPropagation() - e.preventDefault() - this.props.onWidgetResizing("se") - this.setState({dragEnabled: false}) - }} - onMouseUp={() => this.setState({dragEnabled: true})} - />
-
+ ) + } + + // { + // this.setState({ + // showDroppableStyle: showDrop + // }) + // } + // } + // onDragLeave={ () => { + // this.setState({ + // showDroppableStyle: { + // allow: false, + // show: false + // } + // }) + // } + // } -
-
- + // > + // ) } diff --git a/src/canvas/widgets/widgetDragDrop.js b/src/canvas/widgets/widgetDragDrop.js index 65b5a30..6571d53 100644 --- a/src/canvas/widgets/widgetDragDrop.js +++ b/src/canvas/widgets/widgetDragDrop.js @@ -13,7 +13,7 @@ import { useDragContext } from "../../components/draggable/draggableContext" * */ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="widget", - onDragEnter, onDragLeave, onDrop, + onDragEnter, onDragLeave, onDrop, style={}, droppableTags = ["widget"], ...props }) => { // FIXME: It's not possible to move the widget ~10px because, its considered as self drop, so fix it @@ -112,11 +112,20 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid e.stopPropagation() 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 + setShowDroppable({ + allow: false, + show: false + }) + + if (onDrop) { + onDrop(e, draggedElement) } + // 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) { @@ -126,14 +135,6 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid currentElement = currentElement.parentElement // Traverse up to check ancestors } - setShowDroppable({ - allow: false, - show: false - }) - - if (onDrop) { - onDrop(e, draggedElement) - } } @@ -155,8 +156,9 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid setIsDragging(false) } + // TODO: FIXME, currently the draggable div doesn't move with the child, instead only child div moves, simulating childrens movement, add color and check return ( -
  • - Access to UI builder exe for local development + Downloadable UI builder exe for local development
  • @@ -132,7 +132,7 @@ function Premium({ children, className = "" }) {
  • - Access to UI builder exe for local development + Downloadable UI builder exe for local development
  • @@ -207,7 +207,7 @@ function Premium({ children, className = "" }) {
  • - Access to UI builder exe for local development + Downloadable UI builder exe for local development