Don't clear drop target on invalid selection

This commit is contained in:
Andrew Kingston 2022-10-06 09:17:26 +01:00
parent ee4c5af1c9
commit 428b786184
5 changed files with 139 additions and 25 deletions

View File

@ -96,6 +96,7 @@
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.selectedComponentId === id $builderStore.inBuilder && $builderStore.selectedComponentId === id
$: inSelectedPath = $componentStore.selectedComponentPath?.includes(id) $: inSelectedPath = $componentStore.selectedComponentPath?.includes(id)
$: inDropPath = $componentStore.dropPath?.includes(id)
$: inDragPath = inSelectedPath && $builderStore.editMode $: inDragPath = inSelectedPath && $builderStore.editMode
// Derive definition properties which can all be optional, so need to be // 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 // Interactive components can be selected, dragged and highlighted inside
// the builder preview // the builder preview
$: builderInteractive = $: builderInteractive =
$builderStore.inBuilder && insideScreenslot && !isBlock $builderStore.inBuilder && insideScreenslot && !isBlock && !instance.static
$: devToolsInteractive = $devToolsStore.allowSelection && !isBlock $: devToolsInteractive = $devToolsStore.allowSelection && !isBlock
$: interactive = builderInteractive || devToolsInteractive $: interactive = builderInteractive || devToolsInteractive
$: editing = editable && selected && $builderStore.editMode $: editing = editable && selected && $builderStore.editMode
@ -453,10 +454,11 @@
class:interactive class:interactive
class:editing class:editing
class:block={isBlock} class:block={isBlock}
class:explode={children.length && !isLayout && $builderStore.isDragging} class:explode={children.length && !isLayout && inDropPath && false}
data-id={id} data-id={id}
data-name={name} data-name={name}
data-icon={icon} data-icon={icon}
data-placeholder={id === "placeholder"}
> >
<svelte:component this={constructor} bind:this={ref} {...initialSettings}> <svelte:component this={constructor} bind:this={ref} {...initialSettings}>
{#if hasMissingRequiredSettings} {#if hasMissingRequiredSettings}

View File

@ -77,19 +77,16 @@
builderStore.actions.setDragging(false) builderStore.actions.setDragging(false)
} }
// Callback when on top of a component const validateDrop = (dropInfo, e) => {
const onDragOver = e => { if (!dropInfo) {
// Skip if we aren't validly dragging currently return null
if (!dragInfo || !dropInfo) {
return
} }
e.preventDefault()
const { droppableInside, bounds } = dropInfo const { droppableInside, bounds } = dropInfo
const { top, left, height, width } = bounds const { top, left, height, width } = bounds
const mouseY = e.clientY const mouseY = e.clientY
const mouseX = e.clientX 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 snapLimitV = Math.min(40, height * snapFactor)
const snapLimitH = Math.min(40, width * snapFactor) const snapLimitH = Math.min(40, width * snapFactor)
@ -108,20 +105,32 @@
// When no edges match, drop inside if possible // When no edges match, drop inside if possible
if (!sides.length) { if (!sides.length) {
dropInfo.mode = droppableInside ? "inside" : null if (droppableInside) {
dropInfo.side = null return {
return ...dropInfo,
mode: "inside",
side: null,
}
} else {
return null
}
} }
// When one edge matches, use that edge // When one edge matches, use that edge
if (sides.length === 1) { if (sides.length === 1) {
dropInfo.side = sides[0]
if ([Sides.Top, Sides.Left].includes(sides[0])) { if ([Sides.Top, Sides.Left].includes(sides[0])) {
dropInfo.mode = "above" return {
...dropInfo,
mode: "above",
side: sides[0],
}
} else { } else {
dropInfo.mode = "below" return {
...dropInfo,
mode: "below",
side: sides[0],
}
} }
return
} }
// When 2 edges match, work out which is closer // When 2 edges match, work out which is closer
@ -134,9 +143,34 @@
const edge = delta1 < delta2 ? sides[0] : sides[1] const edge = delta1 < delta2 ? sides[0] : sides[1]
dropInfo.side = edge dropInfo.side = edge
if ([Sides.Top, Sides.Left].includes(edge)) { if ([Sides.Top, Sides.Left].includes(edge)) {
dropInfo.mode = "above" return {
...dropInfo,
mode: "above",
side: edge,
}
} else { } 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 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)") const element = e.target.closest(".component:not(.block)")
if ( if (
element && element &&
@ -172,15 +216,20 @@
// dragOver // dragOver
const child = getDOMNodeForComponent(e.target) const child = getDOMNodeForComponent(e.target)
const bounds = child.getBoundingClientRect() const bounds = child.getBoundingClientRect()
dropInfo = { let nextDropInfo = {
target, target,
name: element.dataset.name, name: element.dataset.name,
icon: element.dataset.icon, icon: element.dataset.icon,
droppableInside: element.classList.contains("empty"), droppableInside: element.classList.contains("empty"),
bounds, bounds,
} }
nextDropInfo = validateDrop(nextDropInfo, e)
if (nextDropInfo) {
console.log("set from enter")
dropInfo = nextDropInfo
}
} else { } else {
dropInfo = null // dropInfo = null
} }
} }
@ -200,6 +249,10 @@
} }
} }
$: mode = dropInfo?.mode
$: target = dropInfo?.target
$: builderStore.actions.updateDNDPlaceholder(mode, target)
onMount(() => { onMount(() => {
// Events fired on the draggable target // Events fired on the draggable target
document.addEventListener("dragstart", onDragStart, false) document.addEventListener("dragstart", onDragStart, false)

View File

@ -1,6 +1,7 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { devToolsStore } from "./devTools.js" import { devToolsStore } from "./devTools.js"
import { findComponentPathById } from "../utils/components.js"
const dispatchEvent = (type, data = {}) => { const dispatchEvent = (type, data = {}) => {
window.parent.postMessage({ type, data }) window.parent.postMessage({ type, data })
@ -20,6 +21,9 @@ const createBuilderStore = () => {
navigation: null, navigation: null,
hiddenComponentIds: [], hiddenComponentIds: [],
usedPlugins: null, usedPlugins: null,
dndMode: null,
dndTarget: null,
dropTarget: null,
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
layout: null, layout: null,
@ -102,6 +106,20 @@ const createBuilderStore = () => {
// Notify the builder so we can reload component definitions // Notify the builder so we can reload component definitions
dispatchEvent("reload-plugin") 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 { return {
...store, ...store,

View File

@ -36,8 +36,13 @@ const createComponentStore = () => {
const definition = getComponentDefinition(component?._component) const definition = getComponentDefinition(component?._component)
// Derive the selected component path // Derive the selected component path
const path = const selectedPath =
findComponentPathById(asset?.props, selectedComponentId) || [] findComponentPathById(asset?.props, selectedComponentId) || []
let dropPath = []
if ($builderState.isDragging) {
dropPath =
findComponentPathById(asset?.props, $builderState.dropTarget) || []
}
return { return {
customComponentManifest: $store.customComponentManifest, customComponentManifest: $store.customComponentManifest,
@ -45,9 +50,10 @@ const createComponentStore = () => {
$store.mountedComponents[selectedComponentId], $store.mountedComponents[selectedComponentId],
selectedComponent: component, selectedComponent: component,
selectedComponentDefinition: definition, selectedComponentDefinition: definition,
selectedComponentPath: path?.map(component => component._id), selectedComponentPath: selectedPath?.map(component => component._id),
mountedComponentCount: Object.keys($store.mountedComponents).length, mountedComponentCount: Object.keys($store.mountedComponents).length,
currentAsset: asset, currentAsset: asset,
dropPath: dropPath?.map(component => component._id),
} }
} }
) )

View File

@ -3,6 +3,11 @@ import { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" import { appStore } from "./app"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import {
findComponentById,
findComponentPathById,
} from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -13,7 +18,7 @@ const createScreenStore = () => {
if ($builderStore.inBuilder) { if ($builderStore.inBuilder) {
// Use builder defined definitions if inside the builder preview // Use builder defined definitions if inside the builder preview
activeScreen = $builderStore.screen activeScreen = Helpers.cloneDeep($builderStore.screen)
screens = [activeScreen] screens = [activeScreen]
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
@ -24,8 +29,10 @@ const createScreenStore = () => {
// Find the correct screen by matching the current route // Find the correct screen by matching the current route
screens = $appStore.screens || [] screens = $appStore.screens || []
if ($routeStore.activeRoute) { if ($routeStore.activeRoute) {
activeScreen = screens.find( activeScreen = Helpers.cloneDeep(
screen => screen._id === $routeStore.activeRoute.screenId 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 // Assign ranks to screens, preferring higher roles and home screens
screens.forEach(screen => { screens.forEach(screen => {
const roleId = screen.routing.roleId const roleId = screen.routing.roleId