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