Add working DND placeholder support for grid screens

This commit is contained in:
Andrew Kingston 2025-02-05 14:55:05 +00:00
parent 73d3ce2038
commit 5b5a0d2ba8
No known key found for this signature in database
11 changed files with 185 additions and 53 deletions

View File

@ -223,11 +223,13 @@
})
const onDragStart = (e, component) => {
console.log("DRAG START")
e.dataTransfer.setDragImage(ghost, 0, 0)
previewStore.startDrag(component)
}
const onDragEnd = () => {
console.log("DRAG END")
previewStore.stopDrag()
}
</script>
@ -314,7 +316,6 @@
}
.component:hover {
background: var(--spectrum-global-color-gray-300);
cursor: grab;
}
.component :global(.spectrum-Body) {
line-height: 1.2 !important;

View File

@ -199,8 +199,8 @@
} else if (type === "reload-plugin") {
await componentStore.refreshDefinitions()
} else if (type === "drop-new-component") {
const { component, parent, index } = data
await componentStore.create(component, null, parent, index)
const { component, parent, index, props } = data
await componentStore.create(component, props, parent, index)
} else if (type === "add-parent-component") {
const { componentId, parentType } = data
await componentStore.addParent(componentId, parentType)

View File

@ -1,7 +1,5 @@
import { get } from "svelte/store"
import { BudiStore } from "../BudiStore"
import { componentStore } from "./components"
import { selectedScreen } from "./screens"
type PreviewDevice = "desktop" | "tablet" | "mobile"
type PreviewEventHandler = (name: string, payload?: any) => void
@ -56,21 +54,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
}))
}
async startDrag(componentType: string) {
let componentId
const gridScreen = get(selectedScreen)?.props?.layout === "grid"
if (gridScreen) {
const component = await componentStore.create(componentType, {
_placeholder: true,
_styles: { normal: { opacity: 0 } },
})
componentId = component?._id
}
async startDrag(component: string) {
this.sendEvent("dragging-new-component", {
dragging: true,
componentType,
componentId,
gridScreen,
component,
})
}

View File

@ -8,12 +8,14 @@
dndStore,
dndParent,
dndIsDragging,
isGridScreen,
dndInitialised,
} from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js"
import { DNDPlaceholderID } from "constants"
import { isGridEvent } from "utils/grid"
import { findComponentById } from "@/utils/components.js"
import { isGridEvent } from "@/utils/grid"
import { DNDPlaceholderID } from "@/constants"
const ThrottleRate = 130
@ -219,9 +221,9 @@
processEvent(e.clientX, e.clientY)
}
// Callback when on top of a component.
// Callback when on top of a component
const onDragOver = e => {
if (!source || !target) {
if (!source || !target || $isGridScreen) {
return
}
handleEvent(e)
@ -233,6 +235,14 @@
return
}
// Mark as initialised if this is our first valid drag enter event
if (!$dndInitialised) {
dndStore.actions.markInitialised()
}
if ($isGridScreen) {
return
}
// Find the next valid component to consider dropping over, ignoring nested
// block components
const component = e.target?.closest?.(
@ -262,7 +272,8 @@
builderStore.actions.dropNewComponent(
source.newComponentType,
drop.parent,
drop.index
drop.index,
$dndStore.meta.newComponentProps
)
dropping = false
stopDragging()

View File

@ -1,38 +1,56 @@
<script>
import { onMount } from "svelte"
import { DNDPlaceholderID } from "constants"
import { Utils } from "@budibase/frontend-core"
import { dndInitialised } from "@/stores"
import { DNDPlaceholderID } from "@/constants"
let left, top, height, width
let observing = false
// Observe style changes in the placeholder DOM node and use this to trigger
// a redraw of our overlay (grid screens)
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.attributeName === "style")) {
debouncedUpdate()
}
})
const updatePosition = () => {
let node = document.getElementsByClassName(DNDPlaceholderID)[0]
const insideGrid = node?.dataset.insideGrid === "true"
const wrapperNode = document.getElementsByClassName(DNDPlaceholderID)[0]
let domNode = wrapperNode
const insideGrid = wrapperNode?.dataset.insideGrid === "true"
if (!insideGrid) {
node = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
domNode = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
}
if (!node) {
if (!domNode) {
height = 0
width = 0
} else {
const bounds = node.getBoundingClientRect()
const bounds = domNode.getBoundingClientRect()
left = bounds.left
top = bounds.top
height = bounds.height
width = bounds.width
}
// Initialise observer if not already done
if (!observing && wrapperNode) {
observing = true
observer.observe(wrapperNode, { attributes: true })
}
}
const debouncedUpdate = Utils.domDebounce(updatePosition)
onMount(() => {
const interval = setInterval(debouncedUpdate, 100)
return () => {
observer.disconnect()
clearInterval(interval)
}
})
</script>
{#if left != null && top != null && width && height}
{#if left != null && top != null && width && height && $dndInitialised}
<div
class="overlay"
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
@ -45,7 +63,6 @@
z-index: 800;
background: hsl(160, 64%, 90%);
border-radius: 4px;
transition: all 130ms ease-out;
border: 2px solid var(--spectrum-global-color-static-green-500);
}
</style>

View File

@ -1,15 +1,22 @@
<script>
import { onMount, onDestroy, getContext } from "svelte"
import { builderStore, componentStore } from "stores"
import {
builderStore,
componentStore,
dndIsDragging,
dndIsNewComponent,
dndStore,
isGridScreen,
} from "stores"
import { Utils, memo } from "@budibase/frontend-core"
import { GridRowHeight } from "constants"
import { DNDPlaceholderID, GridRowHeight } from "@/constants"
import {
isGridEvent,
GridParams,
getGridVar,
Devices,
GridDragModes,
} from "utils/grid"
} from "@/utils/grid"
const context = getContext("context")
@ -37,6 +44,21 @@
$: instance = componentStore.actions.getComponentInstance(id)
$: $instance?.setEphemeralStyles($styles)
// Keep DND store up to date with grid styles if dragging a new component
// on to a grid screen
$: {
if ($dndIsNewComponent) {
dndStore.actions.updateNewComponentProps({
_styles: {
normal: $styles,
},
})
}
}
// Reset when not dragging new components
$: !$dndIsDragging && stopDragging()
// Sugar for a combination of both min and max
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
@ -98,6 +120,47 @@
processEvent(e.clientX, e.clientY)
}
const startDraggingPlaceholder = () => {
console.log("START PLACEHOLDER")
const mode = GridDragModes.Move
const id = DNDPlaceholderID
// Find grid parent and read from DOM
const domComponent = document.getElementsByClassName(id)[0]
const domGrid = domComponent?.closest(".grid")
if (!domGrid) {
return
}
const styles = getComputedStyle(domComponent)
const bounds = domComponent.getBoundingClientRect()
// Show as active
domComponent.classList.add("dragging")
domGrid.classList.add("highlight")
builderStore.actions.selectComponent(id)
// Update state
dragInfo = {
domComponent,
domGrid,
id,
gridId: domGrid.parentNode.dataset.id,
mode,
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"]),
},
}
// Add event handler to clear all drag state when dragging ends
console.log("add up listener")
document.addEventListener("mouseup", stopDragging)
}
// Callback when initially starting a drag on a draggable component
const onDragStart = e => {
if (!isGridEvent(e)) {
@ -158,11 +221,15 @@
}
// Add event handler to clear all drag state when dragging ends
dragInfo.domTarget.addEventListener("dragend", stopDragging)
dragInfo.domTarget.addEventListener("dragend", stopDragging, false)
}
const onDragOver = e => {
if (!dragInfo) {
// Check if we're dragging a new component
if ($dndIsDragging && $dndIsNewComponent && $isGridScreen) {
startDraggingPlaceholder()
}
return
}
handleEvent(e)
@ -178,7 +245,7 @@
// Reset DOM
domComponent.classList.remove("dragging")
domGrid.classList.remove("highlight")
domTarget.removeEventListener("dragend", stopDragging)
domTarget?.removeEventListener("dragend", stopDragging)
// Save changes
if ($styles) {
@ -198,5 +265,6 @@
onDestroy(() => {
document.removeEventListener("dragstart", onDragStart, false)
document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("mouseup", stopDragging, false)
})
</script>

View File

@ -36,6 +36,8 @@ import * as internal from "svelte/internal"
window.svelte_internal = internal
window.svelte = svelte
console.log("NEW CLIENT")
// Initialise spectrum icons
// eslint-disable-next-line local-rules/no-budibase-imports
import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-vite.js"
@ -161,14 +163,13 @@ const loadBudibase = async () => {
const block = blockStore.actions.getBlock(data)
block?.eject()
} else if (type === "dragging-new-component") {
const { dragging, component, componentId } = data
const { dragging, component } = data
if (dragging) {
const definition =
componentStore.actions.getComponentDefinition(component)
dndStore.actions.startDraggingNewComponent({
component,
definition,
componentId,
})
} else {
dndStore.actions.reset()

View File

@ -77,11 +77,12 @@ const createBuilderStore = () => {
mode,
})
},
dropNewComponent: (component, parent, index) => {
dropNewComponent: (component, parent, index, props) => {
eventStore.actions.dispatchEvent("drop-new-component", {
component,
parent,
index,
props,
})
},
setEditMode: enabled => {

View File

@ -1,5 +1,7 @@
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
import { derivedMemo } from "@budibase/frontend-core"
import { screenStore } from "@/stores"
import { ScreenslotID } from "@/constants"
const createDndStore = () => {
const initialState = {
@ -11,6 +13,12 @@ const createDndStore = () => {
// Info about where the component would be dropped
drop: null,
// Metadata about the event
meta: {
initialised: false,
newComponentProps: null,
},
}
const store = writable(initialState)
@ -21,35 +29,53 @@ const createDndStore = () => {
})
}
const startDraggingNewComponent = ({
component,
definition,
componentId,
}) => {
const startDraggingNewComponent = ({ component, definition }) => {
console.log("start", component, definition)
if (!component) {
return
}
let target, drop
const screen = get(screenStore)?.activeScreen
const isGridScreen = screen?.props?.layout === "grid"
if (isGridScreen) {
const id = screen?.props?._id
drop = {
parent: id,
index: screen?.props?._children?.length,
}
target = {
id,
parent: ScreenslotID,
node: null,
empty: false,
acceptsChildren: true,
}
}
// Get size of new component so we can show a properly sized placeholder
const width = definition?.size?.width || 128
const height = definition?.size?.height || 64
store.set({
...initialState,
isGridScreen,
source: {
id: null,
parent: null,
bounds: { height, width },
index: null,
newComponentType: component,
newComponentId: componentId,
},
target,
drop,
})
}
const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => {
store.update(state => {
state.target = { id, parent, node, empty, acceptsChildren }
console.log("TARGET", state.target)
return state
})
}
@ -57,6 +83,7 @@ const createDndStore = () => {
const updateDrop = ({ parent, index }) => {
store.update(state => {
state.drop = { parent, index }
console.log("DROP", state.drop)
return state
})
}
@ -65,6 +92,26 @@ const createDndStore = () => {
store.set(initialState)
}
const markInitialised = () => {
store.update(state => ({
...state,
meta: {
...state.meta,
initialised: true,
},
}))
}
const updateNewComponentProps = newComponentProps => {
store.update(state => ({
...state,
meta: {
...state.meta,
newComponentProps,
},
}))
}
return {
subscribe: store.subscribe,
actions: {
@ -73,6 +120,8 @@ const createDndStore = () => {
updateTarget,
updateDrop,
reset,
markInitialised,
updateNewComponentProps,
},
}
}
@ -87,10 +136,7 @@ export const dndParent = derivedMemo(dndStore, x => x.drop?.parent)
export const dndIndex = derivedMemo(dndStore, x => x.drop?.index)
export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds)
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
export const dndNewComponentId = derivedMemo(
dndStore,
x => x.source?.newComponentId
)
export const dndInitialised = derivedMemo(dndStore, x => x.meta.initialised)
export const dndIsNewComponent = derivedMemo(
dndStore,
x => x.source?.newComponentType != null

View File

@ -25,6 +25,7 @@ export {
dndBounds,
dndIsNewComponent,
dndIsDragging,
dndInitialised,
} from "./dnd"
export { sidePanelStore } from "./sidePanel"
export { modalStore } from "./modal"

View File

@ -7,7 +7,7 @@ import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants"
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants"
const createScreenStore = () => {
const store = derived(
@ -99,7 +99,6 @@ const createScreenStore = () => {
normal: {
width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`,
opacity: 0,
"--default-width": $dndBounds?.width || 400,
"--default-height": $dndBounds?.height || 200,
},