From d363c8fe92c3b9d26a7dc7bca7c362e56151f56c Mon Sep 17 00:00:00 2001 From: paul Date: Sun, 15 Sep 2024 19:22:32 +0530 Subject: [PATCH 01/19] working on draggable api --- src/App.js | 2 +- src/canvas/canvas.js | 36 ++++++++++-- src/components/cards.js | 33 ++++++----- src/components/utils/draggable.js | 47 +++++++++------- src/components/utils/draggableDnd.js | 27 +++++++++ src/components/utils/droppable.js | 82 ++++++++++++++++++++++------ src/components/utils/droppableDnd.js | 20 +++++++ src/components/utils/panzoom.js | 2 +- 8 files changed, 190 insertions(+), 59 deletions(-) create mode 100644 src/components/utils/draggableDnd.js create mode 100644 src/components/utils/droppableDnd.js diff --git a/src/App.js b/src/App.js index 6f56097..88734fd 100644 --- a/src/App.js +++ b/src/App.js @@ -156,7 +156,7 @@ function App() { collisionDetection={rectIntersection} onDragStart={handleDragStart} onDragMove={handleDragMove} - onDragEnd={handleDragEnd} + // onDragEnd={handleDragEnd} >
diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 7ab41e7..dd4ae5b 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -5,7 +5,7 @@ import {DndContext} from '@dnd-kit/core' import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons" import { Button, Tooltip, Dropdown } from "antd" -import Droppable from "../components/utils/droppable" +import Droppable from "../components/utils/droppableDnd" import Widget from "./widgets/base" import Cursor from "./constants/cursor" @@ -18,6 +18,7 @@ import { WidgetContext } from './context/widgetContext' // import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" import DotsBackground from "../assets/background/dots.svg" +import DroppableWrapper from "../components/utils/droppable" // const DotsBackground = require("../assets/background/dots.svg") @@ -524,6 +525,30 @@ class Canvas extends React.Component { } + handleDropEvent = (e) => { + + e.preventDefault() + + console.log("event: ", e, this.canvasContainerRef.current.offsetTop) + + const canvasContainerRect = this.getCanvasContainerBoundingRect() + const canvasRect = this.getCanvasBoundingRect() + + const { clientX, clientY } = e + + let finalPosition = { + x: (e.clientX - canvasContainerRect.left - this.state.currentTranslate.x) / this.state.zoom, + y: (e.clientY - canvasContainerRect.top - this.state.currentTranslate.y) / this.state.zoom, + } + + console.log("final: ", finalPosition) + + this.addWidget(Widget, ({id, widgetRef}) => { + widgetRef.current.setPos(finalPosition.x, finalPosition.y) + }) + + } + renderWidget(widget){ const { id, widgetType: ComponentType } = widget // console.log("widet: ", this.widgetRefs, id) @@ -549,7 +574,8 @@ class Canvas extends React.Component {
- +
-
+ - + /> */} ) } diff --git a/src/components/cards.js b/src/components/cards.js index 8ee0b01..e25c639 100644 --- a/src/components/cards.js +++ b/src/components/cards.js @@ -1,9 +1,10 @@ import { useEffect, useMemo, useRef } from "react" -import Draggable from "./utils/draggable" +import Draggable from "./utils/draggableDnd" import { FileImageOutlined, GithubOutlined, GitlabOutlined, LinkOutlined, AudioOutlined, VideoCameraOutlined, FileTextOutlined} from "@ant-design/icons" +import DraggableWrapper from "./utils/draggable" export function DraggableWidgetCard({ name, img, url, innerRef}){ @@ -28,21 +29,25 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){ return ( - -
-
- {name} -
- {name} -
+ // + +
+
+ {name} +
+ {name} +
-
- + + // ) } diff --git a/src/components/utils/draggable.js b/src/components/utils/draggable.js index 7cca531..6b0b7eb 100644 --- a/src/components/utils/draggable.js +++ b/src/components/utils/draggable.js @@ -1,27 +1,32 @@ -import React from "react" -import {useDraggable} from "@dnd-kit/core" -import { CSS } from "@dnd-kit/utilities" -function Draggable(props) { - const {attributes, listeners, setNodeRef, transform} = useDraggable({ - id: props.id, - data: {title: props.children} - }) - const style = transform ? { - transform: CSS.Translate.toString(transform), - } : undefined - - + +const DraggableWrapper = (props) => { + + + const handleDragStart = () => { + console.log("Drag start") + } + + const handleDragOver = () => { + // console.log("Drag over") + } + + const handleDragEnd = (e) => { + // console.log("Drag end: ", e, e.target.closest('div')) + + } + return ( - +
+ {props.children} +
) + } -export default Draggable \ No newline at end of file +export default DraggableWrapper \ No newline at end of file diff --git a/src/components/utils/draggableDnd.js b/src/components/utils/draggableDnd.js new file mode 100644 index 0000000..7cca531 --- /dev/null +++ b/src/components/utils/draggableDnd.js @@ -0,0 +1,27 @@ +import React from "react" +import {useDraggable} from "@dnd-kit/core" +import { CSS } from "@dnd-kit/utilities" + +function Draggable(props) { + const {attributes, listeners, setNodeRef, transform} = useDraggable({ + id: props.id, + data: {title: props.children} + }) + const style = transform ? { + transform: CSS.Translate.toString(transform), + } : undefined + + + return ( + + ) +} + + +export default Draggable \ No newline at end of file diff --git a/src/components/utils/droppable.js b/src/components/utils/droppable.js index a732888..6c6c259 100644 --- a/src/components/utils/droppable.js +++ b/src/components/utils/droppable.js @@ -1,20 +1,68 @@ -import React from 'react' -import {useDroppable} from '@dnd-kit/core' +import { useState } from "react" + + +const DroppableWrapper = ({onDrop, ...props}) => { + + + const [showDroppable, setShowDroppable] = useState({ + show: false, + allow: false + }) + + const handleDragEnter = () => { + console.log("Drag start") + setShowDroppable({ + allow: true, + show: true + }) + } + + const handleDragOver = (e) => { + // console.log("Drag over: ", e) + e.preventDefault() + } + + const handleDropEvent = (e) => { + + setShowDroppable({ + allow: false, + show: false + }) + + if(onDrop){ + onDrop(e) + } + } + + + const handleDragLeave = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + setShowDroppable({ + allow: false, + show: false + }) + } + } + + return ( +
+ {/* { + showDroppable.show && +
+
+ } */} + {props.children} + +
+ ) -function Droppable(props) { - const {isOver, setNodeRef} = useDroppable({ - id: props.id, - }) - const style = { - backgroundColor: isOver ? 'green' : '', - } - - - return ( -
- {props.children} -
- ) } -export default Droppable \ No newline at end of file + +export default DroppableWrapper \ No newline at end of file diff --git a/src/components/utils/droppableDnd.js b/src/components/utils/droppableDnd.js new file mode 100644 index 0000000..a732888 --- /dev/null +++ b/src/components/utils/droppableDnd.js @@ -0,0 +1,20 @@ +import React from 'react' +import {useDroppable} from '@dnd-kit/core' + +function Droppable(props) { + const {isOver, setNodeRef} = useDroppable({ + id: props.id, + }) + const style = { + backgroundColor: isOver ? 'green' : '', + } + + + return ( +
+ {props.children} +
+ ) +} + +export default Droppable \ No newline at end of file diff --git a/src/components/utils/panzoom.js b/src/components/utils/panzoom.js index cff9ae4..6fcb320 100644 --- a/src/components/utils/panzoom.js +++ b/src/components/utils/panzoom.js @@ -1,4 +1,4 @@ -// This is only for testing purpose, not really meant to be used +//NOTE: This is only for testing purpose, not really meant to be used import './polyfills' import { From f692af7f3fda18adee1603925ea4f156a74053ff Mon Sep 17 00:00:00 2001 From: paul Date: Sun, 15 Sep 2024 22:54:53 +0530 Subject: [PATCH 02/19] feat: fixed drag and drop position --- src/canvas/canvas.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index dd4ae5b..5046b94 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -85,7 +85,9 @@ class Canvas extends React.Component { this.getCanvasObjectsBoundingBox = this.getCanvasObjectsBoundingBox.bind(this) this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this) - this.getCanvasBoundingRect = this.getCanvasContainerBoundingRect.bind(this) + + this.getCanvasContainerBoundingRect = this.getCanvasContainerBoundingRect.bind(this) + this.getCanvasBoundingRect = this.getCanvasBoundingRect.bind(this) this.setSelectedWidget = this.setSelectedWidget.bind(this) this.deleteSelectedWidgets = this.deleteSelectedWidgets.bind(this) @@ -531,17 +533,21 @@ class Canvas extends React.Component { console.log("event: ", e, this.canvasContainerRef.current.offsetTop) - const canvasContainerRect = this.getCanvasContainerBoundingRect() - const canvasRect = this.getCanvasBoundingRect() + // const canvasContainerRect = this.getCanvasContainerBoundingRect() + const canvasRect = this.canvasRef.current.getBoundingClientRect() + + console.log("canvas rect: ", canvasRect) const { clientX, clientY } = e let finalPosition = { - x: (e.clientX - canvasContainerRect.left - this.state.currentTranslate.x) / this.state.zoom, - y: (e.clientY - canvasContainerRect.top - this.state.currentTranslate.y) / this.state.zoom, + x: (e.clientX - canvasRect.left) / this.state.zoom, + y: (e.clientY - canvasRect.top) / this.state.zoom, } - console.log("final: ", finalPosition) + + + console.log("final: ", finalPosition, finalPosition.x*this.state.zoom, finalPosition.y*this.state.zoom, this.state.zoom) this.addWidget(Widget, ({id, widgetRef}) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) @@ -576,7 +582,7 @@ class Canvas extends React.Component { - + {/* */}
- + {/* */} {/* Date: Mon, 16 Sep 2024 12:23:15 +0530 Subject: [PATCH 03/19] reworking on drag context --- src/App.js | 36 +------- src/canvas/canvas.js | 25 +++--- src/canvas/toolbar.js | 2 +- src/components/cards.js | 6 +- src/components/draggable/draggable.js | 51 +++++++++++ src/components/draggable/draggableContext.js | 24 ++++++ src/components/draggable/droppable.js | 90 ++++++++++++++++++++ src/components/utils/draggable.js | 32 ------- src/components/utils/draggableDnd.js | 1 + src/components/utils/droppable.js | 68 --------------- 10 files changed, 184 insertions(+), 151 deletions(-) create mode 100644 src/components/draggable/draggable.js create mode 100644 src/components/draggable/draggableContext.js create mode 100644 src/components/draggable/droppable.js delete mode 100644 src/components/utils/draggable.js delete mode 100644 src/components/utils/droppable.js diff --git a/src/App.js b/src/App.js index 88734fd..d5389e1 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,7 @@ import WidgetsContainer from './sidebar/widgetsContainer' import Widget from './canvas/widgets/base' import { DraggableWidgetCard } from './components/cards' +import { DragProvider } from './components/draggable/draggableContext' function App() { @@ -95,15 +96,6 @@ function App() { return } setDropAnimation(null) - - // FIXME: drop offset is not correct - // Calculate the dragged item's bounding rectangle - const itemRect = activeItemElement.getBoundingClientRect(); - const itemCenterX = itemRect.left + itemRect.width / 2 - const itemCenterY = itemRect.top + itemRect.height / 2 - - console.log("widget overlay: ", delta, itemRect) - // Get widget dimensions (assuming you have a way to get these) const widgetWidth = activeItemElement.offsetWidth; // Adjust this based on how you get widget size @@ -114,11 +106,6 @@ function App() { const canvasTranslate = canvasRef.current.getCanvasTranslation() const zoom = canvasRef.current.getZoom() - // let finalPosition = { - // x: (delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom, - // y: (delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom, - // } - let finalPosition = { x: (initialPosition.x + delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom - (widgetWidth / 2), y: (initialPosition.y + delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom - (widgetHeight / 2), @@ -151,32 +138,15 @@ function App() {
- +
{/* dragOverlay (dnd-kit) helps move items from one container to another */} - - {activeSidebarWidget ? ( - - ): - null} - -
+
) } diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 5046b94..453ec94 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -18,7 +18,7 @@ import { WidgetContext } from './context/widgetContext' // import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" import DotsBackground from "../assets/background/dots.svg" -import DroppableWrapper from "../components/utils/droppable" +import DroppableWrapper from "../components/draggable/droppable" // const DotsBackground = require("../assets/background/dots.svg") @@ -527,28 +527,23 @@ class Canvas extends React.Component { } + /** + * Handles drop event to canvas from the sidebar + * @param {DragEvent} e + */ handleDropEvent = (e) => { e.preventDefault() - console.log("event: ", e, this.canvasContainerRef.current.offsetTop) - // const canvasContainerRect = this.getCanvasContainerBoundingRect() const canvasRect = this.canvasRef.current.getBoundingClientRect() - - console.log("canvas rect: ", canvasRect) - const { clientX, clientY } = e - let finalPosition = { - x: (e.clientX - canvasRect.left) / this.state.zoom, - y: (e.clientY - canvasRect.top) / this.state.zoom, + const finalPosition = { + x: (clientX - canvasRect.left) / this.state.zoom, + y: (clientY - canvasRect.top) / this.state.zoom, } - - - console.log("final: ", finalPosition, finalPosition.x*this.state.zoom, finalPosition.y*this.state.zoom, this.state.zoom) - this.addWidget(Widget, ({id, widgetRef}) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) }) @@ -606,10 +601,10 @@ class Canvas extends React.Component { {/* */} - {/* */} + /> ) } diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index d51bb57..b5a3d6d 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -116,7 +116,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { return (
- + +
{ + + const { onDragStart, onDragEnd } = useDragContext() + + const draggableRef = useRef(null) + + /** + * + * @param {DragEvent} event + */ + const handleDragStart = (event) => { + + // event.dataTransfer.setData("text/plain", "") + + if (onDragStart) + onDragStart(draggableRef?.current) + + + } + + const handleDragEnd = (e) => { + // console.log("Drag end: ", e, e.target.closest('div')) + + onDragEnd() + } + + return ( +
+ {children} +
+ ) + +}) + + +export default DraggableWrapper \ No newline at end of file diff --git a/src/components/draggable/draggableContext.js b/src/components/draggable/draggableContext.js new file mode 100644 index 0000000..7d77f15 --- /dev/null +++ b/src/components/draggable/draggableContext.js @@ -0,0 +1,24 @@ +import React, { createContext, useContext, useState } from 'react'; + +const DragContext = createContext() + +export const useDragContext = () => useContext(DragContext) + +// Provider component to wrap around parts of your app that need drag-and-drop functionality +export const DragProvider = ({ children }) => { + const [draggedElement, setDraggedElement] = useState(null) + + const onDragStart = (element) => { + setDraggedElement(element) + } + + const onDragEnd = () => { + setDraggedElement(null) + } + + return ( + + {children} + + ) +} diff --git a/src/components/draggable/droppable.js b/src/components/draggable/droppable.js new file mode 100644 index 0000000..569f33f --- /dev/null +++ b/src/components/draggable/droppable.js @@ -0,0 +1,90 @@ +import { memo, useState } from "react" +import { useDragContext } from "./draggableContext" + + +const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => { + + + const { draggedElement } = useDragContext() + + const [showDroppable, setShowDroppable] = useState({ + show: false, + allow: false + }) + + + const handleDragEnter = (e) => { + console.log("Drag Enter", draggedElement) + + const dragElementType = draggedElement.getAttribute("data-draggable-type") + + if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){ + setShowDroppable({ + allow: true, + show: true + }) + }else{ + setShowDroppable({ + allow: false, + show: true + }) + } + } + + const handleDragOver = (e) => { + // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) + const dragElementType = draggedElement.getAttribute("data-draggable-type") + + if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){ + e.preventDefault() // this is necessary to allow drop to take place + } + + } + + const handleDropEvent = (e) => { + console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) + + setShowDroppable({ + allow: false, + show: false + }) + + if(onDrop){ + onDrop(e) + } + } + + + const handleDragLeave = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + setShowDroppable({ + allow: false, + show: false + }) + } + } + + return ( +
+ { + showDroppable.show && +
+
+ } + {props.children} + +
+ ) + +}) + + +export default DroppableWrapper \ No newline at end of file diff --git a/src/components/utils/draggable.js b/src/components/utils/draggable.js deleted file mode 100644 index 6b0b7eb..0000000 --- a/src/components/utils/draggable.js +++ /dev/null @@ -1,32 +0,0 @@ - - -const DraggableWrapper = (props) => { - - - const handleDragStart = () => { - console.log("Drag start") - } - - const handleDragOver = () => { - // console.log("Drag over") - } - - const handleDragEnd = (e) => { - // console.log("Drag end: ", e, e.target.closest('div')) - - } - - return ( -
- {props.children} -
- ) - -} - - -export default DraggableWrapper \ No newline at end of file diff --git a/src/components/utils/draggableDnd.js b/src/components/utils/draggableDnd.js index 7cca531..2cb3238 100644 --- a/src/components/utils/draggableDnd.js +++ b/src/components/utils/draggableDnd.js @@ -2,6 +2,7 @@ import React from "react" import {useDraggable} from "@dnd-kit/core" import { CSS } from "@dnd-kit/utilities" + function Draggable(props) { const {attributes, listeners, setNodeRef, transform} = useDraggable({ id: props.id, diff --git a/src/components/utils/droppable.js b/src/components/utils/droppable.js deleted file mode 100644 index 6c6c259..0000000 --- a/src/components/utils/droppable.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useState } from "react" - - -const DroppableWrapper = ({onDrop, ...props}) => { - - - const [showDroppable, setShowDroppable] = useState({ - show: false, - allow: false - }) - - const handleDragEnter = () => { - console.log("Drag start") - setShowDroppable({ - allow: true, - show: true - }) - } - - const handleDragOver = (e) => { - // console.log("Drag over: ", e) - e.preventDefault() - } - - const handleDropEvent = (e) => { - - setShowDroppable({ - allow: false, - show: false - }) - - if(onDrop){ - onDrop(e) - } - } - - - const handleDragLeave = (e) => { - if (!e.currentTarget.contains(e.relatedTarget)) { - setShowDroppable({ - allow: false, - show: false - }) - } - } - - return ( -
- {/* { - showDroppable.show && -
-
- } */} - {props.children} - -
- ) - -} - - -export default DroppableWrapper \ No newline at end of file From e5ef6fd37a699c995ebbb24dc2366661dbe95fc5 Mon Sep 17 00:00:00 2001 From: paul Date: Mon, 16 Sep 2024 22:04:24 +0530 Subject: [PATCH 04/19] added key events to canvas --- README.md | 16 ++- notes.md | 1 + src/App.js | 4 + src/canvas/activeWidgetContext.js | 61 ++++++++++ src/canvas/canvas.js | 171 +++++++++++++++++----------- src/canvas/toolbar.js | 18 ++- src/canvas/widgets/base.js | 182 ++++++++++++------------------ src/components/cards.js | 4 +- 8 files changed, 275 insertions(+), 182 deletions(-) create mode 100644 notes.md create mode 100644 src/canvas/activeWidgetContext.js diff --git a/README.md b/README.md index 49f4b19..1cdef42 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ -# TkBuilder +# PyUIBuilder - The only Python GUI builder you'll ever need + + + + + + + +## Features +* Framework agnostic - Can outputs code in multiple frameworks. +* Easy to use. +* Plugins to extend 3rd party UI libraries +* Generate Code. + +## Roadmap diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..e0c1c09 --- /dev/null +++ b/notes.md @@ -0,0 +1 @@ +### State management in react is a f*king mess \ No newline at end of file diff --git a/src/App.js b/src/App.js index d5389e1..aeeca5a 100644 --- a/src/App.js +++ b/src/App.js @@ -13,6 +13,7 @@ import WidgetsContainer from './sidebar/widgetsContainer' import Widget from './canvas/widgets/base' import { DraggableWidgetCard } from './components/cards' import { DragProvider } from './components/draggable/draggableContext' +import { ActiveWidgetProvider } from './canvas/activeWidgetContext' function App() { @@ -141,7 +142,10 @@ function App() {
+ + {/* */} + {/* */}
{/* dragOverlay (dnd-kit) helps move items from one container to another */} diff --git a/src/canvas/activeWidgetContext.js b/src/canvas/activeWidgetContext.js new file mode 100644 index 0000000..f82ed90 --- /dev/null +++ b/src/canvas/activeWidgetContext.js @@ -0,0 +1,61 @@ +import React, { createContext, Component, useContext, useState, createRef, useMemo } from 'react' + + + +// NOTE: using this context provider causes many re-rendering when the canvas is panned the toolbar updates unnecessarily + + +// Create the Context +export const ActiveWidgetContext = createContext() + +// Create the Provider component +export class ActiveWidgetProvider extends Component { + state = { + activeWidgetId: null, + activeWidgetAttrs: {} + } + + updateActiveWidget = (widget) => { + this.setState({ activeWidgetId: widget }) + } + + updateToolAttrs = (widgetAttrs) => { + this.setState({activeWidgetAttrs: widgetAttrs}) + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +// Custom hook for function components +export const useActiveWidget = () => { + const context = useContext(ActiveWidgetContext) + if (context === undefined) { + throw new Error('useActiveWidget must be used within an ActiveWidgetProvider') + } + return useMemo(() => context, [context.activeWidgetId, context.activeWidgetAttrs, context.updateToolAttrs, context.updateActiveWidget]) +} + +// Higher-Order Component for class-based components +export const withActiveWidget = (WrappedComponent) => { + return class extends Component { + + render() { + return ( + + {context => } + + ) + } + } +} + + + diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 453ec94..86ad45e 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -19,6 +19,7 @@ import { WidgetContext } from './context/widgetContext' import DotsBackground from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" +import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" // const DotsBackground = require("../assets/background/dots.svg") @@ -31,6 +32,8 @@ const CanvasModes = { class Canvas extends React.Component { + // static contextType = ActiveWidgetContext + constructor(props) { super(props) @@ -76,6 +79,8 @@ class Canvas extends React.Component { this.mouseDownEvent = this.mouseDownEvent.bind(this) this.mouseMoveEvent = this.mouseMoveEvent.bind(this) this.mouseUpEvent = this.mouseUpEvent.bind(this) + this.keyDownEvent = this.keyDownEvent.bind(this) + this.wheelZoom = this.wheelZoom.bind(this) this.onActiveWidgetUpdate = this.onActiveWidgetUpdate.bind(this) @@ -107,15 +112,42 @@ class Canvas extends React.Component { componentWillUnmount() { + this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent) + this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent) + this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent) + this.canvasContainerRef.current.removeEventListener("wheel", this.wheelZoom) + + this.canvasContainerRef.current.removeEventListener("keydown", this.keyDownEvent) + + // NOTE: this will clear the canvas this.clearCanvas() } - /** + initEvents(){ + + this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) + this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) + this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) + this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom) + + + this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true) + // window.addEventListener("keydown", this.keyDownEvent, true) + + + } + + applyTransform(){ + const { currentTranslate, zoom } = this.state + this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` + } + + /** * * @returns {import("./widgets/base").Widget[]} */ - getWidgets(){ + getWidgets(){ return this.state.widgets } @@ -130,22 +162,6 @@ class Canvas extends React.Component { }) } - initEvents(){ - - this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) - this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) - this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) - - this.canvasContainerRef.current.addEventListener('wheel', (event) => { - this.wheelZoom(event) - }) - - } - - applyTransform(){ - const { currentTranslate, zoom } = this.state - this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` - } /** * returns the widget that contains the target @@ -155,6 +171,7 @@ class Canvas extends React.Component { getWidgetFromTarget(target){ for (let [key, ref] of Object.entries(this.widgetRefs)){ + // console.log("ref: ", ref) if (ref.current.getElement().contains(target)){ return ref.current } @@ -162,6 +179,21 @@ class Canvas extends React.Component { } + keyDownEvent(event){ + + if (event.key === "Delete"){ + this.deleteSelectedWidgets() + } + + if (event.key === "+"){ + this.setZoom(this.state.zoom + 0.1) + } + + if (event.key === "-"){ + this.setZoom(this.state.zoom - 0.1) + } + + } mouseDownEvent(event){ @@ -190,6 +222,10 @@ class Canvas extends React.Component { selectedWidgets: [selectedWidget], toolbarAttrs: selectedWidget.getToolbarAttrs() }) + + this.context.updateActiveWidget(selectedWidget.__id) + this.context.updateToolAttrs(selectedWidget.getToolbarAttrs()) + // this.props.updateActiveWidget(selectedWidget) } this.currentMode = CanvasModes.MOVE_WIDGET } @@ -302,18 +338,6 @@ class Canvas extends React.Component { return this.state.currentTranslate } - /** - * Given a position relative to canvas container, - * returns the position relative to the canvas - */ - getRelativePositionToCanvas(x, y){ - - const canvasRect = this.canvasRef.current.getBoundingClientRect() - let zoom = this.state.zoom - - return {x: (canvasRect.left - x ), y: (canvasRect.top - y)} - } - /** * fits the canvas size to fit the widgets bounding box */ @@ -341,32 +365,30 @@ class Canvas extends React.Component { this.canvasContainerRef.current.style.cursor = cursor } - setZoom(zoom, pos={x:0, y:0}){ + setZoom(zoom, pos){ - // if (zoom < 0.5 || zoom > 2){ - // return - // } - const { currentTranslate } = this.state + let newTranslate = currentTranslate + + if (pos){ + // Calculate the new translation to zoom into the mouse position + const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) + const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) - // Calculate the new translation to zoom into the mouse position - const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) - const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) - - const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) - const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) - - this.setState({ - zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5 - currentTranslate: { + const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) + const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) + newTranslate = { x: newTranslateX, y: newTranslateY } + } + + this.setState({ + zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5 + currentTranslate: newTranslate }, this.applyTransform) - // this.canvasRef.current.style.width = `${100/zoom}%` - // this.canvasRef.current.style.height = `${100/zoom}%` } @@ -390,6 +412,9 @@ class Canvas extends React.Component { widget.current?.deSelect() }) + this.context?.updateActiveWidget("") + this.context.updateToolAttrs({}) + this.setState({ selectedWidgets: [], toolbarAttrs: null, @@ -433,7 +458,6 @@ class Canvas extends React.Component { // Store the ref in the instance variable this.widgetRefs[id] = widgetRef - // console.log("widget ref: ", this.widgetRefs) const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType }] // don't add the widget refs in the state // Update the state to include the new widget's type and ID @@ -452,6 +476,10 @@ class Canvas extends React.Component { return {id, widgetRef} } + /** + * delete's the selected widgets from the canvas + * @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets + */ deleteSelectedWidgets(widgets=[]){ @@ -519,7 +547,9 @@ class Canvas extends React.Component { if (this.state.selectedWidgets.length === 0 || widgetId !== this.state.selectedWidgets[0].__id) return - // console.log("updating...") + console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidgets.at(0).getToolbarAttrs()) + + // console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs()) this.setState({ toolbarAttrs: this.state.selectedWidgets.at(0).getToolbarAttrs() @@ -575,36 +605,39 @@ class Canvas extends React.Component {
+ {/* */} - {/* */} -
- {/* Canvas */} -
-
- { - this.state.widgets.map(this.renderWidget) - } -
+ +
+ {/* Canvas */} +
+
+ { + this.state.widgets.map(this.renderWidget) + }
- {/* */} +
+
+ {/* */}
) } diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index b5a3d6d..af8fca2 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -4,6 +4,7 @@ import { ColorPicker, Input, InputNumber, Select } from "antd" import { capitalize } from "../utils/common" import Tools from "./constants/tools.js" +import { useActiveWidget } from "./activeWidgetContext.js" // FIXME: Maximum recursion error @@ -16,6 +17,9 @@ import Tools from "./constants/tools.js" */ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { + // const { activeWidgetAttrs } = useActiveWidget() + + // console.log("active widget context: ", activeWidgetAttrs) const [toolbarOpen, setToolbarOpen] = useState(isOpen) const [toolbarAttrs, setToolbarAttrs] = useState(attrs) @@ -24,11 +28,19 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { }, [isOpen]) useEffect(() => { + console.log("active widget: ", attrs) setToolbarAttrs(attrs) }, [attrs]) + // useEffect(() => { + + // console.log("active widget: ", activeWidgetAttrs) + // setToolbarAttrs(activeWidgetAttrs || {}) + + // }, [activeWidgetAttrs]) const handleChange = (value, callback) => { + console.log("changed...") if (callback) { callback(value) } @@ -42,14 +54,14 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { const isFirstLevel = parentKey === "" const outerLabelClass = isFirstLevel - ? "tw-text-lg tw-text-blue-700 tw-font-medium" - : "tw-text-lg" + ? "tw-text-base tw-text-blue-700 tw-font-medium" + : "tw-text-base" // Render tool widgets if (typeof val === "object" && val.tool) { return (
-
{val.label}
+
{val.label}
{val.tool === Tools.INPUT && ( this.setWidgetStyling("backgroundColor", value) + onChange: (value) => { + this.setWidgetStyling("backgroundColor", value) + this.setAttrValue("styling.backgroundColor", value) + } }, foregroundColor: { label: "Foreground Color", @@ -145,13 +154,33 @@ class Widget extends React.Component { this.canvas.addEventListener("mouseup", this.stopResizing) } + componentDidUpdate(prevProps, prevState) { + // if (prevState !== this.state) { + // // State has changed + // console.log('State has been updated') + // } else { + // // State has not changed + // console.log('State has not changed') + // } + } + updateState = (newState, callback) => { + + // FIXME: maximum recursion error when updating size this.setState(newState, () => { + const { onWidgetUpdate } = this.props if (onWidgetUpdate) { onWidgetUpdate(this.__id) } + + // const { activeWidgetId, updateToolAttrs } = this.context + + // if (activeWidgetId === this.__id) + // updateToolAttrs(this.getToolbarAttrs()) + if (callback) callback() + }) } @@ -174,7 +203,7 @@ class Widget extends React.Component { onChange: (value) => this.setWidgetName(value) }, size: { - label: "Sizing", + label: "Size", display: "horizontal", width: { label: "Width", @@ -228,16 +257,6 @@ class Widget extends React.Component { mousePress(event) { // event.preventDefault() if (!this._disableSelection) { - - // const widgetSelected = new CustomEvent("selection:created", { - // detail: { - // event, - // id: this.__id, - // element: this - // }, - // // bubbles: true // Allow the event to bubble up the DOM tree - // }) - // this.canvas.dispatchEvent(widgetSelected) } } @@ -264,13 +283,14 @@ class Widget extends React.Component { // don't change position when resizing the widget return } - // this.setState({ - // pos: { x, y } - // }) - this.updateState({ + this.setState({ pos: { x, y } }) + + // this.updateState({ + // pos: { x, y } + // }) } setParent(parentId) { @@ -303,38 +323,6 @@ class Widget extends React.Component { return this.state.size } - getScaleAwareDimensions() { - // Get the bounding rectangle - const rect = this.elementRef.current.getBoundingClientRect() - - // Get the computed style of the element - const style = window.getComputedStyle(this.elementRef.current) - - // Get the transform matrix - const transform = style.transform - - // Extract scale factors from the matrix - let scaleX = 1 - let scaleY = 1 - - if (transform && transform !== 'none') { - // For 2D transforms (a, c, b, d) - const matrix = transform.match(/^matrix\(([^,]+),[^,]+,([^,]+),[^,]+,[^,]+,[^,]+\)$/); - - if (matrix) { - scaleX = parseFloat(matrix[1]) - scaleY = parseFloat(matrix[2]) - } - } - - // Return scaled width and height - return { - width: rect.width / scaleX, - height: rect.height / scaleY - } - } - - getWidgetFunctions() { return this.functions } @@ -400,7 +388,7 @@ class Widget extends React.Component { * @param {string} value - Value of the style * @param {function():void} [callback] - optional callback, thats called after setting the internal state */ - setWidgetStyling(key, value, callback) { + setWidgetStyling(key, value) { const widgetStyle = { ...this.state.widgetStyling, @@ -409,9 +397,6 @@ class Widget extends React.Component { this.setState({ widgetStyling: widgetStyle - }, () => { - if (callback) - callback(widgetStyle) }) } @@ -420,28 +405,15 @@ class Widget extends React.Component { * * @param {number|null} width * @param {number|null} height - * @param {function():void} [callback] - optional callback, thats called after setting the internal state */ - setWidgetSize(width, height, callback) { + setWidgetSize(width, height) { 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.setState({ - size: newSize - }, () => { - if (callback) { - callback(newSize) - } - }) - this.updateState({ size: newSize - }, () => { - if (callback) - callback(newSize) }) } @@ -530,9 +502,6 @@ class Widget extends React.Component { */ render() { - const widgetStyle = this.state.widgetStyling - - let outerStyle = { cursor: this.cursor, zIndex: this.state.zIndex, @@ -552,51 +521,50 @@ class Widget extends React.Component { // console.log("selected: ", this.state.selected) return ( +
-
+ {this.renderContent()} +
- {this.renderContent()} -
+
+ + +
this.startResizing("nw", e)} + /> +
this.startResizing("ne", e)} + /> +
this.startResizing("sw", e)} + /> +
this.startResizing("se", e)} + /> + +
-
- -
this.startResizing("nw", e)} - /> -
this.startResizing("ne", e)} - /> -
this.startResizing("sw", e)} - /> -
this.startResizing("se", e)} - />
- - -
-
) } diff --git a/src/components/cards.js b/src/components/cards.js index 082933e..7bb22e8 100644 --- a/src/components/cards.js +++ b/src/components/cards.js @@ -32,11 +32,11 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){ // -
-
+
{name}
{name} From 0f755b7a906c10c733d3a3c35a29f40bbb32aec0 Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 17 Sep 2024 09:13:54 +0530 Subject: [PATCH 05/19] commented activeWidgetContext provider and start on widget drag --- src/canvas/canvas.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 86ad45e..602238e 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -223,8 +223,8 @@ class Canvas extends React.Component { toolbarAttrs: selectedWidget.getToolbarAttrs() }) - this.context.updateActiveWidget(selectedWidget.__id) - this.context.updateToolAttrs(selectedWidget.getToolbarAttrs()) + // this.context.updateActiveWidget(selectedWidget.__id) + // this.context.updateToolAttrs(selectedWidget.getToolbarAttrs()) // this.props.updateActiveWidget(selectedWidget) } this.currentMode = CanvasModes.MOVE_WIDGET @@ -412,8 +412,8 @@ class Canvas extends React.Component { widget.current?.deSelect() }) - this.context?.updateActiveWidget("") - this.context.updateToolAttrs({}) + // this.context?.updateActiveWidget("") + // this.context.updateToolAttrs({}) this.setState({ selectedWidgets: [], From d3721f2ea2c3bc14f0d81ff405436a6b19a325db Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 17 Sep 2024 11:55:21 +0530 Subject: [PATCH 06/19] fixed widget dragging inside the canvas --- src/canvas/canvas.js | 80 +++++++----- src/canvas/widgets/base.js | 10 +- src/canvas/widgets/draggableWidgetContext.js | 24 ++++ src/canvas/widgets/widgetDragDrop.js | 127 +++++++++++++++++++ src/components/cards.js | 2 +- src/components/draggable/draggable.js | 3 +- src/components/draggable/droppable.js | 4 +- 7 files changed, 214 insertions(+), 36 deletions(-) create mode 100644 src/canvas/widgets/draggableWidgetContext.js create mode 100644 src/canvas/widgets/widgetDragDrop.js diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 602238e..0bdd86f 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -20,6 +20,7 @@ import { WidgetContext } from './context/widgetContext' import DotsBackground from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" +import { DragWidgetProvider } from "./widgets/draggableWidgetContext" // const DotsBackground = require("../assets/background/dots.svg") @@ -302,7 +303,7 @@ class Canvas extends React.Component { const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas - widget.setPos(newPosX, newPosY) + // widget.setPos(newPosX, newPosY) }) } @@ -468,7 +469,7 @@ class Canvas extends React.Component { callback({id, widgetRef}) if (this._onWidgetListUpdated) - this._onWidgetListUpdated(widgets) + this._onWidgetListUpdated(widgets) // inform the parent container }) @@ -476,6 +477,11 @@ class Canvas extends React.Component { return {id, widgetRef} } + getWidgetById(id){ + + return this.widgetRefs[id] + } + /** * delete's the selected widgets from the canvas * @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets @@ -558,13 +564,17 @@ class Canvas extends React.Component { } /** - * Handles drop event to canvas from the sidebar + * Handles drop event to canvas from the sidebar and on canvas widget movement * @param {DragEvent} e */ - handleDropEvent = (e) => { + handleDropEvent = (e, draggedElement) => { e.preventDefault() + + const container = draggedElement.getAttribute("data-container") + console.log("dragged element: ", e, draggedElement, container) + // const canvasContainerRect = this.getCanvasContainerBoundingRect() const canvasRect = this.canvasRef.current.getBoundingClientRect() const { clientX, clientY } = e @@ -572,11 +582,18 @@ class Canvas extends React.Component { const finalPosition = { x: (clientX - canvasRect.left) / this.state.zoom, y: (clientY - canvasRect.top) / this.state.zoom, - } + } - this.addWidget(Widget, ({id, widgetRef}) => { - widgetRef.current.setPos(finalPosition.x, finalPosition.y) - }) + if (container === "sidebar"){ + this.addWidget(Widget, ({id, widgetRef}) => { + widgetRef.current.setPos(finalPosition.x, finalPosition.y) + }) + }else if (container === "canvas"){ + + const widgetObj = this.getWidgetById(draggedElement.getAttribute("data-widget-id")) + // console.log("WidgetObj: ", widgetObj) + widgetObj.current.setPos(finalPosition.x, finalPosition.y) + } } @@ -607,30 +624,33 @@ class Canvas extends React.Component { {/* */} - -
- {/* Canvas */} -
-
- { - this.state.widgets.map(this.renderWidget) - } + className="tw-w-full tw-h-full" + onDrop={this.handleDropEvent}> + {/* */} + +
+ {/* Canvas */} +
+
+ { + this.state.widgets.map(this.renderWidget) + } +
-
- + + {/* */} +
{this.renderContent()} @@ -565,6 +572,7 @@ class Widget extends React.Component {
+ ) } diff --git a/src/canvas/widgets/draggableWidgetContext.js b/src/canvas/widgets/draggableWidgetContext.js new file mode 100644 index 0000000..2532041 --- /dev/null +++ b/src/canvas/widgets/draggableWidgetContext.js @@ -0,0 +1,24 @@ +import React, { createContext, useContext, useState } from 'react'; + +const DragWidgetContext = createContext() + +export const useDragWidgetContext = () => useContext(DragWidgetContext) + +// Provider component to wrap around parts of your app that need drag-and-drop functionality +export const DragWidgetProvider = ({ children }) => { + const [draggedElement, setDraggedElement] = useState(null) + + const onDragStart = (element) => { + setDraggedElement(element) + } + + const onDragEnd = () => { + setDraggedElement(null) + } + + return ( + + {children} + + ) +} diff --git a/src/canvas/widgets/widgetDragDrop.js b/src/canvas/widgets/widgetDragDrop.js new file mode 100644 index 0000000..71ad66b --- /dev/null +++ b/src/canvas/widgets/widgetDragDrop.js @@ -0,0 +1,127 @@ +import { memo, useState } from "react" +import { useDragWidgetContext } from "./draggableWidgetContext" +import { useDragContext } from "../../components/draggable/draggableContext" + + +const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, droppableTags = ["widget"], ...props }) => { + + + // const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext() + const { draggedElement, onDragStart, onDragEnd } = useDragContext() + + const [isDragging, setIsDragging] = useState(false) + + const [showDroppable, setShowDroppable] = useState({ + show: false, + allow: false + }) + + const handleDragStart = (e) => { + setIsDragging(true) + + console.log("Draggable widget ref: ", widgetRef) + onDragStart(widgetRef?.current || null) + + // 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 + dragImage.style.position = 'absolute' + dragImage.style.top = '-9999px' // Move it out of view + + document.body.appendChild(dragImage) + const rect = widgetRef?.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) + } + + const handleDragEnter = (e) => { + + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) { + setShowDroppable({ + allow: true, + show: true + }) + } else { + setShowDroppable({ + allow: false, + show: true + }) + } + } + + const handleDragOver = (e) => { + // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) + const dragEleType = draggedElement.getAttribute("data-draggable-type") + + if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) { + e.preventDefault() // this is necessary to allow drop to take place + } + + } + + const handleDropEvent = (e) => { + + setShowDroppable({ + allow: false, + show: false + }) + + if (onDrop) { + onDrop(e, draggedElement) + } + } + + + const handleDragLeave = (e) => { + if (!e.currentTarget.contains(e.relatedTarget)) { + setShowDroppable({ + allow: false, + show: false + }) + } + } + + const handleDragEnd = () => { + onDragEnd() + setIsDragging(false) + } + + return ( +
+ { + showDroppable.show && +
+
+ } + {props.children} + +
+ ) + +}) + + +export default WidgetDraggable \ No newline at end of file diff --git a/src/components/cards.js b/src/components/cards.js index 7bb22e8..7a7281d 100644 --- a/src/components/cards.js +++ b/src/components/cards.js @@ -30,7 +30,7 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){ return ( // - +
{ +const DraggableWrapper = memo(({dragElementType, className, children, ...props}) => { const { onDragStart, onDragEnd } = useDragContext() @@ -40,6 +40,7 @@ const DraggableWrapper = memo(({dragElementType, className, children}) => { onDragStart={handleDragStart} onDragEnd={handleDragEnd} ref={draggableRef} + {...props} > {children}
diff --git a/src/components/draggable/droppable.js b/src/components/draggable/droppable.js index 569f33f..b7173ab 100644 --- a/src/components/draggable/droppable.js +++ b/src/components/draggable/droppable.js @@ -14,7 +14,6 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => const handleDragEnter = (e) => { - console.log("Drag Enter", draggedElement) const dragElementType = draggedElement.getAttribute("data-draggable-type") @@ -42,7 +41,6 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => } const handleDropEvent = (e) => { - console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer) setShowDroppable({ allow: false, @@ -50,7 +48,7 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => }) if(onDrop){ - onDrop(e) + onDrop(e, draggedElement) } } From 74119ed5869374a6399592ef4983cd5143798a60 Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 17 Sep 2024 18:32:33 +0530 Subject: [PATCH 07/19] added dropstyle to widgets, shows dotted lines --- src/canvas/canvas.js | 5 +- src/canvas/toolbar.js | 2 +- src/canvas/widgets/base.js | 93 +++++++++++++++++--- src/canvas/widgets/widgetDragDrop.js | 65 +++++++++----- src/components/cards.js | 2 +- src/components/draggable/draggableContext.js | 3 +- src/components/draggable/droppable.js | 13 ++- src/sidebar/sidebar.js | 4 +- 8 files changed, 142 insertions(+), 45 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 0bdd86f..a6aef97 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -573,7 +573,7 @@ class Canvas extends React.Component { const container = draggedElement.getAttribute("data-container") - console.log("dragged element: ", e, draggedElement, container) + console.log("Dropped on canvas",) // const canvasContainerRect = this.getCanvasContainerBoundingRect() const canvasRect = this.canvasRef.current.getBoundingClientRect() @@ -585,6 +585,7 @@ class Canvas extends React.Component { } if (container === "sidebar"){ + // if the widget is being dropped from the sidebar, use the info to create the widget first this.addWidget(Widget, ({id, widgetRef}) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) }) @@ -640,7 +641,7 @@ class Canvas extends React.Component { > {/* Canvas */}
{ diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index af8fca2..a16578a 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -129,7 +129,7 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { return (
diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index a68d4f6..3a85939 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -68,6 +68,12 @@ class Widget extends React.Component { enableRename: false, // will open the widgets editable div for renaming resizing: false, resizeCorner: "", + dragEnabled: true, + + showDroppableStyle: { // shows the droppable indicator + allow: false, + show: false, + }, pos: { x: 0, y: 0 }, size: { width: 100, height: 100 }, @@ -484,8 +490,19 @@ class Widget extends React.Component { }) } - handleDragStart = (event) => { - console.log("dragging event: ", event) + handleDrop = (event, dragElement) => { + console.log("dragging event: ", event, dragElement) + + const container = dragElement.getAttribute("data-container") + + if (container === "canvas"){ + + // this.canvas.getWidgetById + + // this._children.push() + + } + } renderContent() { @@ -514,16 +531,29 @@ class Widget extends React.Component { height: `${this.state.size.height}px`, } - let selectionStyle = { - x: "-5px", - y: "-5px", - width: this.boundingRect.width + 5, - height: this.boundingRect.height + 5 - } - + // console.log("Drag enabled: ", this.state.dragEnabled) // console.log("selected: ", this.state.selected) return ( - + { + this.setState({ + showDroppableStyle: showDrop + }) + } + } + onDragLeave={ () => { + this.setState({ + showDroppableStyle: { + allow: false, + show: false + } + }) + } + } + > +
{this.renderContent()} + + { + // show drop style on drag hover + this.state.showDroppableStyle.show && +
+
+ } +
@@ -548,22 +597,38 @@ class Widget extends React.Component {
this.startResizing("nw", e)} + onMouseDown={(e) => { + this.startResizing("nw", e) + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} />
this.startResizing("ne", e)} + onMouseDown={(e) => { + this.startResizing("ne", e) + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} />
this.startResizing("sw", e)} + onMouseDown={(e) => { + this.startResizing("sw", e) + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} />
this.startResizing("se", e)} + onMouseDown={(e) => { + this.startResizing("se", e) + this.setState({dragEnabled: false}) + }} + onMouseLeave={() => this.setState({dragEnabled: true})} />
diff --git a/src/canvas/widgets/widgetDragDrop.js b/src/canvas/widgets/widgetDragDrop.js index 71ad66b..ea58f70 100644 --- a/src/canvas/widgets/widgetDragDrop.js +++ b/src/canvas/widgets/widgetDragDrop.js @@ -1,14 +1,25 @@ -import { memo, useState } from "react" +import { memo, useEffect, useState } from "react" import { useDragWidgetContext } from "./draggableWidgetContext" import { useDragContext } from "../../components/draggable/draggableContext" -const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, droppableTags = ["widget"], ...props }) => { +/** + * @param {} - widgetRef - the widget ref for your widget + * @param {boolean} - enableDraggable - should the widget be draggable + * @param {string} - dragElementType - the widget type of widget so the droppable knows if the widget can be accepted + * @param {() => void} - onDrop - the widget type of widget so the droppable knows if the widget can be accepted + * @param {string[]} - droppableTags - array of widget that can be dropped on the widget + * + */ +const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="widget", + onDragEnter, onDragLeave, onDrop, + droppableTags = ["widget"], ...props }) => { // const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext() - const { draggedElement, onDragStart, onDragEnd } = useDragContext() + const { draggedElement, onDragStart, onDragEnd, overElement, setOverElement } = useDragContext() + // const [dragEnabled, setDragEnabled] = useState(enableDraggable) const [isDragging, setIsDragging] = useState(false) const [showDroppable, setShowDroppable] = useState({ @@ -19,7 +30,6 @@ const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, dro const handleDragStart = (e) => { setIsDragging(true) - console.log("Draggable widget ref: ", widgetRef) onDragStart(widgetRef?.current || null) // Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas @@ -46,17 +56,32 @@ const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, dro const dragEleType = draggedElement.getAttribute("data-draggable-type") + // console.log("Drag entering...", overElement === e.currentTarget) + + setOverElement(e.currentTarget) + + let showDrop = { + allow: true, + show: true + } + if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) { - setShowDroppable({ + showDrop = { allow: true, show: true - }) + } + } else { - setShowDroppable({ + showDrop = { allow: false, show: true - }) + } } + + setShowDroppable(showDrop) + if (onDragEnter) + onDragEnter({element: draggedElement, showDrop}) + } const handleDragOver = (e) => { @@ -70,6 +95,9 @@ const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, dro } const handleDropEvent = (e) => { + e.preventDefault() + e.stopPropagation() + // console.log("Dropped") setShowDroppable({ allow: false, @@ -88,6 +116,10 @@ const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, dro allow: false, show: false }) + + if (onDragLeave) + onDragLeave() + } } @@ -97,27 +129,18 @@ const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, dro } return ( -
- { - showDroppable.show && -
-
- } + > {props.children} - +
) diff --git a/src/components/cards.js b/src/components/cards.js index 7a7281d..567081b 100644 --- a/src/components/cards.js +++ b/src/components/cards.js @@ -32,7 +32,7 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){ // -
diff --git a/src/components/draggable/draggableContext.js b/src/components/draggable/draggableContext.js index 7d77f15..35d8c52 100644 --- a/src/components/draggable/draggableContext.js +++ b/src/components/draggable/draggableContext.js @@ -7,6 +7,7 @@ export const useDragContext = () => useContext(DragContext) // Provider component to wrap around parts of your app that need drag-and-drop functionality export const DragProvider = ({ children }) => { const [draggedElement, setDraggedElement] = useState(null) + const [overElement, setOverElement] = useState(null) // the element the dragged items is over const onDragStart = (element) => { setDraggedElement(element) @@ -17,7 +18,7 @@ export const DragProvider = ({ children }) => { } return ( - + {children} ) diff --git a/src/components/draggable/droppable.js b/src/components/draggable/droppable.js index b7173ab..40765f7 100644 --- a/src/components/draggable/droppable.js +++ b/src/components/draggable/droppable.js @@ -5,7 +5,7 @@ import { useDragContext } from "./draggableContext" const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => { - const { draggedElement } = useDragContext() + const { draggedElement, overElement, setOverElement } = useDragContext() const [showDroppable, setShowDroppable] = useState({ show: false, @@ -17,6 +17,10 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => const dragElementType = draggedElement.getAttribute("data-draggable-type") + // console.log("Current target: ", e.currentTarget) + + setOverElement(e.currentTarget) + if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){ setShowDroppable({ allow: true, @@ -41,6 +45,7 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => } const handleDropEvent = (e) => { + e.stopPropagation() setShowDroppable({ allow: false, @@ -69,15 +74,17 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} > + + {props.children} + { showDroppable.show &&
} - {props.children}
) diff --git a/src/sidebar/sidebar.js b/src/sidebar/sidebar.js index 786cb36..e9147ec 100644 --- a/src/sidebar/sidebar.js +++ b/src/sidebar/sidebar.js @@ -44,8 +44,8 @@ function Sidebar({tabs}){ return (
Date: Tue, 17 Sep 2024 21:23:01 +0530 Subject: [PATCH 08/19] added license and roadmap --- .github/ISSUE_TEMPLATE/bug_report.yml | 50 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 46 ++++++++++++++++++++ .github/funding.yaml | 3 ++ License.txt | 23 ++++++++++ README.md | 3 ++ roadmap.md | 29 +++++++++++++ src/assets/background/dots.svg | 4 +- src/canvas/canvas.js | 41 +++++++++++------- src/canvas/widgets/widgetDragDrop.js | 1 + 9 files changed, 182 insertions(+), 18 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/funding.yaml create mode 100644 License.txt create mode 100644 roadmap.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..330fd67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,50 @@ +name: Bug report +description: Create a report to help us improve +title: "[BUG]: " +labels: ["bug"] +assignees: + - PaulleDemon +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + - type: input + id: template-name + attributes: + label: Template name + description: Please tell us which template is the issue related to + placeholder: Template name + value: "Template name" + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser & version + description: Please tell us which browser and version of the browser you are using + placeholder: Browser name & version + value: "Browser name" + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + + - type: checkboxes + id: related-issues + attributes: + label: This is a new issue + description: I have checked for the same issue in the issues panel + options: + - label: "Yes" + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..7397ae0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,46 @@ +name: New Feature request +description: Have an idea for new feature? well tell us +title: "[FEATURE REQUEST]: " +labels: ["feature request"] +assignees: + - PaulleDemon +body: + - type: markdown + attributes: + value: | + > [!NOTE] + > Please make sure to check the issues tab before requesting a new feature. + + - type: textarea + id: feature-description + attributes: + label: Feature Description + description: "Describe about the feature in brief" + placeholder: "Tell us about the feature request in brief" + value: "Feature description" + validations: + required: true + + - type: textarea + id: functionalities + attributes: + label: Functionalities of the Feature + description: "What should the feature do? eg: a bold button to make the text bold" + placeholder: Functions of the feature + value: "Functions of the feature" + validations: + required: true + + - type: checkboxes + id: related-issues + attributes: + label: This is a new feature request + description: I acknowledge that I have checked the issues section for similar request and found none. + options: + - label: "Yes" + required: true + + - type: markdown + attributes: + value: | + The feature request priority are based on number of reaction, a good number of thumbs up will make it to priority queue \ No newline at end of file diff --git a/.github/funding.yaml b/.github/funding.yaml new file mode 100644 index 0000000..9e2ff5b --- /dev/null +++ b/.github/funding.yaml @@ -0,0 +1,3 @@ +ko_fi: artpaul +github: [PaulleDemon] +buy_me_a_coffee: artpaul \ No newline at end of file diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..a3527e1 --- /dev/null +++ b/License.txt @@ -0,0 +1,23 @@ +License held by paul +Github username: PaulleDemon + +1. License Grant +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions: + +2. Derived Work License +Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be open-sourced under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author. + +3. Distribution Restriction on Executables +You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works. + +4. Commercial Use Restriction +The Software, in its original or modified form, cannot be used for any commercial purpose without prior written permission from the original author. This includes selling, licensing, or offering the Software as part of a service. + +5. Non-Commercial Use +You may freely use, copy, modify, and distribute the source code of the Software for non-commercial purposes, provided that the same terms of this license are maintained in all copies or derivative works. + +6. Changes to the License +The original author reserves the right to modify, amend, or update this license at any time. Any changes will apply only to future versions of the Software. Any prior versions of the Software, including derivative works, will continue to be governed by the version of the license in effect at the time the work was created. + +7. Disclaimer +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 1cdef42..340fe9b 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,6 @@ * Generate Code. ## Roadmap + + +### All code generated by the builder tools are under MIT license and can be used commercially \ No newline at end of file diff --git a/roadmap.md b/roadmap.md new file mode 100644 index 0000000..f3aadf3 --- /dev/null +++ b/roadmap.md @@ -0,0 +1,29 @@ +## Road map for PyUIBuilder + +Any feature that has 👑 beside it, is meant only for [premium users](./readme.md#license--support) + +### 1.0.0 +- [x] Create the initial version for UI builder + +### 1.1.0 +- [ ] Allow swappable layout - (swappy/react-dnd-kit) +- [ ] UI fixes and enhancement +- [ ] Add text editor to help with event handlers + +### 1.5.0 +- [ ] Add canvas support (try fabricjs) +- [ ] Initial version for Electron App 👑 +- [ ] Save files locally 👑 +- [ ] Load UI files 👑 +- [ ] Light/Dark theme 👑 +- [ ] Run the preview 👑 + +### 2.0.0 +- [ ] Support for more third party plugins +- [ ] Support for Kivy +- [ ] Sharable Templates +- [ ] Dark theme 👑 + + +### 3.0.0 +- [ ] Support for PySide / PyQt 👑 (commercial license only) \ No newline at end of file diff --git a/src/assets/background/dots.svg b/src/assets/background/dots.svg index e8b0530..836be93 100644 --- a/src/assets/background/dots.svg +++ b/src/assets/background/dots.svg @@ -1,7 +1,7 @@ - - + + diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index a6aef97..8cae889 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -17,13 +17,16 @@ import { removeDuplicateObjects } from "../utils/common" import { WidgetContext } from './context/widgetContext' // import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" -import DotsBackground from "../assets/background/dots.svg" +// import DotsBackground from "../assets/background/dots.svg" +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" // const DotsBackground = require("../assets/background/dots.svg") + const CanvasModes = { DEFAULT: 0, PAN: 1, @@ -43,8 +46,6 @@ class Canvas extends React.Component { this.canvasRef = React.createRef() this.canvasContainerRef = React.createRef() - this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas - this.currentMode = CanvasModes.DEFAULT @@ -57,9 +58,10 @@ class Canvas extends React.Component { } // this._contextMenuItems = [] + this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...} this.state = { - widgets: [], // don't store the refs directly here, instead store it in widgetRef, store the widget type here + widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass}] zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, @@ -107,7 +109,7 @@ class Canvas extends React.Component { componentDidMount() { this.initEvents() - this.addWidget(Widget) + this.createWidget(Widget) } @@ -298,13 +300,13 @@ class Canvas extends React.Component { }else{ // update the widgets position - this.state.selectedWidgets.forEach(widget => { - const {x, y} = widget.getPos() + // this.state.selectedWidgets.forEach(widget => { + // const {x, y} = widget.getPos() - const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas - const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas - // widget.setPos(newPosX, newPosY) - }) + // const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas + // const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas + // widget.setPos(newPosX, newPosY) + // }) } @@ -450,9 +452,9 @@ class Canvas extends React.Component { /** * - * @param {Widget} widgetComponentType - don't pass instead pass Widget object + * @param {Widget} widgetComponentType - don't pass instead pass Widget object/class */ - addWidget(widgetComponentType, callback){ + createWidget(widgetComponentType, callback){ const widgetRef = React.createRef() const id = `${widgetComponentType.widgetType}_${UID()}` @@ -573,7 +575,7 @@ class Canvas extends React.Component { const container = draggedElement.getAttribute("data-container") - console.log("Dropped on canvas",) + // console.log("Dropped on canvas",) // const canvasContainerRect = this.getCanvasContainerBoundingRect() const canvasRect = this.canvasRef.current.getBoundingClientRect() @@ -586,7 +588,7 @@ class Canvas extends React.Component { if (container === "sidebar"){ // if the widget is being dropped from the sidebar, use the info to create the widget first - this.addWidget(Widget, ({id, widgetRef}) => { + this.createWidget(Widget, ({id, widgetRef}) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) }) }else if (container === "canvas"){ @@ -633,12 +635,19 @@ class Canvas extends React.Component { ref={this.canvasContainerRef} style={{ transition: " transform 0.3s ease-in-out", - backgroundImage: `url('${DotsBackground}')`, + backgroundImage: `url(${DotsBackground})`, backgroundSize: 'cover', // Ensure proper sizing if needed backgroundRepeat: 'no-repeat', }} tabIndex={0} // allow focus > + {/* Canvas */}
Date: Wed, 18 Sep 2024 11:39:07 +0530 Subject: [PATCH 09/19] moved resize handler from widget to canvas, canvas handles resizing --- src/canvas/canvas.js | 120 ++++++++++++++++++------ src/canvas/widgets/base.js | 182 +++++++++++++++++++------------------ src/styles/index.css | 10 ++ 3 files changed, 198 insertions(+), 114 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 8cae889..ef9ea9d 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -61,14 +61,15 @@ class Canvas extends React.Component { this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...} this.state = { - widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass}] + 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: ""}] zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, canvasSize: { width: 500, height: 500 }, contextMenuItems: [], - selectedWidgets: [], + selectedWidget: null, toolbarOpen: true, toolbarAttrs: null @@ -209,20 +210,17 @@ class Canvas extends React.Component { if (selectedWidget){ // if the widget is selected don't pan, instead move the widget if (!selectedWidget._disableSelection){ - - const selectedLength = this.state.selectedWidgets.length - // console.log("selected widget: ", selectedWidget) - if (selectedLength === 0 || (selectedLength === 1 && selectedWidget.__id !== this.state.selectedWidgets[0].__id)){ - this.state.selectedWidgets[0]?.deSelect() // deselect the previous widget before adding the new one - this.state.selectedWidgets[0]?.setZIndex(0) + if (!this.state.selectedWidget || (selectedWidget.__id !== this.state.selectedWidget?.__id)){ + this.state.selectedWidget?.deSelect() // deselect the previous widget before adding the new one + this.state.selectedWidget?.setZIndex(0) selectedWidget.setZIndex(1000) selectedWidget.select() this.setState({ - selectedWidgets: [selectedWidget], + selectedWidget: selectedWidget, toolbarAttrs: selectedWidget.getToolbarAttrs() }) @@ -240,7 +238,7 @@ class Canvas extends React.Component { this.clearSelections() this.currentMode = CanvasModes.PAN this.setCursor(Cursor.GRAB) - + console.log("clear selection") } this.setState({ @@ -253,14 +251,14 @@ class Canvas extends React.Component { }else if (event.button === 2){ //right click - if (this.state.selectedWidgets.length > 0 && this.state.selectedWidgets[0].__id !== selectedWidget.__id){ + if (this.state.selectedWidget && this.state.selectedWidget.__id !== selectedWidget.__id){ this.clearSelections() } if (selectedWidget){ this.setState({ - selectedWidget: [selectedWidget], + selectedWidget: selectedWidget, contextMenuItems: [ { key: "rename", @@ -284,12 +282,18 @@ class Canvas extends React.Component { mouseMoveEvent(event){ + if (this.state.widgetResizing !== ""){ + // if resizing is taking place don't do anything else + this.handleResize(event) + return + } + // console.log("mode: ", this.currentMode, this.getActiveObjects()) if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) { const deltaX = event.clientX - this.mousePos.x const deltaY = event.clientY - this.mousePos.y - if (this.state.selectedWidgets.length === 0){ + if (!this.state.selectedWidget){ // if there aren't any selected widgets, then pan the canvas this.setState(prevState => ({ currentTranslate: { @@ -320,6 +324,10 @@ class Canvas extends React.Component { this.mousePressed = false this.currentMode = CanvasModes.DEFAULT this.setCursor(Cursor.DEFAULT) + + if (this.state.widgetResizing) { + this.setState({ widgetResizing: "" }) + } } wheelZoom(event){ @@ -329,6 +337,59 @@ class Canvas extends React.Component { this.setZoom(zoom, {x: event.offsetX, y: event.offsetY}) } + /** + * handles widgets resizing + * @param {MouseEvent} event - mouse move event + * @returns + */ + handleResize = (event) => { + if (this.state.resizing === "") return + + const widget = this.state.selectedWidget + + if (!widget) return + const resizeCorner = this.state.widgetResizing + const size = widget.getSize() + const pos = widget.getPos() + + const deltaX = event.movementX + const deltaY = event.movementY + + let newSize = { ...size } + let newPos = { ...pos } + + const { width: minWidth, height: minHeight } = widget.minSize + const { width: maxWidth, height: maxHeight } = widget.maxSize + // console.log("resizing: ", deltaX, deltaY, event) + + switch (resizeCorner) { + case "nw": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + newPos.x += (newSize.width !== size.width) ? deltaX : 0 + newPos.y += (newSize.height !== size.height) ? deltaY : 0 + break + case "ne": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + newPos.y += (newSize.height !== size.height) ? deltaY : 0 + break + case "sw": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + newPos.x += (newSize.width !== size.width) ? deltaX : 0 + break + case "se": + newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + break + default: + break + } + + widget.setResize(newPos, newSize) + } + getCanvasContainerBoundingRect(){ return this.canvasContainerRef.current.getBoundingClientRect() } @@ -411,6 +472,10 @@ class Canvas extends React.Component { } clearSelections(){ + + if (!this.state.selectedWidget) + return + this.getActiveObjects().forEach(widget => { widget.current?.deSelect() }) @@ -419,7 +484,7 @@ class Canvas extends React.Component { // this.context.updateToolAttrs({}) this.setState({ - selectedWidgets: [], + selectedWidget: null, toolbarAttrs: null, // toolbarOpen: }) @@ -462,7 +527,8 @@ class Canvas extends React.Component { // Store the ref in the instance variable this.widgetRefs[id] = widgetRef - const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType }] // don't add the widget refs in the state + const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType, children: [], parent: "" }] // don't add the widget refs in the state + // Update the state to include the new widget's type and ID this.setState({ widgets: widgets @@ -491,7 +557,7 @@ class Canvas extends React.Component { deleteSelectedWidgets(widgets=[]){ - let activeWidgets = removeDuplicateObjects([...widgets, ...this.state.selectedWidgets], "__id") + let activeWidgets = removeDuplicateObjects([...widgets, this.state.selectedWidget], "__id") const widgetIds = activeWidgets.map(widget => widget.__id) @@ -520,11 +586,6 @@ class Canvas extends React.Component { clearCanvas(){ // NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets - // for (let [key, value] of Object.entries(this.widgetRefs)){ - // console.log("removed: ", value, value.current?.getElement()) - - // value.current?.remove() - // } this.widgetRefs = {} this.setState({ @@ -552,15 +613,15 @@ class Canvas extends React.Component { onActiveWidgetUpdate(widgetId){ - if (this.state.selectedWidgets.length === 0 || widgetId !== this.state.selectedWidgets[0].__id) + if (!this.state.selectedWidget || widgetId !== this.state.selectedWidget.__id) return - console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidgets.at(0).getToolbarAttrs()) + // console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidget.getToolbarAttrs()) // console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs()) this.setState({ - toolbarAttrs: this.state.selectedWidgets.at(0).getToolbarAttrs() + toolbarAttrs: this.state.selectedWidget.getToolbarAttrs() }) } @@ -601,12 +662,15 @@ class Canvas extends React.Component { } renderWidget(widget){ - const { id, widgetType: ComponentType } = widget + const { id, widgetType: ComponentType, children=[], parent } = widget // console.log("widet: ", this.widgetRefs, id) - + // TODO: need to pass the widget ref for child elements as well return this.setState({widgetResizing: resizeSide })} /> } @@ -631,7 +695,7 @@ class Canvas extends React.Component { onDrop={this.handleDropEvent}> {/* */} -
{/* */} diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 3a85939..52f8fc1 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -142,24 +142,24 @@ class Widget extends React.Component { this.setWidgetStyling = this.setWidgetStyling.bind(this) - this.startResizing = this.startResizing.bind(this) - this.handleResize = this.handleResize.bind(this) - this.stopResizing = this.stopResizing.bind(this) + // this.startResizing = this.startResizing.bind(this) + // this.handleResize = this.handleResize.bind(this) + // this.stopResizing = this.stopResizing.bind(this) } componentDidMount() { this.elementRef.current?.addEventListener("click", this.mousePress) - this.canvas.addEventListener("mousemove", this.handleResize) - this.canvas.addEventListener("mouseup", this.stopResizing) + // this.canvas.addEventListener("mousemove", this.handleResize) + // this.canvas.addEventListener("mouseup", this.stopResizing) } componentWillUnmount() { this.elementRef.current?.removeEventListener("click", this.mousePress) - this.canvas.addEventListener("mousemove", this.handleResize) - this.canvas.addEventListener("mouseup", this.stopResizing) + // this.canvas.addEventListener("mousemove", this.handleResize) + // this.canvas.addEventListener("mouseup", this.stopResizing) } componentDidUpdate(prevProps, prevState) { @@ -287,10 +287,10 @@ class Widget extends React.Component { setPos(x, y) { - if (this.state.resizing) { - // don't change position when resizing the widget - return - } + // if (this.state.resizing) { + // // don't change position when resizing the widget + // return + // } this.setState({ pos: { x, y } @@ -301,19 +301,6 @@ class Widget extends React.Component { // }) } - setParent(parentId) { - this._parent = parentId - } - - addChild(childId) { - this._children.push(childId) - } - - removeChild(childId) { - this._children = this._children.filter(function (item) { - return item !== childId - }) - } getPos() { return this.state.pos @@ -368,11 +355,6 @@ class Widget extends React.Component { }) } - startResizing(corner, event) { - event.stopPropagation() - this.setState({ resizing: true, resizeCorner: corner }) - } - setZIndex(zIndex) { this.setState({ zIndex: zIndex @@ -381,10 +363,6 @@ class Widget extends React.Component { setWidgetName(name) { - // this.setState((prev) => ({ - // widgetName: name.length > 0 ? name : prev.widgetName - // })) - this.updateState({ widgetName: name.length > 0 ? name : this.state.widgetName }) @@ -394,7 +372,6 @@ class Widget extends React.Component { * * @param {string} key - The string in react Style format * @param {string} value - Value of the style - * @param {function():void} [callback] - optional callback, thats called after setting the internal state */ setWidgetStyling(key, value) { @@ -425,57 +402,70 @@ class Widget extends React.Component { }) } - handleResize(event) { - if (!this.state.resizing) return - - const { resizeCorner, size, pos } = this.state - const deltaX = event.movementX - const deltaY = event.movementY - - let newSize = { ...size } - let newPos = { ...pos } - - const { width: minWidth, height: minHeight } = this.minSize - const { width: maxWidth, height: maxHeight } = this.maxSize - // console.log("resizing: ", deltaX, deltaY, event) - - switch (resizeCorner) { - case "nw": - newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) - newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) - newPos.x += (newSize.width !== size.width) ? deltaX : 0 - newPos.y += (newSize.height !== size.height) ? deltaY : 0 - break - case "ne": - newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) - newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) - newPos.y += (newSize.height !== size.height) ? deltaY : 0 - break - case "sw": - newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) - newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) - newPos.x += (newSize.width !== size.width) ? deltaX : 0 - break - case "se": - newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) - newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) - break - default: - break - } - - // this.setState({ size: newSize, pos: newPos }) + setResize(pos, size){ + // useful when resizing the widget relative to the canvas, sets all pos, and size this.updateState({ - size: newSize, - pos: newPos + size: size, + pos: pos }) } - stopResizing() { - if (this.state.resizing) { - this.setState({ resizing: false }) - } - } + // startResizing(corner, event) { + // event.stopPropagation() + // this.setState({ resizing: true, resizeCorner: corner }) + // } + + // handleResize(event) { + // if (!this.state.resizing) return + + // const { resizeCorner, size, pos } = this.state + // const deltaX = event.movementX + // const deltaY = event.movementY + + // let newSize = { ...size } + // let newPos = { ...pos } + + // const { width: minWidth, height: minHeight } = this.minSize + // const { width: maxWidth, height: maxHeight } = this.maxSize + // // console.log("resizing: ", deltaX, deltaY, event) + + // switch (resizeCorner) { + // case "nw": + // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + // newPos.x += (newSize.width !== size.width) ? deltaX : 0 + // newPos.y += (newSize.height !== size.height) ? deltaY : 0 + // break + // case "ne": + // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) + // newPos.y += (newSize.height !== size.height) ? deltaY : 0 + // break + // case "sw": + // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) + // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + // newPos.x += (newSize.width !== size.width) ? deltaX : 0 + // break + // case "se": + // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) + // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) + // break + // default: + // break + // } + + // // this.setState({ size: newSize, pos: newPos }) + // this.updateState({ + // size: newSize, + // pos: newPos + // }) + // } + + // stopResizing() { + // if (this.state.resizing) { + // this.setState({ resizing: false }) + // } + // } openRenaming() { this.setState({ @@ -490,6 +480,22 @@ 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) @@ -509,7 +515,7 @@ class Widget extends React.Component { // throw new NotImplementedError("render method has to be implemented") return (
- + {/* {this.props.children} */}
) } @@ -598,7 +604,8 @@ class Widget extends React.Component { className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500" style={{ cursor: Cursor.NW_RESIZE }} onMouseDown={(e) => { - this.startResizing("nw", e) + // this.startResizing("nw", e) + this.props.onWidgetResizing("nw") this.setState({dragEnabled: false}) }} onMouseLeave={() => this.setState({dragEnabled: true})} @@ -607,7 +614,8 @@ class Widget extends React.Component { 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) => { - this.startResizing("ne", e) + // this.startResizing("ne", e) + this.props.onWidgetResizing("ne") this.setState({dragEnabled: false}) }} onMouseLeave={() => this.setState({dragEnabled: true})} @@ -616,7 +624,8 @@ class Widget extends React.Component { 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) => { - this.startResizing("sw", e) + // this.startResizing("sw", e) + this.props.onWidgetResizing("sw") this.setState({dragEnabled: false}) }} onMouseLeave={() => this.setState({dragEnabled: true})} @@ -625,7 +634,8 @@ class Widget extends React.Component { 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) => { - this.startResizing("se", e) + // this.startResizing("se", e) + this.props.onWidgetResizing("se") this.setState({dragEnabled: false}) }} onMouseLeave={() => this.setState({dragEnabled: true})} diff --git a/src/styles/index.css b/src/styles/index.css index 961219b..c3c03fa 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -17,6 +17,16 @@ body { background-size: cover; } +.stripes-bg { + width: 100%; + height: 100%; + --color: #E1E1E1; + background-color: #F3F3F3; + background-image: linear-gradient(0deg, transparent 24%, var(--color) 25%, var(--color) 26%, transparent 27%,transparent 74%, var(--color) 75%, var(--color) 76%, transparent 77%,transparent), + linear-gradient(90deg, transparent 24%, var(--color) 25%, var(--color) 26%, transparent 27%,transparent 74%, var(--color) 75%, var(--color) 76%, transparent 77%,transparent); + background-size: 55px 55px; + } + .input{ border: 2px solid #e3e5e8; padding: 2px 8px; From ad87e1cc535c246b39812c4e6c48f2f2a0d414ad Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 18 Sep 2024 22:16:34 +0530 Subject: [PATCH 10/19] working on widget-children dnd --- src/canvas/canvas.js | 474 ++++++++++++++++++++++---------- src/canvas/constants/layouts.js | 2 +- src/canvas/constants/tools.js | 2 + src/canvas/toolbar.js | 81 +++++- src/canvas/widgets/base.js | 104 ++----- 5 files changed, 419 insertions(+), 244 deletions(-) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index ef9ea9d..bbdf0a9 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -1,6 +1,6 @@ import React from "react" -import {DndContext} from '@dnd-kit/core' +import { DndContext } from '@dnd-kit/core' import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons" import { Button, Tooltip, Dropdown } from "antd" @@ -18,7 +18,7 @@ import { WidgetContext } from './context/widgetContext' // import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" // import DotsBackground from "../assets/background/dots.svg" -import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" +import { ReactComponent as DotsBackground } from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" @@ -42,14 +42,14 @@ class Canvas extends React.Component { super(props) const { canvasWidgets, onWidgetListUpdated } = props - - this.canvasRef = React.createRef() + + this.canvasRef = React.createRef() this.canvasContainerRef = React.createRef() - + this.currentMode = CanvasModes.DEFAULT - this.minCanvasSize = {width: 500, height: 500} + this.minCanvasSize = { width: 500, height: 500 } this.mousePressed = false this.mousePos = { @@ -62,16 +62,16 @@ 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: ""}] + widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass, children: [], parent: "", layoutType: "flex"}] zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, - canvasSize: { width: 500, height: 500 }, - + canvasSize: { width: 500, height: 500 }, + contextMenuItems: [], selectedWidget: null, - - toolbarOpen: true, + + toolbarOpen: true, toolbarAttrs: null } @@ -79,7 +79,7 @@ class Canvas extends React.Component { this.resetTransforms = this.resetTransforms.bind(this) this.renderWidget = this.renderWidget.bind(this) - + this.mouseDownEvent = this.mouseDownEvent.bind(this) this.mouseMoveEvent = this.mouseMoveEvent.bind(this) this.mouseUpEvent = this.mouseUpEvent.bind(this) @@ -115,7 +115,7 @@ class Canvas extends React.Component { } componentWillUnmount() { - + this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent) this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent) this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent) @@ -128,31 +128,31 @@ class Canvas extends React.Component { this.clearCanvas() } - initEvents(){ + initEvents() { this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom) - + this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true) // window.addEventListener("keydown", this.keyDownEvent, true) - + } - applyTransform(){ + applyTransform() { const { currentTranslate, zoom } = this.state this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` } - /** - * - * @returns {import("./widgets/base").Widget[]} - */ - getWidgets(){ - + /** + * + * @returns {import("./widgets/base").Widget[]} + */ + getWidgets() { + return this.state.widgets } @@ -160,7 +160,7 @@ class Canvas extends React.Component { * returns list of active objects / selected objects on the canvas * @returns Widget[] */ - getActiveObjects(){ + getActiveObjects() { return Object.values(this.widgetRefs).filter((widgetRef) => { return widgetRef.current?.isSelected() }) @@ -172,47 +172,47 @@ class Canvas extends React.Component { * @param {HTMLElement} target * @returns {Widget} */ - getWidgetFromTarget(target){ + getWidgetFromTarget(target) { - for (let [key, ref] of Object.entries(this.widgetRefs)){ - // console.log("ref: ", ref) - if (ref.current.getElement().contains(target)){ + for (let [key, ref] of Object.entries(this.widgetRefs)) { + console.log("ref: ", ref, key) + if (ref.current.getElement().contains(target)) { return ref.current } } } - keyDownEvent(event){ + keyDownEvent(event) { - if (event.key === "Delete"){ + if (event.key === "Delete") { this.deleteSelectedWidgets() } - if (event.key === "+"){ + if (event.key === "+") { this.setZoom(this.state.zoom + 0.1) } - if (event.key === "-"){ + if (event.key === "-") { this.setZoom(this.state.zoom - 0.1) } } - mouseDownEvent(event){ + mouseDownEvent(event) { this.mousePos = { x: event.clientX, y: event.clientY } - + let selectedWidget = this.getWidgetFromTarget(event.target) - if (event.button === 0){ + if (event.button === 0) { this.mousePressed = true - - if (selectedWidget){ + + if (selectedWidget) { // if the widget is selected don't pan, instead move the widget - if (!selectedWidget._disableSelection){ + if (!selectedWidget._disableSelection) { // console.log("selected widget: ", selectedWidget) - if (!this.state.selectedWidget || (selectedWidget.__id !== this.state.selectedWidget?.__id)){ + if (!this.state.selectedWidget || (selectedWidget.__id !== this.state.selectedWidget?.__id)) { this.state.selectedWidget?.deSelect() // deselect the previous widget before adding the new one this.state.selectedWidget?.setZIndex(0) @@ -233,7 +233,7 @@ class Canvas extends React.Component { this.currentMode = CanvasModes.PAN - }else if (!selectedWidget){ + } else if (!selectedWidget) { // get the canvas ready to pan, if there are widgets on the canvas this.clearSelections() this.currentMode = CanvasModes.PAN @@ -248,14 +248,14 @@ class Canvas extends React.Component { // this.setState({ // showContextMenu: false // }) - }else if (event.button === 2){ + } else if (event.button === 2) { //right click - - if (this.state.selectedWidget && this.state.selectedWidget.__id !== selectedWidget.__id){ + + if (this.state.selectedWidget && this.state.selectedWidget.__id !== selectedWidget.__id) { this.clearSelections() } - if (selectedWidget){ + if (selectedWidget) { this.setState({ selectedWidget: selectedWidget, @@ -273,16 +273,16 @@ class Canvas extends React.Component { } ] }) - + } } } - mouseMoveEvent(event){ + mouseMoveEvent(event) { - if (this.state.widgetResizing !== ""){ + if (this.state.widgetResizing !== "") { // if resizing is taking place don't do anything else this.handleResize(event) return @@ -292,8 +292,8 @@ class Canvas extends React.Component { if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) { const deltaX = event.clientX - this.mousePos.x const deltaY = event.clientY - this.mousePos.y - - if (!this.state.selectedWidget){ + + if (!this.state.selectedWidget) { // if there aren't any selected widgets, then pan the canvas this.setState(prevState => ({ currentTranslate: { @@ -302,7 +302,7 @@ class Canvas extends React.Component { } }), this.applyTransform) - }else{ + } else { // update the widgets position // this.state.selectedWidgets.forEach(widget => { // const {x, y} = widget.getPos() @@ -314,13 +314,13 @@ class Canvas extends React.Component { } - this.mousePos = { x: event.clientX, y: event.clientY } + this.mousePos = { x: event.clientX, y: event.clientY } - this.setCursor(Cursor.GRAB) - } + this.setCursor(Cursor.GRAB) + } } - mouseUpEvent(event){ + mouseUpEvent(event) { this.mousePressed = false this.currentMode = CanvasModes.DEFAULT this.setCursor(Cursor.DEFAULT) @@ -330,11 +330,11 @@ class Canvas extends React.Component { } } - wheelZoom(event){ + wheelZoom(event) { let delta = event.deltaY let zoom = this.state.zoom * 0.999 ** delta - - this.setZoom(zoom, {x: event.offsetX, y: event.offsetY}) + + this.setZoom(zoom, { x: event.offsetX, y: event.offsetY }) } /** @@ -385,27 +385,27 @@ class Canvas extends React.Component { break default: break - } + } widget.setResize(newPos, newSize) } - getCanvasContainerBoundingRect(){ + getCanvasContainerBoundingRect() { return this.canvasContainerRef.current.getBoundingClientRect() } - getCanvasBoundingRect(){ + getCanvasBoundingRect() { return this.canvasRef.current.getBoundingClientRect() } - getCanvasTranslation(){ + getCanvasTranslation() { return this.state.currentTranslate } /** * fits the canvas size to fit the widgets bounding box */ - fitCanvasToBoundingBox(padding=0){ + fitCanvasToBoundingBox(padding = 0) { const { top, left, right, bottom } = this.getCanvasObjectsBoundingBox() const width = right - left @@ -425,21 +425,21 @@ class Canvas extends React.Component { canvasStyle.top = `${top - padding}px` } - setCursor(cursor){ + setCursor(cursor) { this.canvasContainerRef.current.style.cursor = cursor } - setZoom(zoom, pos){ - + setZoom(zoom, pos) { + const { currentTranslate } = this.state let newTranslate = currentTranslate - if (pos){ + if (pos) { // Calculate the new translation to zoom into the mouse position const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) - + const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) newTranslate = { @@ -456,10 +456,10 @@ class Canvas extends React.Component { } - getZoom(){ + getZoom() { return this.state.zoom } - + resetTransforms() { this.setState({ zoom: 1, @@ -467,11 +467,11 @@ class Canvas extends React.Component { }, this.applyTransform) } - setSelectedWidget(selectedWidget){ + setSelectedWidget(selectedWidget) { this.setState({ selectedWidget: [selectedWidget] }) } - clearSelections(){ + clearSelections() { if (!this.state.selectedWidget) return @@ -482,7 +482,7 @@ class Canvas extends React.Component { // this.context?.updateActiveWidget("") // this.context.updateToolAttrs({}) - + this.setState({ selectedWidget: null, toolbarAttrs: null, @@ -495,7 +495,7 @@ class Canvas extends React.Component { * returns tha combined bounding rect of all the widgets on the canvas * */ - getCanvasObjectsBoundingBox(){ + getCanvasObjectsBoundingBox() { // Initialize coordinates to opposite extremes let top = Number.POSITIVE_INFINITY @@ -511,15 +511,147 @@ class Canvas extends React.Component { if (rect.right > right) right = rect.right if (rect.bottom > bottom) bottom = rect.bottom } - + return { top, left, right, bottom } } + + /** + * finds widgets from the list of this.state.widgets, also checks the children to find the widgets + * @param {string} widgetId + * @returns + */ + findWidgetFromListById = (widgetId) => { + + const searchWidgetById = (widgets, widgetId) => { + for (let widget of widgets) { + if (widget.id === widgetId) { + return widget + } + + // Recursively search in children + if (widget.children.length > 0) { + const foundInChildren = searchWidgetById(widget.children, widgetId) + if (foundInChildren) { + return foundInChildren // Found in children + } + } + } + return null // Widget not found + } + + return searchWidgetById(this.state.widgets, widgetId) + } + + /** + * Finds the widget from the list and removes it from its current position, even if the widget is in the child position + * @param {Array} widgets - The current list of widgets + * @param {string} widgetId - The ID of the widget to remove + * @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] + }, []) + } + + // Perform the removal operation + return removeWidget(this.state.widgets, widgetId) + + } + + /** + * Adds the child into the children attribute inside the this.widgets list of objects + * // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" } + * @param {string} parentWidgetId + * @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) => { + + // 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) { + + // remove child from current postion + let updatedWidgets = this.removeWidgetFromCurrentList(dragElementID) + + console.log("pre updated widgets: ", updatedWidgets) + + const updatedChildWidget = { + ...childWidgetObj, + parent: parentWidgetId + } + + // Create a new copy of the parent widget with the child added + const updatedParentWidget = { + ...parentWidgetObj, + children: [...parentWidgetObj.children, 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 + + + this.setState({ + widgets: updatedWidgets + }, () => { + + this.widgetRefs[dragElementID] = React.createRef() + + // Optionally, force React to update and re-render the refs + this.forceUpdate() + }) + + } + + } + /** * * @param {Widget} widgetComponentType - don't pass instead pass Widget object/class */ - createWidget(widgetComponentType, callback){ + createWidget(widgetComponentType, callback) { const widgetRef = React.createRef() const id = `${widgetComponentType.widgetType}_${UID()}` @@ -527,25 +659,23 @@ 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: "" }] // don't add the widget refs in the state - + const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType, children: [], parent: "", layoutType: "flex" }] // don't add the widget refs in the state + // Update the state to include the new widget's type and ID this.setState({ widgets: widgets }, () => { if (callback) - callback({id, widgetRef}) + callback({ id, widgetRef }) if (this._onWidgetListUpdated) this._onWidgetListUpdated(widgets) // inform the parent container }) - - - return {id, widgetRef} + return { id, widgetRef } } - getWidgetById(id){ + getWidgetById(id) { return this.widgetRefs[id] } @@ -554,14 +684,14 @@ class Canvas extends React.Component { * delete's the selected widgets from the canvas * @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets */ - deleteSelectedWidgets(widgets=[]){ + deleteSelectedWidgets(widgets = []) { + - let activeWidgets = removeDuplicateObjects([...widgets, this.state.selectedWidget], "__id") - + const widgetIds = activeWidgets.map(widget => widget.__id) - for (let widgetId of widgetIds){ + for (let widgetId of widgetIds) { // this.widgetRefs[widgetId]?.current.remove() delete this.widgetRefs[widgetId] @@ -576,14 +706,14 @@ class Canvas extends React.Component { // value.current?.remove() } - + } /** * removes all the widgets from the canvas */ - clearCanvas(){ + clearCanvas() { // NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets @@ -596,7 +726,10 @@ class Canvas extends React.Component { this._onWidgetListUpdated([]) } - removeWidget(widgetId){ + removeWidget(widgetId) { + + // 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() delete this.widgetRefs[widgetId] @@ -611,7 +744,7 @@ class Canvas extends React.Component { this._onWidgetListUpdated(widgets) } - onActiveWidgetUpdate(widgetId){ + onActiveWidgetUpdate(widgetId) { if (!this.state.selectedWidget || widgetId !== this.state.selectedWidget.__id) return @@ -634,7 +767,7 @@ class Canvas extends React.Component { e.preventDefault() - + const container = draggedElement.getAttribute("data-container") // console.log("Dropped on canvas",) @@ -642,17 +775,17 @@ class Canvas extends React.Component { const canvasRect = this.canvasRef.current.getBoundingClientRect() const { clientX, clientY } = e - const finalPosition = { - x: (clientX - canvasRect.left) / this.state.zoom, - y: (clientY - canvasRect.top) / this.state.zoom, - } + const finalPosition = { + x: (clientX - canvasRect.left) / this.state.zoom, + y: (clientY - canvasRect.top) / this.state.zoom, + } - if (container === "sidebar"){ + if (container === "sidebar") { // if the widget is being dropped from the sidebar, use the info to create the widget first - this.createWidget(Widget, ({id, widgetRef}) => { + this.createWidget(Widget, ({ id, widgetRef }) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) }) - }else if (container === "canvas"){ + } else if (container === "canvas") { const widgetObj = this.getWidgetById(draggedElement.getAttribute("data-widget-id")) // console.log("WidgetObj: ", widgetObj) @@ -661,28 +794,75 @@ class Canvas extends React.Component { } - renderWidget(widget){ - const { id, widgetType: ComponentType, children=[], parent } = widget - // console.log("widet: ", this.widgetRefs, id) - // TODO: need to pass the widget ref for child elements as well - return this.setState({widgetResizing: resizeSide })} - /> + 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 + + 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 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)} + + ) } render() { return (
- +
- + -
{/* */} - + {/* */} - -
- - {/* Canvas */} -
-
- { - this.state.widgets.map(this.renderWidget) - } -
+ +
+ + {/* Canvas */} +
+
+ { + this.state.widgets.map(this.renderWidget) + }
- +
+
{/* */} - + {/* */}
) diff --git a/src/canvas/constants/layouts.js b/src/canvas/constants/layouts.js index 82db7bd..0b97546 100644 --- a/src/canvas/constants/layouts.js +++ b/src/canvas/constants/layouts.js @@ -1,5 +1,5 @@ const Layouts = { - PACK: "flex", + FLEX: "flex", GRID: "grid", PLACE: "absolute" } diff --git a/src/canvas/constants/tools.js b/src/canvas/constants/tools.js index 13f1b63..dba5207 100644 --- a/src/canvas/constants/tools.js +++ b/src/canvas/constants/tools.js @@ -5,6 +5,8 @@ const Tools = { SELECT_DROPDOWN: "select_dropdown", COLOR_PICKER: "color_picker", EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown + + LAYOUT_MANAGER: "layout_manager" } diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index a16578a..0f26853 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -5,6 +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" // FIXME: Maximum recursion error @@ -15,7 +16,7 @@ import { useActiveWidget } from "./activeWidgetContext.js" * @param {string} widgetType * @param {object} attrs - widget attributes */ -const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { +const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { // const { activeWidgetAttrs } = useActiveWidget() @@ -28,16 +29,9 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { }, [isOpen]) useEffect(() => { - console.log("active widget: ", attrs) setToolbarAttrs(attrs) }, [attrs]) - // useEffect(() => { - - // console.log("active widget: ", activeWidgetAttrs) - // setToolbarAttrs(activeWidgetAttrs || {}) - - // }, [activeWidgetAttrs]) const handleChange = (value, callback) => { console.log("changed...") @@ -46,6 +40,69 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { } } + + const renderLayoutManager = (val) => { + + return ( +
+ handleChange(value, val.onChange)} + /> +
+
+ Grids +
+
+ Rows + handleChange(value, val.onChange)} + /> +
+
+ Columns + handleChange(value, val.onChange)} + /> +
+
+
+ +
+ ) + + } + + const renderWidgets = (obj, parentKey = "") => { return Object.entries(obj).map(([key, val], i) => { const keyName = parentKey ? `${parentKey}.${key}` : key @@ -103,6 +160,13 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { onChange={(value) => handleChange(value, val.onChange)} /> )} + + { + val.tool === Tools.LAYOUT_MANAGER && ( + renderLayoutManager(val) + ) + } +
); } @@ -137,7 +201,6 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => { {capitalize(`${widgetType || ""}`)} -
{renderWidgets(toolbarAttrs || {})}
) diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index 52f8fc1..34a5fbe 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -53,7 +53,7 @@ class Widget extends React.Component { } - this.layout = Layouts.PACK + this.layout = Layouts.FLEX this.boundingRect = { x: 0, y: 0, @@ -66,8 +66,6 @@ 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 - resizing: false, - resizeCorner: "", dragEnabled: true, showDroppableStyle: { // shows the droppable indicator @@ -103,8 +101,15 @@ class Widget extends React.Component { }, layout: { label: "Layout", - tool: Tools.SELECT_DROPDOWN, // the tool to display, can be either HTML ELement or a constant string - value: "flex", + tool: Tools.LAYOUT_MANAGER, // the tool to display, can be either HTML ELement or a constant string + value: { + layout: "flex", + direction: "vertical", + grid: { + rows: 1, + cols: 1 + } + }, options: [ { value: "flex", label: "Flex" }, { value: "grid", label: "Grid" }, @@ -141,25 +146,14 @@ class Widget extends React.Component { this.setWidgetName = this.setWidgetName.bind(this) this.setWidgetStyling = this.setWidgetStyling.bind(this) - - // this.startResizing = this.startResizing.bind(this) - // this.handleResize = this.handleResize.bind(this) - // this.stopResizing = this.stopResizing.bind(this) - } componentDidMount() { this.elementRef.current?.addEventListener("click", this.mousePress) - - // this.canvas.addEventListener("mousemove", this.handleResize) - // this.canvas.addEventListener("mouseup", this.stopResizing) } componentWillUnmount() { this.elementRef.current?.removeEventListener("click", this.mousePress) - - // this.canvas.addEventListener("mousemove", this.handleResize) - // this.canvas.addEventListener("mouseup", this.stopResizing) } componentDidUpdate(prevProps, prevState) { @@ -287,11 +281,6 @@ class Widget extends React.Component { setPos(x, y) { - // if (this.state.resizing) { - // // don't change position when resizing the widget - // return - // } - this.setState({ pos: { x, y } }) @@ -410,63 +399,6 @@ class Widget extends React.Component { }) } - // startResizing(corner, event) { - // event.stopPropagation() - // this.setState({ resizing: true, resizeCorner: corner }) - // } - - // handleResize(event) { - // if (!this.state.resizing) return - - // const { resizeCorner, size, pos } = this.state - // const deltaX = event.movementX - // const deltaY = event.movementY - - // let newSize = { ...size } - // let newPos = { ...pos } - - // const { width: minWidth, height: minHeight } = this.minSize - // const { width: maxWidth, height: maxHeight } = this.maxSize - // // console.log("resizing: ", deltaX, deltaY, event) - - // switch (resizeCorner) { - // case "nw": - // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) - // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) - // newPos.x += (newSize.width !== size.width) ? deltaX : 0 - // newPos.y += (newSize.height !== size.height) ? deltaY : 0 - // break - // case "ne": - // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) - // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height - deltaY)) - // newPos.y += (newSize.height !== size.height) ? deltaY : 0 - // break - // case "sw": - // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width - deltaX)) - // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) - // newPos.x += (newSize.width !== size.width) ? deltaX : 0 - // break - // case "se": - // newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX)) - // newSize.height = Math.max(minHeight, Math.min(maxHeight, newSize.height + deltaY)) - // break - // default: - // break - // } - - // // this.setState({ size: newSize, pos: newPos }) - // this.updateState({ - // size: newSize, - // pos: newPos - // }) - // } - - // stopResizing() { - // if (this.state.resizing) { - // this.setState({ resizing: false }) - // } - // } - openRenaming() { this.setState({ selected: true, @@ -500,12 +432,14 @@ class Widget extends React.Component { console.log("dragging event: ", event, dragElement) const container = dragElement.getAttribute("data-container") - + // TODO: check if the drop is allowed if (container === "canvas"){ - - // this.canvas.getWidgetById - - // this._children.push() + + 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 } @@ -604,7 +538,6 @@ class Widget extends React.Component { className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500" style={{ cursor: Cursor.NW_RESIZE }} onMouseDown={(e) => { - // this.startResizing("nw", e) this.props.onWidgetResizing("nw") this.setState({dragEnabled: false}) }} @@ -614,7 +547,6 @@ class Widget extends React.Component { 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) => { - // this.startResizing("ne", e) this.props.onWidgetResizing("ne") this.setState({dragEnabled: false}) }} @@ -624,7 +556,6 @@ class Widget extends React.Component { 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) => { - // this.startResizing("sw", e) this.props.onWidgetResizing("sw") this.setState({dragEnabled: false}) }} @@ -634,7 +565,6 @@ class Widget extends React.Component { 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) => { - // this.startResizing("se", e) this.props.onWidgetResizing("se") this.setState({dragEnabled: false}) }} From c32baab268d92bfa3ea4ee1280bd8fd668f2f564 Mon Sep 17 00:00:00 2001 From: paul Date: Thu, 19 Sep 2024 19:26:10 +0530 Subject: [PATCH 11/19] working on dnd inside the widgets --- src/canvas/canvas.js | 171 +++++++++++---- src/canvas/constants/containers.js | 12 ++ src/canvas/constants/layouts.js | 9 +- src/canvas/toolbar.js | 2 +- src/canvas/widgets/base.js | 300 ++++++++++++++++++--------- src/canvas/widgets/widgetDragDrop.js | 34 ++- 6 files changed, 375 insertions(+), 153 deletions(-) create mode 100644 src/canvas/constants/containers.js 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 ( -
+ - React App + PyUI builder diff --git a/src/assets/logo/bmc.svg b/src/assets/logo/bmc.svg new file mode 100644 index 0000000..edddb83 --- /dev/null +++ b/src/assets/logo/bmc.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/logo/ko-fi.png b/src/assets/logo/ko-fi.png new file mode 100644 index 0000000000000000000000000000000000000000..fdcb58c30c6e94b4c54f358fa1cf7167f92a01b3 GIT binary patch literal 10535 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 literal 0 HcmV?d00001 diff --git a/src/sidebar/sidebar.js b/src/sidebar/sidebar.js index e9147ec..6cbbe09 100644 --- a/src/sidebar/sidebar.js +++ b/src/sidebar/sidebar.js @@ -1,6 +1,11 @@ import { useEffect, useRef, useMemo, useState } from "react"; -import { CloseCircleFilled } from "@ant-design/icons"; +import { CloseCircleFilled, CrownFilled, GithubFilled, ShareAltOutlined } from "@ant-design/icons"; + +import KO_FI from "../assets/logo/ko-fi.png" +import Premium from "./utils/premium"; +import Share from "./utils/share"; + function Sidebar({tabs}){ @@ -44,7 +49,7 @@ function Sidebar({tabs}){ return (
diff --git a/src/sidebar/utils/premium.js b/src/sidebar/utils/premium.js new file mode 100644 index 0000000..6e1c865 --- /dev/null +++ b/src/sidebar/utils/premium.js @@ -0,0 +1,268 @@ +import { useState } from "react" + +import { Modal } from "antd" +import { CrownFilled } from "@ant-design/icons" + + +function Premium({ children, className = "" }) { + + const [premiumModalOpen, setPremiumModalOpen] = useState(false) + + + const onClick = () => { + setPremiumModalOpen(true) + } + + const onClose = (event) => { + event.stopPropagation() + setPremiumModalOpen(false) + } + + + return ( +
+ {children} + Buy Pre-order one Time License} + style={{ zIndex: 14000, gap: '10px', maxWidth: '80vw', placeItems: "center" }} + onCancel={onClose} + centered + onOk={onClose} + footer={null} + width={'auto'} + open={premiumModalOpen} + > +
+ I am Paul, an open-source dev, funding open-source projects by providing custom works. + If you find this tool useful and want to support its development, consider buying a one time license. +
+
+ By buying pre-order license, you get advance features, priority support, early access, upcoming features, and more. + + more. + +
+ +
+

Choose your plan

+
+ {/* Free Plan */} +
+

+ $0 +

+

+ Free to use forever, but for added features and to support open-source development, consider buying a lifetime license. +

+
+
    +
  • + + Access to web-based editor +
  • +
  • + + Commercial use +
  • +
  • + + Access to UI builder exe for local development +
  • +
  • + + Support for PySlide/PyQt +
  • +
  • + + Preview live +
  • +
  • + + Save and load files +
  • +
  • + + Load plugins locally +
  • +
  • + + Dark theme +
  • +
  • + + Priority support +
  • +
  • + + Early access to new features +
  • + +
+
+ + {/* Paid Plan */} +
+
+ Limited time offer +
+
+ Hobby +
+

+ + $129 + $29 + + Forever +

+

+ Support open-source development 🚀. Plus, get added benefits. +

+
+
    +
  • + + Access to web-based editor +
  • +
  • + + Access to UI builder exe for local development +
  • +
  • + + Preview live +
  • +
  • + + Save and load files +
  • +
  • + + Load plugins locally +
  • +
  • + + Dark theme +
  • +
  • + + Priority support +
  • +
  • + + Early access to new features +
  • +
  • + + Support for PySlide/PyQt +
  • +
  • + + Commercial use +
  • + +
+ + + Buy License + + +
+ + {/* Paid Plan */} +
+
+ Limited time offer +
+
+ Commercial +
+

+ + $180 + $49 + + Forever +

+

+ Support open-source development 🚀. Plus, get added benefits. +

+
+
    +
  • + + Access to web-based editor +
  • +
  • + + Access to UI builder exe for local development +
  • +
  • + + Preview live +
  • +
  • + + Save and load files +
  • +
  • + + Load plugins locally +
  • +
  • + + Dark theme +
  • +
  • + + Priority support +
  • +
  • + + Early access to new features +
  • +
  • + + Support for PySlide/PyQt +
  • +
  • + + Commercial use +
  • + +
+ + + Buy License + + +
+
+
+ + +
+ +
+ ) + +} + +export default Premium \ No newline at end of file diff --git a/src/sidebar/utils/share.js b/src/sidebar/utils/share.js new file mode 100644 index 0000000..a07e768 --- /dev/null +++ b/src/sidebar/utils/share.js @@ -0,0 +1,110 @@ +import { useMemo, useState } from "react" + +import { Modal, message } from "antd" +import { CopyOutlined, FacebookFilled, LinkedinFilled, MediumCircleFilled, RedditCircleFilled, TwitchFilled, TwitterCircleFilled } from "@ant-design/icons" + + +function Share({children, className=""}){ + + const [shareModalOpen, setShareModalOpen] = useState(false) + + const shareInfo = useMemo(() => { + + return { + url: encodeURI("https://github.com/PaulleDemon/font-tester-chrome"), + text: "Check out Framework agnostic GUI builder for python" + } + }, []) + + const onClick = () => { + setShareModalOpen(true) + } + + const onClose = (event) => { + event.stopPropagation() + setShareModalOpen(false) + } + + const onCopy = (event) => { + event.stopPropagation() + navigator.clipboard.writeText(`Check out Font tester: ${shareInfo.url}`).then(function() { + message.success("Link copied to clipboard") + + }, function(err) { + message.error("Error copying to clipboard") + }) + } + + return ( +
+ {children} + Share FontTester} + styles={{wrapper: {zIndex: 14000, gap: "10px"}}} + onCancel={onClose} + onOk={onClose} + footer={null} + open={shareModalOpen}> + + + + + +
+ ) + +} + +export default Share \ No newline at end of file From 474601ad127c053acb53840a4ba2ce2f5bb5d92d Mon Sep 17 00:00:00 2001 From: paul Date: Fri, 20 Sep 2024 15:12:43 +0530 Subject: [PATCH 13/19] fixed nested children rendering problem --- roadmap.md | 1 + src/canvas/canvas.js | 177 +++++++++++++++++------------------ src/canvas/toolbar.js | 41 ++++++-- src/canvas/widgets/base.js | 63 +++++++++++-- src/sidebar/utils/premium.js | 2 +- src/sidebar/utils/share.js | 2 +- 6 files changed, 177 insertions(+), 109 deletions(-) diff --git a/roadmap.md b/roadmap.md index f3aadf3..1c3cfec 100644 --- a/roadmap.md +++ b/roadmap.md @@ -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) diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index ca9af32..5326353 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -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) => { diff --git a/src/canvas/toolbar.js b/src/canvas/toolbar.js index c3b64c8..e8fb258 100644 --- a/src/canvas/toolbar.js +++ b/src/canvas/toolbar.js @@ -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)} />
Direction