Add above/below dnd and support for dropping above/below components which also allow dropping inside

This commit is contained in:
Andrew Kingston 2021-09-16 14:28:44 +01:00
parent 5c37238c8a
commit bdc86e4c22
7 changed files with 144 additions and 15 deletions

View File

@ -181,7 +181,8 @@
data-id={id}
data-name={name}
data-draggable={interactive && !isLayout && !isScreen ? "true" : "false"}
data-droppable={definition?.hasChildren ? "true" : "false"}
data-droppable={interactive ? "true" : "false"}
data-droppable-inside={definition?.hasChildren ? "true" : "false"}
>
<svelte:component this={constructor} {...componentSettings}>
{#if children.length}

View File

@ -1,36 +1,116 @@
<script>
import { onMount } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { builderStore } from "stores"
let dragTarget
let dropTarget
let dropMode
let dropInfo
const getDOMNodeForComponent = component => {
const parent = component.closest("[data-type='component']")
const children = Array.from(parent.childNodes)
return children?.find(node => node?.nodeType === 1)
}
// Callback when initially starting a drag on a draggable component
const onDragStart = e => {
// Update state
dragTarget = e.target.dataset.componentId
e.target.style.opacity = 0.5
builderStore.actions.selectComponent(dragTarget)
builderStore.actions.showHoverIndicator(false)
// Highlight being dragged by setting opacity
const child = getDOMNodeForComponent(e.target)
console.log(child)
if (child) {
console.log("set opacity")
child.style.opacity = "0.5"
}
}
// Callback when drag stops (whether dropped or not)
const onDragEnd = e => {
// reset the transparency
dragTarget = null
e.target.style.opacity = ""
// Reset state and styles
dropTarget = null
dropInfo = null
// Reset opacity style
const child = getDOMNodeForComponent(e.target)
if (child) {
child.style.opacity = ""
}
// Re-enable the hover indicator
builderStore.actions.showHoverIndicator(true)
}
// Callback when on top of a component
const onDragOver = e => {
e.preventDefault()
if (dropInfo) {
const { droppableInside, bounds } = dropInfo
const { top, height } = bounds
const mouseY = e.clientY
const elTop = top
const elBottom = top + height
// Determine which edge we're nearest as this is needed for potentially
// any drop mode
let nearestEdge
if (Math.abs(elTop - mouseY) < Math.abs(elBottom - mouseY)) {
nearestEdge = "above"
} else {
nearestEdge = "below"
}
// If not available to drop inside, just check whether we are closer
// to the top or bottom
if (!droppableInside) {
dropMode = nearestEdge
}
// Otherwise determine whether the user wants to drop inside or at
// either edge
else {
const edgeLimit = Math.min(40, height * 0.33)
const insideLimit = [
Math.round(top + edgeLimit),
Math.round(top + height - edgeLimit),
]
if (mouseY >= insideLimit[0] && mouseY <= insideLimit[1]) {
dropMode = "inside"
} else {
dropMode = nearestEdge
}
}
}
}
// Callback when entering a potential drop target
const onDragEnter = e => {
const element = e.target.closest("[data-type='component']")
if (element && element.dataset.droppable === "true") {
if (
element &&
element.dataset.droppable &&
element.dataset.id !== dragTarget
) {
// Store target ID
dropTarget = element.dataset.id
// Precompute and store some info to avoid recalculating everything in
// dragOver
const child = getDOMNodeForComponent(e.target)
const bounds = child.getBoundingClientRect()
dropInfo = {
droppableInside: element.dataset.droppableInside === "true",
bounds,
}
} else {
dropInfo = null
dropTarget = null
}
}
@ -42,11 +122,8 @@
// Callback when dropping a drag on top of some component
const onDrop = e => {
e.preventDefault()
// Check if the target is droppable
const element = e.target.closest("[data-type='component']")
if (element && element.dataset.droppable === "true") {
builderStore.actions.moveComponent(dragTarget, dropTarget, "inside")
if (dropTarget && dropMode) {
builderStore.actions.moveComponent(dragTarget, dropTarget, dropMode)
}
}
@ -76,8 +153,13 @@
</script>
<IndicatorSet
componentId={dropTarget}
color="var(--spectrum-global-color-static-red-600)"
componentId={dropMode === "inside" ? dropTarget : null}
color="var(--spectrum-global-color-static-green-500)"
zIndex="930"
transition
prefix="Inside"
/>
{#if dropMode !== "inside" && dropInfo}
<DNDPositionIndicator bounds={dropInfo.bounds} mode={dropMode} zIndex="940" />
{/if}

View File

@ -0,0 +1,33 @@
<script>
export let bounds
export let mode
export let zIndex
$: x = bounds?.left
$: y = getYPos(bounds, mode)
$: width = bounds?.width
$: valid = bounds != null
const getYPos = (bounds, mode) => {
if (!bounds || !mode) {
return null
}
const { top, height } = bounds
return mode === "above" ? top - 2 : top + height
}
</script>
{#if valid}
<div
class="indicator"
style={`top:${y}px;left:${x}px;width:${width}px;z-index:${zIndex};`}
/>
{/if}
<style>
.indicator {
position: absolute;
height: 2px;
background: var(--spectrum-global-color-static-green-500);
}
</style>

View File

@ -30,7 +30,7 @@
</script>
<IndicatorSet
{componentId}
componentId={$builderStore.showHoverIndicator ? componentId : null}
color="var(--spectrum-global-color-static-blue-200)"
transition
{zIndex}

View File

@ -7,6 +7,7 @@
export let color
export let transition
export let zIndex
export let prefix = null
let indicators = []
let interval
@ -51,6 +52,9 @@
const parents = document.getElementsByClassName(componentId)
if (parents.length) {
text = parents[0].dataset.name
if (prefix) {
text = `${prefix} ${text}`
}
}
// Batch reads to minimize reflow

View File

@ -23,6 +23,7 @@ const createBuilderStore = () => {
theme: null,
customTheme: null,
previewDevice: "desktop",
showHoverIndicator: true,
}
const writableStore = writable(initialState)
const derivedStore = derived(writableStore, $state => {
@ -76,9 +77,16 @@ const createBuilderStore = () => {
mode,
})
},
showHoverIndicator: show => {
writableStore.update(state => {
state.showHoverIndicator = show
return state
})
},
}
return {
...writableStore,
set: state => writableStore.set({ ...initialState, ...state }),
subscribe: derivedStore.subscribe,
actions,
}

View File

@ -33,6 +33,7 @@ export const styleable = (node, styles = {}) => {
const setupStyles = (newStyles = {}) => {
// Use empty state styles as base styles if required, but let them, get
// overridden by any user specified styles
const baseString = node.style.cssText
let baseStyles = {}
if (newStyles.empty) {
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
@ -50,7 +51,7 @@ export const styleable = (node, styles = {}) => {
// Applies a style string to a DOM node
const applyStyles = styleString => {
node.style = styleString
node.style = `${baseString}${styleString}`
node.dataset.componentId = componentId
}