Only allow dropping inside empty components that accept children to massively reduce the amount of unwanted drop targets due to parent container components

This commit is contained in:
Andrew Kingston 2021-09-20 08:26:44 +01:00
parent 2c7e93423e
commit c2aeefae7b
3 changed files with 41 additions and 37 deletions

View File

@ -171,18 +171,23 @@
conditionalSettings = result.settingUpdates conditionalSettings = result.settingUpdates
visible = nextVisible visible = nextVisible
} }
// Drag and drop helper tags
$: draggable = interactive && !isLayout && !isScreen
$: droppable = interactive
$: dropInside = interactive && definition?.hasChildren && !children.length
</script> </script>
{#key propsHash} {#key propsHash}
{#if constructor && componentSettings && (visible || inSelectedPath)} {#if constructor && componentSettings && (visible || inSelectedPath)}
<div <div
class={`component ${id}`} class={`component ${id}`}
data-type={interactive ? "component" : ""} data-type={interactive ? "component" : "readonly"}
data-id={id} data-id={id}
data-name={name} data-name={name}
data-draggable={interactive && !isLayout && !isScreen ? "true" : "false"} data-draggable={draggable}
data-droppable={interactive ? "true" : "false"} data-droppable={droppable}
data-droppable-inside={definition?.hasChildren ? "true" : "false"} data-droppable-inside={dropInside}
> >
<svelte:component this={constructor} {...componentSettings}> <svelte:component this={constructor} {...componentSettings}>
{#if children.length} {#if children.length}
@ -202,6 +207,6 @@
display: contents; display: contents;
} }
[data-draggable="true"] :global(*:hover) { [data-draggable="true"] :global(*:hover) {
cursor: grab !important; cursor: grab;
} }
</style> </style>

View File

@ -5,9 +5,7 @@
import DNDPositionIndicator from "./DNDPositionIndicator.svelte" import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { builderStore } from "stores" import { builderStore } from "stores"
let dragTarget let dragInfo
let dropTarget
let dropMode
let dropInfo let dropInfo
const getDOMNodeForComponent = component => { const getDOMNodeForComponent = component => {
@ -18,26 +16,28 @@
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = e => {
if (!e.target?.dataset?.componentId) { const parent = e.target.closest("[data-type='component']")
const child = getDOMNodeForComponent(e.target)
if (!parent?.dataset?.id || !child) {
return return
} }
// Update state // Update state
dragTarget = e.target.dataset.componentId dragInfo = {
builderStore.actions.selectComponent(dragTarget) target: parent.dataset.id,
parent: parent.dataset.parent,
}
builderStore.actions.selectComponent(dragInfo.target)
builderStore.actions.setDragging(true) builderStore.actions.setDragging(true)
// Highlight being dragged by setting opacity // Highlight being dragged by setting opacity
const child = getDOMNodeForComponent(e.target) child.style.opacity = "0.5"
if (child) {
child.style.opacity = "0.5"
}
} }
// Callback when drag stops (whether dropped or not) // Callback when drag stops (whether dropped or not)
const onDragEnd = e => { const onDragEnd = e => {
// Reset opacity style // Reset opacity style
if (dragTarget) { if (dragInfo) {
const child = getDOMNodeForComponent(e.target) const child = getDOMNodeForComponent(e.target)
if (child) { if (child) {
child.style.opacity = "" child.style.opacity = ""
@ -45,8 +45,7 @@
} }
// Reset state and styles // Reset state and styles
dragTarget = null dragInfo = null
dropTarget = null
dropInfo = null dropInfo = null
builderStore.actions.setDragging(false) builderStore.actions.setDragging(false)
} }
@ -54,7 +53,7 @@
// Callback when on top of a component // Callback when on top of a component
const onDragOver = e => { const onDragOver = e => {
// Skip if we aren't validly dragging currently // Skip if we aren't validly dragging currently
if (!dragTarget || !dropInfo) { if (!dragInfo || !dropInfo) {
return return
} }
@ -77,7 +76,7 @@
// If not available to drop inside, just check whether we are closer // If not available to drop inside, just check whether we are closer
// to the top or bottom // to the top or bottom
if (!droppableInside) { if (!droppableInside) {
dropMode = nearestEdge dropInfo.mode = nearestEdge
} }
// Otherwise determine whether the user wants to drop inside or at // Otherwise determine whether the user wants to drop inside or at
@ -89,9 +88,9 @@
Math.round(top + height - edgeLimit), Math.round(top + height - edgeLimit),
] ]
if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) { if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) {
dropMode = "inside" dropInfo.mode = "inside"
} else { } else {
dropMode = nearestEdge dropInfo.mode = nearestEdge
} }
} }
} }
@ -99,7 +98,7 @@
// Callback when entering a potential drop target // Callback when entering a potential drop target
const onDragEnter = e => { const onDragEnter = e => {
// Skip if we aren't validly dragging currently // Skip if we aren't validly dragging currently
if (!dragTarget) { if (!dragInfo) {
return return
} }
@ -107,7 +106,7 @@
if ( if (
element && element &&
element.dataset.droppable && element.dataset.droppable &&
element.dataset.id !== dragTarget element.dataset.id !== dragInfo.target
) { ) {
// Ensure the dragging flag is always set. // Ensure the dragging flag is always set.
// There's a bit of a race condition between the app reinitialisation // There's a bit of a race condition between the app reinitialisation
@ -117,20 +116,20 @@
} }
// Store target ID // Store target ID
dropTarget = element.dataset.id const target = element.dataset.id
// Precompute and store some info to avoid recalculating everything in // Precompute and store some info to avoid recalculating everything in
// dragOver // dragOver
const child = getDOMNodeForComponent(e.target) const child = getDOMNodeForComponent(e.target)
const bounds = child.getBoundingClientRect() const bounds = child.getBoundingClientRect()
dropInfo = { dropInfo = {
target,
name: element.dataset.name, name: element.dataset.name,
droppableInside: element.dataset.droppableInside === "true", droppableInside: element.dataset.droppableInside === "true",
bounds, bounds,
} }
} else { } else {
dropInfo = null dropInfo = null
dropTarget = null
} }
} }
@ -141,8 +140,12 @@
// Callback when dropping a drag on top of some component // Callback when dropping a drag on top of some component
const onDrop = e => { const onDrop = e => {
e.preventDefault() e.preventDefault()
if (dropTarget && dropMode) { if (dropInfo) {
builderStore.actions.moveComponent(dragTarget, dropTarget, dropMode) builderStore.actions.moveComponent(
dragInfo.target,
dropInfo.target,
dropInfo.mode
)
} }
} }
@ -172,7 +175,7 @@
</script> </script>
<IndicatorSet <IndicatorSet
componentId={dropMode === "inside" ? dropTarget : null} componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
color="var(--spectrum-global-color-static-green-500)" color="var(--spectrum-global-color-static-green-500)"
zIndex="930" zIndex="930"
transition transition
@ -180,9 +183,7 @@
/> />
<DNDPositionIndicator <DNDPositionIndicator
componentId={dropTarget}
{dropInfo} {dropInfo}
mode={dropMode}
color="var(--spectrum-global-color-static-green-500)" color="var(--spectrum-global-color-static-green-500)"
zIndex="940" zIndex="940"
transition transition

View File

@ -1,15 +1,13 @@
<script> <script>
import Indicator from "./Indicator.svelte" import Indicator from "./Indicator.svelte"
export let componentId
export let dropInfo export let dropInfo
export let mode
export let zIndex export let zIndex
export let color export let color
export let transition export let transition
$: dimensions = getDimensions(dropInfo?.bounds, mode) $: dimensions = getDimensions(dropInfo?.bounds, dropInfo?.mode)
$: prefix = mode === "above" ? "Above" : "Below" $: prefix = dropInfo?.mode === "above" ? "Above" : "Below"
$: text = `${prefix} ${dropInfo?.name}` $: text = `${prefix} ${dropInfo?.name}`
const getDimensions = (bounds, mode) => { const getDimensions = (bounds, mode) => {
@ -25,8 +23,8 @@
} }
</script> </script>
{#key `${componentId}-${mode}`} {#key `${dropInfo?.target}-${dropInfo?.mode}`}
{#if dimensions && mode !== "inside"} {#if dimensions && dropInfo?.mode !== "inside"}
<Indicator <Indicator
left={dimensions.left} left={dimensions.left}
top={dimensions.top} top={dimensions.top}