15 Commits

Author SHA1 Message Date
paul
ee953107f3 working on sortable 2025-03-06 11:38:03 +05:30
paul
c9e12cccd4 minor updates 2025-03-05 21:09:40 +05:30
paul
3edf1a974c working on sortable 2025-03-05 21:07:20 +05:30
paul
910ff81342 fixed initial offset problem 2025-03-05 18:05:59 +05:30
paul
59a0c0b583 fixed pointer issue 2025-03-05 15:16:54 +05:30
paul
4d0ea22af6 fixed the offset issue 2025-03-05 14:41:56 +05:30
paul
b1c707a21d working on dnd aaaaa..... screammmmm 2025-03-04 19:15:09 +05:30
paul
1ee137085e bud fixes for dnd 2025-03-04 11:45:26 +05:30
paul
03a4984cbc working on dnd inside a container 2025-03-03 19:17:28 +05:30
paul
72aafd17fe working on drop 2025-03-03 11:56:41 +05:30
paul
4df0e664ba fixed position 2025-03-02 22:17:06 +05:30
paul
62caafcf23 working with dnd-kit/react 2025-03-02 19:20:45 +05:30
paul
cc84a9f2d7 fixed sidebar drop position using dnd-kit 2025-03-02 10:29:59 +05:30
paul
a47191ee72 fixed droppable 2025-03-01 22:21:51 +05:30
paul
6656d3f6c4 working on drag and drop using dndkit 2025-03-01 19:20:46 +05:30
22 changed files with 1133 additions and 297 deletions

View File

@@ -9,7 +9,6 @@ The source code for PyUIBuilder is Dual licensed, most parts of the code comes u
some parts under a different non-commercial use license. Read the second part after the definition of the AGPL license. some parts under a different non-commercial use license. Read the second part after the definition of the AGPL license.
Sub-folders with non-commercial use license will contain the second license as well. Sub-folders with non-commercial use license will contain the second license as well.
- Only the code inside src/canvas/ is kept under a non-commercial use license.
-------------------------- --------------------------
@@ -677,8 +676,7 @@ For more information on this, and how to apply and follow the GNU AGPL, see
-------------------------- --------------------------
This license can be found in multiple folders
Below is the license for code under src/canvas/
License for non-commercial use in short License for non-commercial use in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same licese. * You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same licese.

103
package-lock.json generated
View File

@@ -9,8 +9,10 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/dom": "^0.0.9",
"@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/react": "^0.0.9",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
@@ -2548,10 +2550,22 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@dnd-kit/abstract": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/abstract/-/abstract-0.0.9.tgz",
"integrity": "sha512-4FjHPOREfJJfRu0clCAnYqYc8JNHQl6CL31FjLKR+1wER+YOGkb3s3JntsqvZost5Xm/G/Z4sQ3ouLdyYQ9KdQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/geometry": "^0.0.9",
"@dnd-kit/state": "^0.0.9",
"tslib": "^2.6.2"
}
},
"node_modules/@dnd-kit/accessibility": { "node_modules/@dnd-kit/accessibility": {
"version": "3.1.0", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
@@ -2559,12 +2573,24 @@
"react": ">=16.8.0" "react": ">=16.8.0"
} }
}, },
"node_modules/@dnd-kit/core": { "node_modules/@dnd-kit/collision": {
"version": "6.1.0", "version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/collision/-/collision-0.0.9.tgz",
"integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", "integrity": "sha512-8Brbcd0dM/y2yzg/6EI9O/SvsVAglx0fUDvqPSsXACewR+//OGno1GVKndh35GAVKqDxOgG3Q/fw+CVnMnFv6g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@dnd-kit/accessibility": "^3.1.0", "@dnd-kit/abstract": "^0.0.9",
"@dnd-kit/geometry": "^0.0.9",
"tslib": "^2.6.2"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0" "tslib": "^2.0.0"
}, },
@@ -2573,6 +2599,29 @@
"react-dom": ">=16.8.0" "react-dom": ">=16.8.0"
} }
}, },
"node_modules/@dnd-kit/dom": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/dom/-/dom-0.0.9.tgz",
"integrity": "sha512-nsDcL6MMdNfGOg0xp24c1xy/eNW7fO1uLU0AM2b1SFYJYsbXUMw6sQXq+2eHCQol/FixCoSbjMgjx9D8jQXb+w==",
"license": "MIT",
"dependencies": {
"@dnd-kit/abstract": "^0.0.9",
"@dnd-kit/collision": "^0.0.9",
"@dnd-kit/geometry": "^0.0.9",
"@dnd-kit/state": "^0.0.9",
"tslib": "^2.6.2"
}
},
"node_modules/@dnd-kit/geometry": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/geometry/-/geometry-0.0.9.tgz",
"integrity": "sha512-zqSY7vva+LGs9Y/c37GhD67q+boS27nCmyYYAohlZ29MGz66KwjpbSH5HNHa261kuq90KaaIVQQVHiZGjNfmOw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/state": "^0.0.9",
"tslib": "^2.6.2"
}
},
"node_modules/@dnd-kit/modifiers": { "node_modules/@dnd-kit/modifiers": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz",
@@ -2586,6 +2635,32 @@
"react": ">=16.8.0" "react": ">=16.8.0"
} }
}, },
"node_modules/@dnd-kit/react": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/react/-/react-0.0.9.tgz",
"integrity": "sha512-a2ZUbVVyJwSf5gf0l6I10VqEj8EEe+O1W/q+rSR0fpjbxjwhLT0Gt+udwKZsg3yIKxmxJC4W6eHSS6feNca9aw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/abstract": "^0.0.9",
"@dnd-kit/dom": "^0.0.9",
"@dnd-kit/state": "^0.0.9",
"tslib": "^2.6.2"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@dnd-kit/state": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/@dnd-kit/state/-/state-0.0.9.tgz",
"integrity": "sha512-F43bOum8k/Rh1tfO3DiAUs/zw6KSv5IMm4PGKSPoU2tz+LW+37Kp37NlgwtLLDzx+z6R+6gILnJzE6Q5eVx7OQ==",
"license": "MIT",
"dependencies": {
"@preact/signals-core": "^1.6.0",
"tslib": "^2.6.2"
}
},
"node_modules/@dnd-kit/utilities": { "node_modules/@dnd-kit/utilities": {
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
@@ -3900,6 +3975,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@preact/signals-core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.8.0.tgz",
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@rc-component/async-validator": { "node_modules/@rc-component/async-validator": {
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",

View File

@@ -7,8 +7,10 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/dom": "^0.0.9",
"@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/react": "^0.0.9",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@reduxjs/toolkit": "^2.2.7", "@reduxjs/toolkit": "^2.2.7",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",

View File

@@ -3,6 +3,7 @@
* Github: PaulleDemon * Github: PaulleDemon
*/ */
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { createPortal } from "react-dom"
import { LayoutFilled, ProductFilled, CloudUploadOutlined, DatabaseFilled } from "@ant-design/icons" import { LayoutFilled, ProductFilled, CloudUploadOutlined, DatabaseFilled } from "@ant-design/icons"
// import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core' // import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
@@ -14,7 +15,7 @@ import Sidebar from './sidebar/sidebar'
import UploadsContainer from './sidebar/uploadsContainer' import UploadsContainer from './sidebar/uploadsContainer'
import WidgetsContainer from './sidebar/widgetsContainer' import WidgetsContainer from './sidebar/widgetsContainer'
import { DragProvider } from './components/draggable/draggableContext' import { DragProvider, useDragContext } from './components/draggable/draggableContext'
import PluginsContainer from './sidebar/pluginsContainer' import PluginsContainer from './sidebar/pluginsContainer'
import { FileUploadProvider, useFileUploadContext } from './contexts/fileUploadContext' import { FileUploadProvider, useFileUploadContext } from './contexts/fileUploadContext'
@@ -31,6 +32,9 @@ import generateTkinterCode from './frameworks/tkinter/engine/code'
import TkMainWindow from './frameworks/tkinter/widgets/mainWindow' import TkMainWindow from './frameworks/tkinter/widgets/mainWindow'
import CTkMainWindow from './frameworks/customtk/widgets/mainWindow' import CTkMainWindow from './frameworks/customtk/widgets/mainWindow'
import { DndContext, DragOverlay } from '@dnd-kit/core'
import { SidebarOverlayWidgetCard, SidebarWidgetCard } from './components/cards'
import { DragDropProvider } from '@dnd-kit/react'
function App() { function App() {
@@ -45,6 +49,8 @@ function App() {
// const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files) // const [uploadedAssets, setUploadedAssets] = useState([]) // a global storage for assets, since redux can't store files(serialize files)
const { draggedElement, overElement, setOverElement, widgetClass, dragElementMetaData } = useDragContext()
const [sidebarWidgets, setSidebarWidgets] = useState(TkinterWidgets || []) const [sidebarWidgets, setSidebarWidgets] = useState(TkinterWidgets || [])
const [sidebarPlugins, setSidebarPlugins] = useState(TkinterPluginWidgets || []) const [sidebarPlugins, setSidebarPlugins] = useState(TkinterPluginWidgets || [])
@@ -53,6 +59,7 @@ function App() {
// NOTE: the below reference is no longer required // NOTE: the below reference is no longer required
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
const sidebarTabs = [ const sidebarTabs = [
{ {
name: "Widgets", name: "Widgets",
@@ -196,6 +203,9 @@ function App() {
} }
// console.log("dragged element: ", dragElementMetaData)
return ( return (
<div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg"> <div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg">
<Header className="tw-h-[6vh]" onExportClick={handleCodeGen} <Header className="tw-h-[6vh]" onExportClick={handleCodeGen}
@@ -208,7 +218,7 @@ function App() {
<p>Are you sure you want to change the framework? This will clear the canvas.</p> <p>Are you sure you want to change the framework? This will clear the canvas.</p>
</Modal> */} </Modal> */}
<DragProvider> <DragDropProvider onDragStart={(e) => {console.log("Drag start event: ", e)}}>
<div className="tw-w-full tw-h-[94vh] tw-flex"> <div className="tw-w-full tw-h-[94vh] tw-flex">
<Sidebar tabs={sidebarTabs}/> <Sidebar tabs={sidebarTabs}/>
@@ -217,9 +227,26 @@ function App() {
{/* </ActiveWidgetProvider> */} {/* </ActiveWidgetProvider> */}
</div> </div>
{/* dragOverlay (dnd-kit) helps move items from one container to another */} {/* dragOverlay (dnd-kit) helps move items from one container to another */}
</DragProvider>
{
createPortal(
<DragOverlay dropAnimation={null}>
{
draggedElement ?
<SidebarOverlayWidgetCard {...dragElementMetaData} />
:
null
}
</DragOverlay>,
document.body
)
}
</DragDropProvider>
</div> </div>
) )
} }
export default App; export default App

View File

@@ -9,7 +9,7 @@ some parts under a different non-commercial use license.
----------------------- -----------------------
License in short License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same licese. * You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first. * Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first. * If you plan on making money on this work or derived work you need permission first.

View File

@@ -28,6 +28,7 @@ import WidgetContainer from "./constants/containers"
import { isSubClassOfWidget } from "../utils/widget" import { isSubClassOfWidget } from "../utils/widget"
import { ButtonModal } from "../components/modals" import { ButtonModal } from "../components/modals"
import ResizeWidgetContainer from "./resizeContainer" import ResizeWidgetContainer from "./resizeContainer"
import Droppable from "../components/draggable/dnd/droppableDnd"
// const DotsBackground = require("../assets/background/dots.svg") // const DotsBackground = require("../assets/background/dots.svg")
@@ -197,10 +198,9 @@ class Canvas extends React.Component {
innerWidget = ref.current innerWidget = ref.current
break break
} }
// console.log("refs: ", ref) // console.log("refs: ", ref)
// TODO: remove the ref.current? if there are bugs it would become hard to debug // TODO: remove the ref.current? if there are bugs it would become hard to debug
if (ref.current?.getElement().contains(target)) { if (ref.current?.getElement()?.contains(target)) {
if (!innerWidget) { if (!innerWidget) {
innerWidget = ref.current innerWidget = ref.current
@@ -665,62 +665,80 @@ class Canvas extends React.Component {
* Handles drop event to canvas from the sidebar and on canvas widget movement * Handles drop event to canvas from the sidebar and on canvas widget movement
* @param {DragEvent} e * @param {DragEvent} e
*/ */
handleDropEvent = (e, draggedElement, widgetClass = null) => { handleDropEvent = (e, draggedElement, widgetClass = null, posMetaData) => {
e.preventDefault()
// console.log("Drop event") // e.preventDefault()
this.setState({ isWidgetDragging: false }) this.setState({ isWidgetDragging: false })
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) { if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return return
} }
const container = draggedElement.getAttribute("data-container") const container = draggedElement.getAttribute("data-container")
const canvasRect = this.canvasRef.current.getBoundingClientRect() const canvasRect = this.canvasRef.current.getBoundingClientRect()
const draggedElementRect = draggedElement.getBoundingClientRect() const { clientX, clientY } = e.nativeEvent
const elementWidth = draggedElementRect.width
const elementHeight = draggedElementRect.height
const { clientX, clientY } = e
let finalPosition = { let finalPosition = {
x: (clientX - canvasRect.left) / this.state.zoom, x: ((clientX - canvasRect.left) / this.state.zoom),
y: (clientY - canvasRect.top) / this.state.zoom, y: ((clientY - canvasRect.top) / this.state.zoom),
} }
if (container === WidgetContainer.SIDEBAR) { if (container === WidgetContainer.SIDEBAR) {
if (!widgetClass) { if (!widgetClass) {
throw new Error("WidgetClass has to be passed for widgets dropped from sidebar") throw new Error("WidgetClass has to be passed for widgets dropped from sidebar")
} }
// if the widget is being dropped from the sidebar, use the info to create the widget first // if the widget is being dropped from the sidebar, use the info to create the widget first
this.createWidget(widgetClass, ({ id, widgetRef }) => { this.createWidget(widgetClass, ({ id, widgetRef }) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y) widgetRef.current.setPos(finalPosition.x, finalPosition.y)
// widgetRef.current.setPos(10, 10)
}) })
} else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) { } else if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)) {
// snaps to center // snaps to center
// finalPosition = {
// x: (clientX - canvasRect.left) / this.state.zoom - (elementWidth / 2) / this.state.zoom,
// y: (clientY - canvasRect.top) / this.state.zoom - (elementHeight / 2) / this.state.zoom,
// }
const canvasBoundingRect = this.getCanvasBoundingRect()
const {dragStartCursorPos, initialPos} = posMetaData
// calculate the initial offset from the div to the cursor grab
const initialOffset = {
x: ((dragStartCursorPos.x - canvasBoundingRect.left) / this.state.zoom - this.state.currentTranslate.x) - initialPos.x,
y: ((dragStartCursorPos.y - canvasBoundingRect.top) / this.state.zoom - this.state.currentTranslate.y) - initialPos.y
}
finalPosition = { finalPosition = {
x: (clientX - canvasRect.left) / this.state.zoom - (elementWidth / 2) / this.state.zoom, x: finalPosition.x - initialOffset.x - this.state.currentTranslate.x,
y: (clientY - canvasRect.top) / this.state.zoom - (elementHeight / 2) / this.state.zoom, y: finalPosition.y - initialOffset.y - this.state.currentTranslate.y
} }
let widgetId = draggedElement.getAttribute("data-widget-id") let widgetId = draggedElement.getAttribute("data-widget-id")
const widgetObj = this.getWidgetById(widgetId) const widgetObj = this.getWidgetById(widgetId)
// console.log("WidgetObj: ", widgetObj) // console.log("WidgetObj: ", widgetObj)
if (container === WidgetContainer.CANVAS) { if (container === WidgetContainer.CANVAS) {
widgetObj.current.setPos(finalPosition.x, finalPosition.y) widgetObj.current.setPos(finalPosition.x, finalPosition.y)
} else if (container === WidgetContainer.WIDGET) { } else if (container === WidgetContainer.WIDGET) {
console.log("removing")
// if the widget was inside another widget move it outside // if the widget was inside another widget move it outside
let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId()) let childWidgetObj = this.findWidgetFromListById(widgetObj.current.getId())
let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent) let parentWidgetObj = this.findWidgetFromListById(childWidgetObj.parent)
@@ -782,7 +800,7 @@ class Canvas extends React.Component {
// console.log("event: ", event) // console.log("event: ", event)
// widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" } // widgets data structure { id, widgetType: widgetComponentType, children: [], parent: "" }
const dropWidgetObj = this.findWidgetFromListById(parentWidgetId) const dropWidgetObj = this.findWidgetFromListById(parentWidgetId)
// Find the dragged widget object // Find the dragged wihandleAddWidgetChilddget object
let dragWidgetObj = this.findWidgetFromListById(dragElementID) let dragWidgetObj = this.findWidgetFromListById(dragElementID)
// console.log("Drag widget obj: ", dragWidgetObj, dropWidgetObj) // console.log("Drag widget obj: ", dragWidgetObj, dropWidgetObj)
@@ -1063,6 +1081,7 @@ class Canvas extends React.Component {
} }
render() { render() {
// FIXME: inner canvas not recognized as droppable or some thing
return ( return (
<div className="tw-relative tw-overflow-hidden tw-flex tw-w-full tw-h-full tw-max-h-[100vh]"> <div className="tw-relative tw-overflow-hidden tw-flex tw-w-full tw-h-full tw-max-h-[100vh]">
@@ -1086,17 +1105,21 @@ class Canvas extends React.Component {
</ButtonModal> </ButtonModal>
</div> </div>
<Droppable className={"tw-w-full tw-h-full"}
droppableTags={{ exclude: ["image", "video"] }}
onDrop={this.handleDropEvent}
id="canvas-droppable">
{/* <ActiveWidgetProvider> */} {/* <ActiveWidgetProvider> */}
<DroppableWrapper id="canvas-droppable" {/* <DroppableWrapper id="canvas-droppable"
droppableTags={{ exclude: ["image", "video"] }} droppableTags={{ exclude: ["image", "video"] }}
className="tw-w-full tw-h-full" className="tw-w-full tw-h-full"
onDrop={this.handleDropEvent}> onDrop={this.handleDropEvent}> */}
{/* <DragWidgetProvider> */} {/* <DragWidgetProvider> */}
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{ items: this.state.contextMenuItems, }}> <Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{ items: this.state.contextMenuItems, }}>
<div className="tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden" <div className="tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef} ref={this.canvasContainerRef}
style={{ style={{
transition: " transform 0.3s ease-in-out", // transition: " transform 0.3s ease-in-out",
backgroundImage: `url(${DotsBackground})`, backgroundImage: `url(${DotsBackground})`,
backgroundSize: 'cover', // Ensure proper sizing if needed backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
@@ -1111,7 +1134,7 @@ class Canvas extends React.Component {
}} }}
/> />
{/* Canvas */} {/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none" <div data-canvas className="tw-w-full tw-h-full tw-absolute tw-bg-transparent tw-top-0 tw-select-none"
ref={this.canvasRef}> ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full"> <div className="tw-relative tw-w-full tw-h-full">
{ {
@@ -1127,8 +1150,8 @@ class Canvas extends React.Component {
</div> </div>
</Dropdown> </Dropdown>
{/* </DragWidgetProvider> */} {/* </DragWidgetProvider> */}
</DroppableWrapper> {/* </DroppableWrapper> */}
</Droppable>
<CanvasToolBar isOpen={this.state.toolbarOpen} <CanvasToolBar isOpen={this.state.toolbarOpen}
widgetType={this.state.selectedWidget?.getWidgetType() || ""} widgetType={this.state.selectedWidget?.getWidgetType() || ""}
attrs={this.state.toolbarAttrs} attrs={this.state.toolbarAttrs}

View File

@@ -12,7 +12,8 @@ import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext" import { DragContext } from "../../components/draggable/draggableContext"
import { isNumeric, removeKeyFromObject } from "../../utils/common" import { isNumeric, removeKeyFromObject } from "../../utils/common"
import { info } from "autoprefixer" import { info } from "autoprefixer"
import { message } from "antd" import { Layout, message } from "antd"
import WidgetDnd from "./widgetDnd"
// TODO: make it possible to apply widgetInnerStyle on load // TODO: make it possible to apply widgetInnerStyle on load
@@ -71,7 +72,7 @@ class Widget extends React.Component {
this.state = { this.state = {
zIndex: 0, zIndex: 1,
selected: false, selected: false,
widgetName: widgetName || 'widget', // this will later be converted to variable name widgetName: widgetName || 'widget', // this will later be converted to variable name
enableRename: false, // will open the widgets editable div for renaming enableRename: false, // will open the widgets editable div for renaming
@@ -208,8 +209,7 @@ class Widget extends React.Component {
if (this.state.attrs.styling.backgroundColor) if (this.state.attrs.styling.backgroundColor)
this.setWidgetInnerStyle('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff") this.setWidgetInnerStyle('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff")
this.load(this.props.initialData || {}) // load the initial data this.load(this.props.initialData || {}, console.log("loaded")) // load the initial data
} }
componentWillUnmount() { componentWillUnmount() {
@@ -365,6 +365,7 @@ class Widget extends React.Component {
} }
setPos(x, y) { setPos(x, y) {
console.log("position set: ", x, y)
this.setState({ this.setState({
pos: { x, y } pos: { x, y }
@@ -795,52 +796,20 @@ class Widget extends React.Component {
} }
this.updateState({ attrs: newAttrs }, callback) this.updateState({ attrs: newAttrs }, callback)
}) })
} }
handleDragStart = (e, callback) => { handleDragStart = (e, ) => {
e.stopPropagation()
callback(this.elementRef?.current || null)
// this.props.onWidgetDragStart(this.elementRef?.current)
// Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
const dragImage = this.elementRef?.current.cloneNode(true)
dragImage.style.opacity = '1' // Ensure full opacity
dragImage.style.position = 'absolute'
dragImage.style.top = '-9999px' // Move it out of view
document.body.appendChild(dragImage)
const rect = this.elementRef?.current.getBoundingClientRect()
// snap to mouse pos
// const offsetX = e.clientX - rect.left
// const offsetY = e.clientY - rect.top
// snap to middle
const offsetX = rect.width / 2
const offsetY = rect.height / 2
// 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)
// NOTE: this line will prevent problem's such as self-drop or dropping inside its own children // NOTE: this line will prevent problem's such as self-drop or dropping inside its own children
setTimeout(this.disablePointerEvents, 1) // setTimeout(this.disablePointerEvents, 1)
this.setState({ isDragging: true }) this.setState({ isDragging: true })
} }
handleDragEnter = (e, draggedElement, setOverElement) => { handleDragEnter = (e, draggedElement) => {
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) { if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
@@ -849,15 +818,6 @@ class Widget extends React.Component {
const dragEleType = draggedElement.getAttribute("data-draggable-type") const dragEleType = draggedElement.getAttribute("data-draggable-type")
// console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
// FIXME: the outer widget shouldn't be swallowed by inner widget
if (draggedElement === this.elementRef.current) {
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
setOverElement(this.elementRef.current) // provide context to the provider
let showDrop = { let showDrop = {
allow: true, allow: true,
show: true show: true
@@ -889,39 +849,17 @@ class Widget extends React.Component {
handleDragOver = (e, draggedElement) => { handleDragOver = (e, draggedElement) => {
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")){
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
if (draggedElement === this.elementRef.current) {
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
const allowDrop = (this.droppableTags && this.droppableTags !== null && (Object.keys(this.droppableTags).length === 0 ||
(this.droppableTags.include?.length > 0 && this.droppableTags.include?.includes(dragEleType)) ||
(this.droppableTags.exclude?.length > 0 && !this.droppableTags.exclude?.includes(dragEleType))
))
if (allowDrop) {
e.preventDefault() // NOTE: this is necessary to allow drop to take place
}
} }
handleDropEvent = (e, draggedElement, widgetClass = null) => { handleDropEvent = (e, draggedElement, widgetClass = null) => {
console.log("Dropped: ", draggedElement)
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) { if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist // if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return return
} }
e.preventDefault()
e.stopPropagation()
// FIXME: sometimes the elements showDroppableStyle is not gone, when dropping on the same widget // FIXME: sometimes the elements showDroppableStyle is not gone, when dropping on the same widget
this.setState({ this.setState({
@@ -992,10 +930,7 @@ class Widget extends React.Component {
} }
handleDragLeave = (e, draggedElement, overElement) => { handleDragLeave = (e) => {
e.preventDefault()
e.stopPropagation()
const rect = this.getBoundingRect() const rect = this.getBoundingRect()
@@ -1020,11 +955,10 @@ class Widget extends React.Component {
} }
} }
handleDragEnd = (callback) => { handleDragEnd = () => {
callback()
this.setState({ isDragging: false }) this.setState({ isDragging: false })
this.enablePointerEvents() // this.enablePointerEvents()
// console.log("enable pointer events: ")
// this.props.onWidgetDragEnd(this.elementRef?.current) // this.props.onWidgetDragEnd(this.elementRef?.current)
} }
@@ -1110,42 +1044,60 @@ class Widget extends React.Component {
height: height, height: height,
minWidth: minWidth, minWidth: minWidth,
minHeight: minHeight, minHeight: minHeight,
opacity: this.state.isDragging ? 0.3 : 1, opacity: this.state.isDragging ? 0.6 : 1,
} }
// const boundingRect = this.getBoundingRect console.log("state conatainer: ", this.state.widgetContainer, this.state.widgetName)
const isSortable = this.getParentLayout() === Layouts.FLEX || this.getParentLayout() === Layouts.GRID
// const boundingRect = this.getBoundingRect
// FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden // FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden
// FIXME: re-enable pointer events
return ( return (
// <DragContext.Consumer>
<DragContext.Consumer> <DragContext.Consumer>
{ {
({ draggedElement, widgetClass, onDragStart, onDragEnd, overElement, setOverElement }) => { ({ draggedElement, widgetClass, setInitialOffset }) => {
return ( return (
<div data-widget-id={this.__id}
ref={this.elementRef} <WidgetDnd widgetId={this.__id}
className="tw-shadow-xl tw-w-fit tw-h-fit" disabled={false && this.state.dragEnabled}
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas data-container={this.state.widgetContainer}
data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop dragElementType={this.getWidgetType()}
droppableTags={this.droppableTags}
className="tw-shadow-xl tw-w-full tw-h-full"
currentPos={{ ...this.state.pos }}
onDragStart={this.handleDragStart}
onDragOver={(e) => {this.handleDragOver(e, draggedElement)}}
onDragEnd={(e) => this.handleDragEnd()}
draggable={this.state.dragEnabled} onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass)}} // handle by droppable
onDragEnter={(e) => this.handleDragEnter(e, draggedElement)} // this is by droppable
widgetRef={this.elementRef}
onDragOver={(e) => this.handleDragOver(e, draggedElement)} isSortable={isSortable}
onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}} sortableIndex={0}
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)} canvas={this.canvas}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
// onDragOver={(e) => this.handleDragOver(e, draggedElement)}
// onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}
// onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
// onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
// onDragStart={(e) => this.handleDragStart(e, onDragStart)}
// onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
style={outerStyle}
onDragStart={(e) => this.handleDragStart(e, onDragStart)}
onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
> >
{/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
{/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0" <div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
> >
@@ -1253,7 +1205,9 @@ class Widget extends React.Component {
</div> </div>
</div>
</WidgetDnd>
) )
} }
} }

View File

@@ -0,0 +1,252 @@
import React, { useEffect, useRef, useState } from 'react'
import { useDragDropManager, useDroppable, useDraggable } from '@dnd-kit/react'
import { useDragContext } from '../../components/draggable/draggableContext'
import WidgetContainer from '../constants/containers'
import { useSortable } from '@dnd-kit/react/sortable'
import {
pointerIntersection,
closestCenter,
shapeIntersection
} from '@dnd-kit/collision'
function WidgetDnd({widgetId, canvas, widgetRef, droppableTags,onMousePress, onDrop, onDragStart,
onDragEnd, onDragEnter, onDragOver, currentPos={x: 0, y: 0},
dragElementType, isSortable=false, sortableIndex=0,
...props}) {
const dndRef = useRef(null)
const { draggedElement, setOverElement, widgetClass, setPosMetaData } = useDragContext()
const [isDropDisabled, setIsDropDisabled] = useState(false);
const { ref: dropRef, droppable} = useDroppable({
id: widgetId,
disabled: isDropDisabled,
collisionDetector: pointerIntersection,
// accept: (draggable) => {
// const allowDrop = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
// (droppableTags.include?.length > 0 && droppableTags.include?.includes(draggable.type)) ||
// (droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(draggable.type))
// ))
// return allowDrop
// }
})
const { ref: dragRef, draggable } = useDraggable({
id: widgetId,
feedback: "move",
type: dragElementType,
disabled: props.disabled,
// data: { title: props.children }
})
// const {ref: sortableRef} = useSortable({
// id: widgetId,
// index: sortableIndex,
// disabled: false && isSortable,
// feedback: 'move'
// })
const manager = useDragDropManager()
// const {}
const {onDragStart: onDragContextStart, onDragEnd: onDragContextEnd, disableStyle=false} = useDragContext()
const [allowDrop, setAllowDrop] = useState(false) // indicator if the draggable can be dropped on the droppable
useEffect(() => {
if (draggable.isDragging){
setIsDropDisabled(true)
}else{
setIsDropDisabled(false)
}
}, [draggable.isDragging])
useEffect(() => {
canvas?.addEventListener("pointerdown", handleInitialPosOffset)
canvas?.addEventListener("mousedown", handleInitialPosOffset)
manager?.monitor?.addEventListener("dragstart", handleDragEnter)
manager?.monitor?.addEventListener("dragend", handleDropEvent)
manager?.monitor?.addEventListener("dragmove", handleDragOver)
// manager?.monitor?.addEventListener("dragover", onDragEndhandleDragOver)
return () => {
manager?.monitor?.removeEventListener("dragstart", handleDragEnter)
manager?.monitor?.removeEventListener("dragend", handleDropEvent)
manager?.monitor?.removeEventListener("dragmove", handleDragOver)
canvas?.removeEventListener("mousedown", handleInitialPosOffset)
canvas?.removeEventListener("pointerdown", handleInitialPosOffset)
}
}, [manager, draggedElement, widgetClass, canvas, currentPos])
const handleRef = (node) => {
dndRef.current = node
widgetRef.current = node
dropRef(node)
dragRef(node)
// sortableRef(node)
}
const handleInitialPosOffset = (e) => {
if (!widgetRef?.current.contains(e.target)){
return
}
console.log("canvas bounding rect: ", canvas.getBoundingClientRect(), widgetRef.current)
const {clientX, clientY} = e
const canvasBoundingRect = canvas.getBoundingClientRect()
const posMetaData = {
dragStartCursorPos: {x: clientX, y: clientY},
initialPos: currentPos
}
setPosMetaData(posMetaData)
console.log("initial position calc: ", posMetaData, currentPos, (clientX - canvasBoundingRect.left), (clientY - canvasBoundingRect.top), "client: ", clientX, clientY, canvasBoundingRect)
}
const handleDragEnter = (e) => {
if (draggable.isDragSource){
// if the current widget is being dragged
onDragContextStart(dndRef?.current, widgetClass, {})
dndRef.current.style.zIndex = 10
onDragStart(e)
return
}else if (droppable.isDropTarget){
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
const dragElementType = draggedElement.getAttribute("data-draggable-type")
setOverElement(dndRef.current)
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
setAllowDrop(dropAllowed)
}
}
const handleDragOver = (e) => {
if (droppable.isDropTarget){
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
console.log("Drop allowed: ", dropAllowed, dragElementType )
setAllowDrop(dropAllowed)
onDragOver(e)
}
}
const handleDropEvent = (e) => {
if (draggable.isDragSource){
onDragContextEnd()
onDragEnd(e)
}else if (droppable.isDropTarget){
// setAllowDrop(false)
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
if (onDrop && dropAllowed && (droppable.isDropTarget && !draggable.isDragSource)) {
onDrop(e, draggedElement, widgetClass)
}
}
}
return (
<div ref={handleRef} data-drag-start-within
{...props}
data-widget-id={widgetId}
data-draggable-type={dragElementType}
className={`${props.className || ''} ${draggable.isDragging && "tw-pointer-events-none"} tw-outline
tw-border-2 tw-border-solid tw-border-red-400
tw-relative tw-outline-none`}
>
{props.children}
{/* <div className={`${allowDrop ? "tw-bg-[#82ff1c55]" : "tw-bg-[#eb5d3662]"}
tw-absolute tw-top-0 tw-left-[${rect.}] tw-w-full tw-h-full tw-z-[3]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}>
</div> */}
{
(droppable.isDropTarget && !draggable.isDragSource) &&
<div className={`${allowDrop ? "tw-bg-[#82ff1c55]" : "tw-bg-[#eb5d3662]"}
tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-full tw-z-[3]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}>
</div>
}
</div>
)
}
export default WidgetDnd

View File

@@ -33,7 +33,7 @@ const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="wid
onDragStart(widgetRef?.current || null) onDragStart(widgetRef?.current || null)
console.log("Drag start: ", widgetRef.current) // 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 // 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) const dragImage = widgetRef?.current.cloneNode(true)

View File

@@ -0,0 +1,42 @@
License held by paul
Github username: PaulleDemon
Below is the License for source code of PyUIBuilder. For license regarding source code generated by PyUIBuilder Refer Readme.md file (it has basically no restrictions).
The source code for PyUIBuilder is Dual licensed, most parts of the code comes under AGPL,
some parts under a different non-commercial use license.
-----------------------
License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first.
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and open-source software without proper funding. It's under this license.
Once there enough money to fund this project and my upcoming FOSS projects, The license will be brought back to a single AGPL or much lesser restrictive license. The fastest way to make this happen is by purchasing a one-time license or fund me.
Read the full license below.
--------------------
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.

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from "react" import { useEffect, useMemo, useRef } from "react"
import Draggable from "./utils/draggableDnd" import Draggable from "./draggable/dnd/draggableDnd"
import { GithubOutlined, GitlabOutlined, LinkOutlined, import { GithubOutlined, GitlabOutlined, LinkOutlined,
AudioOutlined, FileTextOutlined, AudioOutlined, FileTextOutlined,
@@ -29,11 +29,20 @@ export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerR
return ( return (
// <Draggable className="tw-cursor-pointer" id={name}> <Draggable id={name}
<DraggableWrapper data-container={"sidebar"} className="tw-cursor-pointer tw-w-fit tw-bg-white tw-h-fit"
data-container={"sidebar"}
dragElementType={widgetClass.widgetType} dragElementType={widgetClass.widgetType}
dragWidgetClass={widgetClass} dragWidgetClass={widgetClass}
className="tw-cursor-pointer tw-w-fit tw-bg-white tw-h-fit"> draggableType={"clone"}
elementMetaData={{
name,
url,
img,
license,
widgetClass,
}}
>
<div ref={innerRef} className="tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col <div ref={innerRef} className="tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden tw-rounded-md tw-overflow-hidden
@@ -72,8 +81,75 @@ export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerR
</div> </div>
</div> </div>
</DraggableWrapper> </Draggable>
// </Draggable> )
}
export function SidebarOverlayWidgetCard({ name, img, url, license, widgetClass, innerRef}){
const urlIcon = useMemo(() => {
if (url){
const host = new URL(url).hostname.toLowerCase()
if (host === "github.com"){
return <GithubOutlined />
}else if(host === "gitlab.com"){
return <GitlabOutlined />
}else{
return <GlobalOutlined />
}
}
}, [url])
return (
<>
{/* <DraggableWrapper data-container={"sidebar"}
dragElementType={widgetClass.widgetType}
dragWidgetClass={widgetClass}
className="tw-cursor-pointer tw-w-fit tw-bg-white tw-h-fit"> */}
<div ref={innerRef} className="tw-bg-white tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px]
tw-border-blue-500 tw-shadow-md">
<div className="tw-h-[200px] tw-pointer-events-none tw-w-full tw-overflow-hidden">
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
</div>
<span className="tw-text-center tw-text-black tw-text-lg">{name}</span>
<div className="tw-flex tw-text-lg tw-place tw-px-4">
<a href={url} className="tw-text-gray-600" target="_blank" rel="noopener noreferrer">
{urlIcon}
</a>
{license?.name &&
<div className="tw-ml-auto tw-text-sm">
{
license.url ?
<a href={license.url} target="_blank" rel="noreferrer noopener"
className="tw-p-[1px] tw-px-2 tw-text-blue-500 tw-border-[1px]
tw-border-solid tw-rounded-sm tw-border-blue-500
tw-shadow-md tw-text-center tw-no-underline">
{license.name}
</a>
:
<div className="tw-p-[1px] tw-px-2 tw-text-blue-500 tw-border-[1px]
tw-border-solid tw-rounded-sm tw-border-blue-500
tw-shadow-md tw-text-center">
{license.name}
</div>
}
</div>
}
</div>
</div>
{/* </DraggableWrapper> */}
</>
) )
} }

View File

@@ -0,0 +1,36 @@
import React, { createContext, useContext, useState } from 'react'
import { isSubClassOfWidget } from '../../utils/widget'
// import Widget from '../../canvas/widgets/base'
export 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 [overElement, setOverElement] = useState(null) // the element the dragged items is over
const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas
const onDragStart = (element, widgetClass=null) => {
setDraggedElement(element)
if (widgetClass && !isSubClassOfWidget(widgetClass))
throw new Error("widgetClass must inherit from the Widget base class")
setWidgetClass(() => widgetClass) // store the class so later it can be passed to the canvas from sidebar
}
const onDragEnd = () => {
setDraggedElement(null)
setWidgetClass(null)
}
return (
<DragDropProvider value={{ draggedElement, overElement, setOverElement,
widgetClass, onDragStart, onDragEnd }}>
{children}
</DragDropProvider>
)
}

View File

@@ -0,0 +1,88 @@
import React, { useEffect, useRef } from "react"
import { useDragDropManager, useDraggable } from "@dnd-kit/react"
import { CSS } from "@dnd-kit/utilities"
import { useDragContext } from "../draggableContext"
function Draggable({dragElementType, draggableType, dragWidgetClass = null, elementMetaData, ...props}) {
const draggableRef = useRef(null);
const { ref, draggable } = useDraggable({
id: dragElementType,
feedback: draggableType || "default",
type: dragElementType
// data: { title: props.children }
})
const {onDragStart, onDragEnd, disableStyle=false} = useDragContext()
const manager = useDragDropManager()
useEffect(() => {
manager?.monitor?.addEventListener("dragstart", handleDragStart)
manager?.monitor?.addEventListener("dragend", handleDragEnd)
return () => {
manager?.monitor?.removeEventListener("dragstart", handleDragStart)
manager?.monitor?.removeEventListener("dragend", handleDragEnd)
}
}, [manager])
// const style = transform ? {
// transform: CSS.Translate.toString(transform),
// } : undefined
const handleRef = (node) => {
draggableRef.current = node
ref(node)
}
const handleDragStart = (event) => {
const {source} = event.operation
if (!draggable.isDragSource){
return
}
// event.dataTransfer.setData("text/plain", "")
// onDragStart(draggableRef?.current, dragWidgetClass)
onDragStart(draggableRef?.current, dragWidgetClass, elementMetaData)
}
const handleDragEnd = (event) => {
// console.log("Drag end: ", e, e.target.closest('div'))
if (!draggable.isDragSource){
return
}
onDragEnd()
}
// TODO: remove element meta data from props
return (
<div
ref={handleRef}
className={`${props.className}`}
// style={!disableStyle ? style : null} //enable this to show like the original item is moving, if commented out the original item will not have css effects
data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
data-draggable-type={dragElementType}
{...props}
>
{props.children}
</div>
)
}
export default Draggable

View File

@@ -0,0 +1,189 @@
import React, { useEffect, useRef, useState } from 'react'
import { useDragDropManager, useDroppable } from '@dnd-kit/react'
import { useDragContext } from '../draggableContext'
import {
pointerIntersection,
closestCenter,
shapeIntersection
} from '@dnd-kit/collision'
function Droppable(props) {
const droppableRef = useRef(null)
const { droppableTags, onDrop } = props
const { draggedElement, setOverElement, widgetClass, posMetaData } = useDragContext()
const { ref, isDropTarget, droppable} = useDroppable({
id: props.id,
collisionDetector: pointerIntersection,
// accept: (draggable) => {
// const allowDrop = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
// (droppableTags.include?.length > 0 && droppableTags.include?.includes(draggable.type)) ||
// (droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(draggable.type))
// ))
// return allowDrop
// }
})
const manager = useDragDropManager()
// const {}
const [allowDrop, setAllowDrop] = useState({show: false, allow: false}) // indicator if the draggable can be dropped on the droppable
useEffect(() => {
manager?.monitor?.addEventListener("dragstart", handleDragEnter)
manager?.monitor?.addEventListener("dragend", handleDragLeave)
manager?.monitor?.addEventListener("dragmove", handleDragOver)
return () => {
manager?.monitor?.removeEventListener("dragstart", handleDragEnter)
manager?.monitor?.removeEventListener("dragend", handleDragLeave)
manager?.monitor?.removeEventListener("dragmove", handleDragOver)
}
}, [manager, draggedElement, widgetClass, posMetaData])
const handleRef = (node) => {
droppableRef.current = node
ref(node)
}
// TODO: handle Drop on Canvas
const handleDragEnter = (e) => {
const {target, source} = e.operation
if (!droppable.isDropTarget){
return
}
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
const dragElementType = draggedElement.getAttribute("data-draggable-type")
setOverElement(document.getElementById(source.id))
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
setAllowDrop({allow: dropAllowed, show: true})
}
const handleDragOver = (e) => {
const {target} = e.operation
if (!droppable.isDropTarget){
return
}
// console.log("Over sir1: ", draggedElement)
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
setAllowDrop({allow: dropAllowed, show: true})
// if (allowDrop) {
// e.preventDefault() // this is necessary to allow drop to take place
// }
}
const handleDropEvent = (e) => {
const {target} = e.operation
if (!droppable.isDropTarget){
return
}
console.log("dropping")
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
// e.stopPropagation()
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
console.log("initial POs: ", posMetaData)
if (onDrop && dropAllowed) {
onDrop(e, draggedElement, widgetClass, posMetaData)
console.log("Dropped")
}
setTimeout(() => setAllowDrop({allow: true, show: false}), 10)
}
const handleDragLeave = (e) => {
// const {target} = e.operation
if (droppable.isDropTarget){
handleDropEvent(e)
}else{
setAllowDrop({allow: false, show: false})
}
}
return (
<div ref={handleRef} className={`${props.className}` || ''}>
{props.children}
{
droppable.isDropTarget &&
<div className={`
tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-full tw-z-[0] tw-p-3
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}>
<div className={`${allowDrop.allow ? "tw-bg-[#82ff1c31]" : "tw-bg-[#eb5d3646]"} tw-w-full tw-h-full tw-pointer-events-none`}>
</div>
</div>
}
</div>
)
}
export default Droppable

View File

@@ -1,5 +1,6 @@
import React, { createContext, useContext, useState } from 'react' import React, { createContext, useContext, useState } from 'react'
import { isSubClassOfWidget } from '../../utils/widget' import { isSubClassOfWidget } from '../../utils/widget'
import { useDndContext } from '@dnd-kit/core'
// import Widget from '../../canvas/widgets/base' // import Widget from '../../canvas/widgets/base'
export const DragContext = createContext() export const DragContext = createContext()
@@ -11,25 +12,44 @@ export const DragProvider = ({ children }) => {
const [draggedElement, setDraggedElement] = useState(null) const [draggedElement, setDraggedElement] = useState(null)
const [overElement, setOverElement] = useState(null) // the element the dragged items is over const [overElement, setOverElement] = useState(null) // the element the dragged items is over
const [posMetaData, setPosMetaData] = useState({dragStartCursorPos: {x: 0, y: 0}, initialPos: {x: 0, y: 0}})
const [dragElementMetaData, setDragElementMetaData] = useState({})
const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas
const onDragStart = (element, widgetClass=null) => { const [isDragging, setIsDragging] = useState(false)
const onDragStart = (element, widgetClass=null, metaData={}) => {
setDraggedElement(element) setDraggedElement(element)
setIsDragging(true)
setDragElementMetaData(metaData)
if (widgetClass && !isSubClassOfWidget(widgetClass)) if (widgetClass && !isSubClassOfWidget(widgetClass))
throw new Error("widgetClass must inherit from the Widget base class") throw new Error("widgetClass must inherit from the Widget base class")
setWidgetClass(() => widgetClass) // store the class so later it can be passed to the canvas from sidebar setWidgetClass(() => widgetClass) // store the class so later it can be passed to the canvas from sidebar
} }
const onDragEnd = () => { const onDragEnd = (pos) => {
setDraggedElement(null) setDraggedElement(null)
setWidgetClass(null) setWidgetClass(null)
setIsDragging(false)
// setInitialOffset({x: 0, y: 0})
setDragElementMetaData({})
} }
return ( return (
<DragContext.Provider value={{ draggedElement, overElement, setOverElement, <DragContext.Provider value={{ draggedElement, overElement, setOverElement,
widgetClass, onDragStart, onDragEnd }}> widgetClass, onDragStart, onDragEnd, isDragging,
dragElementMetaData, setDragElementMetaData,
posMetaData, setPosMetaData
}}>
{children} {children}
</DragContext.Provider> </DragContext.Provider>
) )

View File

@@ -1,28 +0,0 @@
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 (
<button className={`tw-bg-transparent tw-outline-none tw-border-none ${props.className}`}
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}>
{props.children}
</button>
)
}
export default Draggable

View File

@@ -1,20 +0,0 @@
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 (
<div ref={setNodeRef} style={style} className={props.className || ''}>
{props.children}
</div>
)
}
export default Droppable

View File

@@ -0,0 +1,42 @@
License held by paul
Github username: PaulleDemon
Below is the License for source code of PyUIBuilder. For license regarding source code generated by PyUIBuilder Refer Readme.md file (it has basically no restrictions).
The source code for PyUIBuilder is Dual licensed, most parts of the code comes under AGPL,
some parts under a different non-commercial use license.
-----------------------
License in short
* You are free fork, modify and redistribute it given the original or derived works is kept free and open-source and under the same license.
* Another restriction is you can't redistribute in binary or executable formats without asking me first.
* If you plan on making money on this work or derived work you need permission first.
License was planned to be kept under one AGPL or less restrictive license, but since its difficult for me to continue providing free and open-source software without proper funding. It's under this license.
Once there enough money to fund this project and my upcoming FOSS projects, The license will be brought back to a single AGPL or much lesser restrictive license. The fastest way to make this happen is by purchasing a one-time license or fund me.
Read the full license below.
--------------------
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.

View File

@@ -408,7 +408,6 @@ export class TkinterBase extends Widget {
} }
this.updateState({ attrs: newAttrs }, callback) this.updateState({ attrs: newAttrs }, callback)
}) })

View File

@@ -12,6 +12,8 @@ import { QueryClient, QueryClientProvider } from "react-query";
import "./styles/tailwind.css"; import "./styles/tailwind.css";
import "./styles/index.css"; import "./styles/index.css";
import { FileUploadProvider } from "./contexts/fileUploadContext"; import { FileUploadProvider } from "./contexts/fileUploadContext";
import { DndContext } from "@dnd-kit/core";
import { DragProvider } from "./components/draggable/draggableContext";
const originalSetItem = localStorage.setItem; const originalSetItem = localStorage.setItem;
// triggers itemsChaned event whenever the item in localstorage is chanegd. // triggers itemsChaned event whenever the item in localstorage is chanegd.
@@ -69,7 +71,9 @@ root.render(
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient} > <QueryClientProvider client={queryClient} >
<FileUploadProvider> <FileUploadProvider>
<DragProvider>
<App /> <App />
</DragProvider>
</FileUploadProvider> </FileUploadProvider>
</QueryClientProvider> </QueryClientProvider>
</Provider> </Provider>

View File

@@ -0,0 +1,47 @@
class ZIndexPlugin {
constructor(manager, options) {
this.manager = manager;
this.options = options || {};
this.originalZIndexValues = new Map();
}
registerEffect() {
const handleDragStart = (event) => {
const { active } = event;
const draggableElement = document.getElementById(active.id);
if (draggableElement) {
// Store the original z-index
this.originalZIndexValues.set(active.id, draggableElement.style.zIndex);
// Apply the dragged z-index
draggableElement.style.zIndex = this.options.draggedZIndex || '9999';
}
};
const handleDragEnd = (event) => {
const { active } = event;
const draggableElement = document.getElementById(active.id);
if (draggableElement) {
// Restore the original z-index
const originalZIndex = this.originalZIndexValues.get(active.id) || '';
draggableElement.style.zIndex = originalZIndex;
this.originalZIndexValues.delete(active.id);
}
};
// Listen for drag events
this.manager.addEventListener('dragstart', handleDragStart);
this.manager.addEventListener('dragend', handleDragEnd);
// Return cleanup function
return () => {
this.manager.removeEventListener('dragstart', handleDragStart);
this.manager.removeEventListener('dragend', handleDragEnd);
};
}
}
export default ZIndexPlugin;