From b470e57a29c0d6308a4a118bc0c164aa81e6857e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Wed, 28 Sep 2022 14:16:09 +0100 Subject: [PATCH 01/36] Expand component trees with padding when dragging and dropping --- packages/client/src/components/Component.svelte | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 2d586df24d..2a6c46feec 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -453,6 +453,7 @@ class:interactive class:editing class:block={isBlock} + class:explode={children.length && !isLayout && $builderStore.isDragging} data-id={id} data-name={name} data-icon={icon} @@ -481,15 +482,19 @@ .component { display: contents; } - + .component :global(> *) { + transition: padding 250ms ease, border 250ms ease; + } + .component.explode :global(> *) { + padding: 12px 4px !important; + border: 2px dashed var(--spectrum-global-color-gray-400) !important; + } .interactive :global(*:hover) { cursor: pointer; } - .draggable :global(*:hover) { cursor: grab; } - .editing :global(*:hover) { cursor: auto; } From 428b78618423dab0e08a91b52172c719158542de Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 6 Oct 2022 09:17:26 +0100 Subject: [PATCH 02/36] Don't clear drop target on invalid selection --- .../client/src/components/Component.svelte | 6 +- .../src/components/preview/DNDHandler.svelte | 89 +++++++++++++++---- packages/client/src/stores/builder.js | 18 ++++ packages/client/src/stores/components.js | 10 ++- packages/client/src/stores/screens.js | 41 ++++++++- 5 files changed, 139 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 2a6c46feec..f3c3e4b25c 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -96,6 +96,7 @@ $: selected = $builderStore.inBuilder && $builderStore.selectedComponentId === id $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) + $: inDropPath = $componentStore.dropPath?.includes(id) $: inDragPath = inSelectedPath && $builderStore.editMode // Derive definition properties which can all be optional, so need to be @@ -108,7 +109,7 @@ // Interactive components can be selected, dragged and highlighted inside // the builder preview $: builderInteractive = - $builderStore.inBuilder && insideScreenslot && !isBlock + $builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static $: devToolsInteractive = $devToolsStore.allowSelection && !isBlock $: interactive = builderInteractive || devToolsInteractive $: editing = editable && selected && $builderStore.editMode @@ -453,10 +454,11 @@ class:interactive class:editing class:block={isBlock} - class:explode={children.length && !isLayout && $builderStore.isDragging} + class:explode={children.length && !isLayout && inDropPath && false} data-id={id} data-name={name} data-icon={icon} + data-placeholder={id === "placeholder"} > {#if hasMissingRequiredSettings} diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index c37eb93afa..03ee97d610 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -77,19 +77,16 @@ builderStore.actions.setDragging(false) } - // Callback when on top of a component - const onDragOver = e => { - // Skip if we aren't validly dragging currently - if (!dragInfo || !dropInfo) { - return + const validateDrop = (dropInfo, e) => { + if (!dropInfo) { + return null } - e.preventDefault() const { droppableInside, bounds } = dropInfo const { top, left, height, width } = bounds const mouseY = e.clientY const mouseX = e.clientX - const snapFactor = droppableInside ? 0.33 : 0.5 + const snapFactor = droppableInside ? 0.25 : 0.5 const snapLimitV = Math.min(40, height * snapFactor) const snapLimitH = Math.min(40, width * snapFactor) @@ -108,20 +105,32 @@ // When no edges match, drop inside if possible if (!sides.length) { - dropInfo.mode = droppableInside ? "inside" : null - dropInfo.side = null - return + if (droppableInside) { + return { + ...dropInfo, + mode: "inside", + side: null, + } + } else { + return null + } } // When one edge matches, use that edge if (sides.length === 1) { - dropInfo.side = sides[0] if ([Sides.Top, Sides.Left].includes(sides[0])) { - dropInfo.mode = "above" + return { + ...dropInfo, + mode: "above", + side: sides[0], + } } else { - dropInfo.mode = "below" + return { + ...dropInfo, + mode: "below", + side: sides[0], + } } - return } // When 2 edges match, work out which is closer @@ -134,9 +143,34 @@ const edge = delta1 < delta2 ? sides[0] : sides[1] dropInfo.side = edge if ([Sides.Top, Sides.Left].includes(edge)) { - dropInfo.mode = "above" + return { + ...dropInfo, + mode: "above", + side: edge, + } } else { - dropInfo.mode = "below" + return { + ...dropInfo, + mode: "below", + side: edge, + } + } + } + + // Callback when on top of a component + const onDragOver = e => { + // Skip if we aren't validly dragging currently + if (!dragInfo || !dropInfo) { + return + } + + e.preventDefault() + + const nextDropInfo = validateDrop(dropInfo, e) + if (nextDropInfo) { + console.log("set from over") + dropInfo = nextDropInfo + console.log(dropInfo.mode, dropInfo.target) } } @@ -147,6 +181,16 @@ return } + // Update drop target + const dropTarget = e.target.closest(".component") + builderStore.actions.setDropTarget(dropTarget?.dataset.id) + + // // Do nothing if this is the placeholder + // if (element.dataset.id === "placeholder") { + // console.log("placeholder") + // return + // } + const element = e.target.closest(".component:not(.block)") if ( element && @@ -172,15 +216,20 @@ // dragOver const child = getDOMNodeForComponent(e.target) const bounds = child.getBoundingClientRect() - dropInfo = { + let nextDropInfo = { target, name: element.dataset.name, icon: element.dataset.icon, droppableInside: element.classList.contains("empty"), bounds, } + nextDropInfo = validateDrop(nextDropInfo, e) + if (nextDropInfo) { + console.log("set from enter") + dropInfo = nextDropInfo + } } else { - dropInfo = null + // dropInfo = null } } @@ -200,6 +249,10 @@ } } + $: mode = dropInfo?.mode + $: target = dropInfo?.target + $: builderStore.actions.updateDNDPlaceholder(mode, target) + onMount(() => { // Events fired on the draggable target document.addEventListener("dragstart", onDragStart, false) diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index fea070c27c..f935c580b6 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -1,6 +1,7 @@ import { writable, get } from "svelte/store" import { API } from "api" import { devToolsStore } from "./devTools.js" +import { findComponentPathById } from "../utils/components.js" const dispatchEvent = (type, data = {}) => { window.parent.postMessage({ type, data }) @@ -20,6 +21,9 @@ const createBuilderStore = () => { navigation: null, hiddenComponentIds: [], usedPlugins: null, + dndMode: null, + dndTarget: null, + dropTarget: null, // Legacy - allow the builder to specify a layout layout: null, @@ -102,6 +106,20 @@ const createBuilderStore = () => { // Notify the builder so we can reload component definitions dispatchEvent("reload-plugin") }, + updateDNDPlaceholder: (mode, target) => { + console.log(mode, target) + store.update(state => { + state.dndMode = mode + state.dndTarget = target + return state + }) + }, + setDropTarget: target => { + store.update(state => { + state.dropTarget = target + return state + }) + }, } return { ...store, diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index 98edfaddae..ee75be484a 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -36,8 +36,13 @@ const createComponentStore = () => { const definition = getComponentDefinition(component?._component) // Derive the selected component path - const path = + const selectedPath = findComponentPathById(asset?.props, selectedComponentId) || [] + let dropPath = [] + if ($builderState.isDragging) { + dropPath = + findComponentPathById(asset?.props, $builderState.dropTarget) || [] + } return { customComponentManifest: $store.customComponentManifest, @@ -45,9 +50,10 @@ const createComponentStore = () => { $store.mountedComponents[selectedComponentId], selectedComponent: component, selectedComponentDefinition: definition, - selectedComponentPath: path?.map(component => component._id), + selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, currentAsset: asset, + dropPath: dropPath?.map(component => component._id), } } ) diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index 84cd4000c1..2920ee333e 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -3,6 +3,11 @@ import { routeStore } from "./routes" import { builderStore } from "./builder" import { appStore } from "./app" import { RoleUtils } from "@budibase/frontend-core" +import { + findComponentById, + findComponentPathById, +} from "../utils/components.js" +import { Helpers } from "@budibase/bbui" const createScreenStore = () => { const store = derived( @@ -13,7 +18,7 @@ const createScreenStore = () => { if ($builderStore.inBuilder) { // Use builder defined definitions if inside the builder preview - activeScreen = $builderStore.screen + activeScreen = Helpers.cloneDeep($builderStore.screen) screens = [activeScreen] // Legacy - allow the builder to specify a layout @@ -24,8 +29,10 @@ const createScreenStore = () => { // Find the correct screen by matching the current route screens = $appStore.screens || [] if ($routeStore.activeRoute) { - activeScreen = screens.find( - screen => screen._id === $routeStore.activeRoute.screenId + activeScreen = Helpers.cloneDeep( + screens.find( + screen => screen._id === $routeStore.activeRoute.screenId + ) ) } @@ -40,6 +47,34 @@ const createScreenStore = () => { } } + // Insert DND placeholder if required + const { dndTarget, dndMode, selectedComponentId } = $builderStore + const insert = false + if (insert && activeScreen && dndTarget && dndMode) { + let selectedComponent = findComponentById( + activeScreen.props, + selectedComponentId + ) + const placeholder = { + ...selectedComponent, + _id: "placeholder", + static: true, + } + // delete selectedComponent._component + if (dndMode === "inside") { + const target = findComponentById(activeScreen.props, dndTarget) + target._children = [placeholder] + } else { + const path = findComponentPathById(activeScreen.props, dndTarget) + const parent = path?.[path.length - 2] + if (parent) { + const idx = parent._children.findIndex(x => x._id === dndTarget) + const delta = dndMode === "below" ? 1 : -1 + parent._children.splice(idx + delta, 0, placeholder) + } + } + } + // Assign ranks to screens, preferring higher roles and home screens screens.forEach(screen => { const roleId = screen.routing.roleId From 4322345aca988febff85f651d378bb33b1eb7e06 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 08:05:44 +0100 Subject: [PATCH 03/36] Rewrite drag and drop from scratch using mouse position heuristics --- .../client/src/components/Component.svelte | 20 +- .../src/components/preview/DNDHandler.svelte | 351 +++++++++++------- .../src/components/preview/Placeholder.svelte | 11 + .../preview/PlaceholderOverlay.svelte | 43 +++ packages/client/src/index.js | 1 + packages/client/src/stores/builder.js | 19 +- packages/client/src/stores/components.js | 11 +- packages/client/src/stores/screens.js | 32 +- 8 files changed, 315 insertions(+), 173 deletions(-) create mode 100644 packages/client/src/components/preview/Placeholder.svelte create mode 100644 packages/client/src/components/preview/PlaceholderOverlay.svelte diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index f3c3e4b25c..93620606c8 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -27,6 +27,8 @@ export let isLayout = false export let isScreen = false export let isBlock = false + export let parent = null + export let index = 0 // Get parent contexts const context = getContext("context") @@ -96,7 +98,7 @@ $: selected = $builderStore.inBuilder && $builderStore.selectedComponentId === id $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) - $: inDropPath = $componentStore.dropPath?.includes(id) + $: isDndParent = $componentStore.dndParent === id $: inDragPath = inSelectedPath && $builderStore.editMode // Derive definition properties which can all be optional, so need to be @@ -119,7 +121,7 @@ !isLayout && !isScreen && definition?.draggable !== false - $: droppable = interactive && !isLayout && !isScreen + $: droppable = interactive $: builderHidden = $builderStore.inBuilder && $builderStore.hiddenComponentIds?.includes(id) @@ -451,21 +453,24 @@ class:draggable class:droppable class:empty + class:parent={hasChildren} class:interactive class:editing class:block={isBlock} - class:explode={children.length && !isLayout && inDropPath && false} + class:explode={interactive && hasChildren && $builderStore.isDragging} + class:placeholder={id === "placeholder"} data-id={id} data-name={name} data-icon={icon} - data-placeholder={id === "placeholder"} + data-parent={parent} + data-index={index} > {#if hasMissingRequiredSettings} {:else if children.length} - {#each children as child (child._id)} - + {#each children as child, idx (child._id)} + {/each} {:else if emptyState} {#if isScreen} @@ -488,8 +493,9 @@ transition: padding 250ms ease, border 250ms ease; } .component.explode :global(> *) { - padding: 12px 4px !important; + padding: 16px !important; border: 2px dashed var(--spectrum-global-color-gray-400) !important; + border-radius: 4px !important; } .interactive :global(*:hover) { cursor: pointer; diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 03ee97d610..37bd9d3402 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -12,10 +12,12 @@ import { get } from "svelte/store" import IndicatorSet from "./IndicatorSet.svelte" import DNDPositionIndicator from "./DNDPositionIndicator.svelte" - import { builderStore } from "stores" + import { builderStore, componentStore } from "stores" + import PlaceholderOverlay from "./PlaceholderOverlay.svelte" let dragInfo let dropInfo + let placeholderInfo const getEdges = (bounds, mousePoint) => { const { width, height, top, left } = bounds @@ -33,39 +35,37 @@ return Math.sqrt(deltaX * deltaX + deltaY * deltaY) } - const getDOMNodeForComponent = component => { - const parent = component.closest(".component") - const children = Array.from(parent.children) - return children[0] + const getDOMNode = id => { + const component = document.getElementsByClassName(id)[0] + return [...component.children][0] } // Callback when initially starting a drag on a draggable component const onDragStart = e => { - const parent = e.target.closest(".component") - if (!parent?.classList.contains("draggable")) { + const component = e.target.closest(".component") + if (!component?.classList.contains("draggable")) { return } // Update state dragInfo = { - target: parent.dataset.id, - parent: parent.dataset.parent, + target: component.dataset.id, } builderStore.actions.selectComponent(dragInfo.target) builderStore.actions.setDragging(true) // Highlight being dragged by setting opacity - const child = getDOMNodeForComponent(e.target) + const child = getDOMNode(component.dataset.id) if (child) { child.style.opacity = "0.5" } } // Callback when drag stops (whether dropped or not) - const onDragEnd = e => { + const onDragEnd = () => { // Reset opacity style if (dragInfo) { - const child = getDOMNodeForComponent(e.target) + const child = getDOMNode(dragInfo.target) if (child) { child.style.opacity = "" } @@ -77,84 +77,179 @@ builderStore.actions.setDragging(false) } - const validateDrop = (dropInfo, e) => { + const variance = arr => { + const mean = arr.reduce((a, b) => a + b, 0) / arr.length + let squareSum = 0 + arr.forEach(value => { + const delta = value - mean + squareSum += delta * delta + }) + return squareSum / arr.length + } + + const handleEvent = e => { if (!dropInfo) { return null } + e.preventDefault() - const { droppableInside, bounds } = dropInfo - const { top, left, height, width } = bounds + let { id, parent, node, index, acceptsChildren, empty } = dropInfo const mouseY = e.clientY const mouseX = e.clientX - const snapFactor = droppableInside ? 0.25 : 0.5 - const snapLimitV = Math.min(40, height * snapFactor) - const snapLimitH = Math.min(40, width * snapFactor) - // Determine all sies we are within snap range of - let sides = [] - if (mouseY <= top + snapLimitV) { - sides.push(Sides.Top) - } else if (mouseY >= top + height - snapLimitV) { - sides.push(Sides.Bottom) - } - if (mouseX < left + snapLimitH) { - sides.push(Sides.Left) - } else if (mouseX > left + width - snapLimitH) { - sides.push(Sides.Right) + // if (!dropInfo.bounds) { + // } else { + // dropInfo.bounds.top = node.offsetTop + // dropInfo.bounds.left = node.offsetLeft + // console.log(node.offsetTop) + // } + + // console.log("calc") + // dropInfo.bounds = bounds + + // If we're over something that does not accept children then we must go + // above or below this component + if (!acceptsChildren) { + id = parent + acceptsChildren = true + empty = false + node = getDOMNode(parent) + // + // + // const bounds = node.getBoundingClientRect() + // const { top, left, height, width } = bounds + // const snapFactor = 0.5 + // const snapLimitV = Math.min(40, height * snapFactor) + // const snapLimitH = Math.min(40, width * snapFactor) + // + // // Determine all sides we are within snap range of + // let sides = [] + // if (mouseY <= top + snapLimitV) { + // sides.push(Sides.Top) + // } else if (mouseY >= top + height - snapLimitV) { + // sides.push(Sides.Bottom) + // } + // if (mouseX < left + snapLimitH) { + // sides.push(Sides.Left) + // } else if (mouseX > left + width - snapLimitH) { + // sides.push(Sides.Right) + // } + // + // // If we're somehow not in range of any side, do nothing + // if (!sides.length) { + // console.log("no sides match") + // return + // } + // + // let side + // if (sides.length === 1) { + // // When one edge matches, use that edge + // side = sides[0] + // } else { + // // When 2 edges match, work out which is closer + // const mousePoint = [mouseX, mouseY] + // const edges = getEdges(bounds, mousePoint) + // const edge1 = edges[sides[0]] + // const delta1 = calculatePointDelta(mousePoint, edge1) + // const edge2 = edges[sides[1]] + // const delta2 = calculatePointDelta(mousePoint, edge2) + // side = delta1 < delta2 ? sides[0] : sides[1] + // } + // if ([Sides.Top, Sides.Left].includes(side)) { + // // Before, so use the current index + // console.log("before") + // placeholderInfo = { + // parent: parent, + // index: index, + // } + // } else { + // console.log("after") + // // After, so use the next index + // placeholderInfo = { + // parent: parent, + // index: index + 1, + // } + // } } - // When no edges match, drop inside if possible - if (!sides.length) { - if (droppableInside) { - return { - ...dropInfo, - mode: "inside", - side: null, - } - } else { - return null + // We're now hovering over something which does accept children. + // If it is empty, just go inside it + if (empty) { + placeholderInfo = { + parent: id, + index: 0, } + return } - // When one edge matches, use that edge - if (sides.length === 1) { - if ([Sides.Top, Sides.Left].includes(sides[0])) { - return { - ...dropInfo, - mode: "above", - side: sides[0], - } - } else { - return { - ...dropInfo, - mode: "below", - side: sides[0], - } - } + // We're now hovering over something which accepts children and is not + // empty, so we need to work out where to inside the placeholder + + // Check we're actually inside + // if ( + // mouseY < top || + // mouseY > top + height || + // mouseX < left || + // mouseX > left + width + // ) { + // console.log("not inside") + // return + // } + + // Get all DOM nodes of children of this component. + // Filter out the placeholder as we don't want it to affect the index of + // the new placeholder. + const children = [...(node.children || [])] + .filter(x => !x.classList.contains("placeholder")) + .map(x => x.children[0]) + + // Calculate centers of each child + const centers = children.map(child => { + const childBounds = child.getBoundingClientRect() + return [ + childBounds.left + childBounds.width / 2, + childBounds.top + childBounds.height / 2, + ] + }) + + // Calculate variance of X and Y centers to determine layout + const xCoords = centers.map(x => x[0]) + const yCoords = centers.map(x => x[1]) + const xVariance = variance(xCoords) + const yVariance = variance(yCoords) + const column = xVariance <= yVariance + console.log(column ? "COL" : "ROW") + + // Now that we know the layout, find which children in this axis we are + // between + const childPositions = column ? yCoords : xCoords + const mousePosition = column ? mouseY : mouseX + + let idx = 0 + while (idx < children.length && childPositions[idx] < mousePosition) { + idx++ } - // When 2 edges match, work out which is closer - const mousePoint = [mouseX, mouseY] - const edges = getEdges(bounds, mousePoint) - const edge1 = edges[sides[0]] - const delta1 = calculatePointDelta(mousePoint, edge1) - const edge2 = edges[sides[1]] - const delta2 = calculatePointDelta(mousePoint, edge2) - const edge = delta1 < delta2 ? sides[0] : sides[1] - dropInfo.side = edge - if ([Sides.Top, Sides.Left].includes(edge)) { - return { - ...dropInfo, - mode: "above", - side: edge, - } - } else { - return { - ...dropInfo, - mode: "below", - side: edge, - } + placeholderInfo = { + parent: id, + index: idx, } + // // When no edges match, drop inside if possible + // if (!sides.length) { + // if (empty) { + // console.log("allowed inside") + // return { + // ...dropInfo, + // mode: "inside", + // side: null, + // bounds, + // } + // } else { + // // No sides but also not empty? + // console.log("no sides match, but not empty") + // return null + // } + // } } // Callback when on top of a component @@ -163,15 +258,7 @@ if (!dragInfo || !dropInfo) { return } - - e.preventDefault() - - const nextDropInfo = validateDrop(dropInfo, e) - if (nextDropInfo) { - console.log("set from over") - dropInfo = nextDropInfo - console.log(dropInfo.mode, dropInfo.target) - } + handleEvent(e) } // Callback when entering a potential drop target @@ -181,53 +268,53 @@ return } - // Update drop target - const dropTarget = e.target.closest(".component") - builderStore.actions.setDropTarget(dropTarget?.dataset.id) - - // // Do nothing if this is the placeholder + // Do nothing if this is the placeholder // if (element.dataset.id === "placeholder") { // console.log("placeholder") // return // } - const element = e.target.closest(".component:not(.block)") + const component = e.target.closest(".component:not(.block)") if ( - element && - element.classList.contains("droppable") && - element.dataset.id !== dragInfo.target + component && + component.classList.contains("droppable") && + component.dataset.id !== dragInfo.target ) { // Do nothing if this is the same target - if (element.dataset.id === dropInfo?.target) { + if (component.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 - if (!get(builderStore).isDragging) { - builderStore.actions.setDragging(true) - } - - // Store target ID - const target = element.dataset.id + // if (!get(builderStore).isDragging) { + // builderStore.actions.setDragging(true) + // } // Precompute and store some info to avoid recalculating everything in // dragOver - const child = getDOMNodeForComponent(e.target) - const bounds = child.getBoundingClientRect() - let nextDropInfo = { - target, - name: element.dataset.name, - icon: element.dataset.icon, - droppableInside: element.classList.contains("empty"), - bounds, - } - nextDropInfo = validateDrop(nextDropInfo, e) - if (nextDropInfo) { - console.log("set from enter") - dropInfo = nextDropInfo + dropInfo = { + id: component.dataset.id, + parent: component.dataset.parent, + index: parseInt(component.dataset.index), + node: getDOMNode(component.dataset.id), + empty: component.classList.contains("empty"), + acceptsChildren: component.classList.contains("parent"), } + + // console.log( + // "enter", + // component.dataset.name, + // "id", + // dropInfo.id, + // "parent", + // dropInfo.parent, + // "index", + // dropInfo.index + // ) + + handleEvent(e) } else { // dropInfo = null } @@ -240,18 +327,22 @@ // Callback when dropping a drag on top of some component const onDrop = e => { e.preventDefault() + dropInfo = null + placeholderInfo = null + dragInfo = null + builderStore.actions.setDragging(false) if (dropInfo?.mode) { - builderStore.actions.moveComponent( - dragInfo.target, - dropInfo.target, - dropInfo.mode - ) + // builderStore.actions.moveComponent( + // dragInfo.target, + // dropInfo.target, + // dropInfo.mode + // ) } } - $: mode = dropInfo?.mode - $: target = dropInfo?.target - $: builderStore.actions.updateDNDPlaceholder(mode, target) + $: parent = placeholderInfo?.parent + $: index = placeholderInfo?.index + $: builderStore.actions.updateDNDPlaceholder(parent, index) onMount(() => { // Events fired on the draggable target @@ -279,16 +370,20 @@ - +{#if $builderStore.isDragging} + +{/if} + + + + + + + diff --git a/packages/client/src/components/preview/Placeholder.svelte b/packages/client/src/components/preview/Placeholder.svelte new file mode 100644 index 0000000000..3457a49e6d --- /dev/null +++ b/packages/client/src/components/preview/Placeholder.svelte @@ -0,0 +1,11 @@ +
+ + diff --git a/packages/client/src/components/preview/PlaceholderOverlay.svelte b/packages/client/src/components/preview/PlaceholderOverlay.svelte new file mode 100644 index 0000000000..86950c533e --- /dev/null +++ b/packages/client/src/components/preview/PlaceholderOverlay.svelte @@ -0,0 +1,43 @@ + + +{#if left != null} +
+{/if} + + diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 0e8ab8c258..e31de18e4a 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -24,6 +24,7 @@ let app const loadBudibase = async () => { // Update builder store with any builder flags builderStore.set({ + ...get(builderStore), inBuilder: !!window["##BUDIBASE_IN_BUILDER##"], layout: window["##BUDIBASE_PREVIEW_LAYOUT##"], screen: window["##BUDIBASE_PREVIEW_SCREEN##"], diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index f935c580b6..0930ef62b8 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -21,9 +21,9 @@ const createBuilderStore = () => { navigation: null, hiddenComponentIds: [], usedPlugins: null, - dndMode: null, - dndTarget: null, - dropTarget: null, + + dndParent: null, + dndIndex: null, // Legacy - allow the builder to specify a layout layout: null, @@ -106,17 +106,10 @@ const createBuilderStore = () => { // Notify the builder so we can reload component definitions dispatchEvent("reload-plugin") }, - updateDNDPlaceholder: (mode, target) => { - console.log(mode, target) + updateDNDPlaceholder: (parent, index) => { store.update(state => { - state.dndMode = mode - state.dndTarget = target - return state - }) - }, - setDropTarget: target => { - store.update(state => { - state.dropTarget = target + state.dndParent = parent + state.dndIndex = index return state }) }, diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index ee75be484a..e73b498648 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -5,6 +5,7 @@ import { devToolsStore } from "./devTools" import { screenStore } from "./screens" import { builderStore } from "./builder" import Router from "../components/Router.svelte" +import Placeholder from "../components/preview/Placeholder.svelte" import * as AppComponents from "../components/app/index.js" const budibasePrefix = "@budibase/standard-components/" @@ -38,11 +39,6 @@ const createComponentStore = () => { // Derive the selected component path const selectedPath = findComponentPathById(asset?.props, selectedComponentId) || [] - let dropPath = [] - if ($builderState.isDragging) { - dropPath = - findComponentPathById(asset?.props, $builderState.dropTarget) || [] - } return { customComponentManifest: $store.customComponentManifest, @@ -53,7 +49,6 @@ const createComponentStore = () => { selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, currentAsset: asset, - dropPath: dropPath?.map(component => component._id), } } ) @@ -113,6 +108,8 @@ const createComponentStore = () => { // Screenslot is an edge case if (type === "screenslot") { type = `${budibasePrefix}${type}` + } else if (type === "placeholder") { + return {} } // Handle built-in components @@ -132,6 +129,8 @@ const createComponentStore = () => { } if (type === "screenslot") { return Router + } else if (type === "placeholder") { + return Placeholder } // Handle budibase components diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index 2920ee333e..e642fa9004 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -48,30 +48,24 @@ const createScreenStore = () => { } // Insert DND placeholder if required - const { dndTarget, dndMode, selectedComponentId } = $builderStore - const insert = false - if (insert && activeScreen && dndTarget && dndMode) { - let selectedComponent = findComponentById( - activeScreen.props, - selectedComponentId - ) + const { dndParent, dndIndex } = $builderStore + const insert = true + if (insert && activeScreen && dndParent && dndIndex != null) { + // let selectedComponent = findComponentById( + // activeScreen.props, + // selectedComponentId + // ) + // delete selectedComponent._component const placeholder = { - ...selectedComponent, + _component: "placeholder", _id: "placeholder", static: true, } - // delete selectedComponent._component - if (dndMode === "inside") { - const target = findComponentById(activeScreen.props, dndTarget) - target._children = [placeholder] + let parent = findComponentById(activeScreen.props, dndParent) + if (!parent._children?.length) { + parent._children = [placeholder] } else { - const path = findComponentPathById(activeScreen.props, dndTarget) - const parent = path?.[path.length - 2] - if (parent) { - const idx = parent._children.findIndex(x => x._id === dndTarget) - const delta = dndMode === "below" ? 1 : -1 - parent._children.splice(idx + delta, 0, placeholder) - } + parent._children.splice(dndIndex, 0, placeholder) } } From 9df86362b921e4877309e7e7f09ef7e96215b1e5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 08:20:51 +0100 Subject: [PATCH 04/36] Only explode components when dragging over them --- packages/client/src/components/Component.svelte | 4 ++-- packages/client/src/stores/components.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 93620606c8..ba5ca95f5a 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -98,8 +98,8 @@ $: selected = $builderStore.inBuilder && $builderStore.selectedComponentId === id $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) - $: isDndParent = $componentStore.dndParent === id $: inDragPath = inSelectedPath && $builderStore.editMode + $: inDndPath = $componentStore.dndPath?.includes(id) // Derive definition properties which can all be optional, so need to be // coerced to booleans @@ -457,7 +457,7 @@ class:interactive class:editing class:block={isBlock} - class:explode={interactive && hasChildren && $builderStore.isDragging} + class:explode={interactive && hasChildren && inDndPath} class:placeholder={id === "placeholder"} data-id={id} data-name={name} diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index e73b498648..43bc1c2ff6 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -39,6 +39,8 @@ const createComponentStore = () => { // Derive the selected component path const selectedPath = findComponentPathById(asset?.props, selectedComponentId) || [] + const dndPath = + findComponentPathById(asset?.props, $builderState.dndParent) || [] return { customComponentManifest: $store.customComponentManifest, @@ -49,6 +51,7 @@ const createComponentStore = () => { selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, currentAsset: asset, + dndPath: dndPath?.map(component => component._id), } } ) From fd3662e6b2b7de06b6478bdb9366c3521c13376a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 08:46:38 +0100 Subject: [PATCH 05/36] Improve row vs column detection to fix any edge cases --- .../client/src/components/Component.svelte | 6 +- .../src/components/preview/DNDHandler.svelte | 219 ++++-------------- .../preview/DNDPositionIndicator.svelte | 66 ------ 3 files changed, 44 insertions(+), 247 deletions(-) delete mode 100644 packages/client/src/components/preview/DNDPositionIndicator.svelte diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index ba5ca95f5a..0f60c7f707 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -28,7 +28,6 @@ export let isScreen = false export let isBlock = false export let parent = null - export let index = 0 // Get parent contexts const context = getContext("context") @@ -463,14 +462,13 @@ data-name={name} data-icon={icon} data-parent={parent} - data-index={index} > {#if hasMissingRequiredSettings} {:else if children.length} - {#each children as child, idx (child._id)} - + {#each children as child (child._id)} + {/each} {:else if emptyState} {#if isScreen} diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 37bd9d3402..50ea287a9b 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -9,32 +9,14 @@ - -{#key renderKey} - {#if dimensions && dropInfo?.mode !== "inside"} - - {/if} -{/key} From cb6a13fafd431cc4fee5298d5492c38a8bcc2b5f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 12:45:22 +0100 Subject: [PATCH 06/36] Fix issue with layout determination --- .../src/components/preview/DNDHandler.svelte | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 50ea287a9b..f7783990a5 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -117,19 +117,22 @@ }) // Calculate the variance between each set of positions on the children - const variances = Object.keys(childCoords[0]).map(key => { - const coords = childCoords.map(x => x[key]) - return { - variance: variance(coords), - side: key, - } - }) + const variances = Object.keys(childCoords[0]) + .filter(x => x !== "placeholder") + .map(key => { + const coords = childCoords.map(x => x[key]) + return { + variance: variance(coords), + side: key, + } + }) // Sort by variance. The lowest variance position indicates whether we are // in a row or column layout variances.sort((a, b) => { return a.variance < b.variance ? -1 : 1 }) + console.log(variances[0].side) const column = ["centerX", "left", "right"].includes(variances[0].side) console.log(column ? "COL" : "ROW") From 4a0be4523b45a4406f4f482edad080aa89b2b983 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 14:34:47 +0100 Subject: [PATCH 07/36] Hide the selected component when dragging --- packages/client/src/stores/screens.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index e642fa9004..d5525d15c7 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -48,14 +48,14 @@ const createScreenStore = () => { } // Insert DND placeholder if required - const { dndParent, dndIndex } = $builderStore + const { dndParent, dndIndex, selectedComponentId } = $builderStore const insert = true if (insert && activeScreen && dndParent && dndIndex != null) { - // let selectedComponent = findComponentById( - // activeScreen.props, - // selectedComponentId - // ) - // delete selectedComponent._component + let selectedComponent = findComponentById( + activeScreen.props, + selectedComponentId + ) + delete selectedComponent._component const placeholder = { _component: "placeholder", _id: "placeholder", From ab4eebc0cf0876e3bfd03212f17090f18a394cb7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 7 Oct 2022 20:00:25 +0100 Subject: [PATCH 08/36] Reduce jank by computing symmetrical component breakpoint whens considering DND candidate positions and ignoring the hidden selected component --- .../client/src/components/Component.svelte | 7 + .../src/components/preview/DNDHandler.svelte | 172 +++++++++--------- packages/client/src/stores/screens.js | 3 + 3 files changed, 93 insertions(+), 89 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 0f60c7f707..005eb12f2d 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -407,6 +407,7 @@ } const scrollIntoView = () => { + return const node = document.getElementsByClassName(id)?.[0]?.children[0] if (!node) { return @@ -458,6 +459,9 @@ class:block={isBlock} class:explode={interactive && hasChildren && inDndPath} class:placeholder={id === "placeholder"} + class:screen={isScreen} + class:dragging={$builderStore.selectedComponentId === id && + $builderStore.isDragging} data-id={id} data-name={name} data-icon={icon} @@ -504,4 +508,7 @@ .editing :global(*:hover) { cursor: auto; } + .dragging { + pointer-events: none; + } diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index f7783990a5..fbfdf06310 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,12 +1,3 @@ - - diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index d5525d15c7..85293cd0ce 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -65,6 +65,9 @@ const createScreenStore = () => { if (!parent._children?.length) { parent._children = [placeholder] } else { + parent._children = parent._children.filter( + x => x._id !== selectedComponentId + ) parent._children.splice(dndIndex, 0, placeholder) } } From e48bbb451e315741cde790da95c4a43b7f9a7e2b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 8 Oct 2022 15:03:44 +0100 Subject: [PATCH 09/36] Add throttle utility as an improved debounce --- packages/frontend-core/src/utils/utils.js | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index 587d057351..ed76f13494 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -4,6 +4,8 @@ * @param fn the async function to run * @return {Promise} a sequential version of the function */ +import { lastIndexOf } from "lodash/array" + export const sequential = fn => { let queue = [] return async (...params) => { @@ -40,3 +42,37 @@ export const debounce = (callback, minDelay = 1000) => { }) } } + +/** + * Utility to throttle invocations of a synchronous function. This is better + * than a simple debounce invocation for a number of reasons. Features include: + * - First invocation is immediate (no initial delay) + * - Every invocation has the latest params (no stale params) + * - There will always be a final invocation with the last params (no missing + * final update) + * @param callback + * @param minDelay + * @returns {Function} a throttled version function + */ +export const throttle = (callback, minDelay = 1000) => { + let lastParams + let stalled = false + let pending = false + const invoke = (...params) => { + lastParams = params + if (stalled) { + pending = true + return + } + callback(...lastParams) + stalled = true + setTimeout(() => { + stalled = false + if (pending) { + pending = false + invoke(...lastParams) + } + }, minDelay) + } + return invoke +} From e9dfc9ad9f646c3e546f598115fa78c2a03be95e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 8 Oct 2022 15:04:18 +0100 Subject: [PATCH 10/36] Throttle updates to prevent all jank and revert to component center breakboints for DND candidates --- .../client/src/components/Component.svelte | 3 +- .../src/components/preview/DNDHandler.svelte | 111 ++++++++++++++---- 2 files changed, 89 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 005eb12f2d..e6dbbf8579 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -495,7 +495,8 @@ transition: padding 250ms ease, border 250ms ease; } .component.explode :global(> *) { - padding: 16px !important; + padding: 32px !important; + gap: 16px !important; border: 2px dashed var(--spectrum-global-color-gray-400) !important; border-radius: 4px !important; } diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index fbfdf06310..20bbc8bada 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -3,6 +3,7 @@ import IndicatorSet from "./IndicatorSet.svelte" import { builderStore } from "stores" import PlaceholderOverlay from "./PlaceholderOverlay.svelte" + import { Utils } from "@budibase/frontend-core" let dragInfo let dropInfo @@ -75,15 +76,34 @@ }, 0) } - const handleEvent = e => { + let lastX + let lastY + let lastTime + + const processEvent = (mouseX, mouseY) => { if (!dropInfo) { return null } - e.preventDefault() let { id, parent, node, acceptsChildren, empty } = dropInfo - const mouseY = e.clientY - const mouseX = e.clientX + + // Debounce by 10px difference + // if (lastX != null && lastY != null) { + // const delta = Math.abs(mouseY - lastY) + Math.abs(mouseX - lastX) + // if (delta < 10) { + // console.log("delta fail") + // return + // } + // } + // lastX = mouseX + // lastY = mouseY + + // Debounce by time + // if (Date.now() - (lastTime || 0) < 100) { + // console.log("time fail") + // return + // } + // lastTime = Date.now() // If we're over something that does not accept children then we go up a // level and consider the mouse position relative to the parent @@ -112,7 +132,7 @@ const childCoords = [...(node.children || [])].map(node => { const bounds = node.children[0].getBoundingClientRect() return { - // placeholder: node.classList.contains("placeholder"), + placeholder: node.classList.contains("placeholder"), centerX: bounds.left + bounds.width / 2, centerY: bounds.top + bounds.height / 2, left: bounds.left, @@ -139,36 +159,79 @@ const column = ["centerX", "left", "right"].includes(variances[0].side) console.log(column ? "COL" : "ROW") - // Calculate breakpoints between children - let midpoints = [] - for (let i = 0; i < childCoords.length - 1; i++) { - const child1 = childCoords[i] - const child2 = childCoords[i + 1] - let midpoint - if (column) { - const top = Math.min(child1.top, child2.top) - const bottom = Math.max(child1.bottom, child2.bottom) - midpoint = (top + bottom) / 2 - } else { - const left = Math.min(child1.left, child2.left) - const right = Math.max(child1.right, child2.right) - midpoint = (left + right) / 2 - } - midpoints.push(midpoint) - } - // let midpoints = childCoords.map(x => (column ? x.centerY : x.centerX)) + /** SYMMETRICAL BREAKPOINTS **/ + // let breakpoints = [] + // for (let i = 0; i < childCoords.length - 1; i++) { + // const child1 = childCoords[i] + // const child2 = childCoords[i + 1] + // let breakpoint + // if (column) { + // const top = Math.min(child1.top, child2.top) + // const bottom = Math.max(child1.bottom, child2.bottom) + // breakpoint = (top + bottom) / 2 + // } else { + // const left = Math.min(child1.left, child2.left) + // const right = Math.max(child1.right, child2.right) + // breakpoint = (left + right) / 2 + // } + // breakpoints.push(breakpoint) + // } + + /** CENTER BREAKPOINTS **/ + let breakpoints = childCoords + .filter(x => !x.placeholder) + .map(x => { + return column ? x.centerY : x.centerX + }) + + /** NEXT EDGE BREAKPOINTS **/ + // let breakpoints = [] + // for (let i = 0; i < childCoords.length; i++) { + // let breakpoint + // if (column) { + // if (mouseY > childCoords[i].top && mouseY < childCoords[i].bottom) { + // // Inside this container + // if (childCoords[i + 1]) { + // breakpoint = childCoords[i + 1].top + // } else { + // breakpoint = childCoords[i].top + // } + // } else { + // breakpoint = + // mouseY < childCoords[i].bottom + // ? childCoords[i].top + // : childCoords[i].bottom + // } + // } else { + // breakpoint = + // mouseX < childCoords[i].left + // ? childCoords[i].left + // : childCoords[i].right + // } + // breakpoints.push(breakpoint) + // } // Determine the index to drop the component in const mousePosition = column ? mouseY : mouseX + let idx = 0 - while (idx < midpoints.length && midpoints[idx] < mousePosition) { + while (idx < breakpoints.length && breakpoints[idx] < mousePosition) { idx++ } + + // console.log(mousePosition, breakpoints.map(Math.round), idx) + placeholderInfo = { parent: id, index: idx, } } + const throttledProcessEvent = Utils.throttle(processEvent, 250) + + const handleEvent = e => { + e.preventDefault() + throttledProcessEvent(e.clientX, e.clientY) + } // Callback when on top of a component const onDragOver = e => { From 6f73ecdc4cf2d4b5145e66c8a100f9800808a8dd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 8 Oct 2022 15:18:05 +0100 Subject: [PATCH 11/36] Fix row/column detection, add comments and remove old code --- .../src/components/preview/DNDHandler.svelte | 112 +++--------------- 1 file changed, 17 insertions(+), 95 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 20bbc8bada..625c139e5e 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -76,35 +76,12 @@ }, 0) } - let lastX - let lastY - let lastTime - const processEvent = (mouseX, mouseY) => { if (!dropInfo) { return null } - let { id, parent, node, acceptsChildren, empty } = dropInfo - // Debounce by 10px difference - // if (lastX != null && lastY != null) { - // const delta = Math.abs(mouseY - lastY) + Math.abs(mouseX - lastX) - // if (delta < 10) { - // console.log("delta fail") - // return - // } - // } - // lastX = mouseX - // lastY = mouseY - - // Debounce by time - // if (Date.now() - (lastTime || 0) < 100) { - // console.log("time fail") - // return - // } - // lastTime = Date.now() - // If we're over something that does not accept children then we go up a // level and consider the mouse position relative to the parent if (!acceptsChildren) { @@ -114,7 +91,7 @@ } // We're now hovering over something which does accept children. - // If it is empty, just go inside it + // If it is empty, just go inside it. if (empty) { placeholderInfo = { parent: id, @@ -143,13 +120,15 @@ }) // Calculate the variance between each set of positions on the children - const variances = Object.keys(childCoords[0]).map(key => { - const coords = childCoords.map(x => x[key]) - return { - variance: variance(coords), - side: key, - } - }) + const variances = Object.keys(childCoords[0]) + .filter(x => x !== "placeholder") + .map(key => { + const coords = childCoords.map(x => x[key]) + return { + variance: variance(coords), + side: key, + } + }) // Sort by variance. The lowest variance position indicates whether we are // in a row or column layout @@ -157,76 +136,29 @@ return a.variance < b.variance ? -1 : 1 }) const column = ["centerX", "left", "right"].includes(variances[0].side) - console.log(column ? "COL" : "ROW") - /** SYMMETRICAL BREAKPOINTS **/ - // let breakpoints = [] - // for (let i = 0; i < childCoords.length - 1; i++) { - // const child1 = childCoords[i] - // const child2 = childCoords[i + 1] - // let breakpoint - // if (column) { - // const top = Math.min(child1.top, child2.top) - // const bottom = Math.max(child1.bottom, child2.bottom) - // breakpoint = (top + bottom) / 2 - // } else { - // const left = Math.min(child1.left, child2.left) - // const right = Math.max(child1.right, child2.right) - // breakpoint = (left + right) / 2 - // } - // breakpoints.push(breakpoint) - // } - - /** CENTER BREAKPOINTS **/ + // Calculate breakpoints between child components so we can determine the + // index to drop the component in. + // We want to ignore the placeholder from this calculation as it should not + // be considered a real child of the parent. let breakpoints = childCoords .filter(x => !x.placeholder) .map(x => { return column ? x.centerY : x.centerX }) - /** NEXT EDGE BREAKPOINTS **/ - // let breakpoints = [] - // for (let i = 0; i < childCoords.length; i++) { - // let breakpoint - // if (column) { - // if (mouseY > childCoords[i].top && mouseY < childCoords[i].bottom) { - // // Inside this container - // if (childCoords[i + 1]) { - // breakpoint = childCoords[i + 1].top - // } else { - // breakpoint = childCoords[i].top - // } - // } else { - // breakpoint = - // mouseY < childCoords[i].bottom - // ? childCoords[i].top - // : childCoords[i].bottom - // } - // } else { - // breakpoint = - // mouseX < childCoords[i].left - // ? childCoords[i].left - // : childCoords[i].right - // } - // breakpoints.push(breakpoint) - // } - // Determine the index to drop the component in const mousePosition = column ? mouseY : mouseX - let idx = 0 while (idx < breakpoints.length && breakpoints[idx] < mousePosition) { idx++ } - - // console.log(mousePosition, breakpoints.map(Math.round), idx) - placeholderInfo = { parent: id, index: idx, } } - const throttledProcessEvent = Utils.throttle(processEvent, 250) + const throttledProcessEvent = Utils.throttle(processEvent, 200) const handleEvent = e => { e.preventDefault() @@ -235,7 +167,6 @@ // Callback when on top of a component const onDragOver = e => { - // Skip if we aren't validly dragging currently if (!dragInfo || !dropInfo) { return } @@ -244,20 +175,12 @@ // Callback when entering a potential drop target const onDragEnter = e => { - // Skip if we aren't validly dragging currently - if (!dragInfo || !e.target.closest) { + if (!dragInfo) { return } - const component = e.target.closest(".component:not(.block)") + const component = e.target?.closest?.(".component:not(.block)") if (component && component.classList.contains("droppable")) { - // Do nothing if this is the same target - if (component.dataset.id === dropInfo?.target) { - return - } - - // Precompute and store some info to avoid recalculating everything in - // dragOver dropInfo = { id: component.dataset.id, parent: component.dataset.parent, @@ -265,7 +188,6 @@ empty: component.classList.contains("empty"), acceptsChildren: component.classList.contains("parent"), } - handleEvent(e) } } From cf0891c91118c035da96f7ace5fe20c8b761aaaf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 8 Oct 2022 15:28:22 +0100 Subject: [PATCH 12/36] Rename DND state variables for clarity --- .../src/components/preview/DNDHandler.svelte | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 625c139e5e..783578ad21 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -5,12 +5,14 @@ import PlaceholderOverlay from "./PlaceholderOverlay.svelte" import { Utils } from "@budibase/frontend-core" - let dragInfo + let sourceId + let targetInfo let dropInfo - let placeholderInfo - $: parent = placeholderInfo?.parent - $: index = placeholderInfo?.index + // These reactive statements are just a trick to only update the store when + // the value of one of the properties actually changes + $: parent = dropInfo?.parent + $: index = dropInfo?.index $: builderStore.actions.updateDNDPlaceholder(parent, index) // Util to get the inner DOM node by a component ID @@ -35,14 +37,14 @@ console.log("END") // Reset state - dragInfo = null + sourceId = null + targetInfo = null dropInfo = null - placeholderInfo = null builderStore.actions.setDragging(false) // Reset listener - if (dragInfo?.target) { - const component = document.getElementsByClassName(dragInfo.target)[0] + if (sourceId) { + const component = document.getElementsByClassName(sourceId)[0] if (component) { component.removeEventListener("dragend", stopDragging) } @@ -63,10 +65,8 @@ component.addEventListener("dragend", stopDragging) // Update state - dragInfo = { - target: component.dataset.id, - } - builderStore.actions.selectComponent(dragInfo.target) + sourceId = component.dataset.id + builderStore.actions.selectComponent(sourceId) builderStore.actions.setDragging(true) // Execute this asynchronously so we don't kill the drag event by hiding @@ -76,11 +76,13 @@ }, 0) } + // Core logic for handling drop events and determining where to render the + // drop target placeholder const processEvent = (mouseX, mouseY) => { - if (!dropInfo) { + if (!targetInfo) { return null } - let { id, parent, node, acceptsChildren, empty } = dropInfo + let { id, parent, node, acceptsChildren, empty } = targetInfo // If we're over something that does not accept children then we go up a // level and consider the mouse position relative to the parent @@ -93,7 +95,7 @@ // We're now hovering over something which does accept children. // If it is empty, just go inside it. if (empty) { - placeholderInfo = { + dropInfo = { parent: id, index: 0, } @@ -153,7 +155,7 @@ while (idx < breakpoints.length && breakpoints[idx] < mousePosition) { idx++ } - placeholderInfo = { + dropInfo = { parent: id, index: idx, } @@ -167,7 +169,7 @@ // Callback when on top of a component const onDragOver = e => { - if (!dragInfo || !dropInfo) { + if (!sourceId || !targetInfo) { return } handleEvent(e) @@ -175,13 +177,15 @@ // Callback when entering a potential drop target const onDragEnter = e => { - if (!dragInfo) { + if (!sourceId) { return } + // Find the next valid component to consider dropping over, ignoring nested + // block components const component = e.target?.closest?.(".component:not(.block)") if (component && component.classList.contains("droppable")) { - dropInfo = { + targetInfo = { id: component.dataset.id, parent: component.dataset.parent, node: getDOMNode(component.dataset.id), From 9f7504c53fa8e5fb0b2892d942f4143468caeffc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Sat, 8 Oct 2022 15:32:36 +0100 Subject: [PATCH 13/36] Reduce padding when dragging over a component tree and remove unused component class names --- packages/client/src/components/Component.svelte | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index e6dbbf8579..c3d6de5cae 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -459,9 +459,6 @@ class:block={isBlock} class:explode={interactive && hasChildren && inDndPath} class:placeholder={id === "placeholder"} - class:screen={isScreen} - class:dragging={$builderStore.selectedComponentId === id && - $builderStore.isDragging} data-id={id} data-name={name} data-icon={icon} @@ -495,7 +492,7 @@ transition: padding 250ms ease, border 250ms ease; } .component.explode :global(> *) { - padding: 32px !important; + padding: 16px !important; gap: 16px !important; border: 2px dashed var(--spectrum-global-color-gray-400) !important; border-radius: 4px !important; @@ -509,7 +506,4 @@ .editing :global(*:hover) { cursor: auto; } - .dragging { - pointer-events: none; - } From 6cf3a0af5b1ebcb88cb9e1ef77667b23b240d08b Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 10 Oct 2022 09:22:47 +0100 Subject: [PATCH 14/36] Tune DND throttle rate --- packages/client/src/components/preview/DNDHandler.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 783578ad21..a4e737214f 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -160,7 +160,7 @@ index: idx, } } - const throttledProcessEvent = Utils.throttle(processEvent, 200) + const throttledProcessEvent = Utils.throttle(processEvent, 130) const handleEvent = e => { e.preventDefault() From c5b36863d2252b3930ad2546c2416b254aaf3d73 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 10 Oct 2022 09:36:17 +0100 Subject: [PATCH 15/36] Make DND work again by converting new parent+index params into old target+mode --- .../src/components/preview/DNDHandler.svelte | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index a4e737214f..04503f8020 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -1,9 +1,11 @@ + +{#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 @@
@@ -206,6 +214,9 @@
{category.name}
{#each category.children as component}
onDragStart(component.component)} + on:dragend={onDragEnd} data-cy={`component-${component.name}`} class="component" class:selected={selectedIndex === diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 40f52dd1ae..361f11dff2 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -2,21 +2,22 @@ import { onMount, onDestroy } from "svelte" import { get } from "svelte/store" import IndicatorSet from "./IndicatorSet.svelte" - import { builderStore, componentStore } from "stores" + import { + builderStore, + componentStore, + dndStore, + dndParent, + isDragging, + } from "stores" import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte" import { Utils } from "@budibase/frontend-core" import { findComponentById } from "utils/components.js" - let sourceInfo - let targetInfo - let dropInfo - - // These reactive statements are just a trick to only update the store when - // the value of one of the properties actually changes - $: parent = dropInfo?.parent - $: index = dropInfo?.index - $: bounds = sourceInfo?.bounds - $: builderStore.actions.updateDNDPlaceholder(parent, index, bounds) + // Cache some dnd store state as local variables as it massively helps + // performance. It lets us avoid calling svelte getters on every DOM action. + $: source = $dndStore.source + $: target = $dndStore.target + $: drop = $dndStore.drop // Util to get the inner DOM node by a component ID const getDOMNode = id => { @@ -37,19 +38,16 @@ // Callback when drag stops (whether dropped or not) const stopDragging = () => { - // Reset state - sourceInfo = null - targetInfo = null - dropInfo = null - builderStore.actions.setDragging(false) - // Reset listener - if (sourceInfo) { - const component = document.getElementsByClassName(sourceInfo.id)[0] + if (source?.id) { + const component = document.getElementsByClassName(source?.id)[0] if (component) { component.removeEventListener("dragend", stopDragging) } } + + // Reset state + dndStore.actions.reset() } // Callback when initially starting a drag on a draggable component @@ -66,6 +64,7 @@ component.addEventListener("dragend", stopDragging) // Update state + const id = component.dataset.id const parentId = component.dataset.parent const parent = findComponentById( get(componentStore).currentAsset.props, @@ -74,14 +73,13 @@ const index = parent._children.findIndex( x => x._id === component.dataset.id ) - sourceInfo = { - id: component.dataset.id, + dndStore.actions.startDragging({ + id, bounds: component.children[0].getBoundingClientRect(), parent: parentId, index, - } - builderStore.actions.selectComponent(sourceInfo.id) - builderStore.actions.setDragging(true) + }) + builderStore.actions.selectComponent(id) // Set initial drop info to show placeholder exactly where the dragged // component is. @@ -89,20 +87,20 @@ // the same handler as selecting a new component (which causes a client // re-initialisation). setTimeout(() => { - dropInfo = { + dndStore.actions.updateDrop({ parent: parentId, index, - } + }) }, 0) } // Core logic for handling drop events and determining where to render the // drop target placeholder const processEvent = (mouseX, mouseY) => { - if (!targetInfo) { + if (!target) { return null } - let { id, parent, node, acceptsChildren, empty } = targetInfo + let { id, parent, node, acceptsChildren, empty } = target // If we're over something that does not accept children then we go up a // level and consider the mouse position relative to the parent @@ -115,10 +113,10 @@ // We're now hovering over something which does accept children. // If it is empty, just go inside it. if (empty) { - dropInfo = { + dndStore.actions.updateDrop({ parent: id, index: 0, - } + }) return } @@ -188,10 +186,10 @@ while (idx < breakpoints.length && breakpoints[idx] < mousePosition) { idx++ } - dropInfo = { + dndStore.actions.updateDrop({ parent: id, index: idx, - } + }) } const throttledProcessEvent = Utils.throttle(processEvent, 130) @@ -202,7 +200,7 @@ // Callback when on top of a component const onDragOver = e => { - if (!sourceInfo || !targetInfo) { + if (!source || !target) { return } handleEvent(e) @@ -210,69 +208,80 @@ // Callback when entering a potential drop target const onDragEnter = e => { - if (!sourceInfo) { + if (!source) { return } // Find the next valid component to consider dropping over, ignoring nested // block components const component = e.target?.closest?.( - `.component:not(.block):not(.${sourceInfo.id})` + `.component:not(.block):not(.${source.id})` ) if (component && component.classList.contains("droppable")) { - targetInfo = { + dndStore.actions.updateTarget({ id: component.dataset.id, parent: component.dataset.parent, node: getDOMNode(component.dataset.id), empty: component.classList.contains("empty"), acceptsChildren: component.classList.contains("parent"), - } + }) handleEvent(e) } } // Callback when dropping a drag on top of some component const onDrop = () => { - let target, mode - - // Convert parent + index into target + mode - if (sourceInfo && dropInfo?.parent && dropInfo.index != null) { - const parent = findComponentById( - get(componentStore).currentAsset?.props, - dropInfo.parent - ) - if (!parent) { - 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 !== sourceInfo.id - ) - - // Use inside if no existing children - if (!children?.length) { - target = parent._id - mode = "inside" - } else if (dropInfo.index === 0) { - target = children[0]?._id - mode = "above" - } else { - target = children[dropInfo.index - 1]?._id - mode = "below" - } + if (!source || !drop?.parent || drop?.index == null) { + return } - if (target && mode) { - builderStore.actions.moveComponent(sourceInfo.id, target, mode) + // Check if we're adding a new component rather than moving one + if (source.newComponentType) { + builderStore.actions.dropNewComponent( + source.newComponentType, + drop.parent, + drop.index + ) + } + + // Convert parent + index into target + mode + let legacyDropTarget, legacyDropMode + const parent = findComponentById( + get(componentStore).currentAsset?.props, + drop.parent + ) + if (!parent) { + return + } + + // Do nothing if we didn't change the location + if (source.parent === drop.parent && source.index === drop.index) { + return + } + + // Filter out source component and placeholder from consideration + const children = parent._children?.filter( + x => x._id !== "placeholder" && x._id !== source.id + ) + + // Use inside if no existing children + if (!children?.length) { + legacyDropTarget = parent._id + legacyDropMode = "inside" + } else if (drop.index === 0) { + legacyDropTarget = children[0]?._id + legacyDropMode = "above" + } else { + legacyDropTarget = children[drop.index - 1]?._id + legacyDropMode = "below" + } + + if (legacyDropTarget && legacyDropMode) { + builderStore.actions.moveComponent( + source.id, + legacyDropTarget, + legacyDropMode + ) } } @@ -298,13 +307,13 @@ -{#if $builderStore.isDragging} +{#if $isDragging} {/if} diff --git a/packages/client/src/components/preview/DNDPlaceholder.svelte b/packages/client/src/components/preview/DNDPlaceholder.svelte index 9efabdbc27..3725f9e06e 100644 --- a/packages/client/src/components/preview/DNDPlaceholder.svelte +++ b/packages/client/src/components/preview/DNDPlaceholder.svelte @@ -1,8 +1,8 @@ - import { builderStore } from "stores" + import { builderStore, isDragging } from "stores" import IndicatorSet from "./IndicatorSet.svelte" $: color = $builderStore.editMode @@ -8,7 +8,7 @@ { diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 526ed1c10d..17f1c94359 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -6,6 +6,7 @@ import { blockStore, componentStore, environmentStore, + dndStore, } from "./stores" import loadSpectrumIcons from "@budibase/bbui/spectrum-icons-rollup.js" import { get } from "svelte/store" @@ -60,6 +61,24 @@ const loadBudibase = async () => { if (name === "eject-block") { const block = blockStore.actions.getBlock(payload) block?.eject() + } else if (name === "dragging-new-component") { + const { dragging, component } = payload + if (dragging) { + dndStore.actions.startDragging({ + id: null, + parent: null, + bounds: { + height: 64, + width: 64, + }, + index: null, + newComponentType: component, + }) + builderStore.actions.setDraggingNewComponent(true) + } else { + dndStore.actions.reset() + builderStore.actions.setDraggingNewComponent(false) + } } } diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index 0bb5693edb..135c08343e 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -16,13 +16,10 @@ const createBuilderStore = () => { theme: null, customTheme: null, previewDevice: "desktop", - isDragging: false, + draggingNewComponent: false, navigation: null, hiddenComponentIds: [], usedPlugins: null, - dndParent: null, - dndIndex: null, - dndBounds: null, // Legacy - allow the builder to specify a layout layout: null, @@ -70,11 +67,19 @@ const createBuilderStore = () => { mode, }) }, - setDragging: dragging => { - if (dragging === get(store).isDragging) { + dropNewComponent: (component, parent, index) => { + console.log("dispatch", component, parent, index) + dispatchEvent("drop-new-component", { + component, + parent, + index, + }) + }, + setDraggingNewComponent: draggingNewComponent => { + if (draggingNewComponent === get(store).draggingNewComponent) { return } - store.update(state => ({ ...state, isDragging: dragging })) + store.update(state => ({ ...state, draggingNewComponent })) }, setEditMode: enabled => { if (enabled === get(store).editMode) { @@ -111,14 +116,6 @@ const createBuilderStore = () => { // Notify the builder so we can reload component definitions dispatchEvent("reload-plugin") }, - updateDNDPlaceholder: (parent, index, bounds) => { - store.update(state => { - state.dndParent = parent - state.dndIndex = index - state.dndBounds = bounds - return state - }) - }, } return { ...store, diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index e1d6012150..71b7deb5a6 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -4,6 +4,7 @@ import { findComponentById, findComponentPathById } from "../utils/components" import { devToolsStore } from "./devTools" import { screenStore } from "./screens" import { builderStore } from "./builder" +import { dndParent } from "./dnd.js" import Router from "../components/Router.svelte" import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte" import * as AppComponents from "../components/app/index.js" @@ -19,8 +20,8 @@ const createComponentStore = () => { }) const derivedStore = derived( - [store, builderStore, devToolsStore, screenStore], - ([$store, $builderState, $devToolsState, $screenState]) => { + [store, builderStore, devToolsStore, screenStore, dndParent], + ([$store, $builderState, $devToolsState, $screenState, $dndParent]) => { // Avoid any of this logic if we aren't in the builder preview if (!$builderState.inBuilder && !$devToolsState.visible) { return {} @@ -40,8 +41,7 @@ const createComponentStore = () => { // Derive the selected component path const selectedPath = findComponentPathById(asset?.props, selectedComponentId) || [] - const dndPath = - findComponentPathById(asset?.props, $builderState.dndParent) || [] + const dndPath = findComponentPathById(asset?.props, $dndParent) || [] return { customComponentManifest: $store.customComponentManifest, diff --git a/packages/client/src/stores/derived/currentRole.js b/packages/client/src/stores/derived/currentRole.js new file mode 100644 index 0000000000..28287e1ea4 --- /dev/null +++ b/packages/client/src/stores/derived/currentRole.js @@ -0,0 +1,11 @@ +import { derived } from "svelte/store" +import { devToolsStore } from "../devTools.js" +import { authStore } from "../auth.js" + +// Derive the current role of the logged-in user +export const currentRole = derived( + [devToolsStore, authStore], + ([$devToolsStore, $authStore]) => { + return ($devToolsStore.enabled && $devToolsStore.role) || $authStore?.roleId + } +) diff --git a/packages/client/src/stores/derived/index.js b/packages/client/src/stores/derived/index.js new file mode 100644 index 0000000000..85df1aaafa --- /dev/null +++ b/packages/client/src/stores/derived/index.js @@ -0,0 +1,2 @@ +export { isDragging } from "./isDragging.js" +export { currentRole } from "./currentRole.js" diff --git a/packages/client/src/stores/derived/isDragging.js b/packages/client/src/stores/derived/isDragging.js new file mode 100644 index 0000000000..d3edd586c6 --- /dev/null +++ b/packages/client/src/stores/derived/isDragging.js @@ -0,0 +1,10 @@ +import { derived } from "svelte/store" +import { dndStore } from "../dnd" +import { builderStore } from "../builder.js" + +// Derive whether we are dragging or not +export const isDragging = derived( + [dndStore, builderStore], + ([$dndStore, $builderStore]) => + $dndStore.source != null || $builderStore.draggingNewComponent +) diff --git a/packages/client/src/stores/dnd.js b/packages/client/src/stores/dnd.js new file mode 100644 index 0000000000..805ec8c339 --- /dev/null +++ b/packages/client/src/stores/dnd.js @@ -0,0 +1,60 @@ +import { writable, derived } from "svelte/store" + +const createDndStore = () => { + const initialState = { + // Info about the dragged component + source: null, + + // Info about the target component being hovered over + target: null, + + // Info about where the component would be dropped + drop: null, + } + const store = writable(initialState) + + // newComponentType is an optional field to signify we are creating a new + // component rather than moving an existing one + const startDragging = ({ id, parent, bounds, index, newComponentType }) => { + store.set({ + ...initialState, + source: { id, parent, bounds, index, newComponentType }, + }) + } + + const updateTarget = ({ id, parent, node, empty, acceptsChildren }) => { + store.update(state => { + state.target = { id, parent, node, empty, acceptsChildren } + return state + }) + } + + const updateDrop = ({ parent, index }) => { + store.update(state => { + state.drop = { parent, index } + return state + }) + } + + const reset = () => { + store.set(initialState) + } + + return { + subscribe: store.subscribe, + actions: { + startDragging, + updateTarget, + updateDrop, + reset, + }, + } +} + +export const dndStore = createDndStore() +export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent) +export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index) +export const dndBounds = derived( + dndStore, + $dndStore => $dndStore.source?.bounds +) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index 5b77762223..f86c0a0517 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -1,7 +1,3 @@ -import { derived } from "svelte/store" -import { devToolsStore } from "./devTools.js" -import { authStore } from "./auth.js" - export { authStore } from "./auth" export { appStore } from "./app" export { notificationStore } from "./notification" @@ -19,6 +15,7 @@ export { uploadStore } from "./uploads.js" export { rowSelectionStore } from "./rowSelection.js" export { blockStore } from "./blocks.js" export { environmentStore } from "./environment" +export { dndStore, dndIndex, dndParent, dndBounds } from "./dnd" // Context stores are layered and duplicated, so it is not a singleton export { createContextStore } from "./context" @@ -26,10 +23,5 @@ export { createContextStore } from "./context" // Initialises an app by loading screens and routes export { initialise } from "./initialise" -// Derive the current role of the logged-in user -export const currentRole = derived( - [devToolsStore, authStore], - ([$devToolsStore, $authStore]) => { - return ($devToolsStore.enabled && $devToolsStore.role) || $authStore?.roleId - } -) +// Derived state +export * from "./derived" diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index 5c630d2adf..b0bc06eee9 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -2,6 +2,7 @@ import { derived } from "svelte/store" import { routeStore } from "./routes" import { builderStore } from "./builder" import { appStore } from "./app" +import { dndIndex, dndParent } from "./dnd.js" import { RoleUtils } from "@budibase/frontend-core" import { findComponentById, findComponentParent } from "../utils/components.js" import { Helpers } from "@budibase/bbui" @@ -9,8 +10,8 @@ import { DNDPlaceholderID, DNDPlaceholderType } from "constants" const createScreenStore = () => { const store = derived( - [appStore, routeStore, builderStore], - ([$appStore, $routeStore, $builderStore]) => { + [appStore, routeStore, builderStore, dndParent, dndIndex], + ([$appStore, $routeStore, $builderStore, $dndParent, $dndIndex]) => { let activeLayout, activeScreen let screens @@ -46,31 +47,33 @@ const createScreenStore = () => { } // Insert DND placeholder if required - const { dndParent, dndIndex, selectedComponentId } = $builderStore - if (activeScreen && dndParent && dndIndex != null) { - // Remove selected component from tree - let selectedParent = findComponentParent( - activeScreen.props, - selectedComponentId - ) - selectedParent._children = selectedParent._children?.filter( - x => x._id !== selectedComponentId - ) + if (activeScreen && $dndParent && $dndIndex != null) { + // Remove selected component from tree if we are moving an existing + // component + const { selectedComponentId, draggingNewComponent } = $builderStore + if (!draggingNewComponent) { + let selectedParent = findComponentParent( + activeScreen.props, + selectedComponentId + ) + if (selectedParent) { + selectedParent._children = selectedParent._children?.filter( + x => x._id !== selectedComponentId + ) + } + } - // Insert placeholder + // Insert placeholder component const placeholder = { _component: DNDPlaceholderID, _id: DNDPlaceholderType, static: true, } - let parent = findComponentById(activeScreen.props, dndParent) + let parent = findComponentById(activeScreen.props, $dndParent) if (!parent._children?.length) { parent._children = [placeholder] } else { - parent._children = parent._children.filter( - x => x._id !== selectedComponentId - ) - parent._children.splice(dndIndex, 0, placeholder) + parent._children.splice($dndIndex, 0, placeholder) } } From 23eb09ab6aebba56dd983ac5a7bfec5144f02acf Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 15:45:02 +0100 Subject: [PATCH 21/36] Don't reset component padded state for DND until dragging stops, to prevent jankiness due to losing space --- .../client/src/components/Component.svelte | 22 ++++++++++++++----- .../src/components/preview/DNDHandler.svelte | 4 +++- packages/client/src/index.js | 2 +- packages/client/src/stores/components.js | 1 + 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 4f82523ec0..3d65db7206 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -16,7 +16,13 @@ propsAreSame, getSettingsDefinition, } from "utils/componentProps" - import { builderStore, devToolsStore, componentStore, appStore } from "stores" + import { + builderStore, + devToolsStore, + componentStore, + appStore, + isDragging, + } from "stores" import { Helpers } from "@budibase/bbui" import { getActiveConditions, reduceConditionActions } from "utils/conditions" import Placeholder from "components/app/Placeholder.svelte" @@ -152,6 +158,12 @@ // Scroll the selected element into view $: selected && scrollIntoView() + // When dragging and dropping, pad components to allow dropping between + // nested layers. Only reset this when dragging stops. + let pad = false + $: pad = pad || (interactive && hasChildren && inDndPath) + $: $isDragging, (pad = false) + // Update component context $: store.set({ id, @@ -454,11 +466,11 @@ class:draggable class:droppable class:empty - class:parent={hasChildren} class:interactive class:editing + class:pad + class:parent={hasChildren} class:block={isBlock} - class:explode={interactive && hasChildren && inDndPath} class:placeholder={id === DNDPlaceholderID} data-id={id} data-name={name} @@ -490,9 +502,9 @@ display: contents; } .component :global(> *) { - transition: padding 260ms ease, border 260ms ease; + transition: padding 260ms ease-in, border 260ms ease-in; } - .component.explode :global(> *) { + .component.pad :global(> *) { padding: 12px !important; gap: 12px !important; border: 2px dotted var(--spectrum-global-color-gray-400) !important; diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 361f11dff2..e2222300e0 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -13,6 +13,8 @@ import { Utils } from "@budibase/frontend-core" import { findComponentById } from "utils/components.js" + const ThrottleRate = 130 + // Cache some dnd store state as local variables as it massively helps // performance. It lets us avoid calling svelte getters on every DOM action. $: source = $dndStore.source @@ -191,7 +193,7 @@ index: idx, }) } - const throttledProcessEvent = Utils.throttle(processEvent, 130) + const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate) const handleEvent = e => { e.preventDefault() diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 17f1c94359..383421cc35 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -69,7 +69,7 @@ const loadBudibase = async () => { parent: null, bounds: { height: 64, - width: 64, + width: 128, }, index: null, newComponentType: component, diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index 71b7deb5a6..e246299741 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -53,6 +53,7 @@ const createComponentStore = () => { mountedComponentCount: Object.keys($store.mountedComponents).length, currentAsset: asset, dndPath: dndPath?.map(component => component._id), + dndDepth: dndPath?.length || 0, } } ) From d02fb96e6eb4c446ea217fc02440de0752ac68a0 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 18:16:19 +0100 Subject: [PATCH 22/36] Invert some client store dependencies to improve performance and prevent dependency cycles --- packages/client/manifest.json | 4 ++ .../client/src/components/Component.svelte | 11 +++-- .../src/components/preview/DNDHandler.svelte | 19 ++++---- .../components/preview/HoverIndicator.svelte | 4 +- .../preview/SelectionIndicator.svelte | 4 +- .../src/components/preview/SettingsBar.svelte | 4 +- packages/client/src/index.js | 15 ++----- packages/client/src/stores/builder.js | 8 ---- packages/client/src/stores/components.js | 30 +++++-------- .../src/stores/derived/dndComponentPath.js | 13 ++++++ packages/client/src/stores/derived/index.js | 5 ++- .../client/src/stores/derived/isDragging.js | 10 ----- packages/client/src/stores/dnd.js | 45 ++++++++++++++++--- packages/client/src/stores/index.js | 9 +++- packages/client/src/stores/screens.js | 24 +++++++--- 15 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 packages/client/src/stores/derived/dndComponentPath.js delete mode 100644 packages/client/src/stores/derived/isDragging.js diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 0edb5c0f39..14092b7b4d 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -85,6 +85,10 @@ "icon": "Selection", "hasChildren": true, "showSettingsBar": true, + "size": { + "width": 400, + "height": 200 + }, "styles": [ "padding", "size", diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 3d65db7206..fe5f363152 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -21,14 +21,14 @@ devToolsStore, componentStore, appStore, - isDragging, + dndIsDragging, + dndComponentPath, } from "stores" import { Helpers } from "@budibase/bbui" import { getActiveConditions, reduceConditionActions } from "utils/conditions" 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 @@ -105,7 +105,7 @@ $builderStore.inBuilder && $builderStore.selectedComponentId === id $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) $: inDragPath = inSelectedPath && $builderStore.editMode - $: inDndPath = $componentStore.dndPath?.includes(id) + $: inDndPath = $dndComponentPath?.includes(id) // Derive definition properties which can all be optional, so need to be // coerced to booleans @@ -162,7 +162,7 @@ // nested layers. Only reset this when dragging stops. let pad = false $: pad = pad || (interactive && hasChildren && inDndPath) - $: $isDragging, (pad = false) + $: $dndIsDragging, (pad = false) // Update component context $: store.set({ @@ -471,7 +471,6 @@ class:pad class:parent={hasChildren} class:block={isBlock} - class:placeholder={id === DNDPlaceholderID} data-id={id} data-name={name} data-icon={icon} @@ -502,7 +501,7 @@ display: contents; } .component :global(> *) { - transition: padding 260ms ease-in, border 260ms ease-in; + transition: padding 260ms ease-out, border 260ms ease-out; } .component.pad :global(> *) { padding: 12px !important; diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index e2222300e0..392cdceb36 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -4,14 +4,15 @@ import IndicatorSet from "./IndicatorSet.svelte" import { builderStore, - componentStore, + screenStore, dndStore, dndParent, - isDragging, + dndIsDragging, } from "stores" import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte" import { Utils } from "@budibase/frontend-core" import { findComponentById } from "utils/components.js" + import { DNDPlaceholderID } from "constants" const ThrottleRate = 130 @@ -69,13 +70,13 @@ const id = component.dataset.id const parentId = component.dataset.parent const parent = findComponentById( - get(componentStore).currentAsset.props, + get(screenStore).activeScreen?.props, parentId ) const index = parent._children.findIndex( x => x._id === component.dataset.id ) - dndStore.actions.startDragging({ + dndStore.actions.startDraggingExistingComponent({ id, bounds: component.children[0].getBoundingClientRect(), parent: parentId, @@ -127,7 +128,7 @@ let ephemeralDiv if (node.children.length === 1) { ephemeralDiv = document.createElement("div") - ephemeralDiv.classList.add("placeholder") + ephemeralDiv.dataset.id = DNDPlaceholderID node.appendChild(ephemeralDiv) } @@ -138,7 +139,7 @@ const child = node.children?.[0] || node const bounds = child.getBoundingClientRect() return { - placeholder: node.classList.contains("placeholder"), + placeholder: node.dataset.id === DNDPlaceholderID, centerX: bounds.left + bounds.width / 2, centerY: bounds.top + bounds.height / 2, left: bounds.left, @@ -249,7 +250,7 @@ // Convert parent + index into target + mode let legacyDropTarget, legacyDropMode const parent = findComponentById( - get(componentStore).currentAsset?.props, + get(screenStore).activeScreen?.props, drop.parent ) if (!parent) { @@ -263,7 +264,7 @@ // Filter out source component and placeholder from consideration const children = parent._children?.filter( - x => x._id !== "placeholder" && x._id !== source.id + x => x._id !== DNDPlaceholderID && x._id !== source.id ) // Use inside if no existing children @@ -316,6 +317,6 @@ prefix="Inside" /> -{#if $isDragging} +{#if $dndIsDragging} {/if} diff --git a/packages/client/src/components/preview/HoverIndicator.svelte b/packages/client/src/components/preview/HoverIndicator.svelte index ed28938d4e..d5583ed3db 100644 --- a/packages/client/src/components/preview/HoverIndicator.svelte +++ b/packages/client/src/components/preview/HoverIndicator.svelte @@ -1,7 +1,7 @@ - import { builderStore, isDragging } from "stores" + import { builderStore, dndIsDragging } from "stores" import IndicatorSet from "./IndicatorSet.svelte" $: color = $builderStore.editMode @@ -8,7 +8,7 @@ { diff --git a/packages/client/src/index.js b/packages/client/src/index.js index 383421cc35..706cb4fc9f 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -64,20 +64,11 @@ const loadBudibase = async () => { } else if (name === "dragging-new-component") { const { dragging, component } = payload if (dragging) { - dndStore.actions.startDragging({ - id: null, - parent: null, - bounds: { - height: 64, - width: 128, - }, - index: null, - newComponentType: component, - }) - builderStore.actions.setDraggingNewComponent(true) + const definition = + componentStore.actions.getComponentDefinition(component) + dndStore.actions.startDraggingNewComponent({ component, definition }) } else { dndStore.actions.reset() - builderStore.actions.setDraggingNewComponent(false) } } } diff --git a/packages/client/src/stores/builder.js b/packages/client/src/stores/builder.js index 135c08343e..b937b7f696 100644 --- a/packages/client/src/stores/builder.js +++ b/packages/client/src/stores/builder.js @@ -16,7 +16,6 @@ const createBuilderStore = () => { theme: null, customTheme: null, previewDevice: "desktop", - draggingNewComponent: false, navigation: null, hiddenComponentIds: [], usedPlugins: null, @@ -68,19 +67,12 @@ const createBuilderStore = () => { }) }, dropNewComponent: (component, parent, index) => { - console.log("dispatch", component, parent, index) dispatchEvent("drop-new-component", { component, parent, index, }) }, - setDraggingNewComponent: draggingNewComponent => { - if (draggingNewComponent === get(store).draggingNewComponent) { - return - } - store.update(state => ({ ...state, draggingNewComponent })) - }, setEditMode: enabled => { if (enabled === get(store).editMode) { return diff --git a/packages/client/src/stores/components.js b/packages/client/src/stores/components.js index e246299741..b34dfe375d 100644 --- a/packages/client/src/stores/components.js +++ b/packages/client/src/stores/components.js @@ -4,7 +4,6 @@ import { findComponentById, findComponentPathById } from "../utils/components" import { devToolsStore } from "./devTools" import { screenStore } from "./screens" import { builderStore } from "./builder" -import { dndParent } from "./dnd.js" import Router from "../components/Router.svelte" import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte" import * as AppComponents from "../components/app/index.js" @@ -20,28 +19,22 @@ const createComponentStore = () => { }) const derivedStore = derived( - [store, builderStore, devToolsStore, screenStore, dndParent], - ([$store, $builderState, $devToolsState, $screenState, $dndParent]) => { + [store, builderStore, devToolsStore, screenStore], + ([$store, $builderStore, $devToolsStore, $screenStore]) => { + const { inBuilder, selectedComponentId } = $builderStore + // Avoid any of this logic if we aren't in the builder preview - if (!$builderState.inBuilder && !$devToolsState.visible) { + if (!inBuilder && !$devToolsStore.visible) { return {} } - // Derive the selected component instance and definition - let asset - const { screen, selectedComponentId } = $builderState - if ($builderState.inBuilder) { - asset = screen - } else { - asset = $screenState.activeScreen - } - const component = findComponentById(asset?.props, selectedComponentId) + const root = $screenStore.activeScreen?.props + const component = findComponentById(root, selectedComponentId) const definition = getComponentDefinition(component?._component) // Derive the selected component path const selectedPath = - findComponentPathById(asset?.props, selectedComponentId) || [] - const dndPath = findComponentPathById(asset?.props, $dndParent) || [] + findComponentPathById(root, selectedComponentId) || [] return { customComponentManifest: $store.customComponentManifest, @@ -51,9 +44,6 @@ const createComponentStore = () => { selectedComponentDefinition: definition, selectedComponentPath: selectedPath?.map(component => component._id), mountedComponentCount: Object.keys($store.mountedComponents).length, - currentAsset: asset, - dndPath: dndPath?.map(component => component._id), - dndDepth: dndPath?.length || 0, } } ) @@ -101,8 +91,8 @@ const createComponentStore = () => { } const getComponentById = id => { - const asset = get(derivedStore).currentAsset - return findComponentById(asset?.props, id) + const root = get(screenStore).activeScreen?.props + return findComponentById(root, id) } const getComponentDefinition = type => { diff --git a/packages/client/src/stores/derived/dndComponentPath.js b/packages/client/src/stores/derived/dndComponentPath.js new file mode 100644 index 0000000000..58fb395dd6 --- /dev/null +++ b/packages/client/src/stores/derived/dndComponentPath.js @@ -0,0 +1,13 @@ +import { derived } from "svelte/store" +import { findComponentPathById } from "utils/components.js" +import { dndParent } from "../dnd.js" +import { screenStore } from "../screens.js" + +export const dndComponentPath = derived( + [dndParent, screenStore], + ([$dndParent, $screenStore]) => { + const root = $screenStore.activeScreen?.props + const path = findComponentPathById(root, $dndParent) || [] + return path?.map(component => component._id) + } +) diff --git a/packages/client/src/stores/derived/index.js b/packages/client/src/stores/derived/index.js index 85df1aaafa..4f6a6ab91d 100644 --- a/packages/client/src/stores/derived/index.js +++ b/packages/client/src/stores/derived/index.js @@ -1,2 +1,5 @@ -export { isDragging } from "./isDragging.js" +// These derived stores are pulled out from their parent stores to avoid +// dependency loops. By inverting store dependencies and extracting them +// separately we can keep our actual stores lean and performant. export { currentRole } from "./currentRole.js" +export { dndComponentPath } from "./dndComponentPath.js" diff --git a/packages/client/src/stores/derived/isDragging.js b/packages/client/src/stores/derived/isDragging.js deleted file mode 100644 index d3edd586c6..0000000000 --- a/packages/client/src/stores/derived/isDragging.js +++ /dev/null @@ -1,10 +0,0 @@ -import { derived } from "svelte/store" -import { dndStore } from "../dnd" -import { builderStore } from "../builder.js" - -// Derive whether we are dragging or not -export const isDragging = derived( - [dndStore, builderStore], - ([$dndStore, $builderStore]) => - $dndStore.source != null || $builderStore.draggingNewComponent -) diff --git a/packages/client/src/stores/dnd.js b/packages/client/src/stores/dnd.js index 805ec8c339..1e7d7a6d50 100644 --- a/packages/client/src/stores/dnd.js +++ b/packages/client/src/stores/dnd.js @@ -2,6 +2,10 @@ import { writable, derived } from "svelte/store" const createDndStore = () => { const initialState = { + // Flags for whether we are inserting a new component or not + isNewComponent: false, + newComponentType: null, + // Info about the dragged component source: null, @@ -13,12 +17,32 @@ const createDndStore = () => { } const store = writable(initialState) - // newComponentType is an optional field to signify we are creating a new - // component rather than moving an existing one - const startDragging = ({ id, parent, bounds, index, newComponentType }) => { + const startDraggingExistingComponent = ({ id, parent, bounds, index }) => { store.set({ ...initialState, - source: { id, parent, bounds, index, newComponentType }, + source: { id, parent, bounds, index }, + }) + } + + const startDraggingNewComponent = ({ type, definition }) => { + if (!type || !definition) { + return + } + + // Get size of new component so we can show a properly sized placeholder + const width = definition.size?.width || 128 + const height = definition.size?.height || 64 + + store.set({ + ...initialState, + isNewComponent: true, + newComponentType: type, + source: { + id: null, + parent: null, + bounds: { height, width }, + index: null, + }, }) } @@ -43,7 +67,8 @@ const createDndStore = () => { return { subscribe: store.subscribe, actions: { - startDragging, + startDraggingExistingComponent, + startDraggingNewComponent, updateTarget, updateDrop, reset, @@ -52,9 +77,19 @@ const createDndStore = () => { } export const dndStore = createDndStore() + +// The DND store is updated extremely frequently, so we can greatly improve +// performance by deriving any state that needs to be externally observed. +// By doing this and using primitives, we can avoid invalidating other stores +// or components which depend on DND state unless values actually change. +export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source) export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent) export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index) export const dndBounds = derived( dndStore, $dndStore => $dndStore.source?.bounds ) +export const dndIsNewComponent = derived( + dndStore, + $dndStore => $dndStore.isNewComponent +) diff --git a/packages/client/src/stores/index.js b/packages/client/src/stores/index.js index f86c0a0517..c431302d43 100644 --- a/packages/client/src/stores/index.js +++ b/packages/client/src/stores/index.js @@ -15,7 +15,14 @@ export { uploadStore } from "./uploads.js" export { rowSelectionStore } from "./rowSelection.js" export { blockStore } from "./blocks.js" export { environmentStore } from "./environment" -export { dndStore, dndIndex, dndParent, dndBounds } from "./dnd" +export { + dndStore, + dndIndex, + dndParent, + dndBounds, + dndIsNewComponent, + dndIsDragging, +} from "./dnd" // Context stores are layered and duplicated, so it is not a singleton export { createContextStore } from "./context" diff --git a/packages/client/src/stores/screens.js b/packages/client/src/stores/screens.js index b0bc06eee9..0787610d80 100644 --- a/packages/client/src/stores/screens.js +++ b/packages/client/src/stores/screens.js @@ -2,7 +2,7 @@ import { derived } from "svelte/store" import { routeStore } from "./routes" import { builderStore } from "./builder" import { appStore } from "./app" -import { dndIndex, dndParent } from "./dnd.js" +import { dndIndex, dndParent, dndIsNewComponent } from "./dnd.js" import { RoleUtils } from "@budibase/frontend-core" import { findComponentById, findComponentParent } from "../utils/components.js" import { Helpers } from "@budibase/bbui" @@ -10,8 +10,22 @@ import { DNDPlaceholderID, DNDPlaceholderType } from "constants" const createScreenStore = () => { const store = derived( - [appStore, routeStore, builderStore, dndParent, dndIndex], - ([$appStore, $routeStore, $builderStore, $dndParent, $dndIndex]) => { + [ + appStore, + routeStore, + builderStore, + dndParent, + dndIndex, + dndIsNewComponent, + ], + ([ + $appStore, + $routeStore, + $builderStore, + $dndParent, + $dndIndex, + $dndIsNewComponent, + ]) => { let activeLayout, activeScreen let screens @@ -50,8 +64,8 @@ const createScreenStore = () => { if (activeScreen && $dndParent && $dndIndex != null) { // Remove selected component from tree if we are moving an existing // component - const { selectedComponentId, draggingNewComponent } = $builderStore - if (!draggingNewComponent) { + const { selectedComponentId } = $builderStore + if (!$dndIsNewComponent) { let selectedParent = findComponentParent( activeScreen.props, selectedComponentId From 121eddaa5a50e08bba8f6b5e073610699b165106 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 18:17:02 +0100 Subject: [PATCH 23/36] Fix crash when dragging from client preview into component tree --- .../[componentId]/_components/navigation/ComponentTree.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentTree.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentTree.svelte index 427fd98775..5cb6d31345 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentTree.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/navigation/ComponentTree.svelte @@ -76,6 +76,9 @@ const compDef = store.actions.components.getDefinition( $dndStore.source?._component ) + if (!compDef) { + return + } const compTypeName = compDef.name.toLowerCase() const path = findComponentPath(currentScreen.props, component._id) From 78a5f9c891c6f7bcd7cde5015f91ac6486abcc19 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 18:24:47 +0100 Subject: [PATCH 24/36] Fix issues dropping components from new component panel --- .../client/src/components/preview/DNDHandler.svelte | 1 + packages/client/src/stores/dnd.js | 13 ++++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 392cdceb36..61b34eefd3 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -245,6 +245,7 @@ drop.parent, drop.index ) + return } // Convert parent + index into target + mode diff --git a/packages/client/src/stores/dnd.js b/packages/client/src/stores/dnd.js index 1e7d7a6d50..cebdb7c6ae 100644 --- a/packages/client/src/stores/dnd.js +++ b/packages/client/src/stores/dnd.js @@ -2,10 +2,6 @@ import { writable, derived } from "svelte/store" const createDndStore = () => { const initialState = { - // Flags for whether we are inserting a new component or not - isNewComponent: false, - newComponentType: null, - // Info about the dragged component source: null, @@ -24,8 +20,8 @@ const createDndStore = () => { }) } - const startDraggingNewComponent = ({ type, definition }) => { - if (!type || !definition) { + const startDraggingNewComponent = ({ component, definition }) => { + if (!component || !definition) { return } @@ -35,13 +31,12 @@ const createDndStore = () => { store.set({ ...initialState, - isNewComponent: true, - newComponentType: type, source: { id: null, parent: null, bounds: { height, width }, index: null, + newComponentType: component, }, }) } @@ -91,5 +86,5 @@ export const dndBounds = derived( ) export const dndIsNewComponent = derived( dndStore, - $dndStore => $dndStore.isNewComponent + $dndStore => $dndStore.source?.newComponentType != null ) From 2d64ae59c68ad21d32d5cc007efdfd1921b0ff1c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 18:59:23 +0100 Subject: [PATCH 25/36] Add default size for every component type --- packages/client/manifest.json | 296 ++++++++++++++++++++++++---------- 1 file changed, 208 insertions(+), 88 deletions(-) diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 14092b7b4d..763ac46b3f 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -87,7 +87,7 @@ "showSettingsBar": true, "size": { "width": 400, - "height": 200 + "height": 100 }, "styles": [ "padding", @@ -259,6 +259,10 @@ "section" ], "showEmptyState": false, + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "section", @@ -280,6 +284,10 @@ "icon": "Button", "editable": true, "showSettingsBar": true, + "size": { + "width": 105, + "height": 35 + }, "settings": [ { "type": "text", @@ -372,6 +380,10 @@ "illegalChildren": [ "section" ], + "size": { + "width": 400, + "height": 10 + }, "settings": [ { "type": "select", @@ -409,6 +421,10 @@ ], "hasChildren": true, "showSettingsBar": true, + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "dataProvider", @@ -588,6 +604,7 @@ ] }, "card": { + "deprecated": true, "name": "Vertical Card", "description": "A basic card component that can contain content and actions.", "icon": "ViewColumn", @@ -668,6 +685,10 @@ ], "showSettingsBar": true, "editable": true, + "size": { + "width": 400, + "height": 30 + }, "settings": [ { "type": "text", @@ -790,6 +811,10 @@ ], "showSettingsBar": true, "editable": true, + "size": { + "width": 400, + "height": 40 + }, "settings": [ { "type": "text", @@ -907,6 +932,10 @@ "name": "Tag", "icon": "Label", "showSettingsBar": true, + "size": { + "width": 100, + "height": 25 + }, "settings": [ { "type": "text", @@ -958,12 +987,13 @@ "name": "Image", "description": "A basic component for displaying images", "icon": "Image", - "illegalChildren": [ - "section" - ], "styles": [ "size" ], + "size": { + "width": 400, + "height": 300 + }, "settings": [ { "type": "text", @@ -980,9 +1010,10 @@ "styles": [ "size" ], - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 300 + }, "settings": [ { "type": "text", @@ -1040,9 +1071,10 @@ "name": "Icon", "description": "A basic component for displaying icons", "icon": "Shapes", - "illegalChildren": [ - "section" - ], + "size": { + "width": 25, + "height": 25 + }, "settings": [ { "type": "icon", @@ -1159,9 +1191,10 @@ "icon": "Link", "showSettingsBar": true, "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 200, + "height": 30 + }, "settings": [ { "type": "text", @@ -1271,12 +1304,10 @@ ] }, "cardhorizontal": { + "deprecated": true, "name": "Horizontal Card", "description": "A basic card component that can contain content and actions.", "icon": "ViewRow", - "illegalChildren": [ - "section" - ], "settings": [ { "type": "text", @@ -1367,27 +1398,31 @@ "name": "Stat Card", "description": "A card component for displaying numbers.", "icon": "Card", - "illegalChildren": [ - "section" - ], + "size": { + "width": 260, + "height": 143 + }, "settings": [ { "type": "text", "label": "Title", "key": "title", - "placeholder": "Total Revenue" + "placeholder": "Total Revenue", + "defaultValue": "Title" }, { "type": "text", "label": "Value", "key": "value", - "placeholder": "$1,981,983" + "placeholder": "$1,981,983", + "defaultValue": "Value" }, { "type": "text", "label": "Label", "key": "label", - "placeholder": "Stripe" + "placeholder": "Stripe", + "defaultValue": "Label" } ] }, @@ -1395,12 +1430,13 @@ "name": "Embed", "icon": "Code", "description": "Embed content from 3rd party sources", - "illegalChildren": [ - "section" - ], "styles": [ "size" ], + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "text", @@ -1414,9 +1450,10 @@ "name": "Bar Chart", "description": "Bar chart", "icon": "GraphBarVertical", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -1575,9 +1612,10 @@ "name": "Line Chart", "description": "Line chart", "icon": "GraphTrend", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -1735,9 +1773,10 @@ "name": "Area Chart", "description": "Line chart", "icon": "GraphAreaStacked", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -1907,9 +1946,10 @@ "name": "Pie Chart", "description": "Pie chart", "icon": "GraphPie", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -2035,9 +2075,10 @@ "name": "Donut Chart", "description": "Donut chart", "icon": "GraphDonut", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -2163,9 +2204,10 @@ "name": "Candlestick Chart", "description": "Candlestick chart", "icon": "GraphBarVerticalStacked", - "illegalChildren": [ - "section" - ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -2270,6 +2312,10 @@ "styles": [ "size" ], + "size": { + "width": 400, + "height": 400 + }, "settings": [ { "type": "select", @@ -2356,6 +2402,10 @@ "styles": [ "size" ], + "size": { + "width": 400, + "height": 400 + }, "settings": [ { "type": "number", @@ -2376,6 +2426,10 @@ "size" ], "hasChildren": true, + "size": { + "width": 400, + "height": 400 + }, "settings": [ { "type": "select", @@ -2402,13 +2456,14 @@ "stringfield": { "name": "Text Field", "icon": "Text", - "illegalChildren": [ - "section" - ], "styles": [ "size" ], "editable": true, + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/string", @@ -2496,9 +2551,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/number", @@ -2552,9 +2608,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/string", @@ -2608,9 +2665,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/options", @@ -2775,9 +2833,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/array", @@ -2933,9 +2992,10 @@ "name": "Checkbox", "icon": "SelectBox", "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/boolean", @@ -3013,6 +3073,10 @@ "size" ], "editable": true, + "size": { + "width": 400, + "height": 150 + }, "settings": [ { "type": "field/longform", @@ -3088,9 +3152,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/datetime", @@ -3167,9 +3232,10 @@ "styles": [ "size" ], - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/barcode/qr", @@ -3218,29 +3284,27 @@ "size" ], "draggable": false, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 320 + }, "settings": [ { "type": "dataProvider", "label": "Provider", - "key": "dataProvider", - "required": true + "key": "dataProvider" }, { "type": "field", "label": "Latitude Key", "key": "latitudeKey", - "dependsOn": "dataProvider", - "required": true + "dependsOn": "dataProvider" }, { "type": "field", "label": "Longitude Key", "key": "longitudeKey", - "dependsOn": "dataProvider", - "required": true + "dependsOn": "dataProvider" }, { "type": "field", @@ -3334,9 +3398,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 200 + }, "settings": [ { "type": "field/attachment", @@ -3391,9 +3456,10 @@ "size" ], "editable": true, - "illegalChildren": [ - "section" - ], + "size": { + "width": 400, + "height": 50 + }, "settings": [ { "type": "field/link", @@ -3453,6 +3519,10 @@ "size" ], "editable": true, + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "field/json", @@ -3501,6 +3571,10 @@ "size" ], "editable": true, + "size": { + "width": 400, + "height": 200 + }, "settings": [ { "type": "field/attachment", @@ -3563,6 +3637,10 @@ "actions": [ "RefreshDatasource" ], + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "dataSource", @@ -3643,6 +3721,10 @@ ], "hasChildren": true, "showEmptyState": false, + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "dataProvider", @@ -3741,6 +3823,10 @@ "size" ], "hasChildren": false, + "size": { + "width": 200, + "height": 50 + }, "settings": [ { "type": "dataProvider", @@ -3777,21 +3863,28 @@ "styles": [ "size" ], + "size": { + "width": 300, + "height": 120 + }, "settings": [ { "type": "text", "key": "title", - "label": "Title" + "label": "Title", + "defaultValue": "Title" }, { "type": "text", "key": "subtitle", - "label": "Subtitle" + "label": "Subtitle", + "defaultValue": "Subtitle" }, { "type": "text", "key": "description", - "label": "Description" + "label": "Description", + "defaultValue": "Description" }, { "type": "text", @@ -3835,6 +3928,10 @@ "name": "Dynamic Filter", "icon": "Filter", "showSettingsBar": true, + "size": { + "width": 100, + "height": 35 + }, "settings": [ { "type": "dataProvider", @@ -3882,6 +3979,10 @@ "styles": [ "size" ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -4047,6 +4148,10 @@ "styles": [ "size" ], + "size": { + "width": 600, + "height": 400 + }, "settings": [ { "type": "text", @@ -4105,19 +4210,22 @@ "type": "text", "key": "cardTitle", "label": "Title", - "nested": true + "nested": true, + "defaultValue": "Title" }, { "type": "text", "key": "cardSubtitle", "label": "Subtitle", - "nested": true + "nested": true, + "defaultValue": "Subtitle" }, { "type": "text", "key": "cardDescription", "label": "Description", - "nested": true + "nested": true, + "defaultValue": "Description" }, { "type": "text", @@ -4219,6 +4327,10 @@ ], "hasChildren": true, "showSettingsBar": true, + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "dataSource", @@ -4441,6 +4553,10 @@ "styles": [ "size" ], + "size": { + "width": 400, + "height": 100 + }, "settings": [ { "type": "text", @@ -4458,6 +4574,10 @@ ], "block": true, "info": "Form blocks are only compatible with internal or SQL tables", + "size": { + "width": 400, + "height": 400 + }, "settings": [ { "type": "select", From bdae511371c54aad027907843a7ad0871144ecdb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 18:59:32 +0100 Subject: [PATCH 26/36] Fix double empty state around blocks --- packages/client/src/components/Component.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index fe5f363152..6d341b2eb8 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -135,8 +135,9 @@ // Empty states can be shown for these components, but can be disabled // in the component manifest. $: empty = - (interactive && !children.length && hasChildren) || - hasMissingRequiredSettings + !isBlock && + ((interactive && !children.length && hasChildren) || + hasMissingRequiredSettings) $: emptyState = empty && showEmptyState // Enrich component settings From 5f7e0cb8955f75d0ad27c22613d0e28ba14af66a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 19:04:05 +0100 Subject: [PATCH 27/36] Prevent showing placeholder dot when placeholder is invisible --- .../client/src/components/preview/DNDPlaceholderOverlay.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte index a762f35319..12103e3904 100644 --- a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte +++ b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte @@ -25,7 +25,7 @@ }) -{#if left != null} +{#if left != null && top != null && width && height}
Date: Fri, 14 Oct 2022 19:13:44 +0100 Subject: [PATCH 28/36] Use block name in placeholders inside blocks --- packages/client/src/components/Block.svelte | 3 +++ packages/client/src/components/app/Placeholder.svelte | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/Block.svelte b/packages/client/src/components/Block.svelte index 05d92f208c..75474dfd6f 100644 --- a/packages/client/src/components/Block.svelte +++ b/packages/client/src/components/Block.svelte @@ -67,6 +67,9 @@ // any depth id: $component.id, + // Name can be used down the tree in placeholders + name: $component.name, + // We register block components with their raw props so that we can eject // blocks later on registerComponent: registerBlockComponent, diff --git a/packages/client/src/components/app/Placeholder.svelte b/packages/client/src/components/app/Placeholder.svelte index 203071e0b1..54553cb934 100644 --- a/packages/client/src/components/app/Placeholder.svelte +++ b/packages/client/src/components/app/Placeholder.svelte @@ -3,13 +3,14 @@ const { builderStore } = getContext("sdk") const component = getContext("component") + const block = getContext("block") export let text {#if $builderStore.inBuilder}
- {text || $component.name || "Placeholder"} + {text || block?.name || $component.name || "Placeholder"}
{/if} From 011ba2676945a618090ee7166098c01887416169 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 19:34:23 +0100 Subject: [PATCH 29/36] Fix DND not working in field groups and any other component where children are not rendered inside the first DOM node --- packages/client/src/components/preview/DNDHandler.svelte | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/client/src/components/preview/DNDHandler.svelte b/packages/client/src/components/preview/DNDHandler.svelte index 61b34eefd3..a9c52f0099 100644 --- a/packages/client/src/components/preview/DNDHandler.svelte +++ b/packages/client/src/components/preview/DNDHandler.svelte @@ -123,6 +123,14 @@ return } + // As the first DOM node in a component may not necessarily contain the + // child components, we can find to try the parent of the first child + // component and use that as the real parent DOM node + const childNode = node.getElementsByClassName("component")[0] + if (childNode?.parentNode) { + node = childNode.parentNode + } + // Append an ephemeral div to allow us to determine layout if only one // child exists let ephemeralDiv From adc7e8e5fc1ca7f5a99d7f1bb210d2bc9f87b34c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 19:39:38 +0100 Subject: [PATCH 30/36] Fix unused plugins not being able to be dragged into the preview --- packages/client/src/stores/dnd.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/stores/dnd.js b/packages/client/src/stores/dnd.js index cebdb7c6ae..a1f85af92c 100644 --- a/packages/client/src/stores/dnd.js +++ b/packages/client/src/stores/dnd.js @@ -21,13 +21,13 @@ const createDndStore = () => { } const startDraggingNewComponent = ({ component, definition }) => { - if (!component || !definition) { + if (!component) { return } // Get size of new component so we can show a properly sized placeholder - const width = definition.size?.width || 128 - const height = definition.size?.height || 64 + const width = definition?.size?.width || 128 + const height = definition?.size?.height || 64 store.set({ ...initialState, From d166cbb466599afeeb48652b21ddec4a3cfc213f Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 19:45:47 +0100 Subject: [PATCH 31/36] Lint and prevent scrolling to selected component when starting dragging --- packages/builder/src/builderStore/store/frontend.js | 2 +- packages/client/src/components/Component.svelte | 6 +++++- packages/frontend-core/src/utils/utils.js | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 4122954303..848dd4405a 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -1,6 +1,6 @@ import { get, writable } from "svelte/store" import { cloneDeep } from "lodash/fp" -import { selectedScreen, selectedComponent, store } from "builderStore" +import { selectedScreen, selectedComponent } from "builderStore" import { datasources, integrations, diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 6d341b2eb8..5996b182eb 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -421,7 +421,11 @@ } const scrollIntoView = () => { - return + // Don't scroll into view if we selected this component because we were + // starting dragging on it + if (get(dndIsDragging)) { + return + } const node = document.getElementsByClassName(id)?.[0]?.children[0] if (!node) { return diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index ed76f13494..8aa49392fb 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -4,8 +4,6 @@ * @param fn the async function to run * @return {Promise} a sequential version of the function */ -import { lastIndexOf } from "lodash/array" - export const sequential = fn => { let queue = [] return async (...params) => { From 3325b7b62bbbe153b036815f2d459c58c7b7984d Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 14 Oct 2022 20:30:58 +0100 Subject: [PATCH 32/36] Update styles of padded DND components --- packages/client/src/components/Component.svelte | 6 +++--- packages/client/src/utils/styleable.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 5996b182eb..c9872f246b 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -509,9 +509,9 @@ transition: padding 260ms ease-out, border 260ms ease-out; } .component.pad :global(> *) { - padding: 12px !important; - gap: 12px !important; - border: 2px dotted var(--spectrum-global-color-gray-400) !important; + padding: var(--spacing-l) !important; + gap: var(--spacing-l) !important; + border: 2px dashed var(--spectrum-global-color-gray-400) !important; border-radius: 4px !important; } .interactive :global(*) { diff --git a/packages/client/src/utils/styleable.js b/packages/client/src/utils/styleable.js index b07a3213d9..9ad17ceff0 100644 --- a/packages/client/src/utils/styleable.js +++ b/packages/client/src/utils/styleable.js @@ -27,7 +27,7 @@ export const styleable = (node, styles = {}) => { const setupStyles = (newStyles = {}) => { let baseStyles = {} if (newStyles.empty) { - baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)" + baseStyles.border = "2px dashed var(--spectrum-global-color-gray-400)" baseStyles.padding = "var(--spacing-l)" baseStyles.overflow = "hidden" } From 6656a89f1820db0c143bd667f6e810c394ca8499 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 17 Oct 2022 08:46:09 +0100 Subject: [PATCH 33/36] Add ability to drag new blocks into app preview --- .../[componentId]/new/_components/NewComponentPanel.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte index 07781441c5..778a14ffff 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/[screenId]/components/[componentId]/new/_components/NewComponentPanel.svelte @@ -240,8 +240,11 @@ {#each blocks as block}
addComponent(block.component)} + on:dragstart={() => onDragStart(block.component)} + on:dragend={onDragEnd} > {block.name} From 554b219d6255a202b8951215fe7a3734865a789a Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 17 Oct 2022 08:48:32 +0100 Subject: [PATCH 34/36] Use requestAnimationFrame for DND overlay placeholder updates to further improve performance --- .../preview/DNDPlaceholderOverlay.svelte | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte index 12103e3904..6ed2df6a87 100644 --- a/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte +++ b/packages/client/src/components/preview/DNDPlaceholderOverlay.svelte @@ -1,24 +1,27 @@