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

View File

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

View File

@ -7,10 +7,10 @@
screenStore, screenStore,
dndStore, dndStore,
dndParent, dndParent,
dndSource,
dndIsDragging, dndIsDragging,
isGridScreen, isGridScreen,
} from "stores" } from "stores"
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 { isGridEvent } from "@/utils/grid" import { isGridEvent } from "@/utils/grid"
@ -92,6 +92,8 @@
bounds: component.children[0].getBoundingClientRect(), bounds: component.children[0].getBoundingClientRect(),
parent: parentId, parent: parentId,
index, index,
name: component.dataset.name,
icon: component.dataset.icon,
}) })
builderStore.actions.selectComponent(id) builderStore.actions.selectComponent(id)
@ -258,10 +260,10 @@
} }
// Check if we're adding a new component rather than moving one // Check if we're adding a new component rather than moving one
if (source.newComponentType) { if (source.isNew) {
dropping = true dropping = true
builderStore.actions.dropNewComponent( builderStore.actions.dropNewComponent(
source.newComponentType, source.type,
drop.parent, drop.parent,
drop.index, drop.index,
$dndStore.meta.newComponentProps $dndStore.meta.newComponentProps
@ -335,16 +337,3 @@
document.removeEventListener("drop", onDrop, false) document.removeEventListener("drop", onDrop, false)
}) })
</script> </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> <!--<script>-->
import { onMount, tick } from "svelte" <!-- import { onMount, tick } from "svelte"-->
import { Utils } from "@budibase/frontend-core" <!-- import { Utils } from "@budibase/frontend-core"-->
import { componentStore, isGridScreen } from "@/stores" <!-- import { componentStore, dndNewComponentType, isGridScreen } from "@/stores"-->
import { DNDPlaceholderID } from "@/constants" <!-- import { DNDPlaceholderID } from "@/constants"-->
<!-- import IndicatorSet from "components/preview/IndicatorSet.svelte"-->
let left, top, height, width <!-- let left, top, height, width-->
let observing = false <!-- let observing = false-->
let hasGridStyles = false <!-- let hasGridStyles = false-->
// On grid screens, we need to wait for grid styles to be properly set on <!-- // On grid screens, we need to wait for grid styles to be properly set on-->
// the hidden placeholder component before rendering this overlay <!-- // the hidden placeholder component before rendering this overlay-->
$: waitingForGrid = $isGridScreen && !hasGridStyles <!-- $: waitingForGrid = $isGridScreen && !hasGridStyles-->
$: instance = componentStore.actions.getComponentInstance(DNDPlaceholderID) <!-- $: instance = componentStore.actions.getComponentInstance(DNDPlaceholderID)-->
$: state = $instance?.state <!-- $: state = $instance?.state-->
$: styles = $state?.styles?.normal || {} <!-- $: styles = $state?.styles?.normal || {}-->
$: { <!-- $: {-->
if ($isGridScreen && !hasGridStyles) { <!-- if ($isGridScreen && !hasGridStyles) {-->
checkGridStyles(styles) <!-- checkGridStyles(styles)-->
} <!-- }-->
} <!-- }-->
// Wait for grid styles to be set, then tick and await a position update <!-- // We pull the component name from the definition-->
// before finally signalling we're allowed to render <!-- $: definition =-->
const checkGridStyles = async styles => { <!-- componentStore.actions.getComponentDefinition($dndNewComponentType)-->
const hasStyles = Object.keys(styles).some(key => key.startsWith("--grid"))
if (hasStyles) {
await tick()
updatePosition()
hasGridStyles = true
}
}
// Observe style changes in the placeholder DOM node and use this to trigger <!-- // Wait for grid styles to be set, then tick and await a position update-->
// a redraw of our overlay <!-- // before finally signalling we're allowed to render-->
const observer = new MutationObserver(mutations => { <!-- const checkGridStyles = async styles => {-->
if (mutations.some(mutation => mutation.attributeName === "style")) { <!-- const hasStyles = Object.keys(styles).some(key => key.startsWith("&#45;&#45;grid"))-->
debouncedUpdate() <!-- if (hasStyles) {-->
} <!-- await tick()-->
}) <!-- updatePosition()-->
<!-- hasGridStyles = true-->
<!-- }-->
<!-- }-->
const updatePosition = () => { <!-- // Observe style changes in the placeholder DOM node and use this to trigger-->
const wrapperNode = document.getElementsByClassName(DNDPlaceholderID)[0] <!-- // a redraw of our overlay-->
let domNode = wrapperNode <!-- const observer = new MutationObserver(mutations => {-->
const insideGrid = wrapperNode?.dataset.insideGrid === "true" <!-- if (mutations.some(mutation => mutation.attributeName === "style")) {-->
if (!insideGrid) { <!-- debouncedUpdate()-->
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
}
// Initialise observer if not already done <!-- const updatePosition = () => {-->
if (!observing && wrapperNode) { <!-- const wrapperNode = document.getElementsByClassName(DNDPlaceholderID)[0]-->
observing = true <!-- let domNode = wrapperNode-->
observer.observe(wrapperNode, { attributes: true }) <!-- const insideGrid = wrapperNode?.dataset.insideGrid === "true"-->
} <!-- if (!insideGrid) {-->
} <!-- domNode = document.getElementsByClassName(`${DNDPlaceholderID}-dom`)[0]-->
const debouncedUpdate = Utils.domDebounce(updatePosition) <!-- }-->
<!-- if (!domNode) {-->
<!-- height = 0-->
<!-- width = 0-->
<!-- } else {-->
<!-- const bounds = domNode.getBoundingClientRect()-->
<!-- left = bounds.left-->
<!-- top = bounds.top-->
<!-- height = bounds.height-->
<!-- width = bounds.width-->
<!-- }-->
onMount(() => { <!-- // Initialise observer if not already done-->
const interval = setInterval(debouncedUpdate, 100) <!-- if (!observing && wrapperNode) {-->
return () => { <!-- observing = true-->
observer.disconnect() <!-- observer.observe(wrapperNode, { attributes: true })-->
clearInterval(interval) <!-- }-->
} <!-- }-->
}) <!-- const debouncedUpdate = Utils.domDebounce(updatePosition)-->
</script>
{#if left != null && top != null && width && height && !waitingForGrid} <!-- onMount(() => {-->
<div <!-- const interval = setInterval(debouncedUpdate, 100)-->
class="overlay" <!-- return () => {-->
class:animate={!$isGridScreen} <!-- observer.disconnect()-->
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;" <!-- clearInterval(interval)-->
/> <!-- }-->
{/if} <!-- })-->
<!--</script>-->
<style> <!--&lt;!&ndash;{#if left != null && top != null && width && height && !waitingForGrid}&ndash;&gt;-->
.overlay { <!--&lt;!&ndash; <div&ndash;&gt;-->
position: fixed; <!--&lt;!&ndash; class="overlay"&ndash;&gt;-->
z-index: 800; <!--&lt;!&ndash; class:animate={!$isGridScreen}&ndash;&gt;-->
background: hsl(160, 64%, 90%); <!--&lt;!&ndash; style="left:{left}px; top:{top}px; width:{width}px; height:{height}px;"&ndash;&gt;-->
border-radius: 4px; <!--&lt;!&ndash; >&ndash;&gt;-->
border: 2px solid var(--spectrum-global-color-static-green-500); <!--&lt;!&ndash; {definition?.name || ""}&ndash;&gt;-->
} <!--&lt;!&ndash; </div>&ndash;&gt;-->
.overlay.animate { <!--&lt;!&ndash; <IndicatorSet componentId={DNDPlaceholderID} color="red" />&ndash;&gt;-->
transition: all 130ms ease-out;
} <!--&lt;!&ndash;{/if}&ndash;&gt;-->
</style> <!--<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, builderStore,
componentStore, componentStore,
dndIsDragging, dndIsDragging,
dndIsNewComponent,
dndStore, dndStore,
dndSource,
isGridScreen, isGridScreen,
} from "stores" } from "stores"
import { Utils, memo } from "@budibase/frontend-core" 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 // 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 // real component to render in the new position before updating the DND
// store, preventing the green DND overlay from being out of position // store, preventing the green DND overlay from being out of position
if ($dndIsNewComponent && styles) { if ($dndSource?.isNew && styles) {
dndStore.actions.updateNewComponentProps({ dndStore.actions.updateNewComponentProps({
_styles: { _styles: {
normal: styles, normal: styles,
@ -222,7 +222,7 @@
const onDragOver = e => { const onDragOver = e => {
if (!dragInfo) { if (!dragInfo) {
// Check if we're dragging a new component // Check if we're dragging a new component
if ($dndIsDragging && $dndIsNewComponent && $isGridScreen) { if ($dndIsDragging && $dndSource?.isNew && $isGridScreen) {
startDraggingPlaceholder() startDraggingPlaceholder()
} }
return return

View File

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

View File

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

View File

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

View File

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