Type grid DND handler
This commit is contained in:
parent
d50a6910fc
commit
732b2575d1
|
@ -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(() => {
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -105,7 +105,7 @@ export type Component = Readable<{
|
|||
errorState: boolean
|
||||
}>
|
||||
|
||||
export type Context = Readable<{}>
|
||||
export type Context = Readable<Record<string, any>>
|
||||
|
||||
let app: ClientApp
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Reference in New Issue