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 { 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(() => {
|
||||||
|
|
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
Loading…
Reference in New Issue