budibase/packages/client/src/components/preview/GridDNDHandler.svelte

228 lines
7.0 KiB
Svelte

<script>
import { onMount, onDestroy } from "svelte"
import { builderStore, componentStore } from "stores"
import { Utils } from "@budibase/frontend-core"
let dragInfo
let gridStyles
let id
// Some memoisation of primitive types for performance
$: jsonStyles = JSON.stringify(gridStyles)
$: id = dragInfo?.id || id
// Set ephemeral grid styles on the dragged component
$: componentStore.actions.getComponentInstance(id)?.setEphemeralStyles({
...gridStyles,
...(gridStyles ? { "z-index": 999 } : null),
})
// Util to check if a DND event originates from a grid (or inside a grid).
// This is important as we do not handle grid DND in this handler.
const isGridEvent = e => {
return (
e.target
.closest?.(".component")
?.parentNode.closest(".component")
?.childNodes[0].classList.contains("grid") ||
e.target.classList.contains("anchor")
)
}
// Util to get the inner DOM node by a component ID
const getDOMNode = id => {
const component = document.getElementsByClassName(id)[0]
return [...component.children][0]
}
const processEvent = Utils.throttle((mouseX, mouseY) => {
if (!dragInfo?.grid) {
return
}
const { mode, side, gridId, grid } = dragInfo
const {
startX,
startY,
rowStart,
rowEnd,
colStart,
colEnd,
rowDeltaMin,
rowDeltaMax,
colDeltaMin,
colDeltaMax,
} = grid
const domGrid = getDOMNode(gridId)
const cols = parseInt(domGrid.dataset.cols)
const rows = parseInt(domGrid.dataset.rows)
const { width, height } = domGrid.getBoundingClientRect()
const colWidth = width / cols
const diffX = mouseX - startX
let deltaX = Math.round(diffX / colWidth)
const rowHeight = height / rows
const diffY = mouseY - startY
let deltaY = Math.round(diffY / rowHeight)
if (mode === "move") {
deltaY = Math.min(Math.max(deltaY, rowDeltaMin), rowDeltaMax)
deltaX = Math.min(Math.max(deltaX, colDeltaMin), colDeltaMax)
const newStyles = {
"grid-row-start": rowStart + deltaY,
"grid-row-end": rowEnd + deltaY,
"grid-column-start": colStart + deltaX,
"grid-column-end": colEnd + deltaX,
}
if (JSON.stringify(newStyles) !== jsonStyles) {
gridStyles = newStyles
}
} else if (mode === "resize") {
let newStyles = {}
if (side === "right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
} else if (side === "left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
} else if (side === "top") {
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "bottom") {
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "top-right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "top-left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
}
if (JSON.stringify(newStyles) !== jsonStyles) {
gridStyles = newStyles
}
}
}, 100)
const handleEvent = e => {
e.preventDefault()
e.stopPropagation()
processEvent(e.clientX, e.clientY)
}
// Callback when initially starting a drag on a draggable component
const onDragStart = e => {
if (!isGridEvent(e)) {
return
}
// Hide drag ghost image
e.dataTransfer.setDragImage(new Image(), 0, 0)
// Extract state
let mode, id, side
if (e.target.classList.contains("anchor")) {
// Handle resize
mode = "resize"
id = e.target.dataset.id
side = e.target.dataset.side
} else {
// Handle move
mode = "move"
const component = e.target.closest(".component")
id = component.dataset.id
}
// Find grid parent
const domComponent = getDOMNode(id)
const gridId = domComponent?.closest(".grid")?.parentNode.dataset.id
if (!gridId) {
return
}
// Update state
dragInfo = {
domTarget: e.target,
id,
gridId,
mode,
side,
}
// Add event handler to clear all drag state when dragging ends
dragInfo.domTarget.addEventListener("dragend", stopDragging)
}
// Callback when entering a potential drop target
const onDragEnter = e => {
// Skip if we aren't validly dragging currently
if (!dragInfo || dragInfo.grid) {
return
}
const domGrid = getDOMNode(dragInfo.gridId)
const gridCols = parseInt(domGrid.dataset.cols)
const gridRows = parseInt(domGrid.dataset.rows)
const domNode = getDOMNode(dragInfo.id)
const styles = window.getComputedStyle(domNode)
if (domGrid) {
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
const getStyle = x => parseInt(styles?.[x] || "0")
const getColStyle = x => minMax(getStyle(x), 1, gridCols + 1)
const getRowStyle = x => minMax(getStyle(x), 1, gridRows + 1)
dragInfo.grid = {
startX: e.clientX,
startY: e.clientY,
rowStart: getRowStyle("grid-row-start"),
rowEnd: getRowStyle("grid-row-end"),
colStart: getColStyle("grid-column-start"),
colEnd: getColStyle("grid-column-end"),
rowDeltaMin: 1 - getRowStyle("grid-row-start"),
rowDeltaMax: gridRows + 1 - getRowStyle("grid-row-end"),
colDeltaMin: 1 - getColStyle("grid-column-start"),
colDeltaMax: gridCols + 1 - getColStyle("grid-column-end"),
}
handleEvent(e)
}
}
const onDragOver = e => {
if (!dragInfo?.grid) {
return
}
handleEvent(e)
}
// Callback when drag stops (whether dropped or not)
const stopDragging = async () => {
// Save changes
if (gridStyles) {
await builderStore.actions.updateStyles(gridStyles, dragInfo.id)
}
// Reset listener
if (dragInfo?.domTarget) {
dragInfo.domTarget.removeEventListener("dragend", stopDragging)
}
// Reset state
dragInfo = null
gridStyles = null
}
onMount(() => {
document.addEventListener("dragstart", onDragStart, false)
document.addEventListener("dragenter", onDragEnter, false)
document.addEventListener("dragover", onDragOver, false)
})
onDestroy(() => {
document.removeEventListener("dragstart", onDragStart, false)
document.removeEventListener("dragenter", onDragEnter, false)
document.removeEventListener("dragover", onDragOver, false)
})
</script>