working on toolbar section

This commit is contained in:
paul
2024-09-14 16:03:26 +05:30
parent a371ac83a1
commit 4030debcfb
7 changed files with 258 additions and 107 deletions

View File

@@ -1,16 +1,17 @@
import { useRef, useState } from 'react'
import { LayoutFilled, ProductFilled, CloudUploadOutlined } from "@ant-design/icons"
import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
import { snapCenterToCursor } from '@dnd-kit/modifiers'
import Sidebar from './sidebar/sidebar'
import WidgetsContainer from './sidebar/widgetsContainer'
import UploadsContainer from './sidebar/uploadsContainer'
import Canvas from './canvas/canvas'
import Header from './components/header'
import { DndContext, useSensors, useSensor, PointerSensor, closestCorners, DragOverlay, rectIntersection } from '@dnd-kit/core'
import { DraggableWidgetCard } from './components/cards'
import Sidebar from './sidebar/sidebar'
import UploadsContainer from './sidebar/uploadsContainer'
import WidgetsContainer from './sidebar/widgetsContainer'
import Widget from './canvas/widgets/base'
import { snapCenterToCursor } from '@dnd-kit/modifiers'
import { DraggableWidgetCard } from './components/cards'
function App() {

View File

@@ -0,0 +1,10 @@
<svg width="100%" height="100%">
<pattern id="pattern-circles" x="0" y="0" width="50" height="50" patternUnits="userSpaceOnUse" patternContentUnits="userSpaceOnUse">
<circle id="pattern-circle" cx="10" cy="10" r="1.6257413380501518" fill="#000"></circle>
</pattern>
<rect id="rect" x="0" y="0" width="100%" height="100%" fill="url(#pattern-circles)"></rect>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@@ -8,10 +8,17 @@ import { Button, Tooltip, Dropdown } from "antd"
import Droppable from "../components/utils/droppable"
import Widget from "./widgets/base"
import Cursor from "./constants/cursor"
import { UID } from "../utils/uid"
import { removeDuplicateObjects } from "../utils/common"
import CanvasToolBar from "./toolbar"
import { UID } from "../utils/uid"
import { removeDuplicateObjects } from "../utils/common"
// import {ReactComponent as DotsBackground} from "../assets/background/dots.svg"
import DotsBackground from "../assets/background/dots.svg"
// const DotsBackground = require("../assets/background/dots.svg")
const CanvasModes = {
DEFAULT: 0,
@@ -202,13 +209,10 @@ class Canvas extends React.Component {
this.clearSelections()
}
if (selectedWidget)
this.setState({
selectedWidget: [selectedWidget]
})
if (selectedWidget){
this.setState({
selectedWidget: [selectedWidget],
contextMenuItems: [
{
key: "rename",
@@ -223,6 +227,8 @@ class Canvas extends React.Component {
}
]
})
}
}
@@ -330,8 +336,13 @@ class Canvas extends React.Component {
setZoom(zoom, pos={x:0, y:0}){
// if (zoom < 0.5 || zoom > 2){
// return
// }
const { currentTranslate } = this.state
// Calculate the new translation to zoom into the mouse position
const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x)
const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y)
@@ -340,7 +351,7 @@ class Canvas extends React.Component {
const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom)
this.setState({
zoom: zoom,
zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5
currentTranslate: {
x: newTranslateX,
y: newTranslateY
@@ -440,17 +451,22 @@ class Canvas extends React.Component {
const widgetIds = activeWidgets.map(widget => widget.__id)
for (let widgetId of widgetIds){
console.log("removed: ", widgetId)
// this.widgetRefs[widgetId]?.current.remove()
delete this.widgetRefs[widgetId]
this.setState((prevState) => ({
widgets: prevState.widgets.filter(widget => widget.id !== widgetId)
}))
}), () => {
if (this._onWidgetListUpdated)
this._onWidgetListUpdated(this.state.widgets)
})
// value.current?.remove()
}
}
/**
@@ -510,19 +526,24 @@ class Canvas extends React.Component {
<Button icon={<ReloadOutlined />} onClick={this.resetTransforms} />
</Tooltip>
<Tooltip title="Clear canvas">
<Button icon={<CloseOutlined />} onClick={this.clearCanvas} />
<Button danger icon={<DeleteOutlined />} onClick={this.clearCanvas} />
</Tooltip>
</div>
<Droppable id="canvas-droppable" className="tw-w-full tw-h-full">
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}>
<div className="tw-w-full tw-h-full tw-flex tw-relative tw-bg-black tw-overflow-hidden"
<div className="dots-bg tw-w-full tw-h-full tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{transition: " transform 0.3s ease-in-out"}}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url('${DotsBackground}')`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
t tw-bg-green-300"
tw-bg-green-300"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{

View File

@@ -1,6 +1,8 @@
const Tools = {
INPUT: "input",
NUMBER_INPUT: "number_input",
SELECT_DROPDOWN: "select_dropdown",
COLOR_PICKER: "color_picker",
EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown
}

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"
import { ColorPicker, Input } from "antd"
import { ColorPicker, Input, InputNumber, Select } from "antd"
import { capitalize } from "../utils/common"
import Tools from "./constants/tools.js"
@@ -23,20 +23,106 @@ function CanvasToolBar({isOpen, activeWidget, setActiveWidget}){
const handleWidgetNameChange = (e) => {
activeWidget?.setWidgetName(e.target.value) // Update widget's internal state
const updatedWidget = { ...activeWidget } // Create a shallow copy of the widget
updatedWidget?.setWidgetName(e.target.value) // Update widget's internal state
setActiveWidget(updatedWidget) // Update the state with the modified widget
}
const handleChange = (attrPath, value, callback) => {
// console.log("Value: ", attrPath, value)
activeWidget?.setAttrValue(attrPath, value) // Update widget's internal state
const updatedWidget = { ...activeWidget }
if (callback){
callback(value)
}
setActiveWidget(updatedWidget)
}
const renderWidgets = (obj, parentKey = "") => {
return Object.entries(obj).map(([key, val], i) => {
// console.log("parent key: ", parentKey)
// Build a unique identifier for keys that handle nested structures
const keyName = parentKey ? `${parentKey}.${key}` : key
// Check if the current value is an object and has a "tool" property
if (typeof val === "object" && val.tool) {
// Render widgets based on the tool type
return (
<div key={keyName} className="tw-flex tw-flex-col tw-gap-2">
{
parentKey ?
<div className={`tw-text-sm tw-font-medium `}>{val.label}</div>
:
<div className="tw-text-lg tw-text-blue-700">{capitalize(key)}</div>
}
{
val.tool === Tools.NUMBER_INPUT && (
<InputNumber
defaultValue={val.value || 0}
size="small"
onChange={(value) => handleChange(keyName, value, val.onChange)}
/>
)}
{
val.tool === Tools.COLOR_PICKER && (
<ColorPicker
defaultValue={val.value || "#fff"}
disabledAlpha
arrow={false}
size="middle"
showText
format="hex"
placement="bottomRight"
className="tw-w-fit !tw-min-w-[100px]"
onChange={(value) => handleChange(keyName, value.toHexString(), val.onChange)}
/>
)}
{
val.tool === Tools.SELECT_DROPDOWN && (
<Select
options={val.options}
showSearch
value={val.value || ""}
placeholder={`${val.label}`}
onChange={(value) => handleChange(keyName, value, val.onChange)}
/>
)
}
{/* Add more widget types here as needed */}
</div>
)
}
// If the value is another nested object, recursively call renderWidgets
if (typeof val === "object") {
return (
<div key={keyName} className="tw-flex tw-flex-col tw-gap-2">
<div className="tw-text-lg tw-text-blue-700">{capitalize(key)}</div>
{renderWidgets(val, keyName)}
</div>
)
}
return null // Skip rendering for non-object types
})
}
return (
<div className={`tw-absolute tw-top-20 tw-right-5 tw-bg-white ${toolbarOpen ? "tw-w-[320px]": "tw-w-0"}
tw-px-4 tw-p-2 tw-h-[600px] tw-rounded-md tw-z-20 tw-shadow-lg
tw-transition-transform tw-duration-75
tw-flex tw-flex-col
tw-flex tw-flex-col tw-overflow-y-auto
`}
>
<h3 className="tw-text-2xl tw-text-center">
<h3 className="tw-text-xl tw-text-center">
{capitalize(`${activeWidget?.getWidgetType() || ""}`)}
</h3>
@@ -48,25 +134,7 @@ function CanvasToolBar({isOpen, activeWidget, setActiveWidget}){
</div>
<hr />
<div className="tw-flex tw-flex-col tw-gap-4">
{
Object.entries(activeWidget?.state?.attrs || {}).map(([key, value], i) => {
console.log("valyes: ")
return (
<div className="tw-flex tw-flex-col tw-gap-1">
<div className="tw-text-xl">{key}</div>
{
value?.backgroundColor?.tool === Tools.COLOR_PICKER &&
<ColorPicker defaultValue={value?.backgroundColor?.value || "#fff"}
disabledAlpha size="small" showText
format="hex"
placement="bottomRight"
className="tw-w-fit"/>
}
</div>
)
})
}
{renderWidgets(activeWidget?.state?.attrs || {})}
</div>
</div>

View File

@@ -82,27 +82,53 @@ class Widget extends React.Component{
resizing: false,
resizeCorner: "",
pos: {x: 0, y: 0}, // used for outer styling
size: {width: 100, height: 100}, // used for outer styling
position: "absolute",
widgetStyling: {
position: "absolute",
left: 0,
top: 0,
width: 100,
height: 100
// use for widget's inner styling
},
attrs: {
styling: {
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)
},
foregroundColor: {
label: "Foreground Color",
tool: Tools.COLOR_PICKER,
value: ""
value: "",
},
},
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)
},
size: {
width: {
label: "Width",
tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
value: 100,
// onChange: (value) => this.setS("backgroundColor", value)
},
height: {
label: "Height",
tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
value: 100,
// onChange: (value) => this.setS("backgroundColor", value)
},
},
layout: "show", // enables layout use "hide" to hide layout dropdown, takes the layout from this.layout
events: {
event1: {
tool: Tools.EVENT_HANDLER,
@@ -122,6 +148,7 @@ class Widget extends React.Component{
this.isSelected = this.isSelected.bind(this)
this.setWidgetName = this.setWidgetName.bind(this)
this.setAttrValue = this.setAttrValue.bind(this)
this.getPos = this.getPos.bind(this)
this.setPos = this.setPos.bind(this)
@@ -215,17 +242,19 @@ class Widget extends React.Component{
// don't change position when resizing the widget
return
}
this.setState({
pos: {x, y}
})
this.setState((prev) => ({
// pos: {x: x, y: y}
widgetStyling: {
...prev.widgetStyling,
left: x,
top: y,
}
}))
// this.setState((prev) => ({
// // pos: {x: x, y: y}
// widgetStyling: {
// ...prev.widgetStyling,
// left: x,
// top: y,
// }
// }))
// console.log("POs: ", x, y)
}
setParent(parentId){
@@ -243,7 +272,7 @@ class Widget extends React.Component{
}
getPos(){
return {x: this.state.widgetStyling.left, y: this.state.widgetStyling.top}
return this.state.pos
}
getProps(){
@@ -257,7 +286,7 @@ class Widget extends React.Component{
getSize(){
// const boundingRect = this.getBoundingRect()
return {width: this.state.widgetStyling.width, height: this.state.widgetStyling.height}
return {width: this.state.size.width, height: this.state.size.height}
}
getScaleAwareDimensions() {
@@ -304,6 +333,31 @@ class Widget extends React.Component{
return this.elementRef.current
}
/**
* 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
keys.forEach(key => {
nestedObject[key] = { ...nestedObject[key] } // Ensure immutability
nestedObject = nestedObject[key]
})
nestedObject[lastKey].value = value
return { attrs: newAttrs }
})
}
startResizing(corner, event) {
event.stopPropagation()
this.setState({ resizing: true, resizeCorner: corner })
@@ -315,36 +369,54 @@ class Widget extends React.Component{
})
}
setWidgetName(name){
this.setState((prev) => ({
widgetName: name.length > 0 ? name : prev.widgetName
}))
}
setWidgetStyling(key, value){
this.setState((prev) => ({
widgetStyling: {
...prev.widgetStyling,
[key]: value
}
}))
}
handleResize(event) {
if (!this.state.resizing) return
const { resizeCorner, widgetStyling } = this.state
const { resizeCorner, size, pos } = this.state
const deltaX = event.movementX
const deltaY = event.movementY
let newSize = { width: widgetStyling.width, height: widgetStyling.height }
let newPos = { x: widgetStyling.left, y: widgetStyling.top }
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)
// 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 !== widgetStyling.width) ? deltaX : 0
newPos.y += (newSize.height !== widgetStyling.height) ? deltaY : 0
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 !== widgetStyling.height) ? deltaY : 0
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 !== widgetStyling.width) ? deltaX : 0
newPos.x += (newSize.width !== size.width) ? deltaX : 0
break
case "se":
newSize.width = Math.max(minWidth, Math.min(maxWidth, newSize.width + deltaX))
@@ -354,16 +426,7 @@ class Widget extends React.Component{
break
}
// this.setState({ size: newSize, pos: newPos })
this.setState((prev) => ({
widgetStyling: {
...prev.widgetStyling,
left: newPos.x,
top: newPos.y,
width: newSize.width,
height: newSize.height,
}
}))
this.setState({ size: newSize, pos: newPos })
}
stopResizing() {
@@ -385,28 +448,10 @@ class Widget extends React.Component{
})
}
setWidgetName(name){
this.setState((prev) => ({
widgetName: name.length > 0 ? name : prev.widgetName
}))
}
setWidgetStyling(key, value){
this.setState((prev) => ({
widgetStyling: {
...prev.widgetStyling,
[key]: value
}
}))
}
renderContent(){
// throw new NotImplementedError("render method has to be implemented")
return (
<div className="tw-w-full tw-h-full tw-bg-red-400">
<div className="tw-w-full tw-h-full tw-rounded-md tw-bg-red-500" style={this.state.widgetStyling}>
</div>
)
@@ -421,15 +466,14 @@ class Widget extends React.Component{
const widgetStyle = this.state.widgetStyling
let style = {
...widgetStyle,
let outerStyle = {
cursor: this.cursor,
zIndex: this.state.zIndex,
position: "absolute", // don't change this if it has to be movable on the canvas
top: `${widgetStyle.top}px`,
left: `${widgetStyle.left}px`,
width: `${widgetStyle.width}px`,
height: `${widgetStyle.height}px`,
top: `${this.state.pos.y}px`,
left: `${this.state.pos.x}px`,
width: `${this.state.size.width}px`,
height: `${this.state.size.height}px`,
}
let selectionStyle = {
@@ -442,8 +486,8 @@ class Widget extends React.Component{
// console.log("selected: ", this.state.selected)
return (
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-w-fit tw-h-fit"
style={style}
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
>
{this.renderContent()}

View File

@@ -11,6 +11,11 @@ body {
-moz-osx-font-smoothing: grayscale;
}
.dots-bg{
background-image: url("../assets/background/dots.svg");
background-repeat: no-repeat;
background-size: cover;
}
.input{
border: 2px solid #e3e5e8;
@@ -22,4 +27,4 @@ body {
.input:active, .input:focus, .input:focus-within{
border-color: #60a5fa;
}
}