Add above/below dnd and support for dropping above/below components which also allow dropping inside
This commit is contained in:
parent
5c37238c8a
commit
bdc86e4c22
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
|
@ -30,7 +30,7 @@
|
|||
</script>
|
||||
|
||||
<IndicatorSet
|
||||
{componentId}
|
||||
componentId={$builderStore.showHoverIndicator ? componentId : null}
|
||||
color="var(--spectrum-global-color-static-blue-200)"
|
||||
transition
|
||||
{zIndex}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue