Rewrite drag and drop from scratch using mouse position heuristics
This commit is contained in:
parent
428b786184
commit
4322345aca
|
@ -27,6 +27,8 @@
|
||||||
export let isLayout = false
|
export let isLayout = false
|
||||||
export let isScreen = false
|
export let isScreen = false
|
||||||
export let isBlock = false
|
export let isBlock = false
|
||||||
|
export let parent = null
|
||||||
|
export let index = 0
|
||||||
|
|
||||||
// Get parent contexts
|
// Get parent contexts
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
|
@ -96,7 +98,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)
|
$: isDndParent = $componentStore.dndParent === 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
|
||||||
|
@ -119,7 +121,7 @@
|
||||||
!isLayout &&
|
!isLayout &&
|
||||||
!isScreen &&
|
!isScreen &&
|
||||||
definition?.draggable !== false
|
definition?.draggable !== false
|
||||||
$: droppable = interactive && !isLayout && !isScreen
|
$: droppable = interactive
|
||||||
$: builderHidden =
|
$: builderHidden =
|
||||||
$builderStore.inBuilder && $builderStore.hiddenComponentIds?.includes(id)
|
$builderStore.inBuilder && $builderStore.hiddenComponentIds?.includes(id)
|
||||||
|
|
||||||
|
@ -451,21 +453,24 @@
|
||||||
class:draggable
|
class:draggable
|
||||||
class:droppable
|
class:droppable
|
||||||
class:empty
|
class:empty
|
||||||
|
class:parent={hasChildren}
|
||||||
class:interactive
|
class:interactive
|
||||||
class:editing
|
class:editing
|
||||||
class:block={isBlock}
|
class:block={isBlock}
|
||||||
class:explode={children.length && !isLayout && inDropPath && false}
|
class:explode={interactive && hasChildren && $builderStore.isDragging}
|
||||||
|
class:placeholder={id === "placeholder"}
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
data-icon={icon}
|
data-icon={icon}
|
||||||
data-placeholder={id === "placeholder"}
|
data-parent={parent}
|
||||||
|
data-index={index}
|
||||||
>
|
>
|
||||||
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
<svelte:component this={constructor} bind:this={ref} {...initialSettings}>
|
||||||
{#if hasMissingRequiredSettings}
|
{#if hasMissingRequiredSettings}
|
||||||
<ComponentPlaceholder />
|
<ComponentPlaceholder />
|
||||||
{:else if children.length}
|
{:else if children.length}
|
||||||
{#each children as child (child._id)}
|
{#each children as child, idx (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} parent={id} index={idx} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if emptyState}
|
{:else if emptyState}
|
||||||
{#if isScreen}
|
{#if isScreen}
|
||||||
|
@ -488,8 +493,9 @@
|
||||||
transition: padding 250ms ease, border 250ms ease;
|
transition: padding 250ms ease, border 250ms ease;
|
||||||
}
|
}
|
||||||
.component.explode :global(> *) {
|
.component.explode :global(> *) {
|
||||||
padding: 12px 4px !important;
|
padding: 16px !important;
|
||||||
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
border: 2px dashed var(--spectrum-global-color-gray-400) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
}
|
}
|
||||||
.interactive :global(*:hover) {
|
.interactive :global(*:hover) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -12,10 +12,12 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
import IndicatorSet from "./IndicatorSet.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
import { builderStore } from "stores"
|
import { builderStore, componentStore } from "stores"
|
||||||
|
import PlaceholderOverlay from "./PlaceholderOverlay.svelte"
|
||||||
|
|
||||||
let dragInfo
|
let dragInfo
|
||||||
let dropInfo
|
let dropInfo
|
||||||
|
let placeholderInfo
|
||||||
|
|
||||||
const getEdges = (bounds, mousePoint) => {
|
const getEdges = (bounds, mousePoint) => {
|
||||||
const { width, height, top, left } = bounds
|
const { width, height, top, left } = bounds
|
||||||
|
@ -33,39 +35,37 @@
|
||||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDOMNodeForComponent = component => {
|
const getDOMNode = id => {
|
||||||
const parent = component.closest(".component")
|
const component = document.getElementsByClassName(id)[0]
|
||||||
const children = Array.from(parent.children)
|
return [...component.children][0]
|
||||||
return children[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 => {
|
||||||
const parent = e.target.closest(".component")
|
const component = e.target.closest(".component")
|
||||||
if (!parent?.classList.contains("draggable")) {
|
if (!component?.classList.contains("draggable")) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update state
|
// Update state
|
||||||
dragInfo = {
|
dragInfo = {
|
||||||
target: parent.dataset.id,
|
target: component.dataset.id,
|
||||||
parent: parent.dataset.parent,
|
|
||||||
}
|
}
|
||||||
builderStore.actions.selectComponent(dragInfo.target)
|
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)
|
const child = getDOMNode(component.dataset.id)
|
||||||
if (child) {
|
if (child) {
|
||||||
child.style.opacity = "0.5"
|
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 = () => {
|
||||||
// Reset opacity style
|
// Reset opacity style
|
||||||
if (dragInfo) {
|
if (dragInfo) {
|
||||||
const child = getDOMNodeForComponent(e.target)
|
const child = getDOMNode(dragInfo.target)
|
||||||
if (child) {
|
if (child) {
|
||||||
child.style.opacity = ""
|
child.style.opacity = ""
|
||||||
}
|
}
|
||||||
|
@ -77,84 +77,179 @@
|
||||||
builderStore.actions.setDragging(false)
|
builderStore.actions.setDragging(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateDrop = (dropInfo, e) => {
|
const variance = arr => {
|
||||||
|
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
|
||||||
|
let squareSum = 0
|
||||||
|
arr.forEach(value => {
|
||||||
|
const delta = value - mean
|
||||||
|
squareSum += delta * delta
|
||||||
|
})
|
||||||
|
return squareSum / arr.length
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEvent = e => {
|
||||||
if (!dropInfo) {
|
if (!dropInfo) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
const { droppableInside, bounds } = dropInfo
|
let { id, parent, node, index, acceptsChildren, empty } = dropInfo
|
||||||
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.25 : 0.5
|
|
||||||
const snapLimitV = Math.min(40, height * snapFactor)
|
|
||||||
const snapLimitH = Math.min(40, width * snapFactor)
|
|
||||||
|
|
||||||
// Determine all sies we are within snap range of
|
// if (!dropInfo.bounds) {
|
||||||
let sides = []
|
// } else {
|
||||||
if (mouseY <= top + snapLimitV) {
|
// dropInfo.bounds.top = node.offsetTop
|
||||||
sides.push(Sides.Top)
|
// dropInfo.bounds.left = node.offsetLeft
|
||||||
} else if (mouseY >= top + height - snapLimitV) {
|
// console.log(node.offsetTop)
|
||||||
sides.push(Sides.Bottom)
|
// }
|
||||||
}
|
|
||||||
if (mouseX < left + snapLimitH) {
|
// console.log("calc")
|
||||||
sides.push(Sides.Left)
|
// dropInfo.bounds = bounds
|
||||||
} else if (mouseX > left + width - snapLimitH) {
|
|
||||||
sides.push(Sides.Right)
|
// If we're over something that does not accept children then we must go
|
||||||
|
// above or below this component
|
||||||
|
if (!acceptsChildren) {
|
||||||
|
id = parent
|
||||||
|
acceptsChildren = true
|
||||||
|
empty = false
|
||||||
|
node = getDOMNode(parent)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// const bounds = node.getBoundingClientRect()
|
||||||
|
// const { top, left, height, width } = bounds
|
||||||
|
// const snapFactor = 0.5
|
||||||
|
// const snapLimitV = Math.min(40, height * snapFactor)
|
||||||
|
// const snapLimitH = Math.min(40, width * snapFactor)
|
||||||
|
//
|
||||||
|
// // Determine all sides we are within snap range of
|
||||||
|
// let sides = []
|
||||||
|
// if (mouseY <= top + snapLimitV) {
|
||||||
|
// sides.push(Sides.Top)
|
||||||
|
// } else if (mouseY >= top + height - snapLimitV) {
|
||||||
|
// sides.push(Sides.Bottom)
|
||||||
|
// }
|
||||||
|
// if (mouseX < left + snapLimitH) {
|
||||||
|
// sides.push(Sides.Left)
|
||||||
|
// } else if (mouseX > left + width - snapLimitH) {
|
||||||
|
// sides.push(Sides.Right)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // If we're somehow not in range of any side, do nothing
|
||||||
|
// if (!sides.length) {
|
||||||
|
// console.log("no sides match")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// let side
|
||||||
|
// if (sides.length === 1) {
|
||||||
|
// // When one edge matches, use that edge
|
||||||
|
// side = sides[0]
|
||||||
|
// } else {
|
||||||
|
// // When 2 edges match, work out which is closer
|
||||||
|
// const mousePoint = [mouseX, mouseY]
|
||||||
|
// const edges = getEdges(bounds, mousePoint)
|
||||||
|
// const edge1 = edges[sides[0]]
|
||||||
|
// const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||||
|
// const edge2 = edges[sides[1]]
|
||||||
|
// const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||||
|
// side = delta1 < delta2 ? sides[0] : sides[1]
|
||||||
|
// }
|
||||||
|
// if ([Sides.Top, Sides.Left].includes(side)) {
|
||||||
|
// // Before, so use the current index
|
||||||
|
// console.log("before")
|
||||||
|
// placeholderInfo = {
|
||||||
|
// parent: parent,
|
||||||
|
// index: index,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// console.log("after")
|
||||||
|
// // After, so use the next index
|
||||||
|
// placeholderInfo = {
|
||||||
|
// parent: parent,
|
||||||
|
// index: index + 1,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// When no edges match, drop inside if possible
|
// We're now hovering over something which does accept children.
|
||||||
if (!sides.length) {
|
// If it is empty, just go inside it
|
||||||
if (droppableInside) {
|
if (empty) {
|
||||||
return {
|
placeholderInfo = {
|
||||||
...dropInfo,
|
parent: id,
|
||||||
mode: "inside",
|
index: 0,
|
||||||
side: null,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// When one edge matches, use that edge
|
// We're now hovering over something which accepts children and is not
|
||||||
if (sides.length === 1) {
|
// empty, so we need to work out where to inside the placeholder
|
||||||
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
|
||||||
return {
|
// Check we're actually inside
|
||||||
...dropInfo,
|
// if (
|
||||||
mode: "above",
|
// mouseY < top ||
|
||||||
side: sides[0],
|
// mouseY > top + height ||
|
||||||
}
|
// mouseX < left ||
|
||||||
} else {
|
// mouseX > left + width
|
||||||
return {
|
// ) {
|
||||||
...dropInfo,
|
// console.log("not inside")
|
||||||
mode: "below",
|
// return
|
||||||
side: sides[0],
|
// }
|
||||||
}
|
|
||||||
}
|
// Get all DOM nodes of children of this component.
|
||||||
|
// Filter out the placeholder as we don't want it to affect the index of
|
||||||
|
// the new placeholder.
|
||||||
|
const children = [...(node.children || [])]
|
||||||
|
.filter(x => !x.classList.contains("placeholder"))
|
||||||
|
.map(x => x.children[0])
|
||||||
|
|
||||||
|
// Calculate centers of each child
|
||||||
|
const centers = children.map(child => {
|
||||||
|
const childBounds = child.getBoundingClientRect()
|
||||||
|
return [
|
||||||
|
childBounds.left + childBounds.width / 2,
|
||||||
|
childBounds.top + childBounds.height / 2,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate variance of X and Y centers to determine layout
|
||||||
|
const xCoords = centers.map(x => x[0])
|
||||||
|
const yCoords = centers.map(x => x[1])
|
||||||
|
const xVariance = variance(xCoords)
|
||||||
|
const yVariance = variance(yCoords)
|
||||||
|
const column = xVariance <= yVariance
|
||||||
|
console.log(column ? "COL" : "ROW")
|
||||||
|
|
||||||
|
// Now that we know the layout, find which children in this axis we are
|
||||||
|
// between
|
||||||
|
const childPositions = column ? yCoords : xCoords
|
||||||
|
const mousePosition = column ? mouseY : mouseX
|
||||||
|
|
||||||
|
let idx = 0
|
||||||
|
while (idx < children.length && childPositions[idx] < mousePosition) {
|
||||||
|
idx++
|
||||||
}
|
}
|
||||||
|
|
||||||
// When 2 edges match, work out which is closer
|
placeholderInfo = {
|
||||||
const mousePoint = [mouseX, mouseY]
|
parent: id,
|
||||||
const edges = getEdges(bounds, mousePoint)
|
index: idx,
|
||||||
const edge1 = edges[sides[0]]
|
|
||||||
const delta1 = calculatePointDelta(mousePoint, edge1)
|
|
||||||
const edge2 = edges[sides[1]]
|
|
||||||
const delta2 = calculatePointDelta(mousePoint, edge2)
|
|
||||||
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
|
||||||
dropInfo.side = edge
|
|
||||||
if ([Sides.Top, Sides.Left].includes(edge)) {
|
|
||||||
return {
|
|
||||||
...dropInfo,
|
|
||||||
mode: "above",
|
|
||||||
side: edge,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
...dropInfo,
|
|
||||||
mode: "below",
|
|
||||||
side: edge,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// // When no edges match, drop inside if possible
|
||||||
|
// if (!sides.length) {
|
||||||
|
// if (empty) {
|
||||||
|
// console.log("allowed inside")
|
||||||
|
// return {
|
||||||
|
// ...dropInfo,
|
||||||
|
// mode: "inside",
|
||||||
|
// side: null,
|
||||||
|
// bounds,
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // No sides but also not empty?
|
||||||
|
// console.log("no sides match, but not empty")
|
||||||
|
// return null
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when on top of a component
|
// Callback when on top of a component
|
||||||
|
@ -163,15 +258,7 @@
|
||||||
if (!dragInfo || !dropInfo) {
|
if (!dragInfo || !dropInfo) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
handleEvent(e)
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const nextDropInfo = validateDrop(dropInfo, e)
|
|
||||||
if (nextDropInfo) {
|
|
||||||
console.log("set from over")
|
|
||||||
dropInfo = nextDropInfo
|
|
||||||
console.log(dropInfo.mode, dropInfo.target)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback when entering a potential drop target
|
// Callback when entering a potential drop target
|
||||||
|
@ -181,53 +268,53 @@
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update drop target
|
// Do nothing if this is the placeholder
|
||||||
const dropTarget = e.target.closest(".component")
|
|
||||||
builderStore.actions.setDropTarget(dropTarget?.dataset.id)
|
|
||||||
|
|
||||||
// // Do nothing if this is the placeholder
|
|
||||||
// if (element.dataset.id === "placeholder") {
|
// if (element.dataset.id === "placeholder") {
|
||||||
// console.log("placeholder")
|
// console.log("placeholder")
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const element = e.target.closest(".component:not(.block)")
|
const component = e.target.closest(".component:not(.block)")
|
||||||
if (
|
if (
|
||||||
element &&
|
component &&
|
||||||
element.classList.contains("droppable") &&
|
component.classList.contains("droppable") &&
|
||||||
element.dataset.id !== dragInfo.target
|
component.dataset.id !== dragInfo.target
|
||||||
) {
|
) {
|
||||||
// Do nothing if this is the same target
|
// Do nothing if this is the same target
|
||||||
if (element.dataset.id === dropInfo?.target) {
|
if (component.dataset.id === dropInfo?.target) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
// after selecting the DND component and setting this the first time
|
// after selecting the DND component and setting this the first time
|
||||||
if (!get(builderStore).isDragging) {
|
// if (!get(builderStore).isDragging) {
|
||||||
builderStore.actions.setDragging(true)
|
// builderStore.actions.setDragging(true)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Store target 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)
|
dropInfo = {
|
||||||
const bounds = child.getBoundingClientRect()
|
id: component.dataset.id,
|
||||||
let nextDropInfo = {
|
parent: component.dataset.parent,
|
||||||
target,
|
index: parseInt(component.dataset.index),
|
||||||
name: element.dataset.name,
|
node: getDOMNode(component.dataset.id),
|
||||||
icon: element.dataset.icon,
|
empty: component.classList.contains("empty"),
|
||||||
droppableInside: element.classList.contains("empty"),
|
acceptsChildren: component.classList.contains("parent"),
|
||||||
bounds,
|
|
||||||
}
|
|
||||||
nextDropInfo = validateDrop(nextDropInfo, e)
|
|
||||||
if (nextDropInfo) {
|
|
||||||
console.log("set from enter")
|
|
||||||
dropInfo = nextDropInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// console.log(
|
||||||
|
// "enter",
|
||||||
|
// component.dataset.name,
|
||||||
|
// "id",
|
||||||
|
// dropInfo.id,
|
||||||
|
// "parent",
|
||||||
|
// dropInfo.parent,
|
||||||
|
// "index",
|
||||||
|
// dropInfo.index
|
||||||
|
// )
|
||||||
|
|
||||||
|
handleEvent(e)
|
||||||
} else {
|
} else {
|
||||||
// dropInfo = null
|
// dropInfo = null
|
||||||
}
|
}
|
||||||
|
@ -240,18 +327,22 @@
|
||||||
// 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()
|
||||||
|
dropInfo = null
|
||||||
|
placeholderInfo = null
|
||||||
|
dragInfo = null
|
||||||
|
builderStore.actions.setDragging(false)
|
||||||
if (dropInfo?.mode) {
|
if (dropInfo?.mode) {
|
||||||
builderStore.actions.moveComponent(
|
// builderStore.actions.moveComponent(
|
||||||
dragInfo.target,
|
// dragInfo.target,
|
||||||
dropInfo.target,
|
// dropInfo.target,
|
||||||
dropInfo.mode
|
// dropInfo.mode
|
||||||
)
|
// )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: mode = dropInfo?.mode
|
$: parent = placeholderInfo?.parent
|
||||||
$: target = dropInfo?.target
|
$: index = placeholderInfo?.index
|
||||||
$: builderStore.actions.updateDNDPlaceholder(mode, target)
|
$: builderStore.actions.updateDNDPlaceholder(parent, index)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Events fired on the draggable target
|
// Events fired on the draggable target
|
||||||
|
@ -279,16 +370,20 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
componentId={$builderStore.dndParent}
|
||||||
color="var(--spectrum-global-color-static-green-500)"
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
zIndex="930"
|
zIndex="930"
|
||||||
transition
|
transition
|
||||||
prefix="Inside"
|
prefix="Inside"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DNDPositionIndicator
|
{#if $builderStore.isDragging}
|
||||||
{dropInfo}
|
<PlaceholderOverlay />
|
||||||
color="var(--spectrum-global-color-static-green-500)"
|
{/if}
|
||||||
zIndex="940"
|
|
||||||
transition
|
<!--<DNDPositionIndicator-->
|
||||||
/>
|
<!-- {dropInfo}-->
|
||||||
|
<!-- color="var(--spectrum-global-color-static-green-500)"-->
|
||||||
|
<!-- zIndex="940"-->
|
||||||
|
<!-- transition-->
|
||||||
|
<!--/>-->
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div id="placeholder" class="placeholder" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.placeholder {
|
||||||
|
display: block;
|
||||||
|
min-height: 64px;
|
||||||
|
min-width: 64px;
|
||||||
|
flex: 0 0 64px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let left, top, height, width
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const node = document.getElementById("placeholder")
|
||||||
|
if (!node) {
|
||||||
|
height = 0
|
||||||
|
width = 0
|
||||||
|
} else {
|
||||||
|
const bounds = node.getBoundingClientRect()
|
||||||
|
left = bounds.left
|
||||||
|
top = bounds.top
|
||||||
|
height = bounds.height
|
||||||
|
width = bounds.width
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if left != null}
|
||||||
|
<div
|
||||||
|
class="overlay"
|
||||||
|
style="left: {left}px; top: {top}px; width: {width}px; height: {height}px;"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 800;
|
||||||
|
background: hsl(160, 64%, 90%);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 130ms ease-out;
|
||||||
|
border: 2px solid var(--spectrum-global-color-static-green-500);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -24,6 +24,7 @@ let app
|
||||||
const loadBudibase = async () => {
|
const loadBudibase = async () => {
|
||||||
// Update builder store with any builder flags
|
// Update builder store with any builder flags
|
||||||
builderStore.set({
|
builderStore.set({
|
||||||
|
...get(builderStore),
|
||||||
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
inBuilder: !!window["##BUDIBASE_IN_BUILDER##"],
|
||||||
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
layout: window["##BUDIBASE_PREVIEW_LAYOUT##"],
|
||||||
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
|
||||||
|
|
|
@ -21,9 +21,9 @@ const createBuilderStore = () => {
|
||||||
navigation: null,
|
navigation: null,
|
||||||
hiddenComponentIds: [],
|
hiddenComponentIds: [],
|
||||||
usedPlugins: null,
|
usedPlugins: null,
|
||||||
dndMode: null,
|
|
||||||
dndTarget: null,
|
dndParent: null,
|
||||||
dropTarget: null,
|
dndIndex: null,
|
||||||
|
|
||||||
// Legacy - allow the builder to specify a layout
|
// Legacy - allow the builder to specify a layout
|
||||||
layout: null,
|
layout: null,
|
||||||
|
@ -106,17 +106,10 @@ 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) => {
|
updateDNDPlaceholder: (parent, index) => {
|
||||||
console.log(mode, target)
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.dndMode = mode
|
state.dndParent = parent
|
||||||
state.dndTarget = target
|
state.dndIndex = index
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
setDropTarget: target => {
|
|
||||||
store.update(state => {
|
|
||||||
state.dropTarget = target
|
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { devToolsStore } from "./devTools"
|
||||||
import { screenStore } from "./screens"
|
import { screenStore } from "./screens"
|
||||||
import { builderStore } from "./builder"
|
import { builderStore } from "./builder"
|
||||||
import Router from "../components/Router.svelte"
|
import Router from "../components/Router.svelte"
|
||||||
|
import Placeholder from "../components/preview/Placeholder.svelte"
|
||||||
import * as AppComponents from "../components/app/index.js"
|
import * as AppComponents from "../components/app/index.js"
|
||||||
|
|
||||||
const budibasePrefix = "@budibase/standard-components/"
|
const budibasePrefix = "@budibase/standard-components/"
|
||||||
|
@ -38,11 +39,6 @@ const createComponentStore = () => {
|
||||||
// Derive the selected component path
|
// Derive the selected component path
|
||||||
const selectedPath =
|
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,
|
||||||
|
@ -53,7 +49,6 @@ const createComponentStore = () => {
|
||||||
selectedComponentPath: selectedPath?.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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -113,6 +108,8 @@ const createComponentStore = () => {
|
||||||
// Screenslot is an edge case
|
// Screenslot is an edge case
|
||||||
if (type === "screenslot") {
|
if (type === "screenslot") {
|
||||||
type = `${budibasePrefix}${type}`
|
type = `${budibasePrefix}${type}`
|
||||||
|
} else if (type === "placeholder") {
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle built-in components
|
// Handle built-in components
|
||||||
|
@ -132,6 +129,8 @@ const createComponentStore = () => {
|
||||||
}
|
}
|
||||||
if (type === "screenslot") {
|
if (type === "screenslot") {
|
||||||
return Router
|
return Router
|
||||||
|
} else if (type === "placeholder") {
|
||||||
|
return Placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle budibase components
|
// Handle budibase components
|
||||||
|
|
|
@ -48,30 +48,24 @@ const createScreenStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert DND placeholder if required
|
// Insert DND placeholder if required
|
||||||
const { dndTarget, dndMode, selectedComponentId } = $builderStore
|
const { dndParent, dndIndex } = $builderStore
|
||||||
const insert = false
|
const insert = true
|
||||||
if (insert && activeScreen && dndTarget && dndMode) {
|
if (insert && activeScreen && dndParent && dndIndex != null) {
|
||||||
let selectedComponent = findComponentById(
|
// let selectedComponent = findComponentById(
|
||||||
activeScreen.props,
|
// activeScreen.props,
|
||||||
selectedComponentId
|
// selectedComponentId
|
||||||
)
|
// )
|
||||||
|
// delete selectedComponent._component
|
||||||
const placeholder = {
|
const placeholder = {
|
||||||
...selectedComponent,
|
_component: "placeholder",
|
||||||
_id: "placeholder",
|
_id: "placeholder",
|
||||||
static: true,
|
static: true,
|
||||||
}
|
}
|
||||||
// delete selectedComponent._component
|
let parent = findComponentById(activeScreen.props, dndParent)
|
||||||
if (dndMode === "inside") {
|
if (!parent._children?.length) {
|
||||||
const target = findComponentById(activeScreen.props, dndTarget)
|
parent._children = [placeholder]
|
||||||
target._children = [placeholder]
|
|
||||||
} else {
|
} else {
|
||||||
const path = findComponentPathById(activeScreen.props, dndTarget)
|
parent._children.splice(dndIndex, 0, placeholder)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue