added swappable layout

This commit is contained in:
paul
2024-09-21 18:37:28 +05:30
parent 248de2167c
commit 3ad39c74d6
6 changed files with 423 additions and 180 deletions

View File

@@ -13,6 +13,7 @@ import { ActiveWidgetContext } from "../activeWidgetContext"
import { DragWidgetProvider } from "./draggableWidgetContext"
import WidgetDraggable from "./widgetDragDrop"
import WidgetContainer from "../constants/containers"
import { DragContext } from "../../components/draggable/draggableContext"
@@ -48,14 +49,15 @@ class Widget extends React.Component {
this.icon = "" // antd icon name representing this widget
this.elementRef = React.createRef()
this.elementRef = React.createRef() // this is the outer ref for draggable area
this.swappableAreaRef = React.createRef() // helps identify if the users intent is to swap or drop inside the widget
this.innerAreaRef = React.createRef() // this is the inner area where swap is prevented and only drop is accepted
this.functions = {
"load": { "args1": "number", "args2": "string" }
}
this.layout = Layouts.FLEX
this.droppableTags = ["widget"] // This indicates if the draggable can be dropped on this widget
this.boundingRect = {
x: 0,
y: 0,
@@ -68,6 +70,8 @@ class Widget extends React.Component {
selected: false,
widgetName: widgetName || 'widget', // this will later be converted to variable name
enableRename: false, // will open the widgets editable div for renaming
isDragging: false, // tells if the widget is currently being dragged
dragEnabled: true,
widgetContainer: WidgetContainer.CANVAS, // what is the parent of the widget
@@ -532,22 +536,6 @@ class Widget extends React.Component {
})
}
handleDrop = (event, dragElement) => {
console.log("dragging event: ", event, dragElement)
const container = dragElement.getAttribute("data-container")
// TODO: check if the drop is allowed
if (container === "canvas"){
this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id"))
}else if (container === "sidebar"){
this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first
}
}
/**
*
@@ -591,7 +579,190 @@ class Widget extends React.Component {
}
// FIXME: children outside the bounding box
/**
*
* @depreciated - This function is depreciated in favour of handleDropEvent()
*/
handleDrop = (event, dragElement) => {
// THIS function is depreciated in favour of handleDropEvent()
console.log("dragging event: ", event, dragElement)
const container = dragElement.getAttribute("data-container")
// TODO: check if the drop is allowed
if (container === "canvas"){
this.props.onAddChildWidget(this.__id, dragElement.getAttribute("data-widget-id"))
}else if (container === "sidebar"){
this.props.onAddChildWidget(this.__id, null, true) // if dragged from the sidebar create the widget first
}
}
handleDragStart = (e, callback) => {
e.stopPropagation()
this.setState({isDragging: true})
callback(this.elementRef?.current || null)
console.log("Drag start: ", this.elementRef)
// Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
const dragImage = this.elementRef?.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 = this.elementRef?.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)
}
handleDragEnter = (e, draggedElement, setOverElement) => {
const dragEleType = draggedElement.getAttribute("data-draggable-type")
console.log("Drag entering...", dragEleType, draggedElement, this.droppableTags)
// FIXME: the outer widget shouldn't be swallowed by inner widget
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
setOverElement(e.currentTarget) // provide context to the provider
let showDrop = {
allow: true,
show: true
}
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
showDrop = {
allow: true,
show: true
}
} else {
showDrop = {
allow: false,
show: true
}
}
this.setState({
showDroppableStyle: showDrop
})
}
handleDragOver = (e, draggedElement) => {
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length === 0 || this.droppableTags.includes(dragEleType)) {
e.preventDefault() // NOTE: this is necessary to allow drop to take place
}
}
handleDropEvent = (e, draggedElement) => {
e.preventDefault()
e.stopPropagation()
// FIXME: sometimes the elements shoDroppableStyle is not gone, when dropping on the same widget
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
}, () => {
console.log("droppable cleared: ", this.elementRef.current, this.state.showDroppableStyle)
})
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (this.droppableTags.length > 0 && !this.droppableTags.includes(dragEleType)) {
return // prevent drop if the draggable element doesn't match
}
if (draggedElement === this.elementRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
let currentElement = e.currentTarget
while (currentElement) {
if (currentElement === draggedElement) {
console.log("Dropped into a descendant element, ignoring drop")
return // Exit early to prevent the drop
}
currentElement = currentElement.parentElement // Traverse up to check ancestors
}
const container = draggedElement.getAttribute("data-container")
const thisContainer = this.elementRef.current.getAttribute("data-container")
// console.log("Dropped as swappable: ", e.target, this.swappableAreaRef.current.contains(e.target))
// If swaparea is true, then it swaps instead of adding it as a child, also make sure that the parent widget(this widget) is on the widget and not on the canvas
const swapArea = (this.swappableAreaRef.current.contains(e.target) && !this.innerAreaRef.current.contains(e.target) && thisContainer === WidgetContainer.WIDGET)
// TODO: check if the drop is allowed
if ([WidgetContainer.CANVAS, WidgetContainer.WIDGET].includes(container)){
// console.log("Dropped on meee: ", swapArea, this.swappableAreaRef.current.contains(e.target), thisContainer)
this.props.onAddChildWidget({parentWidgetId: this.__id,
dragElementID: draggedElement.getAttribute("data-widget-id"),
swap: swapArea || false
})
}else if (container === WidgetContainer.SIDEBAR){
// console.log("Dropped on Sidebar: ", this.__id)
this.props.onAddChildWidget({parentWidgetId: this.__id, create: true}) // if dragged from the sidebar create the widget first
}
}
handleDragLeave = (e, draggedElement) => {
// console.log("Left: ", e.currentTarget, e.relatedTarget, draggedElement)
if (!e.currentTarget.contains(draggedElement)) {
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
})
}
}
handleDragEnd = (callback) => {
callback()
this.setState({isDragging: false})
}
// FIXME: children outside the bounding box, add tw-overflow-hidden
renderContent() {
// throw new NotImplementedError("render method has to be implemented")
return (
@@ -616,125 +787,165 @@ class Widget extends React.Component {
left: `${this.state.pos.x}px`,
width: `${this.state.size.width}px`,
height: `${this.state.size.height}px`,
opacity: this.state.isDragging ? 0.3 : 1
}
// console.log("selected: ", this.state.dragEnabled)
return (
<WidgetDraggable widgetRef={this.elementRef}
enableDrag={this.state.dragEnabled}
onDrop={this.handleDrop}
onDragEnter={({dragElement, showDrop}) => {
this.setState({
showDroppableStyle: showDrop
})
}
}
onDragLeave={ () => {
this.setState({
showDroppableStyle: {
allow: false,
show: false
}
})
}
}
>
<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={this.state.widgetContainer} // indicates how the canvas should handle dragging, one is sidebar other is canvas
>
<DragContext.Consumer>
<div className="tw-relative tw-w-full tw-h-full tw-top-0 tw-left-0"
>
{this.renderContent()}
{
({draggedElement, onDragStart, onDragEnd, setOverElement}) => (
{
// show drop style on drag hover
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"
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
draggable={this.state.dragEnabled}
onDragOver={(e) => this.handleDragOver(e, draggedElement)}
onDrop={(e) => this.handleDropEvent(e, draggedElement)}
`}
style={
{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}
}
>
onDragEnter={(e) => this.handleDragEnter(e, draggedElement, setOverElement)}
onDragLeave={(e) => this.handleDragLeave(e, draggedElement)}
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-scale-[1.1] tw-opacity-1 tw-z-[-1] `}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
ref={this.swappableAreaRef}
>
{/* helps with swappable: if the mouse is in this area while hovering/dropping, then swap */}
</div>
<div className="tw-relative tw-w-full tw-h-full" ref={this.innerAreaRef}>
{this.renderContent()}
</div>
}
<div className={`tw-absolute tw-bg-transparent tw-scale-[1.1] tw-opacity-100
tw-w-full tw-h-full tw-top-0
${this.state.selected ? 'tw-border-2 tw-border-solid tw-border-blue-500' : 'tw-hidden'}`}>
{
// show drop style on drag hover
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-relative tw-w-full tw-h-full">
<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"
/>
`}
style={{
width: "calc(100% + 10px)",
height: "calc(100% + 10px)",
}}
>
</div>
}
<div className={`tw-absolute 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)",
}}
>
<div className="tw-relative tw-w-full tw-h-full">
<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
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>
)
}
</DragContext.Consumer>
// <WidgetDraggable widgetRef={this.elementRef}
// enableDrag={this.state.dragEnabled}
// onDrop={this.handleDrop}
// onDragEnter={({dragElement, showDrop}) => {
// this.setState({
// showDroppableStyle: showDrop
// })
// }
// }
// onDragLeave={ () => {
// this.setState({
// showDroppableStyle: {
// allow: false,
// show: false
// }
// })
// }
// }
</div>
</div>
</WidgetDraggable>
// >
// </WidgetDraggable>
)
}