From 0ce0f5c823cffba2db2d700437820d6ec7c6da90 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 07:28:59 +0100 Subject: [PATCH 01/24] Add initial DND implementation with working functionality for dropping inside components --- .../AppPreview/CurrentItemPreview.svelte | 16 ++++ .../client/src/components/ClientApp.svelte | 2 + .../client/src/components/Component.svelte | 4 + .../src/components/preview/DNDHandler.svelte | 83 +++++++++++++++++++ packages/client/src/stores/builder.js | 9 +- packages/client/src/utils/styleable.js | 2 + 6 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 packages/client/src/components/preview/DNDHandler.svelte diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 494050f690..422eba73b0 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -1,5 +1,6 @@ + + diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index 3f36af1d06..3a7eb1df61 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -64,13 +64,18 @@ const createBuilderStore = () => { dispatchEvent("preview-loaded") }, setSelectedPath: path => { - console.log("set to ") - console.log(path) writableStore.update(state => { state.selectedPath = path return state }) }, + moveComponent: (componentId, destinationComponentId, mode) => { + dispatchEvent("move-component", { + componentId, + destinationComponentId, + mode, + }) + }, } return { ...writableStore, diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index d9925af91a..32171b9478 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -23,6 +23,8 @@ export const styleable = (node, styles = {}) => { let applyHoverStyles let selectComponent + node.setAttribute("draggable", true) + // Creates event listeners and applies initial styles const setupStyles = (newStyles = {}) => { // Use empty state styles as base styles if required, but let them, get From d86e5718d56e58b0de7eba38c6ba89ea8bf02d7a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 07:35:19 +0100 Subject: [PATCH 02/24] Prevent DND if target is a child of source --- .../design/AppPreview/CurrentItemPreview.svelte | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 422eba73b0..a8f1b507f4 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -8,7 +8,7 @@ import ConfirmDialog from "components/common/ConfirmDialog.svelte" import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui" import ErrorSVG from "assets/error.svg?raw" - import { findComponent } from "builderStore/storeUtils" + import { findComponent, findComponentPath } from "builderStore/storeUtils" let iframe let layout @@ -123,6 +123,17 @@ get(currentAsset).props, data.destinationComponentId ) + + // Stop if the target is a child of source + const path = findComponentPath( + sourceComponent, + data.destinationComponentId + ) + const ids = path.map(component => component._id) + if (ids.includes(data.destinationComponentId)) { + return + } + if (sourceComponent && destinationComponent) { store.actions.components.copy(sourceComponent, true) store.actions.components.paste(destinationComponent, data.mode) From 97800563c4c3b0176424c9a365bf007afd5f726e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 07:52:49 +0100 Subject: [PATCH 03/24] Prevent dragging the screen or layout components, and prevent dragging any layout components when previewing a screen --- packages/client/src/components/ClientApp.svelte | 5 ++++- packages/client/src/components/Component.svelte | 7 +++++-- packages/client/src/components/Screen.svelte | 2 +- packages/client/src/utils/styleable.js | 6 +++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index 5c46161577..fb9117832f 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -105,7 +105,10 @@
{#key $screenStore.activeLayout._id} - + {/key} diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 20d8b53609..414d87a0aa 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -11,6 +11,8 @@ import Placeholder from "components/app/Placeholder.svelte" export let instance = {} + export let isLayout = false + export let isScreen = false // The enriched component settings let enrichedSettings @@ -178,7 +180,8 @@ data-type={interactive ? "component" : ""} data-id={id} data-name={name} - data-droppable={definition?.hasChildren || false} + data-draggable={interactive && !isLayout && !isScreen ? "true" : "false"} + data-droppable={definition?.hasChildren ? "true" : "false"} > {#if children.length} @@ -197,7 +200,7 @@ .component { display: contents; } - .component > :global(*:hover) { + [data-draggable="true"] { cursor: pointer; } diff --git a/packages/client/src/components/Screen.svelte b/packages/client/src/components/Screen.svelte index a759119320..4878df157f 100644 --- a/packages/client/src/components/Screen.svelte +++ b/packages/client/src/components/Screen.svelte @@ -22,6 +22,6 @@ {#key screenDefinition?._id} - + {/key} diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index 32171b9478..27f6c6a0f6 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -23,7 +23,11 @@ export const styleable = (node, styles = {}) => { let applyHoverStyles let selectComponent - node.setAttribute("draggable", true) + // Allow dragging if required + const parent = node.closest("[data-type='component']") + if (parent && parent.dataset.draggable === "true") { + node.setAttribute("draggable", true) + } // Creates event listeners and applies initial styles const setupStyles = (newStyles = {}) => { From 5c37238c8a0e8571eb592bd97d6ad9213e292d90 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 14:11:05 +0100 Subject: [PATCH 04/24] Fix progress circle never animating because of wrong default prop --- packages/bbui/src/ProgressCircle/ProgressCircle.svelte | 2 +- .../client/src/components/preview/DNDPositionIndicator.svelte | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 packages/client/src/components/preview/DNDPositionIndicator.svelte diff --git a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte index a86de55423..0428263346 100644 --- a/packages/bbui/src/ProgressCircle/ProgressCircle.svelte +++ b/packages/bbui/src/ProgressCircle/ProgressCircle.svelte @@ -13,7 +13,7 @@ } } - export let value = false + export let value = null export let minValue = 0 export let maxValue = 100 diff --git a/packages/client/src/components/preview/DNDPositionIndicator.svelte b/packages/client/src/components/preview/DNDPositionIndicator.svelte new file mode 100644 index 0000000000..e69de29bb2 From bdc86e4c22c2eb98058384d06ad4a2e6473254f0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 14:28:44 +0100 Subject: [PATCH 05/24] Add above/below dnd and support for dropping above/below components which also allow dropping inside --- .../client/src/components/Component.svelte | 3 +- .../src/components/preview/DNDHandler.svelte | 106 ++++++++++++++++-- .../preview/DNDPositionIndicator.svelte | 33 ++++++ .../components/preview/HoverIndicator.svelte | 2 +- .../components/preview/IndicatorSet.svelte | 4 + packages/client/src/stores/builder.js | 8 ++ packages/client/src/utils/styleable.js | 3 +- 7 files changed, 144 insertions(+), 15 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 414d87a0aa..a7ac7b0cd0 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -181,7 +181,8 @@ data-id={id} data-name={name} data-draggable={interactive && !isLayout && !isScreen ? "true" : "false"} - data-droppable={definition?.hasChildren ? "true" : "false"} + data-droppable={interactive ? "true" : "false"} + data-droppable-inside={definition?.hasChildren ? "true" : "false"} > {#if children.length} diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 88b9176efb..c3630051ee 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,36 +1,116 @@ + +{#if dropMode !== "inside" && dropInfo} + +{/if} diff --git a/packages/client/src/components/preview/DNDPositionIndicator.svelte b/packages/client/src/components/preview/DNDPositionIndicator.svelte index e69de29bb2..d86b79b88f 100644 --- a/packages/client/src/components/preview/DNDPositionIndicator.svelte +++ b/packages/client/src/components/preview/DNDPositionIndicator.svelte @@ -0,0 +1,33 @@ + + +{#if valid} +
+{/if} + + diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index 9518c6d101..3cae1e4c85 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -30,7 +30,7 @@ { theme: null, customTheme: null, previewDevice: "desktop", + showHoverIndicator: true, } const writableStore = writable(initialState) const derivedStore = derived(writableStore, $state => { @@ -76,9 +77,16 @@ const createBuilderStore = () => { mode, }) }, + showHoverIndicator: show => { + writableStore.update(state => { + state.showHoverIndicator = show + return state + }) + }, } return { ...writableStore, + set: state => writableStore.set({ ...initialState, ...state }), subscribe: derivedStore.subscribe, actions, } diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index 27f6c6a0f6..899854ecc9 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -33,6 +33,7 @@ export const styleable = (node, styles = {}) => { const setupStyles = (newStyles = {}) => { // Use empty state styles as base styles if required, but let them, get // overridden by any user specified styles + const baseString = node.style.cssText let baseStyles = {} if (newStyles.empty) { baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)" @@ -50,7 +51,7 @@ export const styleable = (node, styles = {}) => { // Applies a style string to a DOM node const applyStyles = styleString => { - node.style = styleString + node.style = `${baseString}${styleString}` node.dataset.componentId = componentId } From 46867b8a198a28785aa7311b4526cec22a4253fd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 15:08:42 +0100 Subject: [PATCH 06/24] Add labels to DND to describe where the component will be dropped --- .../src/components/preview/DNDHandler.svelte | 12 +++-- .../preview/DNDPositionIndicator.svelte | 53 +++++++++++-------- .../src/components/preview/Indicator.svelte | 8 ++- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index c3630051ee..d1a4934826 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,6 +1,7 @@ -{#if valid} -
-{/if} - - +{#key mode} + {#if dimensions} + + {/if} +{/key} diff --git a/packages/client/src/components/preview/Indicator.svelte b/packages/client/src/components/preview/Indicator.svelte index 95364d0278..66363e3992 100644 --- a/packages/client/src/components/preview/Indicator.svelte +++ b/packages/client/src/components/preview/Indicator.svelte @@ -9,6 +9,9 @@ export let color export let zIndex export let transition = false + export let flip = false + + $: flipped = flip || top < 20
{#if text} -
+
{text}
{/if} @@ -63,6 +66,7 @@ } .text.flipped { border-top-left-radius: 4px; + border-bottom-left-radius: 4px; transform: translateY(0%); top: -2px; } From 601a4935a9a207a50925ebb7f04e9bcc797ab7e3 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 15:49:58 +0100 Subject: [PATCH 07/24] Ensure transitions work properly when showing and hiding DND candidate positions --- packages/client/src/components/preview/DNDHandler.svelte | 5 ++--- .../src/components/preview/DNDPositionIndicator.svelte | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index d1a4934826..111c009a3a 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -25,9 +25,7 @@ // Highlight being dragged by setting opacity const child = getDOMNodeForComponent(e.target) - console.log(child) if (child) { - console.log("set opacity") child.style.opacity = "0.5" } } @@ -163,7 +161,8 @@ /> import Indicator from "./Indicator.svelte" + export let componentId export let dropInfo export let mode export let zIndex @@ -24,8 +25,8 @@ } -{#key mode} - {#if dimensions} +{#key `${componentId}-${mode}`} + {#if dimensions && mode !== "inside"} Date: Thu, 16 Sep 2021 16:02:45 +0100 Subject: [PATCH 08/24] Ensure hover indicator is correctly hidden when using DND and improve DND labels --- .../src/components/preview/DNDHandler.svelte | 10 ++++++++-- .../components/preview/DNDPositionIndicator.svelte | 3 +-- .../client/src/components/preview/Indicator.svelte | 14 +++++++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 111c009a3a..409eeb2d8f 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,7 +1,7 @@
{#if text} -
+
{text}
{/if} @@ -45,6 +46,9 @@ .indicator.flipped { border-top-left-radius: 4px; } + .indicator.line { + border-radius: 4px !important; + } .text { background-color: var(--color); color: white; @@ -70,4 +74,8 @@ transform: translateY(0%); top: -2px; } + .text.line { + transform: translateY(-50%) !important; + border-radius: 4px !important; + } From e625d2e4a321a20ff62c1904c9e65ce4ec0df636 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 16 Sep 2021 17:39:39 +0100 Subject: [PATCH 09/24] Reduce duplication in move componment handler from dnd callback --- .../AppPreview/CurrentItemPreview.svelte | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte index 3901252ee9..4e51850fe1 100644 --- a/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte @@ -125,29 +125,24 @@ // loading is supported loading = false } else if (type === "move-component") { - // Copy - const sourceComponent = findComponent( - get(currentAsset).props, - data.componentId - ) - const destinationComponent = findComponent( - get(currentAsset).props, - data.destinationComponentId - ) + const { componentId, destinationComponentId } = data + const rootComponent = get(currentAsset).props + + // Get source and destination components + const source = findComponent(rootComponent, componentId) + const destination = findComponent(rootComponent, destinationComponentId) // Stop if the target is a child of source - const path = findComponentPath( - sourceComponent, - data.destinationComponentId - ) + const path = findComponentPath(source, destinationComponentId) const ids = path.map(component => component._id) if (ids.includes(data.destinationComponentId)) { return } - if (sourceComponent && destinationComponent) { - store.actions.components.copy(sourceComponent, true) - store.actions.components.paste(destinationComponent, data.mode) + // Cut and paste the component to the new destination + if (source && destination) { + store.actions.components.copy(source, true) + store.actions.components.paste(destination, data.mode) } } else { console.warning(`Client sent unknown event type: ${type}`) From 7eeb215e51722e4afd42f034f3a973d764cb1034 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 17 Sep 2021 14:17:50 +0100 Subject: [PATCH 10/24] Refactor to use generic flag for dragging and hide settings bar when dragging --- .../client/src/components/preview/DNDHandler.svelte | 11 +++++------ .../src/components/preview/HoverIndicator.svelte | 2 +- .../client/src/components/preview/SettingsBar.svelte | 2 +- packages/client/src/stores/builder.js | 7 +++---- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 409eeb2d8f..c1a6a74465 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -21,6 +21,7 @@ // Update state dragTarget = e.target.dataset.componentId builderStore.actions.selectComponent(dragTarget) + builderStore.actions.setDragging(true) // Highlight being dragged by setting opacity const child = getDOMNodeForComponent(e.target) @@ -34,15 +35,13 @@ // Reset state and styles dropTarget = null dropInfo = null + builderStore.actions.setDragging(false) // Reset opacity style const child = getDOMNodeForComponent(e.target) if (child) { child.style.opacity = "" } - - // Re-enable the hover indicator - builderStore.actions.showHoverIndicator(true) } // Callback when on top of a component @@ -96,11 +95,11 @@ element.dataset.droppable && element.dataset.id !== dragTarget ) { - // Disable hover selection again to ensure it's always disabled. + // Ensure the dragging flag is always set. // There's a bit of a race condition between the app reinitialisation // after selecting the DND component and setting this the first time - if (get(builderStore).showHoverIndicator) { - builderStore.actions.showHoverIndicator(false) + if (!get(builderStore).isDragging) { + builderStore.actions.setDragging(true) } // Store target ID diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index 3cae1e4c85..28109cb262 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -30,7 +30,7 @@ setting.showInBar) ?? [] const updatePosition = () => { diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index ce653a8fc1..0d738e4a11 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -23,7 +23,7 @@ const createBuilderStore = () => { theme: null, customTheme: null, previewDevice: "desktop", - showHoverIndicator: true, + isDragging: false, } const writableStore = writable(initialState) const derivedStore = derived(writableStore, $state => { @@ -77,16 +77,15 @@ const createBuilderStore = () => { mode, }) }, - showHoverIndicator: show => { + setDragging: dragging => { writableStore.update(state => { - state.showHoverIndicator = show + state.isDragging = dragging return state }) }, } return { ...writableStore, - set: state => writableStore.set({ ...initialState, ...state }), subscribe: derivedStore.subscribe, actions, } From 54bf420ef89b6fe7eb6ec2c43051ec22d49f4d34 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 17 Sep 2021 14:30:54 +0100 Subject: [PATCH 11/24] Apply grab cursor when hovering over a draggable component --- packages/client/src/components/Component.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index a7ac7b0cd0..980e7cd80b 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -201,7 +201,7 @@ .component { display: contents; } - [data-draggable="true"] { - cursor: pointer; + [data-draggable="true"] :global(*:hover) { + cursor: grab !important; } From f0dde4a4a425c625893b32a255d3ea02c5dc7027 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 08:06:01 +0100 Subject: [PATCH 12/24] Fix a few possible crashes by dragging in certain ways, and display on screen when an invalid drop target is hovered over --- .../src/components/preview/DNDHandler.svelte | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index c1a6a74465..fe4d7f4d59 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -18,6 +18,10 @@ // Callback when initially starting a drag on a draggable component const onDragStart = e => { + if (!e.target?.dataset?.componentId) { + return + } + // Update state dragTarget = e.target.dataset.componentId builderStore.actions.selectComponent(dragTarget) @@ -32,63 +36,73 @@ // Callback when drag stops (whether dropped or not) const onDragEnd = e => { + // Reset opacity style + if (dragTarget) { + const child = getDOMNodeForComponent(e.target) + if (child) { + child.style.opacity = "" + } + } + // Reset state and styles + dragTarget = null dropTarget = null dropInfo = null builderStore.actions.setDragging(false) - - // Reset opacity style - const child = getDOMNodeForComponent(e.target) - if (child) { - child.style.opacity = "" - } } // Callback when on top of a component const onDragOver = e => { + // Skip if we aren't validly dragging currently + if (!dragTarget || !dropInfo) { + return + } + e.preventDefault() + const { droppableInside, bounds } = dropInfo + const { top, height } = bounds + const mouseY = e.clientY + const elTop = top + const elBottom = top + height - if (dropInfo) { - const { droppableInside, bounds } = dropInfo - const { top, height } = bounds - const mouseY = e.clientY - const elTop = top - const elBottom = top + height + // Determine which edge we're nearest as this is needed for potentially + // any drop mode + let nearestEdge + if (Math.abs(elTop - mouseY) < Math.abs(elBottom - mouseY)) { + nearestEdge = "above" + } else { + nearestEdge = "below" + } - // Determine which edge we're nearest as this is needed for potentially - // any drop mode - let nearestEdge - if (Math.abs(elTop - mouseY) < Math.abs(elBottom - mouseY)) { - nearestEdge = "above" + // If not available to drop inside, just check whether we are closer + // to the top or bottom + if (!droppableInside) { + dropMode = nearestEdge + } + + // Otherwise determine whether the user wants to drop inside or at + // either edge + else { + const edgeLimit = Math.min(40, height * 0.33) + const insideLimit = [ + Math.round(top + edgeLimit), + Math.round(top + height - edgeLimit), + ] + if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) { + dropMode = "inside" } else { - nearestEdge = "below" - } - - // If not available to drop inside, just check whether we are closer - // to the top or bottom - if (!droppableInside) { dropMode = nearestEdge } - - // Otherwise determine whether the user wants to drop inside or at - // either edge - else { - const edgeLimit = Math.min(40, height * 0.33) - const insideLimit = [ - Math.round(top + edgeLimit), - Math.round(top + height - edgeLimit), - ] - if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) { - dropMode = "inside" - } else { - dropMode = nearestEdge - } - } } } // Callback when entering a potential drop target const onDragEnter = e => { + // Skip if we aren't validly dragging currently + if (!dragTarget) { + return + } + const element = e.target.closest("[data-type='component']") if ( element && From 8cf3971b471a4c67a47de30f58702859f8148367 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 08:26:44 +0100 Subject: [PATCH 13/24] Only allow dropping inside empty components that accept children to massively reduce the amount of unwanted drop targets due to parent container components --- .../client/src/components/Component.svelte | 15 ++++-- .../src/components/preview/DNDHandler.svelte | 53 ++++++++++--------- .../preview/DNDPositionIndicator.svelte | 10 ++-- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 980e7cd80b..6192004b42 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -171,18 +171,23 @@ conditionalSettings = result.settingUpdates visible = nextVisible } + + // Drag and drop helper tags + $: draggable = interactive && !isLayout && !isScreen + $: droppable = interactive + $: dropInside = interactive && definition?.hasChildren && !children.length {#key propsHash} {#if constructor && componentSettings && (visible || inSelectedPath)}
{#if children.length} @@ -202,6 +207,6 @@ display: contents; } [data-draggable="true"] :global(*:hover) { - cursor: grab !important; + cursor: grab; } diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index fe4d7f4d59..2437386929 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -5,9 +5,7 @@ import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import { builderStore } from "stores" - let dragTarget - let dropTarget - let dropMode + let dragInfo let dropInfo const getDOMNodeForComponent = component => { @@ -18,26 +16,28 @@ // Callback when initially starting a drag on a draggable component const onDragStart = e => { - if (!e.target?.dataset?.componentId) { + const parent = e.target.closest("[data-type='component']") + const child = getDOMNodeForComponent(e.target) + if (!parent?.dataset?.id || !child) { return } // Update state - dragTarget = e.target.dataset.componentId - builderStore.actions.selectComponent(dragTarget) + dragInfo = { + target: parent.dataset.id, + parent: parent.dataset.parent, + } + builderStore.actions.selectComponent(dragInfo.target) builderStore.actions.setDragging(true) // Highlight being dragged by setting opacity - const child = getDOMNodeForComponent(e.target) - if (child) { - child.style.opacity = "0.5" - } + child.style.opacity = "0.5" } // Callback when drag stops (whether dropped or not) const onDragEnd = e => { // Reset opacity style - if (dragTarget) { + if (dragInfo) { const child = getDOMNodeForComponent(e.target) if (child) { child.style.opacity = "" @@ -45,8 +45,7 @@ } // Reset state and styles - dragTarget = null - dropTarget = null + dragInfo = null dropInfo = null builderStore.actions.setDragging(false) } @@ -54,7 +53,7 @@ // Callback when on top of a component const onDragOver = e => { // Skip if we aren't validly dragging currently - if (!dragTarget || !dropInfo) { + if (!dragInfo || !dropInfo) { return } @@ -77,7 +76,7 @@ // If not available to drop inside, just check whether we are closer // to the top or bottom if (!droppableInside) { - dropMode = nearestEdge + dropInfo.mode = nearestEdge } // Otherwise determine whether the user wants to drop inside or at @@ -89,9 +88,9 @@ Math.round(top + height - edgeLimit), ] if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) { - dropMode = "inside" + dropInfo.mode = "inside" } else { - dropMode = nearestEdge + dropInfo.mode = nearestEdge } } } @@ -99,7 +98,7 @@ // Callback when entering a potential drop target const onDragEnter = e => { // Skip if we aren't validly dragging currently - if (!dragTarget) { + if (!dragInfo) { return } @@ -107,7 +106,7 @@ if ( element && element.dataset.droppable && - element.dataset.id !== dragTarget + element.dataset.id !== dragInfo.target ) { // Ensure the dragging flag is always set. // There's a bit of a race condition between the app reinitialisation @@ -117,20 +116,20 @@ } // Store target ID - dropTarget = element.dataset.id + const target = element.dataset.id // Precompute and store some info to avoid recalculating everything in // dragOver const child = getDOMNodeForComponent(e.target) const bounds = child.getBoundingClientRect() dropInfo = { + target, name: element.dataset.name, droppableInside: element.dataset.droppableInside === "true", bounds, } } else { dropInfo = null - dropTarget = null } } @@ -141,8 +140,12 @@ // Callback when dropping a drag on top of some component const onDrop = e => { e.preventDefault() - if (dropTarget && dropMode) { - builderStore.actions.moveComponent(dragTarget, dropTarget, dropMode) + if (dropInfo) { + builderStore.actions.moveComponent( + dragInfo.target, + dropInfo.target, + dropInfo.mode + ) } } @@ -172,7 +175,7 @@ import Indicator from "./Indicator.svelte" - export let componentId export let dropInfo - export let mode export let zIndex export let color export let transition - $: dimensions = getDimensions(dropInfo?.bounds, mode) - $: prefix = mode === "above" ? "Above" : "Below" + $: dimensions = getDimensions(dropInfo?.bounds, dropInfo?.mode) + $: prefix = dropInfo?.mode === "above" ? "Above" : "Below" $: text = `${prefix} ${dropInfo?.name}` const getDimensions = (bounds, mode) => { @@ -25,8 +23,8 @@ } -{#key `${componentId}-${mode}`} - {#if dimensions && mode !== "inside"} +{#key `${dropInfo?.target}-${dropInfo?.mode}`} + {#if dimensions && dropInfo?.mode !== "inside"} Date: Mon, 20 Sep 2021 09:12:35 +0100 Subject: [PATCH 14/24] Prevent DND targetting either above or below the screen/layout, and fix bug determinging whether a drop target is valid --- packages/client/src/components/Component.svelte | 2 +- packages/client/src/components/preview/DNDHandler.svelte | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 6192004b42..5e02e345d4 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -174,7 +174,7 @@ // Drag and drop helper tags $: draggable = interactive && !isLayout && !isScreen - $: droppable = interactive + $: droppable = interactive && !isLayout && !isScreen $: dropInside = interactive && definition?.hasChildren && !children.length diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 2437386929..90e2a55ca4 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -105,9 +105,14 @@ const element = e.target.closest("[data-type='component']") if ( element && - element.dataset.droppable && + element.dataset.droppable === "true" && element.dataset.id !== dragInfo.target ) { + // Do nothing if this is the same target + if (element.dataset.id === dropInfo?.target) { + return + } + // Ensure the dragging flag is always set. // There's a bit of a race condition between the app reinitialisation // after selecting the DND component and setting this the first time From 9ca0aeae8e23ce22e846b78b09bc6224d08eec00 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 12:14:40 +0100 Subject: [PATCH 15/24] Fix spectrum button not being able to be dragged --- packages/client/src/components/app/Button.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/client/src/components/app/Button.svelte b/packages/client/src/components/app/Button.svelte index ed465e3e66..9900c740d2 100644 --- a/packages/client/src/components/app/Button.svelte +++ b/packages/client/src/components/app/Button.svelte @@ -31,4 +31,7 @@ .spectrum-Button--overBackground:hover { color: #555; } + .spectrum-Button::after { + display: none; + } From 10f754a9ad52982a3322afdc8350477d5b8aa947 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 12:16:23 +0100 Subject: [PATCH 16/24] Remove preview specific data tags from components when running real apps --- packages/client/src/components/Component.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 5e02e345d4..f590e2aa9f 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -63,7 +63,9 @@ $builderStore.inBuilder && $builderStore.selectedComponentId === instance._id $: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) - $: interactive = $builderStore.previewType === "layout" || insideScreenslot + $: interactive = + $builderStore.inBuilder && + ($builderStore.previewType === "layout" || insideScreenslot) $: evaluateConditions(enrichedSettings?._conditions) $: componentSettings = { ...enrichedSettings, ...conditionalSettings } From 07e0cbb210c5357f83b3e5532b39b86ac3b5fed6 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 12:20:34 +0100 Subject: [PATCH 17/24] Remove old and no longer needed component-id data tag applied by styleable helper --- packages/builder/cypress/support/commands.js | 2 +- packages/client/src/utils/styleable.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js index ea6ca81e66..dbfabeda3f 100644 --- a/packages/builder/cypress/support/commands.js +++ b/packages/builder/cypress/support/commands.js @@ -145,7 +145,7 @@ Cypress.Commands.add("getComponent", componentId => { .its("body") .should("not.be.null") .then(cy.wrap) - .find(`[data-component-id=${componentId}]`) + .find(`[data-id=${componentId}]`) }) Cypress.Commands.add("navigateToFrontend", () => { diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index 899854ecc9..3fce1bcf26 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -52,7 +52,6 @@ export const styleable = (node, styles = {}) => { // Applies a style string to a DOM node const applyStyles = styleString => { node.style = `${baseString}${styleString}` - node.dataset.componentId = componentId } // Applies the "normal" style definition From 488ec4d988686899650a48b53e6badeac1ecdbfa Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 15:34:51 +0100 Subject: [PATCH 18/24] Refactor client app data tags to be classnames and simplify logic --- .../client/src/components/Component.svelte | 69 ++++++++++--------- .../src/components/app/Container.svelte | 4 +- .../src/components/preview/DNDHandler.svelte | 18 ++--- .../components/preview/HoverIndicator.svelte | 2 +- packages/client/src/utils/styleable.js | 4 +- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index f590e2aa9f..3d863c276e 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -51,11 +51,11 @@ $: children = instance._children || [] $: id = instance._id $: name = instance._instanceName - $: empty = - !children.length && - definition?.hasChildren && - definition?.showEmptyState !== false && - $builderStore.inBuilder + $: interactive = + $builderStore.inBuilder && + ($builderStore.previewType === "layout" || insideScreenslot) + $: empty = interactive && !children.length && definition?.hasChildren + $: emptyState = empty && definition?.showEmptyState !== false $: rawProps = getRawProps(instance) $: instanceKey = JSON.stringify(rawProps) $: updateComponentProps(rawProps, instanceKey, $context) @@ -63,9 +63,6 @@ $builderStore.inBuilder && $builderStore.selectedComponentId === instance._id $: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) - $: interactive = - $builderStore.inBuilder && - ($builderStore.previewType === "layout" || insideScreenslot) $: evaluateConditions(enrichedSettings?._conditions) $: componentSettings = { ...enrichedSettings, ...conditionalSettings } @@ -73,8 +70,8 @@ $: componentStore.set({ id, children: children.length, - styles: { ...instance._styles, id, empty, interactive }, - empty, + styles: { ...instance._styles, id, empty: emptyState, interactive }, + empty: emptyState, selected, name, }) @@ -177,38 +174,44 @@ // Drag and drop helper tags $: draggable = interactive && !isLayout && !isScreen $: droppable = interactive && !isLayout && !isScreen - $: dropInside = interactive && definition?.hasChildren && !children.length {#key propsHash} - {#if constructor && componentSettings && (visible || inSelectedPath)} -
- - {#if children.length} - {#each children as child (child._id)} - - {/each} - {:else if empty} - - {/if} - -
- {/if} + {#key empty} + {#if constructor && componentSettings && (visible || inSelectedPath)} + + +
+ + {#if children.length} + {#each children as child (child._id)} + + {/each} + {:else if emptyState} + + {/if} + +
+ {/if} + {/key} {/key} diff --git a/packages/client/src/components/app/Container.svelte b/packages/client/src/components/app/Container.svelte index 5b2f951c8d..148179c98f 100644 --- a/packages/client/src/components/app/Container.svelte +++ b/packages/client/src/components/app/Container.svelte @@ -34,7 +34,7 @@ display: flex; max-width: 100%; } - .valid-container :global([data-type="component"] > *) { + .valid-container :global(.component > *) { max-width: 100%; } .direction-row { @@ -46,7 +46,7 @@ /* Grow containers inside a row need 0 width 0 so that they ignore content */ /* The nested selector for data-type is the wrapper around all components */ - .direction-row :global(> [data-type="component"] > .size-grow) { + .direction-row :global(> .component > .size-grow) { width: 0; } diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 90e2a55ca4..d7d2d94793 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -9,16 +9,15 @@ let dropInfo const getDOMNodeForComponent = component => { - const parent = component.closest("[data-type='component']") + const parent = component.closest(".component") const children = Array.from(parent.childNodes) return children?.find(node => node?.nodeType === 1) } // Callback when initially starting a drag on a draggable component const onDragStart = e => { - const parent = e.target.closest("[data-type='component']") - const child = getDOMNodeForComponent(e.target) - if (!parent?.dataset?.id || !child) { + const parent = e.target.closest(".component") + if (!parent?.classList.contains("draggable")) { return } @@ -31,7 +30,10 @@ builderStore.actions.setDragging(true) // Highlight being dragged by setting opacity - child.style.opacity = "0.5" + const child = getDOMNodeForComponent(e.target) + if (child) { + child.style.opacity = "0.5" + } } // Callback when drag stops (whether dropped or not) @@ -102,10 +104,10 @@ return } - const element = e.target.closest("[data-type='component']") + const element = e.target.closest(".component") if ( element && - element.dataset.droppable === "true" && + element.classList.contains("droppable") && element.dataset.id !== dragInfo.target ) { // Do nothing if this is the same target @@ -130,7 +132,7 @@ dropInfo = { target, name: element.dataset.name, - droppableInside: element.dataset.droppableInside === "true", + droppableInside: element.classList.contains("empty"), bounds, } } else { diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index 28109cb262..1a9e6477ac 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -7,7 +7,7 @@ $: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 const onMouseOver = e => { - const element = e.target.closest("[data-type='component']") + const element = e.target.closest(".interactive.component") const newId = element?.dataset?.id if (newId !== componentId) { componentId = newId diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index 3fce1bcf26..89d983d73b 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -24,8 +24,8 @@ export const styleable = (node, styles = {}) => { let selectComponent // Allow dragging if required - const parent = node.closest("[data-type='component']") - if (parent && parent.dataset.draggable === "true") { + const parent = node.closest(".component") + if (parent && parent.classList.contains("draggable")) { node.setAttribute("draggable", true) } From 9febe391bb96f9cba99d5c1c63f750a2433f2898 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 15:35:27 +0100 Subject: [PATCH 19/24] Refactor DND labels to before and after rather then above and below --- .../client/src/components/preview/DNDPositionIndicator.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/preview/DNDPositionIndicator.svelte b/packages/client/src/components/preview/DNDPositionIndicator.svelte index 8565d42f9e..19a44cfccc 100644 --- a/packages/client/src/components/preview/DNDPositionIndicator.svelte +++ b/packages/client/src/components/preview/DNDPositionIndicator.svelte @@ -7,7 +7,7 @@ export let transition $: dimensions = getDimensions(dropInfo?.bounds, dropInfo?.mode) - $: prefix = dropInfo?.mode === "above" ? "Above" : "Below" + $: prefix = dropInfo?.mode === "above" ? "Before" : "After" $: text = `${prefix} ${dropInfo?.name}` const getDimensions = (bounds, mode) => { From 495c20f85173899c2859fa6176c98e0782e89e3d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 20 Sep 2021 15:41:20 +0100 Subject: [PATCH 20/24] Simplify client app component re-render keying --- .../client/src/components/Component.svelte | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 3d863c276e..faf0226604 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -65,6 +65,7 @@ $: inSelectedPath = $builderStore.selectedComponentPath?.includes(id) $: evaluateConditions(enrichedSettings?._conditions) $: componentSettings = { ...enrichedSettings, ...conditionalSettings } + $: renderKey = `${propsHash}-${emptyState}` // Update component context $: componentStore.set({ @@ -176,32 +177,30 @@ $: droppable = interactive && !isLayout && !isScreen -{#key propsHash} - {#key empty} - {#if constructor && componentSettings && (visible || inSelectedPath)} - - -
- - {#if children.length} - {#each children as child (child._id)} - - {/each} - {:else if emptyState} - - {/if} - -
- {/if} - {/key} +{#key renderKey} + {#if constructor && componentSettings && (visible || inSelectedPath)} + + +
+ + {#if children.length} + {#each children as child (child._id)} + + {/each} + {:else if emptyState} + + {/if} + +
+ {/if} {/key} From 0297b3de3fa604bdbeef377fb50c4b1c9b1befdd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 Sep 2021 08:47:43 +0100 Subject: [PATCH 24/24] Simplify some DND style rules --- .../client/src/components/preview/Indicator.svelte | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/src/components/preview/Indicator.svelte b/packages/client/src/components/preview/Indicator.svelte index 212cd1671d..5f3a4b70c7 100644 --- a/packages/client/src/components/preview/Indicator.svelte +++ b/packages/client/src/components/preview/Indicator.svelte @@ -70,18 +70,18 @@ justify-content: flex-start; align-items: center; } + .text.line { + transform: translateY(-50%); + border-radius: 4px; + } .text.flipped { border-top-left-radius: 4px; border-bottom-left-radius: 4px; transform: translateY(0%); top: -2px; } - .text.line { - transform: translateY(-50%) !important; - border-radius: 4px !important; - } .text.right { - right: -2px !important; - left: auto !important; + right: -2px; + left: auto; }