fixed widget dragging inside the canvas

This commit is contained in:
paul
2024-09-17 11:55:21 +05:30
parent 0f755b7a90
commit d3721f2ea2
7 changed files with 214 additions and 36 deletions

View File

@@ -20,6 +20,7 @@ import { WidgetContext } from './context/widgetContext'
import 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")
@@ -302,7 +303,7 @@ class Canvas extends React.Component {
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)
// widget.setPos(newPosX, newPosY)
})
}
@@ -468,7 +469,7 @@ class Canvas extends React.Component {
callback({id, widgetRef})
if (this._onWidgetListUpdated)
this._onWidgetListUpdated(widgets)
this._onWidgetListUpdated(widgets) // inform the parent container
})
@@ -476,6 +477,11 @@ class Canvas extends React.Component {
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
@@ -558,13 +564,17 @@ class Canvas extends React.Component {
}
/**
* Handles drop event to canvas from the sidebar
* Handles drop event to canvas from the sidebar and on canvas widget movement
* @param {DragEvent} e
*/
handleDropEvent = (e) => {
handleDropEvent = (e, draggedElement) => {
e.preventDefault()
const container = draggedElement.getAttribute("data-container")
console.log("dragged element: ", e, draggedElement, container)
// const canvasContainerRect = this.getCanvasContainerBoundingRect()
const canvasRect = this.canvasRef.current.getBoundingClientRect()
const { clientX, clientY } = e
@@ -572,11 +582,18 @@ class Canvas extends React.Component {
const finalPosition = {
x: (clientX - canvasRect.left) / this.state.zoom,
y: (clientY - canvasRect.top) / this.state.zoom,
}
}
this.addWidget(Widget, ({id, widgetRef}) => {
widgetRef.current.setPos(finalPosition.x, finalPosition.y)
})
if (container === "sidebar"){
this.addWidget(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)
}
}
@@ -607,30 +624,33 @@ class Canvas extends React.Component {
{/* <ActiveWidgetProvider> */}
<DroppableWrapper id="canvas-droppable"
className="tw-w-full tw-h-full" onDrop={this.handleDropEvent}>
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}>
<div className="dots-bg tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url('${DotsBackground}')`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
tabIndex={0} // allow focus
>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
tw-bg-green-300"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{
this.state.widgets.map(this.renderWidget)
}
className="tw-w-full tw-h-full"
onDrop={this.handleDropEvent}>
{/* <DragWidgetProvider> */}
<Dropdown trigger={['contextMenu']} mouseLeaveDelay={0} menu={{items: this.state.contextMenuItems, }}>
<div className="dots-bg tw-w-full tw-h-full tw-outline-none tw-flex tw-relative tw-bg-[#f2f2f2] tw-overflow-hidden"
ref={this.canvasContainerRef}
style={{
transition: " transform 0.3s ease-in-out",
backgroundImage: `url('${DotsBackground}')`,
backgroundSize: 'cover', // Ensure proper sizing if needed
backgroundRepeat: 'no-repeat',
}}
tabIndex={0} // allow focus
>
{/* Canvas */}
<div data-canvas className="tw-w-full tw-h-full tw-absolute tw-top-0 tw-select-none
tw-bg-green-300"
ref={this.canvasRef}>
<div className="tw-relative tw-w-full tw-h-full">
{
this.state.widgets.map(this.renderWidget)
}
</div>
</div>
</div>
</div>
</Dropdown>
</Dropdown>
{/* </DragWidgetProvider> */}
</DroppableWrapper>
<CanvasToolBar isOpen={this.state.toolbarOpen}

View File

@@ -10,6 +10,8 @@ import DraggableWrapper from "../../components/draggable/draggable"
import DroppableWrapper from "../../components/draggable/droppable"
import { ActiveWidgetContext } from "../activeWidgetContext"
import { DragWidgetProvider } from "./draggableWidgetContext"
import WidgetDraggable from "./widgetDragDrop"
/**
@@ -521,8 +523,13 @@ class Widget extends React.Component {
// console.log("selected: ", this.state.selected)
return (
<div data-id={this.__id} ref={this.elementRef} className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
<WidgetDraggable widgetRef={this.elementRef}>
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-absolute tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={"canvas"} // indicates how the canvas should handle dragging, one is sidebar other is canvas
>
{this.renderContent()}
@@ -565,6 +572,7 @@ class Widget extends React.Component {
</div>
</div>
</WidgetDraggable>
)
}

View File

@@ -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 (
<DragWidgetContext.Provider value={{ draggedElement, onDragStart, onDragEnd }}>
{children}
</DragWidgetContext.Provider>
)
}

View File

@@ -0,0 +1,127 @@
import { memo, useState } from "react"
import { useDragWidgetContext } from "./draggableWidgetContext"
import { useDragContext } from "../../components/draggable/draggableContext"
const WidgetDraggable = memo(({ widgetRef, dragElementType="widget", onDrop, droppableTags = ["widget"], ...props }) => {
// const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext()
const { draggedElement, onDragStart, onDragEnd } = useDragContext()
const [isDragging, setIsDragging] = useState(false)
const [showDroppable, setShowDroppable] = useState({
show: false,
allow: false
})
const handleDragStart = (e) => {
setIsDragging(true)
console.log("Draggable widget ref: ", widgetRef)
onDragStart(widgetRef?.current || null)
// 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")
if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
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 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) => {
setShowDroppable({
allow: false,
show: false
})
if (onDrop) {
onDrop(e, draggedElement)
}
}
const handleDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowDroppable({
allow: false,
show: false
})
}
}
const handleDragEnd = () => {
onDragEnd()
setIsDragging(false)
}
return (
<div className={`${props.className} tw-w-fit tw-h-fit`}
onDragOver={handleDragOver}
onDrop={handleDropEvent}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
draggable
style={{ opacity: isDragging ? 0 : 1}} // hide the initial position when dragging
>
{
showDroppable.show &&
<div className={`${showDroppable.allow ? "tw-border-green-600" : "tw-border-red-600"}
tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg
`}>
</div>
}
{props.children}
</div>
)
})
export default WidgetDraggable

View File

@@ -30,7 +30,7 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){
return (
// <Draggable className="tw-cursor-pointer" id={name}>
<DraggableWrapper dragElementType={"widget"} className="tw-cursor-pointer tw-w-fit tw-h-fit">
<DraggableWrapper data-container={"sidebar"} dragElementType={"widget"} className="tw-cursor-pointer tw-w-fit tw-h-fit">
<div ref={innerRef} className="tw-select-none tw-h-[240px] tw-w-[280px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden

View File

@@ -7,7 +7,7 @@ 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, className, children}) => {
const DraggableWrapper = memo(({dragElementType, className, children, ...props}) => {
const { onDragStart, onDragEnd } = useDragContext()
@@ -40,6 +40,7 @@ const DraggableWrapper = memo(({dragElementType, className, children}) => {
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
ref={draggableRef}
{...props}
>
{children}
</div>

View File

@@ -14,7 +14,6 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
const handleDragEnter = (e) => {
console.log("Drag Enter", draggedElement)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
@@ -42,7 +41,6 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
}
const handleDropEvent = (e) => {
console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
setShowDroppable({
allow: false,
@@ -50,7 +48,7 @@ const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) =>
})
if(onDrop){
onDrop(e)
onDrop(e, draggedElement)
}
}