diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 9466bc461b..4f82523ec0 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -22,6 +22,7 @@ import Placeholder from "components/app/Placeholder.svelte" import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte" import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte" + import { DNDPlaceholderID } from "constants" export let instance = {} export let isLayout = false @@ -458,7 +459,7 @@ class:editing class:block={isBlock} class:explode={interactive && hasChildren && inDndPath} - class:placeholder={id === "placeholder"} + class:placeholder={id === DNDPlaceholderID} data-id={id} data-name={name} data-icon={icon} diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 01929d58d8..40f52dd1ae 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -3,11 +3,11 @@ import { get } from "svelte/store" import IndicatorSet from "./IndicatorSet.svelte" import { builderStore, componentStore } from "stores" - import PlaceholderOverlay from "./PlaceholderOverlay.svelte" + import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte" import { Utils } from "@budibase/frontend-core" import { findComponentById } from "utils/components.js" - let sourceId + let sourceInfo let targetInfo let dropInfo @@ -15,7 +15,8 @@ // the value of one of the properties actually changes $: parent = dropInfo?.parent $: index = dropInfo?.index - $: builderStore.actions.updateDNDPlaceholder(parent, index) + $: bounds = sourceInfo?.bounds + $: builderStore.actions.updateDNDPlaceholder(parent, index, bounds) // Util to get the inner DOM node by a component ID const getDOMNode = id => { @@ -37,14 +38,14 @@ // Callback when drag stops (whether dropped or not) const stopDragging = () => { // Reset state - sourceId = null + sourceInfo = null targetInfo = null dropInfo = null builderStore.actions.setDragging(false) // Reset listener - if (sourceId) { - const component = document.getElementsByClassName(sourceId)[0] + if (sourceInfo) { + const component = document.getElementsByClassName(sourceInfo.id)[0] if (component) { component.removeEventListener("dragend", stopDragging) } @@ -65,14 +66,33 @@ component.addEventListener("dragend", stopDragging) // Update state - sourceId = component.dataset.id - builderStore.actions.selectComponent(sourceId) + const parentId = component.dataset.parent + const parent = findComponentById( + get(componentStore).currentAsset.props, + parentId + ) + const index = parent._children.findIndex( + x => x._id === component.dataset.id + ) + sourceInfo = { + id: component.dataset.id, + bounds: component.children[0].getBoundingClientRect(), + parent: parentId, + index, + } + builderStore.actions.selectComponent(sourceInfo.id) builderStore.actions.setDragging(true) - // Execute this asynchronously so we don't kill the drag event by hiding - // the component in the same handler as starting the drag event + // Set initial drop info to show placeholder exactly where the dragged + // component is. + // Execute this asynchronously to prevent bugs caused by updating state in + // the same handler as selecting a new component (which causes a client + // re-initialisation). setTimeout(() => { - onDragEnter(e) + dropInfo = { + parent: parentId, + index, + } }, 0) } @@ -182,7 +202,7 @@ // Callback when on top of a component const onDragOver = e => { - if (!sourceId || !targetInfo) { + if (!sourceInfo || !targetInfo) { return } handleEvent(e) @@ -190,14 +210,14 @@ // Callback when entering a potential drop target const onDragEnter = e => { - if (!sourceId) { + if (!sourceInfo) { return } // Find the next valid component to consider dropping over, ignoring nested // block components const component = e.target?.closest?.( - `.component:not(.block):not(.${sourceId})` + `.component:not(.block):not(.${sourceInfo.id})` ) if (component && component.classList.contains("droppable")) { targetInfo = { @@ -216,7 +236,7 @@ let target, mode // Convert parent + index into target + mode - if (sourceId && dropInfo?.parent && dropInfo.index != null) { + if (sourceInfo && dropInfo?.parent && dropInfo.index != null) { const parent = findComponentById( get(componentStore).currentAsset?.props, dropInfo.parent @@ -225,9 +245,17 @@ return } + // Do nothing if we didn't change the location + if ( + sourceInfo.parent === dropInfo.parent && + sourceInfo.index === dropInfo.index + ) { + return + } + // Filter out source component and placeholder from consideration const children = parent._children?.filter( - x => x._id !== "placeholder" && x._id !== sourceId + x => x._id !== "placeholder" && x._id !== sourceInfo.id ) // Use inside if no existing children @@ -244,7 +272,7 @@ } if (target && mode) { - builderStore.actions.moveComponent(sourceId, target, mode) + builderStore.actions.moveComponent(sourceInfo.id, target, mode) } } @@ -278,5 +306,5 @@ /> {#if $builderStore.isDragging} - + {/if} diff --git a/packages/client/src/components/preview/DNDPlaceholder.svelte b/packages/client/src/components/preview/DNDPlaceholder.svelte new file mode 100644 index 0000000000..9efabdbc27 --- /dev/null +++ b/packages/client/src/components/preview/DNDPlaceholder.svelte @@ -0,0 +1,33 @@ + + +{#if style} +
+
+
+{/if} + + diff --git a/packages/client/src/components/preview/PlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte similarity index 88% rename from packages/client/src/components/preview/PlaceholderOverlay.svelte rename to packages/client/src/components/preview/DNDPlaceholderOverlay.svelte index 86950c533e..a762f35319 100644 --- a/packages/client/src/components/preview/PlaceholderOverlay.svelte +++ b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte @@ -1,11 +1,12 @@