Type grid DND handler

This commit is contained in:
Andrew Kingston 2025-02-06 14:21:39 +00:00
parent d50a6910fc
commit 732b2575d1
No known key found for this signature in database
5 changed files with 110 additions and 63 deletions

View File

@ -1,4 +1,4 @@
<script> <script lang="ts">
import { onMount, onDestroy, getContext } from "svelte" import { onMount, onDestroy, getContext } from "svelte"
import { import {
builderStore, builderStore,
@ -15,9 +15,37 @@
GridParams, GridParams,
getGridVar, getGridVar,
Devices, Devices,
GridDragModes, GridDragMode,
} from "@/utils/grid" } from "@/utils/grid"
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") const context = getContext("context")
// Smallest possible 1x1 transparent GIF // Smallest possible 1x1 transparent GIF
@ -25,11 +53,11 @@
ghost.src = ghost.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
let dragInfo let dragInfo: GridDragInfo | undefined
let styles = memo() let styles = memo<Record<string, number> | undefined>()
// Grid CSS variables // Grid CSS variables
$: device = $context.device.mobile ? Devices.Mobile : Devices.Desktop $: device = $context.device?.mobile ? Devices.Mobile : Devices.Desktop
$: vars = { $: vars = {
colStart: getGridVar(device, GridParams.ColStart), colStart: getGridVar(device, GridParams.ColStart),
colEnd: getGridVar(device, GridParams.ColEnd), colEnd: getGridVar(device, GridParams.ColEnd),
@ -47,7 +75,10 @@
// Reset when not dragging new components // Reset when not dragging new components
$: !$dndIsDragging && stopDragging() $: !$dndIsDragging && stopDragging()
const applyStyles = async (instance, styles) => { const applyStyles = async (
instance: any,
styles: Record<string, number> | undefined
) => {
instance?.setEphemeralStyles(styles) instance?.setEphemeralStyles(styles)
// If dragging a new component on to a grid screen, tick to allow the // If dragging a new component on to a grid screen, tick to allow the
@ -63,24 +94,28 @@
} }
// Sugar for a combination of both min and max // Sugar for a combination of both min and max
const minMax = (value, min, max) => Math.min(max, Math.max(min, value)) const minMax = (value: number, min: number, max: number) =>
Math.min(max, Math.max(min, value))
const processEvent = Utils.domDebounce((mouseX, mouseY) => { const processEvent = Utils.domDebounce((mouseX: number, mouseY: number) => {
if (!dragInfo?.grid) { if (!dragInfo?.grid) {
return return
} }
const { mode, side, grid, domGrid } = dragInfo const { mode, grid, domGrid } = dragInfo
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
if (!domGrid) { if (!domGrid) {
return return
} }
const cols = parseInt(domGrid.dataset.cols) const cols = parseInt(domGrid.dataset.cols || "")
const colSize = parseInt(domGrid.dataset.colSize) const colSize = parseInt(domGrid.dataset.colSize || "")
if (isNaN(cols) || isNaN(colSize)) {
throw "DOM grid missing required dataset attributes"
}
const diffX = mouseX - startX const diffX = mouseX - startX
let deltaX = Math.round(diffX / colSize) let deltaX = Math.round(diffX / colSize)
const diffY = mouseY - startY const diffY = mouseY - startY
let deltaY = Math.round(diffY / GridRowHeight) let deltaY = Math.round(diffY / GridRowHeight)
if (mode === GridDragModes.Move) { if (mode === GridDragMode.Move) {
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd) deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
deltaY = Math.max(deltaY, 1 - rowStart) deltaY = Math.max(deltaY, 1 - rowStart)
const newStyles = { const newStyles = {
@ -90,8 +125,9 @@
[vars.rowEnd]: rowEnd + deltaY, [vars.rowEnd]: rowEnd + deltaY,
} }
styles.set(newStyles) styles.set(newStyles)
} else if (mode === GridDragModes.Resize) { } else if (mode === GridDragMode.Resize) {
let newStyles = {} const { side } = dragInfo
let newStyles: Record<string, number> = {}
if (side === "right") { if (side === "right") {
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1) newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
} else if (side === "left") { } else if (side === "left") {
@ -117,7 +153,7 @@
} }
}) })
const handleEvent = e => { const handleEvent = (e: DragEvent) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
processEvent(e.clientX, e.clientY) processEvent(e.clientX, e.clientY)
@ -128,7 +164,10 @@
const startDraggingPlaceholder = () => { const startDraggingPlaceholder = () => {
const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0] const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0]
const domGrid = domComponent?.closest(".grid") const domGrid = domComponent?.closest(".grid")
if (!domGrid) { if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return return
} }
const styles = getComputedStyle(domComponent) const styles = getComputedStyle(domComponent)
@ -143,21 +182,21 @@
domComponent, domComponent,
domGrid, domGrid,
id: DNDPlaceholderID, id: DNDPlaceholderID,
gridId: domGrid.parentNode.dataset.id, gridId: domGrid.parentElement!.dataset.id!,
mode: GridDragModes.Move, mode: GridDragMode.Move,
grid: { grid: {
startX: bounds.left + bounds.width / 2, startX: bounds.left + bounds.width / 2,
startY: bounds.top + bounds.height / 2, startY: bounds.top + bounds.height / 2,
rowStart: parseInt(styles["grid-row-start"]), rowStart: parseInt(styles.gridRowStart),
rowEnd: parseInt(styles["grid-row-end"]), rowEnd: parseInt(styles.gridRowEnd),
colStart: parseInt(styles["grid-column-start"]), colStart: parseInt(styles.gridColumnStart),
colEnd: parseInt(styles["grid-column-end"]), colEnd: parseInt(styles.gridColumnEnd),
}, },
} }
} }
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = (e: DragEvent) => {
if (!isGridEvent(e)) { if (!isGridEvent(e)) {
return return
} }
@ -166,27 +205,30 @@
e.dataTransfer.setDragImage(ghost, 0, 0) e.dataTransfer.setDragImage(ghost, 0, 0)
// Extract state // Extract state
let mode, id, side let mode: GridDragMode, id: string, side
if (e.target.dataset.indicator === "true") { if (e.target.dataset.indicator === "true") {
mode = e.target.dataset.dragMode mode = e.target.dataset.dragMode as GridDragMode
id = e.target.dataset.id id = e.target.dataset.id!
side = e.target.dataset.side side = e.target.dataset.side as GridDragSide
} else { } else {
// Handle move // Handle move
mode = GridDragModes.Move mode = GridDragMode.Move
const component = e.target.closest(".component") const component = e.target.closest(".component") as HTMLElement
id = component.dataset.id id = component.dataset.id!
} }
// If holding ctrl/cmd then leave behind a duplicate of this component // If holding ctrl/cmd then leave behind a duplicate of this component
if (mode === GridDragModes.Move && (e.ctrlKey || e.metaKey)) { if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
builderStore.actions.duplicateComponent(id, "above", false) builderStore.actions.duplicateComponent(id, "above", false)
} }
// Find grid parent and read from DOM // Find grid parent and read from DOM
const domComponent = document.getElementsByClassName(id)[0] const domComponent = document.getElementsByClassName(id)[0]
const domGrid = domComponent?.closest(".grid") const domGrid = domComponent?.closest(".grid")
if (!domGrid) { if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return return
} }
const styles = getComputedStyle(domComponent) const styles = getComputedStyle(domComponent)
@ -202,24 +244,24 @@
domComponent, domComponent,
domGrid, domGrid,
id, id,
gridId: domGrid.parentNode.dataset.id, gridId: domGrid.parentElement!.dataset.id!,
mode, mode,
side, side,
grid: { grid: {
startX: e.clientX, startX: e.clientX,
startY: e.clientY, startY: e.clientY,
rowStart: parseInt(styles["grid-row-start"]), rowStart: parseInt(styles.gridRowStart),
rowEnd: parseInt(styles["grid-row-end"]), rowEnd: parseInt(styles.gridRowEnd),
colStart: parseInt(styles["grid-column-start"]), colStart: parseInt(styles.gridColumnStart),
colEnd: parseInt(styles["grid-column-end"]), colEnd: parseInt(styles.gridColumnEnd),
}, },
} }
// Add event handler to clear all drag state when dragging ends // Add event handler to clear all drag state when dragging ends
dragInfo.domTarget.addEventListener("dragend", stopDragging, false) dragInfo.domTarget!.addEventListener("dragend", stopDragging, false)
} }
const onDragOver = e => { const onDragOver = (e: DragEvent) => {
if (!dragInfo) { if (!dragInfo) {
// Check if we're dragging a new component // Check if we're dragging a new component
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) { if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
@ -248,8 +290,8 @@
} }
// Reset state // Reset state
dragInfo = null dragInfo = undefined
styles.set(null) styles.set(undefined)
} }
onMount(() => { onMount(() => {

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { GridDragModes } from "@/utils/grid" import { GridDragMode } from "@/utils/grid"
export let top: number export let top: number
export let left: number export let left: number
@ -50,7 +50,7 @@
class:right={alignRight} class:right={alignRight}
draggable="true" draggable="true"
data-indicator="true" data-indicator="true"
data-drag-mode={GridDragModes.Move} data-drag-mode={GridDragMode.Move}
data-id={componentId} data-id={componentId}
> >
{#if icon} {#if icon}
@ -69,7 +69,7 @@
class="anchor {side}" class="anchor {side}"
draggable="true" draggable="true"
data-indicator="true" data-indicator="true"
data-drag-mode={GridDragModes.Resize} data-drag-mode={GridDragMode.Resize}
data-side={side} data-side={side}
data-id={componentId} data-id={componentId}
> >

View File

@ -105,7 +105,7 @@ export type Component = Readable<{
errorState: boolean errorState: boolean
}> }>
export type Context = Readable<{}> export type Context = Readable<Record<string, any>>
let app: ClientApp let app: ClientApp

View File

@ -37,38 +37,43 @@ interface GridMetadata {
*/ */
// Enum representing the different CSS variables we use for grid metadata // Enum representing the different CSS variables we use for grid metadata
export const GridParams = { export enum GridParams {
HAlign: "h-align", HAlign = "h-align",
VAlign: "v-align", VAlign = "v-align",
ColStart: "col-start", ColStart = "col-start",
ColEnd: "col-end", ColEnd = "col-end",
RowStart: "row-start", RowStart = "row-start",
RowEnd: "row-end", RowEnd = "row-end",
} }
// Classes used in selectors inside grid containers to control child styles // Classes used in selectors inside grid containers to control child styles
export const GridClasses = { export enum GridClasses {
DesktopFill: "grid-desktop-grow", DesktopFill = "grid-desktop-grow",
MobileFill: "grid-mobile-grow", MobileFill = "grid-mobile-grow",
} }
// Enum for device preview type, included in grid CSS variables // Enum for device preview type, included in grid CSS variables
export const Devices = { export enum Devices {
Desktop: "desktop", Desktop = "desktop",
Mobile: "mobile", Mobile = "mobile",
} }
export const GridDragModes = { export enum GridDragMode {
Resize: "resize", Resize = "resize",
Move: "move", Move = "move",
} }
// Builds a CSS variable name for a certain piece of grid metadata // Builds a CSS variable name for a certain piece of grid metadata
export const getGridVar = (device: string, param: string) => export const getGridVar = (device: string, param: string) =>
`--grid-${device}-${param}` `--grid-${device}-${param}`
export interface GridEvent extends DragEvent {
target: HTMLElement
dataTransfer: DataTransfer
}
// Determines whether a JS event originated from immediately within a grid // Determines whether a JS event originated from immediately within a grid
export const isGridEvent = (e: Event): boolean => { export const isGridEvent = (e: Event): e is GridEvent => {
if (!(e.target instanceof HTMLElement)) { if (!(e.target instanceof HTMLElement)) {
return false return false
} }

View File

@ -1,7 +1,7 @@
import { Readable, Writable } from "svelte/store" import { Readable, Writable } from "svelte/store"
declare module "./memo" { declare module "./memo" {
export function memo<T>(value: T): Writable<T> export function memo<T>(value?: T): Writable<T>
export function derivedMemo<TStore, TResult>( export function derivedMemo<TStore, TResult>(
store: Readable<TStore>, store: Readable<TStore>,