import React from "react" import {DndContext} from '@dnd-kit/core' import { CloseOutlined, DeleteOutlined, EditOutlined, FullscreenOutlined, ReloadOutlined } from "@ant-design/icons" import { Button, Tooltip, Dropdown } from "antd" import Droppable from "../components/utils/droppableDnd" import Widget from "./widgets/base" import Cursor from "./constants/cursor" import CanvasToolBar from "./toolbar" import { UID } from "../utils/uid" import { removeDuplicateObjects } from "../utils/common" import { WidgetContext } from './context/widgetContext' // import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" // import DotsBackground from "../assets/background/dots.svg" import {ReactComponent as DotsBackground} from "../assets/background/dots.svg" import DroppableWrapper from "../components/draggable/droppable" import { ActiveWidgetContext, ActiveWidgetProvider, withActiveWidget } from "./activeWidgetContext" import { DragWidgetProvider } from "./widgets/draggableWidgetContext" // const DotsBackground = require("../assets/background/dots.svg") const CanvasModes = { DEFAULT: 0, PAN: 1, MOVE_WIDGET: 2 // when the mode is move widget } class Canvas extends React.Component { // static contextType = ActiveWidgetContext constructor(props) { super(props) const { canvasWidgets, onWidgetListUpdated } = props this.canvasRef = React.createRef() this.canvasContainerRef = React.createRef() this.currentMode = CanvasModes.DEFAULT this.minCanvasSize = {width: 500, height: 500} this.mousePressed = false this.mousePos = { x: 0, y: 0 } // this._contextMenuItems = [] this.widgetRefs = {} // stores the actual refs to the widgets inside the canvas {id: ref, id2, ref2...} this.state = { widgets: [], // stores the mapping to widgetRefs, stores id and WidgetType, later used for rendering [{id: , widgetType: WidgetClass}] zoom: 1, isPanning: false, currentTranslate: { x: 0, y: 0 }, canvasSize: { width: 500, height: 500 }, contextMenuItems: [], selectedWidgets: [], toolbarOpen: true, toolbarAttrs: null } this._onWidgetListUpdated = onWidgetListUpdated // a function callback when the widget is added to the canvas 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.keyDownEvent = this.keyDownEvent.bind(this) this.wheelZoom = this.wheelZoom.bind(this) this.onActiveWidgetUpdate = this.onActiveWidgetUpdate.bind(this) this.getWidgets = this.getWidgets.bind(this) this.getActiveObjects = this.getActiveObjects.bind(this) this.getWidgetFromTarget = this.getWidgetFromTarget.bind(this) this.getCanvasObjectsBoundingBox = this.getCanvasObjectsBoundingBox.bind(this) this.fitCanvasToBoundingBox = this.fitCanvasToBoundingBox.bind(this) this.getCanvasContainerBoundingRect = this.getCanvasContainerBoundingRect.bind(this) this.getCanvasBoundingRect = this.getCanvasBoundingRect.bind(this) this.setSelectedWidget = this.setSelectedWidget.bind(this) this.deleteSelectedWidgets = this.deleteSelectedWidgets.bind(this) this.removeWidget = this.removeWidget.bind(this) this.clearSelections = this.clearSelections.bind(this) this.clearCanvas = this.clearCanvas.bind(this) // this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this) } componentDidMount() { this.initEvents() this.createWidget(Widget) } componentWillUnmount() { this.canvasContainerRef.current.removeEventListener("mousedown", this.mouseDownEvent) this.canvasContainerRef.current.removeEventListener("mouseup", this.mouseUpEvent) this.canvasContainerRef.current.removeEventListener("mousemove", this.mouseMoveEvent) this.canvasContainerRef.current.removeEventListener("wheel", this.wheelZoom) this.canvasContainerRef.current.removeEventListener("keydown", this.keyDownEvent) // NOTE: this will clear the canvas this.clearCanvas() } initEvents(){ this.canvasContainerRef.current.addEventListener("mousedown", this.mouseDownEvent) this.canvasContainerRef.current.addEventListener("mouseup", this.mouseUpEvent) this.canvasContainerRef.current.addEventListener("mousemove", this.mouseMoveEvent) this.canvasContainerRef.current.addEventListener("wheel", this.wheelZoom) this.canvasContainerRef.current.addEventListener("keydown", this.keyDownEvent, true) // window.addEventListener("keydown", this.keyDownEvent, true) } applyTransform(){ const { currentTranslate, zoom } = this.state this.canvasRef.current.style.transform = `translate(${currentTranslate.x}px, ${currentTranslate.y}px) scale(${zoom})` } /** * * @returns {import("./widgets/base").Widget[]} */ getWidgets(){ return this.state.widgets } /** * returns list of active objects / selected objects on the canvas * @returns Widget[] */ getActiveObjects(){ return Object.values(this.widgetRefs).filter((widgetRef) => { return widgetRef.current?.isSelected() }) } /** * returns the widget that contains the target * @param {HTMLElement} target * @returns {Widget} */ getWidgetFromTarget(target){ for (let [key, ref] of Object.entries(this.widgetRefs)){ // console.log("ref: ", ref) if (ref.current.getElement().contains(target)){ return ref.current } } } keyDownEvent(event){ if (event.key === "Delete"){ this.deleteSelectedWidgets() } if (event.key === "+"){ this.setZoom(this.state.zoom + 0.1) } if (event.key === "-"){ this.setZoom(this.state.zoom - 0.1) } } mouseDownEvent(event){ this.mousePos = { x: event.clientX, y: event.clientY } let selectedWidget = this.getWidgetFromTarget(event.target) if (event.button === 0){ this.mousePressed = true if (selectedWidget){ // if the widget is selected don't pan, instead move the widget if (!selectedWidget._disableSelection){ const selectedLength = this.state.selectedWidgets.length // console.log("selected widget: ", selectedWidget) if (selectedLength === 0 || (selectedLength === 1 && selectedWidget.__id !== this.state.selectedWidgets[0].__id)){ this.state.selectedWidgets[0]?.deSelect() // deselect the previous widget before adding the new one this.state.selectedWidgets[0]?.setZIndex(0) selectedWidget.setZIndex(1000) selectedWidget.select() this.setState({ selectedWidgets: [selectedWidget], toolbarAttrs: selectedWidget.getToolbarAttrs() }) // this.context.updateActiveWidget(selectedWidget.__id) // this.context.updateToolAttrs(selectedWidget.getToolbarAttrs()) // this.props.updateActiveWidget(selectedWidget) } this.currentMode = CanvasModes.MOVE_WIDGET } this.currentMode = CanvasModes.PAN }else if (!selectedWidget){ // get the canvas ready to pan, if there are widgets on the canvas this.clearSelections() this.currentMode = CanvasModes.PAN this.setCursor(Cursor.GRAB) } this.setState({ contextMenuItems: [], toolbarOpen: true }) // this.setState({ // showContextMenu: false // }) }else if (event.button === 2){ //right click if (this.state.selectedWidgets.length > 0 && this.state.selectedWidgets[0].__id !== selectedWidget.__id){ this.clearSelections() } if (selectedWidget){ this.setState({ selectedWidget: [selectedWidget], contextMenuItems: [ { key: "rename", label: (
selectedWidget.openRenaming()}> Rename
), icons: , }, { key: "delete", label: (
this.deleteSelectedWidgets([selectedWidget])}> Delete
), icons: , danger: true } ] }) } } } mouseMoveEvent(event){ // console.log("mode: ", this.currentMode, this.getActiveObjects()) if (this.mousePressed && [CanvasModes.PAN, CanvasModes.MOVE_WIDGET].includes(this.currentMode)) { const deltaX = event.clientX - this.mousePos.x const deltaY = event.clientY - this.mousePos.y if (this.state.selectedWidgets.length === 0){ // if there aren't any selected widgets, then pan the canvas this.setState(prevState => ({ currentTranslate: { x: prevState.currentTranslate.x + deltaX, y: prevState.currentTranslate.y + deltaY, } }), this.applyTransform) }else{ // update the widgets position // this.state.selectedWidgets.forEach(widget => { // const {x, y} = widget.getPos() // const newPosX = x + (deltaX/this.state.zoom) // account for the zoom, since the widget is relative to canvas // const newPosY = y + (deltaY/this.state.zoom) // account for the zoom, since the widget is relative to canvas // widget.setPos(newPosX, newPosY) // }) } this.mousePos = { x: event.clientX, y: event.clientY } this.setCursor(Cursor.GRAB) } } mouseUpEvent(event){ this.mousePressed = false this.currentMode = CanvasModes.DEFAULT this.setCursor(Cursor.DEFAULT) } wheelZoom(event){ let delta = event.deltaY let zoom = this.state.zoom * 0.999 ** delta this.setZoom(zoom, {x: event.offsetX, y: event.offsetY}) } getCanvasContainerBoundingRect(){ return this.canvasContainerRef.current.getBoundingClientRect() } getCanvasBoundingRect(){ return this.canvasRef.current.getBoundingClientRect() } getCanvasTranslation(){ return this.state.currentTranslate } /** * fits the canvas size to fit the widgets bounding box */ fitCanvasToBoundingBox(padding=0){ const { top, left, right, bottom } = this.getCanvasObjectsBoundingBox() const width = right - left const height = bottom - top const newWidth = Math.max(width + padding, this.minCanvasSize.width) const newHeight = Math.max(height + padding, this.minCanvasSize.height) const canvasStyle = this.canvasRef.current.style // Adjust the canvas dimensions canvasStyle.width = `${newWidth}px` canvasStyle.height = `${newHeight}px` // Adjust the canvas position if needed canvasStyle.left = `${left - padding}px` canvasStyle.top = `${top - padding}px` } setCursor(cursor){ this.canvasContainerRef.current.style.cursor = cursor } setZoom(zoom, pos){ const { currentTranslate } = this.state let newTranslate = currentTranslate if (pos){ // Calculate the new translation to zoom into the mouse position const offsetX = pos.x - (this.canvasContainerRef.current.clientWidth / 2 + currentTranslate.x) const offsetY = pos.y - (this.canvasContainerRef.current.clientHeight / 2 + currentTranslate.y) const newTranslateX = currentTranslate.x - offsetX * (zoom - this.state.zoom) const newTranslateY = currentTranslate.y - offsetY * (zoom - this.state.zoom) newTranslate = { x: newTranslateX, y: newTranslateY } } this.setState({ zoom: Math.max(0.5, Math.min(zoom, 1.5)), // clamp between 0.5 and 1.5 currentTranslate: newTranslate }, this.applyTransform) } getZoom(){ return this.state.zoom } resetTransforms() { this.setState({ zoom: 1, currentTranslate: { x: 0, y: 0 } }, this.applyTransform) } setSelectedWidget(selectedWidget){ this.setState({ selectedWidget: [selectedWidget] }) } clearSelections(){ this.getActiveObjects().forEach(widget => { widget.current?.deSelect() }) // this.context?.updateActiveWidget("") // this.context.updateToolAttrs({}) this.setState({ selectedWidgets: [], toolbarAttrs: null, // toolbarOpen: }) } /** * returns tha combined bounding rect of all the widgets on the canvas * */ getCanvasObjectsBoundingBox(){ // Initialize coordinates to opposite extremes let top = Number.POSITIVE_INFINITY let left = Number.POSITIVE_INFINITY let right = Number.NEGATIVE_INFINITY let bottom = Number.NEGATIVE_INFINITY for (let widget of Object.values(this.widgetRefs)) { const rect = widget.current.getBoundingRect() // Update the top, left, right, and bottom coordinates if (rect.top < top) top = rect.top if (rect.left < left) left = rect.left if (rect.right > right) right = rect.right if (rect.bottom > bottom) bottom = rect.bottom } return { top, left, right, bottom } } /** * * @param {Widget} widgetComponentType - don't pass instead pass Widget object/class */ createWidget(widgetComponentType, callback){ const widgetRef = React.createRef() const id = `${widgetComponentType.widgetType}_${UID()}` // Store the ref in the instance variable this.widgetRefs[id] = widgetRef const widgets = [...this.state.widgets, { id, widgetType: widgetComponentType }] // don't add the widget refs in the state // Update the state to include the new widget's type and ID this.setState({ widgets: widgets }, () => { if (callback) callback({id, widgetRef}) if (this._onWidgetListUpdated) this._onWidgetListUpdated(widgets) // inform the parent container }) return {id, widgetRef} } getWidgetById(id){ return this.widgetRefs[id] } /** * delete's the selected widgets from the canvas * @param {null|Widget} widgets - optional widgets that can be deleted along the selected widgets */ deleteSelectedWidgets(widgets=[]){ let activeWidgets = removeDuplicateObjects([...widgets, ...this.state.selectedWidgets], "__id") const widgetIds = activeWidgets.map(widget => widget.__id) for (let widgetId of widgetIds){ // 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() } } /** * removes all the widgets from the canvas */ clearCanvas(){ // NOTE: Don't remove from it using remove() function since, it already removed from the DOM tree when its removed from widgets // for (let [key, value] of Object.entries(this.widgetRefs)){ // console.log("removed: ", value, value.current?.getElement()) // value.current?.remove() // } this.widgetRefs = {} this.setState({ widgets: [] }) if (this._onWidgetListUpdated) this._onWidgetListUpdated([]) } removeWidget(widgetId){ // this.widgetRefs[widgetId]?.current.remove() delete this.widgetRefs[widgetId] const widgets = this.state.widgets.filter(widget => widget.id !== widgetId) this.setState({ widgets: widgets }) if (this._onWidgetListUpdated) this._onWidgetListUpdated(widgets) } onActiveWidgetUpdate(widgetId){ if (this.state.selectedWidgets.length === 0 || widgetId !== this.state.selectedWidgets[0].__id) return console.log("updating...", this.state.toolbarAttrs, this.state.selectedWidgets.at(0).getToolbarAttrs()) // console.log("attrs: ", this.state.selectedWidgets.at(0).getToolbarAttrs()) this.setState({ toolbarAttrs: this.state.selectedWidgets.at(0).getToolbarAttrs() }) } /** * Handles drop event to canvas from the sidebar and on canvas widget movement * @param {DragEvent} e */ handleDropEvent = (e, draggedElement) => { e.preventDefault() const container = draggedElement.getAttribute("data-container") // console.log("Dropped on canvas",) // const canvasContainerRect = this.getCanvasContainerBoundingRect() const canvasRect = this.canvasRef.current.getBoundingClientRect() const { clientX, clientY } = e const finalPosition = { x: (clientX - canvasRect.left) / this.state.zoom, y: (clientY - canvasRect.top) / this.state.zoom, } if (container === "sidebar"){ // if the widget is being dropped from the sidebar, use the info to create the widget first this.createWidget(Widget, ({id, widgetRef}) => { widgetRef.current.setPos(finalPosition.x, finalPosition.y) }) }else if (container === "canvas"){ const widgetObj = this.getWidgetById(draggedElement.getAttribute("data-widget-id")) // console.log("WidgetObj: ", widgetObj) widgetObj.current.setPos(finalPosition.x, finalPosition.y) } } renderWidget(widget){ const { id, widgetType: ComponentType } = widget // console.log("widet: ", this.widgetRefs, id) return } render() { return (
{/* */} {/* */}
{/* Canvas */}
{ this.state.widgets.map(this.renderWidget) }
{/*
*/}
{/*
*/}
) } } export default Canvas