Use new technique for DND selection indicators

This commit is contained in:
Andrew Kingston 2025-02-06 09:13:20 +00:00
parent 0471e113e3
commit 0ad0ded2e2
No known key found for this signature in database
11 changed files with 188 additions and 148 deletions

View File

@ -44,7 +44,7 @@
import MaintenanceScreen from "components/MaintenanceScreen.svelte"
import SnippetsProvider from "./context/SnippetsProvider.svelte"
import EmbedProvider from "./context/EmbedProvider.svelte"
import GridNewComponentDNDHandler from "components/preview/GridNewComponentDNDHandler.svelte"
import DNDSelectionIndicators from "./preview/DNDSelectionIndicators.svelte"
// Provide contexts
setContext("sdk", SDK)
@ -267,7 +267,7 @@
{#if $builderStore.inBuilder}
<DNDHandler />
<GridDNDHandler />
<GridNewComponentDNDHandler />
<DNDSelectionIndicators />
{/if}
</div>
</SnippetsProvider>

View File

@ -120,7 +120,7 @@
$: children = instance._children || []
$: id = instance._id
$: name = isRoot ? "Screen" : instance._instanceName
$: icon = definition?.icon
$: icon = instance._icon || definition?.icon
// Determine if the component is selected or is part of the critical path
// leading to the selected component

View File

@ -7,10 +7,10 @@
screenStore,
dndStore,
dndParent,
dndSource,
dndIsDragging,
isGridScreen,
} from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "@/utils/components.js"
import { isGridEvent } from "@/utils/grid"
@ -92,6 +92,8 @@
bounds: component.children[0].getBoundingClientRect(),
parent: parentId,
index,
name: component.dataset.name,
icon: component.dataset.icon,
})
builderStore.actions.selectComponent(id)
@ -258,10 +260,10 @@
}
// Check if we're adding a new component rather than moving one
if (source.newComponentType) {
if (source.isNew) {
dropping = true
builderStore.actions.dropNewComponent(
source.newComponentType,
source.type,
drop.parent,
drop.index,
$dndStore.meta.newComponentProps
@ -335,16 +337,3 @@
document.removeEventListener("drop", onDrop, false)
})
</script>
{#if !$isGridScreen}
<IndicatorSet
componentId={$dndParent}
color="var(--spectrum-global-color-static-green-500)"
zIndex={920}
prefix="Inside"
/>
{/if}
{#if $dndIsDragging}
<DNDPlaceholderOverlay />
{/if}

View File

@ -1,96 +1,108 @@
<script>
import { onMount, tick } from "svelte"
import { Utils } from "@budibase/frontend-core"
import { componentStore, isGridScreen } from "@/stores"
import { DNDPlaceholderID } from "@/constants"
<!--<script>-->
<!-- import { onMount, tick } from "svelte"-->
<!-- import { Utils } from "@budibase/frontend-core"-->
<!-- import { componentStore, dndNewComponentType, isGridScreen } from "@/stores"-->
<!-- import { DNDPlaceholderID } from "@/constants"-->
<!-- import IndicatorSet from "components/preview/IndicatorSet.svelte"-->
let left, top, height, width
let observing = false
let hasGridStyles = false
<!-- let left, top, height, width-->
<!-- let observing = false-->
<!-- let hasGridStyles = false-->
// On grid screens, we need to wait for grid styles to be properly set on
// the hidden placeholder component before rendering this overlay
$: waitingForGrid = $isGridScreen && !hasGridStyles
$: instance = componentStore.actions.getComponentInstance(DNDPlaceholderID)
$: state = $instance?.state
$: styles = $state?.styles?.normal || {}
$: {
if ($isGridScreen && !hasGridStyles) {
checkGridStyles(styles)
}
}
<!-- // On grid screens, we need to wait for grid styles to be properly set on-->
<!-- // the hidden placeholder component before rendering this overlay-->
<!-- $: waitingForGrid = $isGridScreen && !hasGridStyles-->
<!-- $: instance = componentStore.actions.getComponentInstance(DNDPlaceholderID)-->
<!-- $: state = $instance?.state-->
<!-- $: styles = $state?.styles?.normal || {}-->
<!-- $: {-->
<!-- if ($isGridScreen && !hasGridStyles) {-->
<!-- checkGridStyles(styles)-->
<!-- }-->
<!-- }-->
// Wait for grid styles to be set, then tick and await a position update
// before finally signalling we're allowed to render
const checkGridStyles = async styles => {
const hasStyles = Object.keys(styles).some(key => key.startsWith("--grid"))
if (hasStyles) {
await tick()
updatePosition()
hasGridStyles = true
}
}
<!-- // We pull the component name from the definition-->
<!-- $: definition =-->
<!-- componentStore.actions.getComponentDefinition($dndNewComponentType)-->
// Observe style changes in the placeholder DOM node and use this to trigger
// a redraw of our overlay
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => mutation.attributeName === "style")) {
debouncedUpdate()
}
})
<!-- // Wait for grid styles to be set, then tick and await a position update-->
<!-- // before finally signalling we're allowed to render-->
<!-- const checkGridStyles = async styles => {-->
<!-- const hasStyles = Object.keys(styles).some(key => key.startsWith("&#45;&#45;grid"))-->
<!-- if (hasStyles) {-->
<!-- await tick()-->
<!-- updatePosition()-->
<!-- hasGridStyles = true-->
<!-- }-->
<!-- }-->
const updatePosition = () => {
const wrapperNode = document.getElementsByClassName(DNDPlaceholderID)[0]
let domNode = wrapperNode
const insideGrid = wrapperNode?.dataset.insideGrid === "true"
if (!insideGrid) {
domNode = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]
}
if (!domNode) {
height = 0
width = 0
} else {
const bounds = domNode.getBoundingClientRect()
left = bounds.left
top = bounds.top
height = bounds.height
width = bounds.width
}
<!-- // Observe style changes in the placeholder DOM node and use this to trigger-->
<!-- // a redraw of our overlay-->
<!-- const observer = new MutationObserver(mutations => {-->
<!-- if (mutations.some(mutation => mutation.attributeName === "style")) {-->
<!-- debouncedUpdate()-->
<!-- }-->
<!-- })-->
// Initialise observer if not already done
if (!observing && wrapperNode) {
observing = true
observer.observe(wrapperNode, { attributes: true })
}
}
const debouncedUpdate = Utils.domDebounce(updatePosition)
<!-- const updatePosition = () => {-->
<!-- const wrapperNode = document.getElementsByClassName(DNDPlaceholderID)[0]-->
<!-- let domNode = wrapperNode-->
<!-- const insideGrid = wrapperNode?.dataset.insideGrid === "true"-->
<!-- if (!insideGrid) {-->
<!-- domNode = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]-->
<!-- }-->
<!-- if (!domNode) {-->
<!-- height = 0-->
<!-- width = 0-->
<!-- } else {-->
<!-- const bounds = domNode.getBoundingClientRect()-->
<!-- left = bounds.left-->
<!-- top = bounds.top-->
<!-- height = bounds.height-->
<!-- width = bounds.width-->
<!-- }-->
onMount(() => {
const interval = setInterval(debouncedUpdate, 100)
return () => {
observer.disconnect()
clearInterval(interval)
}
})
</script>
<!-- // Initialise observer if not already done-->
<!-- if (!observing && wrapperNode) {-->
<!-- observing = true-->
<!-- observer.observe(wrapperNode, { attributes: true })-->
<!-- }-->
<!-- }-->
<!-- const debouncedUpdate = Utils.domDebounce(updatePosition)-->
{#if left != null && top != null && width && height && !waitingForGrid}
<div
class="overlay"
class:animate={!$isGridScreen}
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
/>
{/if}
<!-- onMount(() => {-->
<!-- const interval = setInterval(debouncedUpdate, 100)-->
<!-- return () => {-->
<!-- observer.disconnect()-->
<!-- clearInterval(interval)-->
<!-- }-->
<!-- })-->
<!--</script>-->
<style>
.overlay {
position: fixed;
z-index: 800;
background: hsl(160, 64%, 90%);
border-radius: 4px;
border: 2px solid var(--spectrum-global-color-static-green-500);
}
.overlay.animate {
transition: all 130ms ease-out;
}
</style>
<!--&lt;!&ndash;{#if left != null && top != null && width && height && !waitingForGrid}&ndash;&gt;-->
<!--&lt;!&ndash; <div&ndash;&gt;-->
<!--&lt;!&ndash; class="overlay"&ndash;&gt;-->
<!--&lt;!&ndash; class:animate={!$isGridScreen}&ndash;&gt;-->
<!--&lt;!&ndash; style="left:{left}px; top:{top}px; width:{width}px; height:{height}px;"&ndash;&gt;-->
<!--&lt;!&ndash; >&ndash;&gt;-->
<!--&lt;!&ndash; {definition?.name || ""}&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!--&lt;!&ndash; <IndicatorSet componentId={DNDPlaceholderID} color="red" />&ndash;&gt;-->
<!--&lt;!&ndash;{/if}&ndash;&gt;-->
<!--<style>-->
<!-- .overlay {-->
<!-- position: fixed;-->
<!-- z-index: 800;-->
<!-- background: hsl(160, 64%, 90%);-->
<!-- border-radius: 4px;-->
<!-- border: 2px solid var(&#45;&#45;spectrum-global-color-static-green-500);-->
<!-- display: grid;-->
<!-- place-items: center;-->
<!-- color: hsl(160, 64%, 40%);-->
<!-- font-size: 14px;-->
<!-- }-->
<!-- .overlay.animate {-->
<!-- transition: all 130ms ease-out;-->
<!-- }-->
<!--</style>-->

View File

@ -0,0 +1,27 @@
<script lang="ts">
import { isGridScreen, dndParent, dndSource } from "@/stores"
import { DNDPlaceholderID } from "@/constants"
import IndicatorSet from "./IndicatorSet.svelte"
</script>
{#if !$isGridScreen}
<IndicatorSet
componentId={$dndParent}
color="var(--spectrum-global-color-static-green-400)"
zIndex={920}
prefix="Inside"
/>
{/if}
{#if $dndIsDragging}
<IndicatorSet
componentId={DNDPlaceholderID}
color="var(--spectrum-global-color-static-green-500)"
zIndex={930}
allowResizeAnchors={false}
background="hsl(160, 64%, 90%)"
animate={!$isGridScreen}
text={$dndSource?.name}
icon={$dndSource?.icon}
/>
{/if}

View File

@ -4,8 +4,8 @@
builderStore,
componentStore,
dndIsDragging,
dndIsNewComponent,
dndStore,
dndSource,
isGridScreen,
} from "stores"
import { Utils, memo } from "@budibase/frontend-core"
@ -53,7 +53,7 @@
// If dragging a new component on to a grid screen, tick to allow the
// real component to render in the new position before updating the DND
// store, preventing the green DND overlay from being out of position
if ($dndIsNewComponent && styles) {
if ($dndSource?.isNew && styles) {
dndStore.actions.updateNewComponentProps({
_styles: {
normal: styles,
@ -222,7 +222,7 @@
const onDragOver = e => {
if (!dragInfo) {
// Check if we're dragging a new component
if ($dndIsDragging && $dndIsNewComponent && $isGridScreen) {
if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
startDraggingPlaceholder()
}
return

View File

@ -14,6 +14,8 @@
export let line = false
export let alignRight = false
export let showResizeAnchors = false
export let background = null
export let animate = false
const AnchorSides = [
"right",
@ -33,10 +35,12 @@
class="indicator"
class:flipped
class:line
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex}; --bg: {background ||
'none'};"
class:withText={!!text}
class:vCompact={height < 40}
class:hCompact={width < 40}
class:animate
>
{#if text || icon}
<div
@ -84,6 +88,7 @@
border: 2px solid var(--color);
pointer-events: none;
border-radius: 4px;
background: var(--bg);
}
.indicator.withText {
border-top-left-radius: 0;
@ -94,6 +99,9 @@
.indicator.line {
border-radius: 4px !important;
}
.indicator.animate {
transition: all 130ms ease-out;
}
/* Label styles */
.label {

View File

@ -9,6 +9,10 @@
export let zIndex = 900
export let prefix = null
export let allowResizeAnchors = false
export let background = null
export let animate = false
export let text = null
export let icon = null
// Offset = 6 (clip-root padding) - 1 (half the border thickness)
const config = memo($$props)
@ -24,8 +28,8 @@
// Computed state
indicators: [],
text: null,
icon: null,
text,
icon,
insideGrid: false,
error: false,
})
@ -61,7 +65,6 @@
const observeMutations = element => {
mutationObserver.observe(element, {
attributes: true,
attributeFilter: ["style"],
})
observingMutations = true
}
@ -108,17 +111,19 @@
}
// Check if we're inside a grid
if (allowResizeAnchors) {
nextState.insideGrid = elements[0]?.dataset.insideGrid === "true"
}
nextState.insideGrid = elements[0]?.dataset.insideGrid === "true"
// Get text to display
nextState.text = elements[0].dataset.name
if (nextState.prefix) {
nextState.text = `${nextState.prefix} ${nextState.text}`
// Get text and icon to display
if (!text) {
nextState.text = elements[0].dataset.name
if (nextState.prefix) {
nextState.text = `${nextState.prefix} ${nextState.text}`
}
}
if (elements[0].dataset.icon) {
nextState.icon = elements[0].dataset.icon
if (!icon) {
if (elements[0].dataset.icon) {
nextState.icon = elements[0].dataset.icon
}
}
nextState.error = elements[0].classList.contains("error")
@ -205,5 +210,7 @@
color={state.error ? errorColor : state.color}
componentId={state.componentId}
zIndex={state.zIndex}
{background}
{animate}
/>
{/each}

View File

@ -21,10 +21,17 @@ const createDndStore = () => {
}
const store = writable(initialState)
const startDraggingExistingComponent = ({ id, parent, bounds, index }) => {
const startDraggingExistingComponent = ({
id,
parent,
bounds,
index,
name,
icon,
}) => {
store.set({
...initialState,
source: { id, parent, bounds, index },
source: { id, parent, bounds, index, name, icon, isNew: false },
})
}
@ -62,7 +69,10 @@ const createDndStore = () => {
parent: null,
bounds: { height, width },
index: null,
newComponentType: component,
type: component,
isNew: true,
name: `New ${definition.name}`,
icon: definition.icon,
},
target,
drop,
@ -118,9 +128,5 @@ export const dndStore = createDndStore()
// or components which depend on DND state unless values actually change.
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 dndSource = derivedMemo(dndStore, x => x.source)
export const dndIsDragging = derivedMemo(dndStore, x => !!x.source)
export const dndIsNewComponent = derivedMemo(
dndStore,
x => x.source?.newComponentType != null
)

View File

@ -18,14 +18,7 @@ export { environmentStore } from "./environment"
export { eventStore } from "./events"
export { orgStore } from "./org"
export { roleStore } from "./roles"
export {
dndStore,
dndIndex,
dndParent,
dndBounds,
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
export { dndStore, dndIndex, dndParent, dndIsDragging, dndSource } from "./dnd"
export { sidePanelStore } from "./sidePanel"
export { modalStore } from "./modal"
export { hoverStore } from "./hover"

View File

@ -3,7 +3,7 @@ import { routeStore } from "./routes"
import { builderStore } from "./builder"
import { appStore } from "./app"
import { orgStore } from "./org"
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
import { dndIndex, dndParent, dndSource } from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
@ -18,8 +18,7 @@ const createScreenStore = () => {
orgStore,
dndParent,
dndIndex,
dndIsNewComponent,
dndBounds,
dndSource,
],
([
$appStore,
@ -28,8 +27,7 @@ const createScreenStore = () => {
$orgStore,
$dndParent,
$dndIndex,
$dndIsNewComponent,
$dndBounds,
$dndSource,
]) => {
let activeLayout, activeScreen
let screens
@ -85,7 +83,7 @@ const createScreenStore = () => {
// Remove selected component from tree if we are moving an existing
// component
if (!$dndIsNewComponent && selectedParent) {
if (!$dndSource.isNew && selectedParent) {
selectedParent._children = selectedParent._children?.filter(
x => x._id !== selectedComponentId
)
@ -97,11 +95,11 @@ const createScreenStore = () => {
_id: DNDPlaceholderID,
_styles: {
normal: {
width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`,
width: `${$dndSource?.bounds?.width || 400}px`,
height: `${$dndSource?.bounds?.height || 200}px`,
opacity: 0,
"--default-width": $dndBounds?.width || 400,
"--default-height": $dndBounds?.height || 200,
"--default-width": $dndSource?.bounds?.width || 400,
"--default-height": $dndSource?.bounds?.height || 200,
},
},
static: true,