- {/* Canvas */}
-
-
- {
- this.state.widgets.map(this.renderWidget)
- }
-
+ {/*
*/}
+
+ {/* */}
+
+
+
+ {/* Canvas */}
+
+
+ {
+ this.state.widgets.map(this.renderWidget)
+ }
+
-
+ {/* */}
+
-
+
+ {/* */}
)
}
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 82db7bd..a93a1da 100644
--- a/src/canvas/constants/layouts.js
+++ b/src/canvas/constants/layouts.js
@@ -1,7 +1,12 @@
-const Layouts = {
- PACK: "flex",
+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/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 d51bb57..593b305 100644
--- a/src/canvas/toolbar.js
+++ b/src/canvas/toolbar.js
@@ -4,6 +4,8 @@ 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
@@ -14,11 +16,15 @@ import Tools from "./constants/tools.js"
* @param {string} widgetType
* @param {object} attrs - widget attributes
*/
-const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
+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)
+
useEffect(() => {
setToolbarOpen(isOpen)
}, [isOpen])
@@ -34,6 +40,93 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
}
}
+
+ const renderLayoutManager = (val) => {
+
+ return (
+
+
handleChange({ ...val.value, layout: value }, val.onChange)}
+ />
+
+
+ Direction
+ handleChange({ ...val.value, direction: value }, val.onChange)}
+ />
+
+
+ Gap
+ {
+ handleChange({ ...val.value, gap: value }, val.onChange)
+ }}
+ />
+
+
+
Grids
+
+
+ Rows
+ {
+ let newGrid = {
+ rows: value,
+ cols: val.value?.grid.cols
+ }
+ handleChange({ ...val.value, grid: newGrid }, val.onChange)
+ }}
+ />
+
+
+ Columns
+ {
+ let newGrid = {
+ rows: val.value?.grid.cols,
+ cols: value
+ }
+ handleChange({ ...val.value, grid: newGrid }, val.onChange)
+ }}
+ />
+
+
+
+
+
+ )
+
+ }
+
+
const renderWidgets = (obj, parentKey = "") => {
return Object.entries(obj).map(([key, val], i) => {
const keyName = parentKey ? `${parentKey}.${key}` : key
@@ -42,14 +135,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 && (
{
{val.tool === Tools.COLOR_PICKER && (
{
onChange={(value) => handleChange(value, val.onChange)}
/>
)}
+
+ {
+ val.tool === Tools.LAYOUT_MANAGER && (
+ renderLayoutManager(val)
+ )
+ }
+
);
}
@@ -116,8 +217,8 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
return (
@@ -125,7 +226,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 a846531..51761fa 100644
--- a/src/canvas/widgets/base.js
+++ b/src/canvas/widgets/base.js
@@ -2,11 +2,21 @@ 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"
+import { ActiveWidgetContext } from "../activeWidgetContext"
+import { DragWidgetProvider } from "./draggableWidgetContext"
+import WidgetDraggable from "./widgetDragDrop"
+import WidgetContainer from "../constants/containers"
+import { DragContext } from "../../components/draggable/draggableContext"
+
+
+
+const ATTRS_KEYS = ['value', 'label', 'tool', 'onChange', 'toolProps'] // these are attrs keywords, don't use these keywords as keys while defining the attrs property
+
/**
* Base class to be extended
@@ -15,6 +25,8 @@ class Widget extends React.Component {
static widgetType = "widget"
+ // static contextType = ActiveWidgetContext
+
constructor(props) {
super(props)
@@ -28,9 +40,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
@@ -38,14 +47,15 @@ class Widget extends React.Component {
this.icon = "" // antd icon name representing this widget
- this.elementRef = React.createRef()
+ this.elementRef = React.createRef() // this is the outer ref for draggable area
+ this.swappableAreaRef = React.createRef() // helps identify if the users intent is to swap or drop inside the widget
+ this.innerAreaRef = React.createRef() // this is the inner area where swap is prevented and only drop is accepted
this.functions = {
"load": { "args1": "number", "args2": "string" }
}
-
- this.layout = Layouts.PACK
+ this.droppableTags = ["widget"] // This indicates if the draggable can be dropped on this widget
this.boundingRect = {
x: 0,
y: 0,
@@ -58,15 +68,28 @@ 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: "",
+
+ isDragging: false, // tells if the widget is currently being dragged
+ dragEnabled: true,
+
+ widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget
+
+ showDroppableStyle: { // shows the droppable indicator
+ allow: false,
+ show: false,
+ },
pos: { x: 0, y: 0 },
size: { width: 100, height: 100 },
- position: "absolute",
+ positionType: PosType.ABSOLUTE,
widgetStyling: {
// use for widget's inner styling
+ backgroundColor: "#fff",
+ display: "flex",
+ flexDirection: "row",
+ gap: 10,
+ flexWrap: "wrap"
},
attrs: {
@@ -74,26 +97,42 @@ class Widget extends React.Component {
backgroundColor: {
label: "Background Color",
tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string
- value: "",
- onChange: (value) => this.setWidgetStyling("backgroundColor", value)
+ value: "#fff",
+ onChange: (value) => {
+ this.setWidgetStyling("backgroundColor", value)
+ this.setAttrValue("styling.backgroundColor", value)
+ }
},
foregroundColor: {
label: "Foreground Color",
tool: Tools.COLOR_PICKER,
- value: "",
+ value: "#000",
},
label: "Styling"
},
layout: {
label: "Layout",
- tool: Tools.SELECT_DROPDOWN, // the tool to display, can be either HTML ELement or a constant string
- value: "flex",
- options: [
- { value: "flex", label: "Flex" },
- { value: "grid", label: "Grid" },
- { value: "place", label: "Place" },
- ],
- onChange: (value) => this.setWidgetStyling("backgroundColor", value)
+ tool: Tools.LAYOUT_MANAGER, // the tool to display, can be either HTML ELement or a constant string
+ value: {
+ layout: "flex",
+ direction: "row",
+ grid: {
+ rows: 1,
+ cols: 1
+ },
+ gap: 10,
+ },
+ toolProps: {
+ options: [
+ { value: "flex", label: "Flex" },
+ { value: "grid", label: "Grid" },
+ { value: "place", label: "Place" },
+ ],
+ },
+ onChange: (value) => {
+ // this.setAttrValue("layout", value)
+ this.setLayout(value)
+ }
},
events: {
event1: {
@@ -107,6 +146,8 @@ class Widget extends React.Component {
this.mousePress = this.mousePress.bind(this)
this.getElement = this.getElement.bind(this)
+ this.getId = this.getId.bind(this)
+
this.getPos = this.getPos.bind(this)
this.getSize = this.getSize.bind(this)
this.getWidgetName = this.getWidgetName.bind(this)
@@ -123,35 +164,44 @@ class Widget extends React.Component {
this.setAttrValue = this.setAttrValue.bind(this)
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)
+ this.setPosType = this.setPosType.bind(this)
}
componentDidMount() {
this.elementRef.current?.addEventListener("click", this.mousePress)
- this.canvas.addEventListener("mousemove", this.handleResize)
- this.canvas.addEventListener("mouseup", this.stopResizing)
+ // FIXME: initial layout is not set properly
+ console.log("prior layout: ", this.state.attrs.layout.value)
+ this.setLayout(this.state.attrs.layout.value)
+ this.setWidgetStyling('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff")
+
+ this.load(this.props.initialData || {}) // load the initial data
+
+
}
componentWillUnmount() {
this.elementRef.current?.removeEventListener("click", this.mousePress)
-
- this.canvas.addEventListener("mousemove", this.handleResize)
- this.canvas.addEventListener("mouseup", this.stopResizing)
}
updateState = (newState, callback) => {
+
+ // FIXME: maximum recursion error when updating size, color etc
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()
+
})
}
@@ -166,6 +216,7 @@ class Widget extends React.Component {
getToolbarAttrs(){
return ({
+ id: this.__id,
widgetName: {
label: "Widget Name",
tool: Tools.INPUT, // the tool to display, can be either HTML ELement or a constant string
@@ -174,7 +225,7 @@ class Widget extends React.Component {
onChange: (value) => this.setWidgetName(value)
},
size: {
- label: "Sizing",
+ label: "Size",
display: "horizontal",
width: {
label: "Width",
@@ -218,26 +269,13 @@ class Widget extends React.Component {
return this.state.attrs
}
- /**
- * removes the element/widget
- */
- remove() {
- this.canvas.removeWidget(this.__id)
+ getId(){
+ return this.__id
}
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)
}
}
@@ -258,34 +296,29 @@ 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) {
- if (this.state.resizing) {
- // 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) {
- 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
@@ -303,38 +336,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
}
@@ -347,34 +348,79 @@ 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
* @param {any} value
*/
setAttrValue(path, value) {
- this.setState((prevState) => {
- // Split the path to access the nested property (e.g., "styling.backgroundColor")
- const keys = path.split('.')
- const lastKey = keys.pop()
- // Traverse the state and update the nested value immutably
- let newAttrs = { ...prevState.attrs }
- let nestedObject = newAttrs
+ const keys = path.split('.')
+ const lastKey = keys.pop()
- keys.forEach(key => {
- nestedObject[key] = { ...nestedObject[key] } // Ensure immutability
- nestedObject = nestedObject[key]
- })
- nestedObject[lastKey].value = value
-
- return { attrs: newAttrs }
+ // Traverse the state and update the nested value immutably
+ let newAttrs = { ...this.state.attrs }
+ let nestedObject = newAttrs
+
+ keys.forEach(key => {
+ nestedObject[key] = { ...nestedObject[key] } // Ensure immutability
+ nestedObject = nestedObject[key]
})
+
+ nestedObject[lastKey].value = value
+
+ this.updateState({attrs: newAttrs})
}
- startResizing(corner, event) {
- event.stopPropagation()
- this.setState({ resizing: true, resizeCorner: corner })
+ /**
+ * 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) {
@@ -385,22 +431,37 @@ 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
})
}
+ setLayout(value){
+ // FIXME: when the parent layout is place, the child widgets should have position absolute
+ const {layout, direction, grid={rows: 1, cols: 1}, gap=10} = value
+
+ const widgetStyle = {
+ ...this.state.widgetStyling,
+ display: layout,
+ flexDirection: direction,
+ gap: `${gap}px`,
+ flexWrap: "wrap"
+ // TODO: add grid rows and cols
+ }
+
+ this.setAttrValue("layout", value)
+ this.updateState({
+ widgetStyling: widgetStyle
+ })
+
+ }
+
/**
*
* @param {string} key - The string in react Style format
* @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 +470,6 @@ class Widget extends React.Component {
this.setState({
widgetStyling: widgetStyle
- }, () => {
- if (callback)
- callback(widgetStyle)
})
}
@@ -420,83 +478,26 @@ 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)
})
}
- 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 })
- }
- }
-
openRenaming() {
this.setState({
selected: true,
@@ -510,15 +511,272 @@ class Widget extends React.Component {
})
}
- handleDragStart = (event) => {
- console.log("dragging event: ", event)
+ enableDrag = () => {
+ this.setState({
+ dragEnabled: true
+ })
}
+ disableDrag = () => {
+ this.setState({
+ dragEnabled: false
+ })
+ }
+
+
+ /**
+ *
+ * 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) => {
+
+ if (Object.keys(data).length === 0) return // no data to load
+
+ for (let [key, value] of Object.entries(data.attrs|{}))
+ this.setAttrValue(key, value)
+
+ delete data.attrs
+
+ /**
+ * const obj = { a: 1, b: 2, c: 3 }
+ * const { b, ...newObj } = obj
+ * console.log(newObj) // { a: 1, c: 3 }
+ */
+
+ this.setState(data)
+
+ }
+
+ /**
+ *
+ * @depreciated - This function is depreciated in favour of handleDropEvent()
+ */
+ handleDrop = (event, dragElement) => {
+ // THIS function is depreciated in favour of handleDropEvent()
+ // console.log("dragging event: ", event, dragElement)
+ const container = dragElement.getAttribute("data-container")
+ // TODO: check if the drop is allowed
+ if (container === "canvas"){
+
+ this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id"))
+
+ }else if (container === "sidebar"){
+
+ this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first
+
+ }
+
+ }
+
+ handleDragStart = (e, callback) => {
+ e.stopPropagation()
+
+ callback(this.elementRef?.current || null)
+
+ // 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
+ setTimeout(this.disablePointerEvents, 1)
+
+ this.setState({isDragging: true})
+
+ }
+
+ handleDragEnter = (e, draggedElement, setOverElement) => {
+
+ const dragEleType = draggedElement.getAttribute("data-draggable-type")
+
+ // console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
+ // FIXME: the outer widget shouldn't be swallowed by inner widget
+ if (draggedElement === this.elementRef.current){
+ // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
+ return
+ }
+
+ setOverElement(e.currentTarget) // provide context to the provider
+
+ let showDrop = {
+ allow: true,
+ show: true
+ }
+
+ if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
+ showDrop = {
+ allow: true,
+ show: true
+ }
+
+ } else {
+ showDrop = {
+ allow: false,
+ show: true
+ }
+ }
+
+ this.setState({
+ showDroppableStyle: showDrop
+ })
+
+ }
+
+ handleDragOver = (e, draggedElement) => {
+ if (draggedElement === this.elementRef.current){
+ // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
+ return
+ }
+
+ // console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
+ const dragEleType = draggedElement.getAttribute("data-draggable-type")
+
+ if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
+ e.preventDefault() // NOTE: this is necessary to allow drop to take place
+ }
+
+ }
+
+ handleDropEvent = (e, draggedElement, widgetClass=null) => {
+ e.preventDefault()
+ e.stopPropagation()
+ // FIXME: sometimes the elements showDroppableStyle is not gone, when dropping on the same widget
+ this.setState({
+ showDroppableStyle: {
+ allow: false,
+ show: false
+ }
+ }, () => {
+ console.log("droppable cleared: ", this.elementRef.current, this.state.showDroppableStyle)
+ })
+
+
+ const dragEleType = draggedElement.getAttribute("data-draggable-type")
+
+ if (this.droppableTags.length > 0 && !this.droppableTags.includes(dragEleType)) {
+ return // prevent drop if the draggable element doesn't match
+ }
+
+ // if (draggedElement === this.elementRef.current){
+ // // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
+ // return
+ // }
+
+ // let currentElement = e.currentTarget
+ // while (currentElement) {
+ // if (currentElement === draggedElement) {
+ // // if the parent is dropped accidentally into the child don't allow drop
+ // console.log("Dropped into a descendant element, ignoring drop")
+ // return // Exit early to prevent the drop
+ // }
+ // currentElement = currentElement.parentElement // Traverse up to check ancestors
+ // }
+
+ const container = draggedElement.getAttribute("data-container")
+
+ const thisContainer = this.elementRef.current.getAttribute("data-container")
+ // console.log("Dropped as swappable: ", e.target, this.swappableAreaRef.current.contains(e.target))
+ // If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas
+ const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET)
+
+ // TODO: check if the drop is allowed
+ if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)){
+ // console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer)
+
+ this.props.onAddChildWidget({parentWidgetId: this.__id,
+ dragElementID: draggedElement.getAttribute("data-widget-id"),
+ swap: swapArea || false
+ })
+
+ }else if (container === WidgetContainer.SIDEBAR){
+
+ // console.log("Dropped on Sidebar: ", this.__id)
+ this.props.onCreateWidgetRequest(widgetClass, ({id, widgetRef}) => {
+ this.props.onAddChildWidget({parentWidgetId: this.__id, dragElementID: id}) // if dragged from the sidebar create the widget first
+ })
+
+ }
+
+ }
+
+
+ handleDragLeave = (e, draggedElement) => {
+
+ // console.log("Left: ", e.currentTarget, e.relatedTarget, draggedElement)
+
+ if (!e.currentTarget.contains(draggedElement)) {
+ this.setState({
+ showDroppableStyle: {
+ allow: false,
+ show: false
+ }
+ })
+
+ }
+ }
+
+ handleDragEnd = (callback) => {
+ callback()
+ this.setState({isDragging: false})
+ this.enablePointerEvents()
+ }
+
+ disablePointerEvents = () => {
+
+ if (this.elementRef.current)
+ this.elementRef.current.style.pointerEvents = "none"
+ }
+
+ enablePointerEvents = () => {
+ if (this.elementRef.current)
+ this.elementRef.current.style.pointerEvents = "auto"
+ }
+
+ // FIXME: children outside the bounding box, add tw-overflow-hidden
renderContent() {
// throw new NotImplementedError("render method has to be implemented")
return (
-
-
+
+ {this.props.children}
)
}
@@ -530,73 +788,171 @@ class Widget extends React.Component {
*/
render() {
- const widgetStyle = this.state.widgetStyling
-
-
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`,
height: `${this.state.size.height}px`,
+ opacity: this.state.isDragging ? 0.3 : 1,
}
- let selectionStyle = {
- x: "-5px",
- y: "-5px",
- width: this.boundingRect.width + 5,
- height: this.boundingRect.height + 5
- }
-
- // console.log("selected: ", this.state.selected)
return (
-
+
+ {
+ ({draggedElement, widgetClass, onDragStart, onDragEnd, setOverElement}) => (
- {this.renderContent()}
-
+
this.handleDragOver(e, draggedElement)}
+ onDrop={(e) => this.handleDropEvent(e, draggedElement, widgetClass)}
-
-
+ onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
+ onDragLeave={(e) => this.handleDragLeave(e, draggedElement)}
-
this.startResizing("nw", e)}
- />
-
this.startResizing("ne", e)}
- />
-
this.startResizing("sw", e)}
- />
-
this.startResizing("se", e)}
- />
+ onDragStart={(e) => this.handleDragStart(e, onDragStart)}
+ onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
+ >
+ {/* FIXME: Swappable when the parent layout is flex/grid and gap is more, this trick won't work, add bg color to check */}
+ {/* FIXME: Swappable, when the parent layout is gap is 0, it doesn't work well */}
+
+
+ {/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
+
+
+ {this.renderContent()}
+
+ {
+ // show drop style on drag hover
+ this.state.showDroppableStyle.show &&
+
+ `}
+ style={{
+ width: "calc(100% + 10px)",
+ height: "calc(100% + 10px)",
+ }}
+ >
+
+ }
+
+
+
+
{/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
+
+
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ this.props.onWidgetResizing("nw")
+ this.setState({dragEnabled: false})
+ }}
+ onMouseUp={() => this.setState({dragEnabled: true})}
+ />
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ this.props.onWidgetResizing("ne")
+ this.setState({dragEnabled: false})
+ }}
+ onMouseUp={() => this.setState({dragEnabled: true})}
+ />
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ this.props.onWidgetResizing("sw")
+ this.setState({dragEnabled: false})
+ }}
+ onMouseUp={() => this.setState({dragEnabled: true})}
+ />
+
{
+ e.stopPropagation()
+ e.preventDefault()
+ this.props.onWidgetResizing("se")
+ this.setState({dragEnabled: false})
+ }}
+ onMouseUp={() => this.setState({dragEnabled: true})}
+ />
+
+
+
+
+
+
+ )
+ }
-
-
+
+ //
{
+ // this.setState({
+ // showDroppableStyle: showDrop
+ // })
+ // }
+ // }
+ // onDragLeave={ () => {
+ // this.setState({
+ // showDroppableStyle: {
+ // allow: false,
+ // show: false
+ // }
+ // })
+ // }
+ // }
+
+ // >
+ //
)
}
diff --git a/src/canvas/widgets/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..58557a5
--- /dev/null
+++ b/src/canvas/widgets/widgetDragDrop.js
@@ -0,0 +1,176 @@
+import { memo, useEffect, useRef, useState } from "react"
+import { useDragWidgetContext } from "./draggableWidgetContext"
+import { useDragContext } from "../../components/draggable/draggableContext"
+
+
+// FIXME: sometimes even after drag end the showDroppable is visible
+/**
+ * @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, style={},
+ droppableTags = ["widget"], ...props }) => {
+
+ // const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext()
+ const { draggedElement, onDragStart, onDragEnd, overElement, setOverElement } = useDragContext()
+
+ // const [dragEnabled, setDragEnabled] = useState(enableDraggable)
+ const [isDragging, setIsDragging] = useState(false)
+
+ const [showDroppable, setShowDroppable] = useState({
+ show: false,
+ allow: false
+ })
+
+ 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
+ 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")
+
+ // console.log("Drag entering...", overElement === e.currentTarget)
+ 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)
+
+ let showDrop = {
+ allow: true,
+ show: true
+ }
+
+ if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
+ showDrop = {
+ allow: true,
+ show: true
+ }
+
+ } else {
+ showDrop = {
+ allow: false,
+ show: true
+ }
+ }
+
+ setShowDroppable(showDrop)
+ if (onDragEnter)
+ onDragEnter({element: draggedElement, showDrop})
+
+ }
+
+ 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")
+
+ if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
+ e.preventDefault() // this is necessary to allow drop to take place
+ }
+
+ }
+
+ const handleDropEvent = (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ console.log("Dropped: ", draggedElement, props.children)
+
+ setShowDroppable({
+ allow: false,
+ show: false
+ })
+
+ if (onDrop) {
+ onDrop(e, draggedElement)
+ }
+
+ // if (draggedElement === widgetRef.current){
+ // // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
+ // return
+ // }
+
+ let currentElement = e.currentTarget
+ while (currentElement) {
+ if (currentElement === draggedElement) {
+ console.log("Dropped into a descendant element, ignoring drop")
+ return // Exit early to prevent the drop
+ }
+ currentElement = currentElement.parentElement // Traverse up to check ancestors
+ }
+
+ }
+
+
+ const handleDragLeave = (e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setShowDroppable({
+ allow: false,
+ show: false
+ })
+
+ if (onDragLeave)
+ onDragLeave()
+
+ }
+ }
+
+ const handleDragEnd = () => {
+ onDragEnd()
+ setIsDragging(false)
+ }
+
+ // TODO: FIXME, currently the draggable div doesn't move with the child, instead only child div moves, simulating childrens movement, add color and check
+ return (
+
+ {props.children}
+
+ )
+
+})
+
+
+export default WidgetDraggable
\ No newline at end of file
diff --git a/src/components/cards.js b/src/components/cards.js
index 8ee0b01..38f18a4 100644
--- a/src/components/cards.js
+++ b/src/components/cards.js
@@ -1,12 +1,14 @@
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 "./draggable/draggable"
+import { useDragContext } from "./draggable/draggableContext"
-export function DraggableWidgetCard({ name, img, url, innerRef}){
+export function SidebarWidgetCard({ name, img, url, widgetClass, innerRef}){
const urlIcon = useMemo(() => {
if (url){
@@ -23,26 +25,32 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){
}, [url])
- useEffect(() => {
- }, [])
-
return (
-
-
-
-
-
-
{name}
-
+ //
+
+
+
+
+
+
+
{name}
+
+
-
-
+
+ //
)
}
@@ -80,7 +88,7 @@ export function DraggableAssetCard({file}){
return (
-
{ file.fileType === "image" &&
@@ -105,7 +113,7 @@ export function DraggableAssetCard({file}){
}
-
{file.name}
+
{file.name}
)
diff --git a/src/components/draggable/draggable.js b/src/components/draggable/draggable.js
new file mode 100644
index 0000000..1050e7b
--- /dev/null
+++ b/src/components/draggable/draggable.js
@@ -0,0 +1,49 @@
+import { memo, useRef } from "react"
+import { useDragContext } from "./draggableContext"
+
+
+/**
+ *
+ * @param {string} - dragElementType - this will set the data-draggable-type which can be accessed on droppable to check if its allowed or not
+ * @returns
+ */
+const DraggableWrapper = memo(({dragElementType, dragWidgetClass=null, className, children, ...props}) => {
+
+ const { onDragStart, onDragEnd } = useDragContext()
+
+ const draggableRef = useRef(null)
+
+ /**
+ *
+ * @param {DragEvent} event
+ */
+ const handleDragStart = (event) => {
+
+ // event.dataTransfer.setData("text/plain", "")
+ onDragStart(draggableRef?.current, dragWidgetClass)
+
+ }
+
+ 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..4925b6d
--- /dev/null
+++ b/src/components/draggable/draggableContext.js
@@ -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 (
+
+ {children}
+
+ )
+}
diff --git a/src/components/draggable/droppable.js b/src/components/draggable/droppable.js
new file mode 100644
index 0000000..eb7c9bc
--- /dev/null
+++ b/src/components/draggable/droppable.js
@@ -0,0 +1,95 @@
+import { memo, useState } from "react"
+import { useDragContext } from "./draggableContext"
+
+
+const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => {
+
+
+ const { draggedElement, overElement, setOverElement, widgetClass } = useDragContext()
+
+ const [showDroppable, setShowDroppable] = useState({
+ show: false,
+ allow: false
+ })
+
+
+ const handleDragEnter = (e) => {
+
+ 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,
+ 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) => {
+ e.stopPropagation()
+
+ setShowDroppable({
+ allow: false,
+ show: false
+ })
+
+ if(onDrop){
+ onDrop(e, draggedElement, widgetClass)
+ }
+ }
+
+
+ const handleDragLeave = (e) => {
+ if (!e.currentTarget.contains(e.relatedTarget)) {
+ setShowDroppable({
+ allow: false,
+ show: false
+ })
+ }
+ }
+
+ return (
+
+
+ {props.children}
+
+ {
+ showDroppable.show &&
+
+
+ }
+
+
+ )
+
+})
+
+
+export default DroppableWrapper
\ No newline at end of file
diff --git a/src/components/utils/draggable.js b/src/components/utils/draggableDnd.js
similarity index 99%
rename from src/components/utils/draggable.js
rename to src/components/utils/draggableDnd.js
index 7cca531..2cb3238 100644
--- a/src/components/utils/draggable.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/droppableDnd.js
similarity index 100%
rename from src/components/utils/droppable.js
rename to src/components/utils/droppableDnd.js
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 {
diff --git a/src/frameworks/tkinter/assets/widgets/button.png b/src/frameworks/tkinter/assets/widgets/button.png
new file mode 100644
index 0000000..2d45182
Binary files /dev/null and b/src/frameworks/tkinter/assets/widgets/button.png differ
diff --git a/src/frameworks/tkinter/sidebarWidgets.js b/src/frameworks/tkinter/sidebarWidgets.js
new file mode 100644
index 0000000..b60ac4a
--- /dev/null
+++ b/src/frameworks/tkinter/sidebarWidgets.js
@@ -0,0 +1,41 @@
+
+import Widget from "../../canvas/widgets/base"
+
+import ButtonWidget from "./assets/widgets/button.png"
+
+
+const TkinterSidebar = [
+ {
+ name: "Main window",
+ img: ButtonWidget,
+ link: "https://github.com",
+ widgetClass: Widget
+ },
+ {
+ name: "Top Level",
+ img: ButtonWidget,
+ link: "https://github.com",
+ widgetClass: Widget
+ },
+ {
+ name: "Frame",
+ img: ButtonWidget,
+ link: "https://github.com",
+ widgetClass: Widget
+ },
+ {
+ name: "Button",
+ img: ButtonWidget,
+ link: "https://github.com",
+ widgetClass: Widget
+ },
+ {
+ name: "Input",
+ img: ButtonWidget,
+ link: "https://github.com",
+ widgetClass: Widget
+ },
+]
+
+
+export default TkinterSidebar
\ No newline at end of file
diff --git a/src/sidebar/sidebar.js b/src/sidebar/sidebar.js
index 786cb36..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,8 +49,8 @@ function Sidebar({tabs}){
return (
diff --git a/src/sidebar/utils/premium.js b/src/sidebar/utils/premium.js
new file mode 100644
index 0000000..a5239bf
--- /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)
+ }
+
+ // FIXME: the pricing section is not responsive
+ 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
+
+
+
+ Downloadable 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
+
+
+
+ Downloadable 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
+
+
+
+ Downloadable 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..705a708
--- /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 PyUI Builder with others}
+ styles={{wrapper: {zIndex: 14000, gap: "10px"}}}
+ onCancel={onClose}
+ onOk={onClose}
+ footer={null}
+ open={shareModalOpen}>
+
+
+
+
+
+
+ )
+
+}
+
+export default Share
\ No newline at end of file
diff --git a/src/sidebar/widgetsContainer.js b/src/sidebar/widgetsContainer.js
index 3f28f02..b755e8a 100644
--- a/src/sidebar/widgetsContainer.js
+++ b/src/sidebar/widgetsContainer.js
@@ -2,11 +2,12 @@ import { useEffect, useMemo, useState } from "react"
import { CloseCircleFilled, SearchOutlined } from "@ant-design/icons"
-import {DraggableWidgetCard} from "../components/cards"
+import {SidebarWidgetCard} from "../components/cards"
import ButtonWidget from "../assets/widgets/button.png"
import { filterObjectListStartingWith } from "../utils/filter"
+import Widget from "../canvas/widgets/base"
/**
@@ -14,55 +15,30 @@ import { filterObjectListStartingWith } from "../utils/filter"
* @param {function} onWidgetsUpdate - this is a callback that will be called once the sidebar is populated with widgets
* @returns
*/
-function WidgetsContainer({onWidgetsUpdate}){
+function WidgetsContainer({sidebarContent, onWidgetsUpdate}){
- const widgets = useMemo(() => {
- return [
- {
- name: "TopLevel",
- img: ButtonWidget,
- link: "https://github.com"
- },
- {
- name: "Frame",
- img: ButtonWidget,
- link: "https://github.com"
- },
- {
- name: "Button",
- img: ButtonWidget,
- link: "https://github.com"
- },
- {
- name: "Input",
- img: ButtonWidget,
- link: "https://github.com"
- },
- ]
- }, [])
const [searchValue, setSearchValue] = useState("")
- const [widgetData, setWidgetData] = useState(widgets)
+ const [widgetData, setWidgetData] = useState(sidebarContent)
useEffect(() => {
- setWidgetData(widgets)
+ setWidgetData(sidebarContent)
+ // if (onWidgetsUpdate){
+ // onWidgetsUpdate(widgets)
+ // }
- if (onWidgetsUpdate){
- onWidgetsUpdate(widgets)
- }
-
- }, [widgets])
+ }, [sidebarContent])
useEffect(() => {
if (searchValue.length > 0){
- const searchData = filterObjectListStartingWith(widgets, "name", searchValue)
+ const searchData = filterObjectListStartingWith(sidebarContent, "name", searchValue)
setWidgetData(searchData)
}else{
- setWidgetData(widgets)
+ setWidgetData(sidebarContent)
}
}, [searchValue])
@@ -94,10 +70,11 @@ function WidgetsContainer({onWidgetsUpdate}){
{
widgetData.map((widget, index) => {
return (
-
)
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;
diff --git a/src/utils/widget.js b/src/utils/widget.js
new file mode 100644
index 0000000..f839b39
--- /dev/null
+++ b/src/utils/widget.js
@@ -0,0 +1,16 @@
+import Widget from "../canvas/widgets/base"
+
+// checks if the object is instance is instance of widget class
+export const isSubClassOfWidget = (_class) => {
+
+ return Widget.isPrototypeOf(_class) || _class === Widget
+}
+
+
+
+export const isInstanceOfWidget = (_class) => {
+
+ console.log("Widget is instance of Object: ", _class)
+
+ return _class instanceof Widget
+}