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

View File

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

View File

@ -1,7 +1,5 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { BudiStore } from "../BudiStore" import { BudiStore } from "../BudiStore"
import { componentStore } from "./components"
import { selectedScreen } from "./screens"
type PreviewDevice = "desktop" | "tablet" | "mobile" type PreviewDevice = "desktop" | "tablet" | "mobile"
type PreviewEventHandler = (name: string, payload?: any) => void type PreviewEventHandler = (name: string, payload?: any) => void
@ -56,21 +54,10 @@ export class PreviewStore extends BudiStore<PreviewState> {
})) }))
} }
async startDrag(componentType: string) { async startDrag(component: 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
}
this.sendEvent("dragging-new-component", { this.sendEvent("dragging-new-component", {
dragging: true, dragging: true,
componentType, component,
componentId,
gridScreen,
}) })
} }

View File

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

View File

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

View File

@ -1,15 +1,22 @@
<script> <script>
import { onMount, onDestroy, getContext } from "svelte" 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 { Utils, memo } from "@budibase/frontend-core"
import { GridRowHeight } from "constants" import { DNDPlaceholderID, GridRowHeight } from "@/constants"
import { import {
isGridEvent, isGridEvent,
GridParams, GridParams,
getGridVar, getGridVar,
Devices, Devices,
GridDragModes, GridDragModes,
} from "utils/grid" } from "@/utils/grid"
const context = getContext("context") const context = getContext("context")
@ -37,6 +44,21 @@
$: instance = componentStore.actions.getComponentInstance(id) $: instance = componentStore.actions.getComponentInstance(id)
$: $instance?.setEphemeralStyles($styles) $: $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 // Sugar for a combination of both min and max
const minMax = (value, min, max) => Math.min(max, Math.max(min, value)) const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
@ -98,6 +120,47 @@
processEvent(e.clientX, e.clientY) 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 // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = e => {
if (!isGridEvent(e)) { if (!isGridEvent(e)) {
@ -158,11 +221,15 @@
} }
// 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) dragInfo.domTarget.addEventListener("dragend", stopDragging, false)
} }
const onDragOver = e => { const onDragOver = e => {
if (!dragInfo) { if (!dragInfo) {
// Check if we're dragging a new component
if ($dndIsDragging && $dndIsNewComponent && $isGridScreen) {
startDraggingPlaceholder()
}
return return
} }
handleEvent(e) handleEvent(e)
@ -178,7 +245,7 @@
// Reset DOM // Reset DOM
domComponent.classList.remove("dragging") domComponent.classList.remove("dragging")
domGrid.classList.remove("highlight") domGrid.classList.remove("highlight")
domTarget.removeEventListener("dragend", stopDragging) domTarget?.removeEventListener("dragend", stopDragging)
// Save changes // Save changes
if ($styles) { if ($styles) {
@ -198,5 +265,6 @@
onDestroy(() => { onDestroy(() => {
document.removeEventListener("dragstart", onDragStart, false) document.removeEventListener("dragstart", onDragStart, false)
document.removeEventListener("dragover", onDragOver, false) document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("mouseup", stopDragging, false)
}) })
</script> </script>

View File

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

View File

@ -77,11 +77,12 @@ const createBuilderStore = () => {
mode, mode,
}) })
}, },
dropNewComponent: (component, parent, index) => { dropNewComponent: (component, parent, index, props) => {
eventStore.actions.dispatchEvent("drop-new-component", { eventStore.actions.dispatchEvent("drop-new-component", {
component, component,
parent, parent,
index, index,
props,
}) })
}, },
setEditMode: enabled => { 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 { derivedMemo } from "@budibase/frontend-core"
import { screenStore } from "@/stores"
import { ScreenslotID } from "@/constants"
const createDndStore = () => { const createDndStore = () => {
const initialState = { const initialState = {
@ -11,6 +13,12 @@ const createDndStore = () => {
// Info about where the component would be dropped // Info about where the component would be dropped
drop: null, drop: null,
// Metadata about the event
meta: {
initialised: false,
newComponentProps: null,
},
} }
const store = writable(initialState) const store = writable(initialState)
@ -21,35 +29,53 @@ const createDndStore = () => {
}) })
} }
const startDraggingNewComponent = ({ const startDraggingNewComponent = ({ component, definition }) => {
component, console.log("start", component, definition)
definition,
componentId,
}) => {
if (!component) { if (!component) {
return 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 // Get size of new component so we can show a properly sized placeholder
const width = definition?.size?.width || 128 const width = definition?.size?.width || 128
const height = definition?.size?.height || 64 const height = definition?.size?.height || 64
store.set({ store.set({
...initialState, ...initialState,
isGridScreen,
source: { source: {
id: null, id: null,
parent: null, parent: null,
bounds: { height, width }, bounds: { height, width },
index: null, index: null,
newComponentType: component, newComponentType: component,
newComponentId: componentId,
}, },
target,
drop,
}) })
} }
const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => { const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => {
store.update(state => { store.update(state => {
state.target = { id, parent, node, empty, acceptsChildren } state.target = { id, parent, node, empty, acceptsChildren }
console.log("TARGET", state.target)
return state return state
}) })
} }
@ -57,6 +83,7 @@ const createDndStore = () => {
const updateDrop = ({ parent, index }) => { const updateDrop = ({ parent, index }) => {
store.update(state => { store.update(state => {
state.drop = { parent, index } state.drop = { parent, index }
console.log("DROP", state.drop)
return state return state
}) })
} }
@ -65,6 +92,26 @@ const createDndStore = () => {
store.set(initialState) 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 { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
@ -73,6 +120,8 @@ const createDndStore = () => {
updateTarget, updateTarget,
updateDrop, updateDrop,
reset, 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 dndIndex = derivedMemo(dndStore, x => x.drop?.index)
export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds) export const dndBounds = derivedMemo(dndStore, x => x.source?.bounds)
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source) export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
export const dndNewComponentId = derivedMemo( export const dndInitialised = derivedMemo(dndStore, x => x.meta.initialised)
dndStore,
x => x.source?.newComponentId
)
export const dndIsNewComponent = derivedMemo( export const dndIsNewComponent = derivedMemo(
dndStore, dndStore,
x => x.source?.newComponentType != null x => x.source?.newComponentType != null

View File

@ -25,6 +25,7 @@ export {
dndBounds, dndBounds,
dndIsNewComponent, dndIsNewComponent,
dndIsDragging, dndIsDragging,
dndInitialised,
} from "./dnd" } from "./dnd"
export { sidePanelStore } from "./sidePanel" export { sidePanelStore } from "./sidePanel"
export { modalStore } from "./modal" 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 { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "constants" import { DNDPlaceholderID, ScreenslotID, ScreenslotType } from "@/constants"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -99,7 +99,6 @@ const createScreenStore = () => {
normal: { normal: {
width: `${$dndBounds?.width || 400}px`, width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`, height: `${$dndBounds?.height || 200}px`,
opacity: 0,
"--default-width": $dndBounds?.width || 400, "--default-width": $dndBounds?.width || 400,
"--default-height": $dndBounds?.height || 200, "--default-height": $dndBounds?.height || 200,
}, },