diff --git a/src/canvas/canvas.js b/src/canvas/canvas.js index 77b8446..6ce0b3a 100644 --- a/src/canvas/canvas.js +++ b/src/canvas/canvas.js @@ -19,8 +19,9 @@ class Canvas extends React.Component { this.widgetRefs = {} this.modes = { - DEFAULT: '', - PAN: 'pan', + DEFAULT: 0, + PAN: 1, + MOVE_WIDGET: 2 // when the mode is move widget } this.currentMode = this.modes.DEFAULT @@ -31,14 +32,27 @@ class Canvas extends React.Component { } this.state = { - widgets: [], + widgets: [], // don't store the widget directly here, instead store it in widgetRef, else the changes in the widget will re-render the whole canvas zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, } + this.selectedWidgets = [] + this.resetTransforms = this.resetTransforms.bind(this) this.renderWidget = this.renderWidget.bind(this) + + this.mouseDownEvent = this.mouseDownEvent.bind(this) + this.mouseMoveEvent = this.mouseMoveEvent.bind(this) + this.mouseUpEvent = this.mouseUpEvent.bind(this) + + this.getWidgets = this.getWidgets.bind(this) + this.getActiveObjects = this.getActiveObjects.bind(this) + this.getWidgetFromTarget = this.getWidgetFromTarget.bind(this) + + this.clearSelections = this.clearSelections.bind(this) + this.clearCanvas = this.clearCanvas.bind(this) // this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this) } @@ -46,40 +60,14 @@ class Canvas extends React.Component { componentDidMount() { this.initEvents() - // this.widgets.push(new Widget()) - - // let widgetRef = React.createRef() - - // this.widgetRefs[widgetRef.current.__id] = widgetRef - - // // Update the state to include the new widget's ID - // this.setState((prevState) => ({ - // widgetIds: [...prevState.widgetIds, widgetRef.current.__id] - // })) - this.addWidget(Widget) } componentWillUnmount() { - - } - - mouseDownEvent(event){ - - this.mousePressed = true - - this.mousePos.x = event.e.clientX - this.mousePos.y = event.e.clientY - - } - - mouseMoveEvent(event){ - - } - - mouseUpEvent(event){ - this.mousePressed = false + + // NOTE: this will clear the canvas + this.clearCanvas() } /** @@ -92,61 +80,23 @@ class Canvas extends React.Component { } /** - * returns list of active objects on the canvas + * returns list of active objects / selected objects on the canvas * @returns Widget[] */ getActiveObjects(){ - - return this.getWidgets().filter((widget) => { - return widget.isSelected + return Object.values(this.widgetRefs).filter((widgetRef) => { + return widgetRef.current?.isSelected() }) - } initEvents(){ - this.canvasContainerRef.current.addEventListener("mousedown", (event) => { - this.mousePressed = true - this.mousePos = { x: event.clientX, y: event.clientY } - - if (this.state.widgets.length > 0){ - - this.currentMode = this.modes.PAN - this.setCursor(Cursor.GRAB) - - } - - }) - - this.canvasContainerRef.current.addEventListener("mouseup", () => { - this.mousePressed = false - this.currentMode = this.modes.DEFAULT - this.setCursor(Cursor.DEFAULT) - }) - - this.canvasContainerRef.current.addEventListener("mousemove", (event) => { - // console.log("event: ", event) - if (this.mousePressed && this.currentMode === this.modes.PAN) { - const deltaX = event.clientX - this.mousePos.x - const deltaY = event.clientY - this.mousePos.y - - this.setState(prevState => ({ - currentTranslate: { - x: prevState.currentTranslate.x + deltaX, - y: prevState.currentTranslate.y + deltaY, - } - }), this.applyTransform) - - this.mousePos = { x: event.clientX, y: event.clientY } - - this.setCursor(Cursor.GRAB) - } + this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) + this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) + this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) - - }) - - this.canvasContainerRef.current.addEventListener("selection:created", () => { + this.canvasRef.current.addEventListener("selection:created", () => { console.log("selected") this.currentMode = this.modes.DEFAULT }) @@ -162,6 +112,89 @@ class Canvas extends React.Component { this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` } + /** + * returns the widget that contains the target + * @param {HTMLElement} target + * @returns {Widget} + */ + getWidgetFromTarget(target){ + + for (let [key, ref] of Object.entries(this.widgetRefs)){ + if (ref.current.getElement().contains(target)){ + return ref.current + } + } + + } + + + mouseDownEvent(event){ + this.mousePressed = true + this.mousePos = { x: event.clientX, y: event.clientY } + + let selectedWidget = this.getWidgetFromTarget(event.target) + if (selectedWidget){ + // if the widget is selected don't pan, instead move the widget + + if (!selectedWidget._disableSelection){ + selectedWidget.select() + this.selectedWidgets.push(selectedWidget) + this.currentMode = this.modes.MOVE_WIDGET + } + + this.currentMode = this.modes.PAN + + + + }else if (this.state?.widgets?.length > 0){ + + this.clearSelections() + this.currentMode = this.modes.PAN + this.setCursor(Cursor.GRAB) + + } + + } + + mouseMoveEvent(event){ + // console.log("mode: ", this.currentMode, this.getActiveObjects()) + if (this.mousePressed && [this.modes.PAN, this.modes.MOVE_WIDGET].includes(this.currentMode)) { + const deltaX = event.clientX - this.mousePos.x + const deltaY = event.clientY - this.mousePos.y + + + if (this.selectedWidgets.length === 0){ + this.setState(prevState => ({ + currentTranslate: { + x: prevState.currentTranslate.x + deltaX, + y: prevState.currentTranslate.y + deltaY, + } + }), this.applyTransform) + }else{ + // update the widgets position + this.selectedWidgets.forEach(widget => { + const {x, y} = widget.getPos() + + const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas + const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas + + widget.setPos(newPosX, newPosY) + }) + } + + + this.mousePos = { x: event.clientX, y: event.clientY } + + this.setCursor(Cursor.GRAB) + } + } + + mouseUpEvent(event){ + this.mousePressed = false + this.currentMode = this.modes.DEFAULT + this.setCursor(Cursor.DEFAULT) + } + wheelZoom(event){ let delta = event.deltaY let zoom = this.state.zoom * 0.999 ** delta @@ -242,9 +275,16 @@ class Canvas extends React.Component { }, this.applyTransform) } + clearSelections(){ + this.getActiveObjects().forEach(widget => { + widget.current?.deSelect() + }) + this.selectedWidgets = [] + } + /** * - * @param {Widget} widgetComponentType - don't pass instead pass Widget + * @param {Widget} widgetComponentType - don't pass instead pass Widget object */ addWidget(widgetComponentType){ const widgetRef = React.createRef() @@ -253,18 +293,45 @@ class Canvas extends React.Component { // Store the ref in the instance variable this.widgetRefs[id] = widgetRef - console.log("widget ref: ", this.widgetRefs) + // console.log("widget ref: ", this.widgetRefs) // Update the state to include the new widget's type and ID this.setState((prevState) => ({ - widgets: [...prevState.widgets, { id, type: widgetComponentType }] + widgets: [...prevState.widgets, { id, type: widgetComponentType }] })) } + /** + * removes all the widgets from the canvas + */ + clearCanvas(){ + + for (let [key, value] of Object.entries(this.widgetRefs)){ + console.log("removed: ", key, value) + value.current?.remove() + } + + this.widgetRefs = {} + this.setState(() => ({ + widgets: [] + })) + } + + removeWidget(widgetId){ + + this.widgetRefs[widgetId]?.current.remove() + delete this.widgetRefs[widgetId] + + // TODO: remove from widgets + // this.setState(() => ({ + // widgets: [] + // })) + } + renderWidget(widget){ const { id, type: ComponentType } = widget - console.log("widet: ", this.widgetRefs, id) + // console.log("widet: ", this.widgetRefs, id) - return + return } render() { diff --git a/src/canvas/utils/utils.js b/src/canvas/utils/utils.js new file mode 100644 index 0000000..ea0369c --- /dev/null +++ b/src/canvas/utils/utils.js @@ -0,0 +1,5 @@ + +// given a string, converts to a python naming variable (snake case) +export function toSnakeCase(str) { + return str.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_').toLowerCase() +} \ No newline at end of file diff --git a/src/canvas/widgets/base.js b/src/canvas/widgets/base.js index e0324ee..8acc012 100644 --- a/src/canvas/widgets/base.js +++ b/src/canvas/widgets/base.js @@ -4,6 +4,8 @@ import { NotImplementedError } from "../../utils/errors" import Tools from "../constants/tools" import Layouts from "../constants/layouts" import Cursor from "../constants/cursor" +import { toSnakeCase } from "../utils/utils" +import EditableDiv from "../../components/editableDiv" @@ -17,13 +19,15 @@ class Widget extends React.Component{ constructor(props){ super(props) - const {id} = props + const {id, widgetName, canvasRef} = props console.log("Id: ", id) // this id has to be unique inside the canvas, it will be set automatically and should never be changed this.__id = id this._zIndex = 0 - this._selected = false + this.canvas = canvasRef?.current || null + + // this._selected = false this._disableResize = false this._disableSelection = false @@ -33,7 +37,7 @@ class Widget extends React.Component{ this.elementRef = React.createRef() - this.props = { + this.attrs = { styling: { backgroundColor: { tool: Tools.COLOR_PICKER, // the tool to display, can be either HTML ELement or a constant string @@ -70,10 +74,21 @@ class Widget extends React.Component{ attrs: { // attributes // replace this with this.props }, - zIndex: 0 + zIndex: 0, + pos: {x: 0, y: 0}, + selected: false, + widgetName: widgetName || 'unnamed widget' // this will later be converted to variable name } - } + this.mousePress = this.mousePress.bind(this) + this.getElement = this.getElement.bind(this) + + this.isSelected = this.isSelected.bind(this) + + this.getPos = this.getPos.bind(this) + this.setPos = this.setPos.bind(this) + + } setComponentAdded(added=true){ @@ -92,10 +107,22 @@ class Widget extends React.Component{ this.elementRef.current?.removeEventListener("click", this.mousePress) } - mousePress(event){ + // TODO: add context menu items such as delete, add etc + contextMenu(){ + } + + getVariableName(){ + return toSnakeCase(this.state.widgetName) + } + + mousePress(event){ + // event.preventDefault() if (!this._disableSelection){ - this._selected = true + this.setState((prev) => ({ + ...prev, + selected: false + })) const widgetSelected = new CustomEvent("selection:created", { detail: { @@ -103,22 +130,44 @@ class Widget extends React.Component{ id: this.__id, element: this }, + // bubbles: true // Allow the event to bubble up the DOM tree }) - console.log("dispatched") - document.dispatchEvent(widgetSelected) + // document.dispatchEvent(widgetSelected) + // console.log("dispatched", this.canvas) + this.canvas.dispatchEvent(widgetSelected) } } select(){ - this._selected = true + this.setState((prev) => ({ + ...prev, + selected: true + })) } deSelect(){ - this._selected = false + this.setState((prev) => ({ + ...prev, + selected: false + })) + } + + isSelected(){ + return this.state.selected + } + + setPos(x, y){ + this.setState({ + pos: {x: x, y: y} + }) + } + + getPos(){ + return this.state.pos } getProps(){ - return this.props + return this.attrs } getWidgetFunctions(){ @@ -129,6 +178,10 @@ class Widget extends React.Component{ return this.__id } + getElement(){ + return this.elementRef.current + } + renderContent(){ // throw new NotImplementedError("render method has to be implemented") return ( @@ -146,8 +199,8 @@ class Widget extends React.Component{ let style = { cursor: this.cursor, - top: "40px", - left: "40px", + top: `${this.state.pos.y}px`, + left: `${this.state.pos.x}px`, width: this.boundingRect.width, height: this.boundingRect.height } @@ -159,19 +212,41 @@ class Widget extends React.Component{ height: this.boundingRect.height + 5 } + const onWidgetNameChange = (value) => { + + this.setState((prev) => ({ + ...prev, + widgetName: value.length > 0 ? value : prev.widgetName + })) + } + return ( -
{this.renderContent()} -
+
-
+
+ {/*
e.preventDefault()} className="tw-text-sm tw-w-fit tw-min-w-[100px] tw-absolute tw--top-2"> + {this._widgetName} +
*/} + { this.state.selected && + + }
+ +
) diff --git a/src/components/editableDiv.js b/src/components/editableDiv.js new file mode 100644 index 0000000..de5e1ce --- /dev/null +++ b/src/components/editableDiv.js @@ -0,0 +1,52 @@ +import React, { useState, useRef, useEffect } from 'react' + + +function EditableDiv({value, onChange, maxLength=Infinity, className='', inputClassName}) { + const [isEditable, setIsEditable] = useState(false) + const [content, setContent] = useState(value) + const inputRef = useRef(null) + + useEffect(() => { + + setContent(value) + + }, [value]) + + const handleInput = (event) => { + console.log("value: ", event.target.value) + onChange(event.target.value) + + } + + const handleDoubleClick = () => { + setIsEditable(true) + setTimeout(() => inputRef.current.focus(), 1) + } + + const handleBlur = () => { + setIsEditable(false) + } + + return ( +
+ {!isEditable && {content}} + +
+ ) +} + +export default EditableDiv