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 {
builderStore,
@ -15,9 +15,37 @@
GridParams,
getGridVar,
Devices,
GridDragModes,
GridDragMode,
} 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")
// Smallest possible 1x1 transparent GIF
@ -25,11 +53,11 @@
ghost.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
let dragInfo
let styles = memo()
let dragInfo: GridDragInfo | undefined
let styles = memo<Record<string, number> | undefined>()
// Grid CSS variables
$: device = $context.device.mobile ? Devices.Mobile : Devices.Desktop
$: device = $context.device?.mobile ? Devices.Mobile : Devices.Desktop
$: vars = {
colStart: getGridVar(device, GridParams.ColStart),
colEnd: getGridVar(device, GridParams.ColEnd),
@ -47,7 +75,10 @@
// Reset when not dragging new components
$: !$dndIsDragging && stopDragging()
const applyStyles = async (instance, styles) => {
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
@ -63,24 +94,28 @@
}
// 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) {
return
}
const { mode, side, grid, domGrid } = dragInfo
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)
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
let deltaY = Math.round(diffY / GridRowHeight)
if (mode === GridDragModes.Move) {
if (mode === GridDragMode.Move) {
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
deltaY = Math.max(deltaY, 1 - rowStart)
const newStyles = {
@ -90,8 +125,9 @@
[vars.rowEnd]: rowEnd + deltaY,
}
styles.set(newStyles)
} else if (mode === GridDragModes.Resize) {
let 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") {
@ -117,7 +153,7 @@
}
})
const handleEvent = e => {
const handleEvent = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
processEvent(e.clientX, e.clientY)
@ -128,7 +164,10 @@
const startDraggingPlaceholder = () => {
const domComponent = document.getElementsByClassName(DNDPlaceholderID)[0]
const domGrid = domComponent?.closest(".grid")
if (!domGrid) {
if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return
}
const styles = getComputedStyle(domComponent)
@ -143,21 +182,21 @@
domComponent,
domGrid,
id: DNDPlaceholderID,
gridId: domGrid.parentNode.dataset.id,
mode: GridDragModes.Move,
gridId: domGrid.parentElement!.dataset.id!,
mode: GridDragMode.Move,
grid: {
startX: bounds.left + bounds.width / 2,
startY: bounds.top + bounds.height / 2,
rowStart: parseInt(styles["grid-row-start"]),
rowEnd: parseInt(styles["grid-row-end"]),
colStart: parseInt(styles["grid-column-start"]),
colEnd: parseInt(styles["grid-column-end"]),
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 => {
const onDragStart = (e: DragEvent) => {
if (!isGridEvent(e)) {
return
}
@ -166,27 +205,30 @@
e.dataTransfer.setDragImage(ghost, 0, 0)
// Extract state
let mode, id, side
let mode: GridDragMode, id: string, side
if (e.target.dataset.indicator === "true") {
mode = e.target.dataset.dragMode
id = e.target.dataset.id
side = e.target.dataset.side
mode = e.target.dataset.dragMode as GridDragMode
id = e.target.dataset.id!
side = e.target.dataset.side as GridDragSide
} else {
// Handle move
mode = GridDragModes.Move
const component = e.target.closest(".component")
id = component.dataset.id
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 === GridDragModes.Move && (e.ctrlKey || e.metaKey)) {
if (mode === GridDragMode.Move && (e.ctrlKey || e.metaKey)) {
builderStore.actions.duplicateComponent(id, "above", false)
}
// Find grid parent and read from DOM
const domComponent = document.getElementsByClassName(id)[0]
const domGrid = domComponent?.closest(".grid")
if (!domGrid) {
if (
!(domComponent instanceof HTMLElement) ||
!(domGrid instanceof HTMLElement)
) {
return
}
const styles = getComputedStyle(domComponent)
@ -202,24 +244,24 @@
domComponent,
domGrid,
id,
gridId: domGrid.parentNode.dataset.id,
gridId: domGrid.parentElement!.dataset.id!,
mode,
side,
grid: {
startX: e.clientX,
startY: e.clientY,
rowStart: parseInt(styles["grid-row-start"]),
rowEnd: parseInt(styles["grid-row-end"]),
colStart: parseInt(styles["grid-column-start"]),
colEnd: parseInt(styles["grid-column-end"]),
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)
dragInfo.domTarget!.addEventListener("dragend", stopDragging, false)
}
const onDragOver = e => {
const onDragOver = (e: DragEvent) => {
if (!dragInfo) {
// Check if we're dragging a new component
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
@ -248,8 +290,8 @@
}
// Reset state
dragInfo = null
styles.set(null)
dragInfo = undefined
styles.set(undefined)
}
onMount(() => {

View File

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

View File

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

View File

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

View File

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