316 lines
9.5 KiB
Svelte
316 lines
9.5 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy, getContext } from "svelte"
|
|
import {
|
|
builderStore,
|
|
componentStore,
|
|
dndIsDragging,
|
|
dndStore,
|
|
dndSource,
|
|
isGridScreen,
|
|
} from "@/stores"
|
|
import { Utils, memo } from "@budibase/frontend-core"
|
|
import { DNDPlaceholderID, GridRowHeight } from "@/constants"
|
|
import {
|
|
isGridEvent,
|
|
GridParams,
|
|
getGridVar,
|
|
Devices,
|
|
GridDragMode,
|
|
} from "@/utils/grid"
|
|
import { DropPosition } from "@budibase/types"
|
|
|
|
type GridDragSide =
|
|
| "top"
|
|
| "right"
|
|
| "bottom"
|
|
| "left"
|
|
| "top-left"
|
|
| "top-right"
|
|
| "bottom-left"
|
|
| "bottom-right"
|
|
|
|
interface GridDragInfo {
|
|
mode: GridDragMode
|
|
side?: GridDragSide
|
|
domTarget?: HTMLElement
|
|
domComponent: HTMLElement
|
|
domGrid: HTMLElement
|
|
id: string
|
|
gridId: string
|
|
grid: {
|
|
startX: number
|
|
startY: number
|
|
rowStart: number
|
|
rowEnd: number
|
|
colStart: number
|
|
colEnd: number
|
|
}
|
|
}
|
|
|
|
const context = getContext("context")
|
|
|
|
// Smallest possible 1x1 transparent GIF
|
|
const ghost = new Image(1, 1)
|
|
ghost.src =
|
|
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
|
|
|
let scrollElement: HTMLElement
|
|
let dragInfo: GridDragInfo | undefined
|
|
let styles = memo<Record<string, number> | undefined>()
|
|
|
|
// Grid CSS variables
|
|
$: device = $context.device?.mobile ? Devices.Mobile : Devices.Desktop
|
|
$: vars = {
|
|
colStart: getGridVar(device, GridParams.ColStart),
|
|
colEnd: getGridVar(device, GridParams.ColEnd),
|
|
rowStart: getGridVar(device, GridParams.RowStart),
|
|
rowEnd: getGridVar(device, GridParams.RowEnd),
|
|
}
|
|
|
|
// Some memoisation of primitive types for performance
|
|
$: id = dragInfo?.id
|
|
|
|
// Set ephemeral styles
|
|
$: instance = componentStore.actions.getComponentInstance(id)
|
|
$: applyStyles($instance, $styles)
|
|
|
|
// Reset when not dragging new components
|
|
$: !$dndIsDragging && stopDragging()
|
|
|
|
const scrollOffset = () => scrollElement?.scrollTop || 0
|
|
|
|
const applyStyles = async (
|
|
instance: any,
|
|
styles: Record<string, number> | undefined
|
|
) => {
|
|
instance?.setEphemeralStyles(styles)
|
|
|
|
// If dragging a new component on to a grid screen, tick to allow the
|
|
// real component to render in the new position before updating the DND
|
|
// store, preventing the green DND overlay from being out of position
|
|
if ($dndSource?.isNew && styles) {
|
|
dndStore.actions.updateNewComponentProps({
|
|
_styles: {
|
|
normal: styles,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sugar for a combination of both min and max
|
|
const minMax = (value: number, min: number, max: number) =>
|
|
Math.min(max, Math.max(min, value))
|
|
|
|
const processEvent = Utils.domDebounce((mouseX: number, mouseY: number) => {
|
|
if (!dragInfo?.grid) {
|
|
return
|
|
}
|
|
const { mode, grid, domGrid } = dragInfo
|
|
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
|
|
if (!domGrid) {
|
|
return
|
|
}
|
|
const cols = parseInt(domGrid.dataset.cols || "")
|
|
const colSize = parseInt(domGrid.dataset.colSize || "")
|
|
if (isNaN(cols) || isNaN(colSize)) {
|
|
throw "DOM grid missing required dataset attributes"
|
|
}
|
|
const diffX = mouseX - startX
|
|
let deltaX = Math.round(diffX / colSize)
|
|
const diffY = mouseY - startY + scrollOffset()
|
|
let deltaY = Math.round(diffY / GridRowHeight)
|
|
if (mode === GridDragMode.Move) {
|
|
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
|
|
deltaY = Math.max(deltaY, 1 - rowStart)
|
|
const newStyles = {
|
|
[vars.colStart]: colStart + deltaX,
|
|
[vars.colEnd]: colEnd + deltaX,
|
|
[vars.rowStart]: rowStart + deltaY,
|
|
[vars.rowEnd]: rowEnd + deltaY,
|
|
}
|
|
styles.set(newStyles)
|
|
} else if (mode === GridDragMode.Resize) {
|
|
const { side } = dragInfo
|
|
let newStyles: Record<string, number> = {}
|
|
if (side === "right") {
|
|
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
|
|
} else if (side === "left") {
|
|
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
|
|
} else if (side === "top") {
|
|
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
|
|
} else if (side === "bottom") {
|
|
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
|
|
} else if (side === "bottom-right") {
|
|
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
|
|
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
|
|
} else if (side === "bottom-left") {
|
|
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
|
|
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
|
|
} else if (side === "top-right") {
|
|
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
|
|
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
|
|
} else if (side === "top-left") {
|
|
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
|
|
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
|
|
}
|
|
styles.set(newStyles)
|
|
}
|
|
})
|
|
|
|
const handleEvent = (e: DragEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
processEvent(e.clientX, e.clientY)
|
|
}
|
|
|
|
// Callback when dragging a new component over the preview iframe in a valid
|
|
// position for the first time
|
|
const startDraggingPlaceholder = () => {
|
|
const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0]
|
|
const domGrid = domComponent?.closest(".grid")
|
|
if (
|
|
!(domComponent instanceof HTMLElement) ||
|
|
!(domGrid instanceof HTMLElement)
|
|
) {
|
|
return
|
|
}
|
|
const styles = getComputedStyle(domComponent)
|
|
const bounds = domComponent.getBoundingClientRect()
|
|
|
|
// Show as active
|
|
domComponent.classList.add("dragging")
|
|
domGrid.classList.add("highlight")
|
|
|
|
// Update state
|
|
dragInfo = {
|
|
domComponent,
|
|
domGrid,
|
|
id: DNDPlaceholderID,
|
|
gridId: domGrid.parentElement!.dataset.id!,
|
|
mode: GridDragMode.Move,
|
|
grid: {
|
|
startX: bounds.left + bounds.width / 2,
|
|
startY: bounds.top + bounds.height / 2 + scrollOffset(),
|
|
rowStart: parseInt(styles.gridRowStart),
|
|
rowEnd: parseInt(styles.gridRowEnd),
|
|
colStart: parseInt(styles.gridColumnStart),
|
|
colEnd: parseInt(styles.gridColumnEnd),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Callback when initially starting a drag on a draggable component
|
|
const onDragStart = (e: DragEvent) => {
|
|
if (!isGridEvent(e)) {
|
|
return
|
|
}
|
|
|
|
// Hide drag ghost image
|
|
e.dataTransfer.setDragImage(ghost, 0, 0)
|
|
|
|
// Extract state
|
|
let mode: GridDragMode, id: string, side
|
|
if (e.target.dataset.indicator === "true") {
|
|
mode = e.target.dataset.dragMode as GridDragMode
|
|
id = e.target.dataset.id!
|
|
side = e.target.dataset.side as GridDragSide
|
|
} else {
|
|
// Handle move
|
|
mode = GridDragMode.Move
|
|
const component = e.target.closest(".component") as HTMLElement
|
|
id = component.dataset.id!
|
|
}
|
|
|
|
// If holding ctrl/cmd then leave behind a duplicate of this component
|
|
if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
|
|
builderStore.actions.duplicateComponent(id, DropPosition.ABOVE, false)
|
|
}
|
|
|
|
// Find grid parent and read from DOM
|
|
const domComponent = document.getElementsByClassName(id)[0]
|
|
const domGrid = domComponent?.closest(".grid")
|
|
if (
|
|
!(domComponent instanceof HTMLElement) ||
|
|
!(domGrid instanceof HTMLElement)
|
|
) {
|
|
return
|
|
}
|
|
const styles = getComputedStyle(domComponent)
|
|
|
|
// Show as active
|
|
domComponent.classList.add("dragging")
|
|
domGrid.classList.add("highlight")
|
|
builderStore.actions.selectComponent(id)
|
|
|
|
// Update state
|
|
dragInfo = {
|
|
domTarget: e.target,
|
|
domComponent,
|
|
domGrid,
|
|
id,
|
|
gridId: domGrid.parentElement!.dataset.id!,
|
|
mode,
|
|
side,
|
|
grid: {
|
|
startX: e.clientX,
|
|
startY: e.clientY + scrollOffset(),
|
|
rowStart: parseInt(styles.gridRowStart),
|
|
rowEnd: parseInt(styles.gridRowEnd),
|
|
colStart: parseInt(styles.gridColumnStart),
|
|
colEnd: parseInt(styles.gridColumnEnd),
|
|
},
|
|
}
|
|
|
|
// Add event handler to clear all drag state when dragging ends
|
|
dragInfo.domTarget!.addEventListener("dragend", stopDragging, false)
|
|
}
|
|
|
|
const onDragOver = (e: DragEvent) => {
|
|
if (!dragInfo) {
|
|
// Check if we're dragging a new component
|
|
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
|
|
startDraggingPlaceholder()
|
|
}
|
|
return
|
|
}
|
|
handleEvent(e)
|
|
}
|
|
|
|
// Callback when drag stops (whether dropped or not)
|
|
const stopDragging = async () => {
|
|
if (!dragInfo) {
|
|
return
|
|
}
|
|
const { id, domTarget, domGrid, domComponent } = dragInfo
|
|
|
|
// Reset DOM
|
|
domComponent.classList.remove("dragging")
|
|
domGrid.classList.remove("highlight")
|
|
domTarget?.removeEventListener("dragend", stopDragging)
|
|
|
|
// Save changes
|
|
if ($styles) {
|
|
builderStore.actions.updateStyles($styles, id)
|
|
}
|
|
|
|
// Reset state
|
|
dragInfo = undefined
|
|
styles.set(undefined)
|
|
}
|
|
|
|
onMount(() => {
|
|
scrollElement = document.getElementsByClassName(
|
|
"screen-wrapper"
|
|
)[0] as HTMLElement
|
|
document.addEventListener("dragstart", onDragStart, false)
|
|
document.addEventListener("dragover", onDragOver, false)
|
|
document.addEventListener("scroll", processEvent)
|
|
})
|
|
|
|
onDestroy(() => {
|
|
document.removeEventListener("dragstart", onDragStart, false)
|
|
document.removeEventListener("dragover", onDragOver, false)
|
|
document.removeEventListener("scroll", processEvent)
|
|
})
|
|
</script>
|