working on dnd inside a container

This commit is contained in:
paul
2025-03-03 19:17:28 +05:30
parent 72aafd17fe
commit 03a4984cbc
6 changed files with 417 additions and 121 deletions

View File

@@ -191,8 +191,6 @@ class Canvas extends React.Component {
getWidgetFromTarget(target) {
// TODO: improve search, currently O(n), but can be improved via this.state.widgets or something
console.log("Target: ", target, )
let innerWidget = null
for (let [key, ref] of Object.entries(this.widgetRefs)) {
@@ -200,9 +198,6 @@ class Canvas extends React.Component {
innerWidget = ref.current
break
}
console.log("ref: ", ref.current)
// console.log("refs: ", ref)
// TODO: remove the ref.current? if there are bugs it would become hard to debug
if (ref.current?.getElement()?.contains(target)) {

View File

@@ -13,6 +13,7 @@ import { DragContext } from "../../components/draggable/draggableContext"
import { isNumeric, removeKeyFromObject } from "../../utils/common"
import { info } from "autoprefixer"
import { message } from "antd"
import WidgetDnd from "./widgetDnd"
// TODO: make it possible to apply widgetInnerStyle on load
@@ -1104,9 +1105,9 @@ class Widget extends React.Component {
...this.state.widgetOuterStyling,
cursor: this.cursor,
zIndex: this.state.zIndex,
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`,
// 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: width,
height: height,
minWidth: minWidth,
@@ -1115,7 +1116,7 @@ class Widget extends React.Component {
}
// const boundingRect = this.getBoundingRect
// TODO: rewrite Drag and drop
// FIXME: if the parent container has tw-overflow-none, then the resizable indicator are also hidden
return (
@@ -1126,137 +1127,153 @@ class Widget extends React.Component {
return (
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-shadow-xl tw-w-fit tw-h-fit"
style={outerStyle}
data-draggable-type={this.getWidgetType()} // helps with droppable
data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
<WidgetDnd id={this.__id}
disabled={false && this.state.dragEnabled}
data-draggable-type={this.getWidgetType()} // helps with droppable
dragElementType={this.getWidgetType()}
// onDragOver={(e) => this.handleDragOver(e, draggedElement)}
// onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}
draggable={this.state.dragEnabled}
// onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
// onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
onDragOver={(e) => this.handleDragOver(e, draggedElement)}
onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}
// onDragStart={(e) => this.handleDragStart(e, onDragStart)}
// onDragEnd={(e) => this.handleDragEnd(onDragEnd)}
// style={outerStyle}
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
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 */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
<div className={`tw-absolute tw-top-[-5px] tw-left-[-5px]
tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
tw-w-full tw-h-full tw-bg-red-400
tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
// swapable area
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
<div className="tw-relative tw-top-0 tw-left-0 tw-w-full tw-h-full" ref={this.innerAreaRef}
>
{this.renderContent()}
</div>
{
// show drop style on drag hover
draggedElement && this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
<div data-widget-id={this.__id}
ref={this.elementRef}
className="tw-shadow-xl tw-w-fit tw-h-fit"
// data-draggable-type={this.getWidgetType()} // helps with droppable
// data-container={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
`}
// data-drag-start-within // this attribute indicates that the drag is occurring from within the project and not a outside file drop
// draggable={this.state.dragEnabled}
// onDragOver={(e) => this.handleDragOver(e, draggedElement)}
// onDrop={(e) => {this.handleDropEvent(e, draggedElement, widgetClass); onDragEnd()}}
// onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
// onDragLeave={(e) => this.handleDragLeave(e, draggedElement, overElement)}
// 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 */}
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
<div className={`tw-absolute tw-top-[-5px] tw-left-[-5px]
tw-border-1 tw-opacity-0 tw-border-solid tw-border-black
tw-w-full tw-h-full tw-bg-red-400
tw-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
// swapable area
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
}
{/* FIXME: the resize handles get clipped in parent container */}
<div className={`tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw-w-full tw-h-full
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
style={{
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
zIndex: -1,
}}
>
<div className={`"tw-relative tw-w-full tw-h-full"`}> {/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
<div className="tw-relative tw-top-0 tw-left-0 tw-w-full tw-h-full" ref={this.innerAreaRef}
>
{this.renderContent()}
</div>
{
// show drop style on drag hover
draggedElement && this.state.showDroppableStyle.show &&
<div className={`${this.state.showDroppableStyle.allow ? "tw-border-blue-600" : "tw-border-red-600"}
tw-absolute tw-top-[-5px] tw-left-[-5px] tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({ dragEnabled: false })
`}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
>
</div>
}
{/* FIXME: the resize handles get clipped in parent container */}
<div className={`tw-absolute tw-z-[-1] tw-bg-transparent tw-top-[-10px] tw-left-[-10px] tw-opacity-100
tw-w-full tw-h-full
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}
style={{
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
zIndex: -1,
}}
>
<div className={`"tw-relative tw-w-full tw-h-full"`}> {/* ${this.state.isDragging ? "tw-pointer-events-none" : "tw-pointer-events-auto"} */}
<EditableDiv value={this.state.widgetName} onChange={this.setWidgetName}
maxLength={40}
openEdit={this.state.enableRename}
className="tw-text-sm tw-w-fit tw-max-w-[160px] tw-text-clip tw-min-w-[150px]
tw-overflow-hidden tw-absolute tw--top-6 tw-h-6"
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.NW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("nw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--top-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("ne")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--left-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SW_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("sw")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
<div
className="tw-w-2 tw-h-2 tw-absolute tw--right-1 tw--bottom-1 tw-bg-blue-500"
style={{ cursor: Cursor.SE_RESIZE }}
onMouseDown={(e) => {
e.stopPropagation()
e.preventDefault()
this.props.onWidgetResizing("se")
this.setState({ dragEnabled: false })
}}
onMouseUp={() => this.setState({ dragEnabled: true })}
/>
</div>
</div>
</div>
</div>
</div>
)
</WidgetDnd>
)
}
}

View File

@@ -0,0 +1,236 @@
import React, { useEffect, useRef, useState } from 'react'
import { useDragDropManager, useDroppable, useDraggable } from '@dnd-kit/react'
import { useDragContext } from '../../components/draggable/draggableContext'
// FIXME: fix the issue of self drop
function WidgetDnd({droppableTags,
...props}) {
const dndRef = useRef(null)
const {dragElementType} = props
const { draggedElement, setOverElement, widgetClass } = useDragContext()
const { ref: dropRef, isDropTarget, droppable} = useDroppable({
id: props.id,
// accept: (draggable) => {
// const allowDrop = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
// (droppableTags.include?.length > 0 && droppableTags.include?.includes(draggable.type)) ||
// (droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(draggable.type))
// ))
// return allowDrop
// }
})
const { ref: dragRef, draggable } = useDraggable({
id: dragElementType,
feedback: "default",
type: dragElementType,
disabled: props.disabled,
// data: { title: props.children }
})
const manager = useDragDropManager()
// const {}
const {onDragStart, onDragEnd, disableStyle=false} = useDragContext()
const [allowDrop, setAllowDrop] = useState({show: false, allow: false}) // indicator if the draggable can be dropped on the droppable
useEffect(() => {
manager?.monitor?.addEventListener("dragstart", handleDragEnter)
manager?.monitor?.addEventListener("dragend", handleDropEvent)
manager?.monitor?.addEventListener("dragmove", handleDragOver)
// manager?.monitor?.addEventListener("dragover", onDragEndhandleDragOver)
return () => {
manager?.monitor?.removeEventListener("dragstart", handleDragEnter)
manager?.monitor?.removeEventListener("dragend", handleDropEvent)
manager?.monitor?.removeEventListener("dragmove", handleDragOver)
}
}, [manager, draggedElement])
const handleRef = (node) => {
dndRef.current = node
dropRef(node)
dragRef(node)
}
const handleDragEnter = (e) => {
const {target, source} = e.operation
console.log("is drag source: ", props.id, draggable.isDragSource)
if (draggable.isDragSource){
onDragStart(dndRef?.current, widgetClass)
}
if (target && target?.id !== props?.id){
return
}
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
const dragElementType = draggedElement.getAttribute("data-draggable-type")
setOverElement(document.getElementById(source.id))
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
console.log("droppable tags: ", dropAllowed)
setAllowDrop({allow: dropAllowed, show: true})
}
const handleDragOver = (e) => {
const {target} = e.operation
if (target && target?.id !== props?.id){
return
}
// console.log("Over sir1: ", draggedElement)
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
setAllowDrop({allow: dropAllowed, show: true})
// if (allowDrop) {
// e.preventDefault() // this is necessary to allow drop to take place
// }
}
const handleDropEvent = (e) => {
const {target} = e.operation
if (target && target?.id !== props?.id){
return
}
setAllowDrop({allow: false, show: false})
if (!draggedElement || !draggedElement.getAttribute("data-drag-start-within")) {
// if the drag is starting from outside (eg: file drop) or if drag doesn't exist
return
}
// e.stopPropagation()
const dragElementType = draggedElement.getAttribute("data-draggable-type")
const dropAllowed = (droppableTags && droppableTags !== null && (Object.keys(droppableTags).length === 0 ||
(droppableTags.include?.length > 0 && droppableTags.include?.includes(dragElementType)) ||
(droppableTags.exclude?.length > 0 && !droppableTags.exclude?.includes(dragElementType))
))
// if (onDrop && dropAllowed) {
// onDrop(e, draggedElement, widgetClass)
// }
}
// const handleDragLeave = (e) => {
// const {target} = e.operation
// if (target && target.id === props.id){
// handleDropEvent(e)
// }else{
// setAllowDrop({allow: false, show: false})
// }
// }
// const handleDragStart = (event) => {
// const {source} = event.operation
// if (!source || (source && source.id !== props.dragElementType)){
// return
// } if (!source || (source && source.id !== props.dragElementType)){
// return
// }
// // event.dataTransfer.setData("text/plain", "")
// // onDragStart(draggableRef?.current, dragWidgetClass)
// onDragStart(draggableRef?.current, dragWidgetClass, elementMetaData)
// }
// const handleDragEnd = (event) => {
// // console.log("Drag end: ", e, e.target.closest('div'))
// const {source} = event.operation
// if (!source || (source && source.id !== props.dragElementType)){
// return
// }
// // onDragEnd()
// }
return (
<div ref={handleRef} data-drag-start-within
draggable
data-draggable-type={dragElementType}
className={`${props.className} tw-relative` || ''} {...props} style={{zIndex: 10}}>
{props.children}
{/* {
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 tw-pointer-events-none
`}>
</div>
} */}
{
droppable.isDropTarget &&
<div className={`${allowDrop.allow ? "tw-bg-[#82ff1c6e]" : "tw-bg-[#eb5d366e]"}
tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-full tw-z-[3]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}>
</div>
}
</div>
)
}
export default WidgetDnd

View File

@@ -34,6 +34,7 @@ export function SidebarWidgetCard({ name, img, url, license, widgetClass, innerR
data-container={"sidebar"}
dragElementType={widgetClass.widgetType}
dragWidgetClass={widgetClass}
draggableType={"clone"}
elementMetaData={{
name,
url,

View File

@@ -10,7 +10,7 @@ function Draggable(props) {
const { ref } = useDraggable({
id: props.dragElementType,
feedback: "clone",
feedback: props.draggableType || "default",
type: props.dragElementType
// data: { title: props.children }
})

View File

@@ -0,0 +1,47 @@
class ZIndexPlugin {
constructor(manager, options) {
this.manager = manager;
this.options = options || {};
this.originalZIndexValues = new Map();
}
registerEffect() {
const handleDragStart = (event) => {
const { active } = event;
const draggableElement = document.getElementById(active.id);
if (draggableElement) {
// Store the original z-index
this.originalZIndexValues.set(active.id, draggableElement.style.zIndex);
// Apply the dragged z-index
draggableElement.style.zIndex = this.options.draggedZIndex || '9999';
}
};
const handleDragEnd = (event) => {
const { active } = event;
const draggableElement = document.getElementById(active.id);
if (draggableElement) {
// Restore the original z-index
const originalZIndex = this.originalZIndexValues.get(active.id) || '';
draggableElement.style.zIndex = originalZIndex;
this.originalZIndexValues.delete(active.id);
}
};
// Listen for drag events
this.manager.addEventListener('dragstart', handleDragStart);
this.manager.addEventListener('dragend', handleDragEnd);
// Return cleanup function
return () => {
this.manager.removeEventListener('dragstart', handleDragStart);
this.manager.removeEventListener('dragend', handleDragEnd);
};
}
}
export default ZIndexPlugin;