Add grid functionality separately to DND

This commit is contained in:
Andrew Kingston 2022-10-18 16:18:22 +01:00
parent 774566d03b
commit 15bbc78847
13 changed files with 251 additions and 622 deletions

View File

@ -21,7 +21,6 @@
devToolsStore,
componentStore,
appStore,
dndIsDragging,
dndComponentPath,
} from "stores"
import { Helpers } from "@budibase/bbui"
@ -163,7 +162,7 @@
// nested layers. Only reset this when dragging stops.
let pad = false
$: pad = pad || (interactive && hasChildren && inDndPath)
$: $dndIsDragging, (pad = false)
$: $builderStore.dragging, (pad = false)
// Update component context
$: store.set({
@ -427,7 +426,7 @@
const scrollIntoView = () => {
// Don't scroll into view if we selected this component because we were
// starting dragging on it
if (get(dndIsDragging)) {
if (get(builderStore).dragging) {
return
}
const node = document.getElementsByClassName(id)?.[0]?.children[0]

View File

@ -1,11 +1,14 @@
<script>
import { getContext } from "svelte"
import { fade } from "svelte/transition"
import GridDNDHandler from "../preview/GridDNDHandler.svelte"
const component = getContext("component")
const { styleable, builderStore } = getContext("sdk")
$: coords = generateCoords(12)
const cols = 12
let node
$: coords = generateCoords(cols)
const generateCoords = num => {
let grid = []
@ -18,44 +21,36 @@
}
</script>
<div class="grid" use:styleable={$component.styles}>
<div
bind:this={node}
class="grid"
use:styleable={$component.styles}
data-cols={cols}
>
<div class="underlay">
{#each coords as coord}
<div class="placeholder" />
{/each}
</div>
<slot />
{#if $builderStore.isDragging}
<div
class="overlay"
in:fade={{ duration: 130 }}
out:fade|self={{ duration: 130 }}
>
{#each coords as coord}
<div
class="placeholder grid-coord"
data-row={coord.row}
data-col={coord.col}
/>
{/each}
</div>
{/if}
</div>
{#if $builderStore.inBuilder && node}
<GridDNDHandler {node} />
{/if}
<style>
.grid {
position: relative;
min-height: 400px;
}
.grid,
.underlay,
.overlay {
.underlay {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(12, 1fr);
}
.underlay,
.overlay {
.underlay {
position: absolute;
top: 0;
left: 0;
@ -68,12 +63,6 @@
.underlay {
z-index: -1;
}
.overlay {
z-index: 999;
background-color: var(--spectrum-global-color-gray-500);
border-color: var(--spectrum-global-color-gray-500);
opacity: 0.3;
}
.placeholder {
background-color: var(--spectrum-global-color-gray-100);

View File

@ -2,13 +2,7 @@
import { onMount, onDestroy } from "svelte"
import { get } from "svelte/store"
import IndicatorSet from "./IndicatorSet.svelte"
import {
builderStore,
screenStore,
dndStore,
dndParent,
dndIsDragging,
} from "stores"
import { builderStore, screenStore, dndStore, dndParent } from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js"
@ -22,6 +16,10 @@
$: target = $dndStore.target
$: drop = $dndStore.drop
const insideGrid = e => {
return e.target?.closest?.(".grid") != null
}
// Util to get the inner DOM node by a component ID
const getDOMNode = id => {
const component = document.getElementsByClassName(id)[0]
@ -51,10 +49,14 @@
// Reset state
dndStore.actions.reset()
builderStore.actions.setDragging(false)
}
// Callback when initially starting a drag on a draggable component
const onDragStart = e => {
if (insideGrid(e)) {
return
}
const component = e.target.closest(".component")
if (!component?.classList.contains("draggable")) {
return
@ -83,6 +85,7 @@
index,
})
builderStore.actions.selectComponent(id)
builderStore.actions.setDragging(true)
// Set initial drop info to show placeholder exactly where the dragged
// component is.
@ -99,9 +102,9 @@
// Core logic for handling drop events and determining where to render the
// drop target placeholder
const processEvent = (mouseX, mouseY) => {
const processEvent = Utils.throttle((mouseX, mouseY) => {
if (!target) {
return null
return
}
let { id, parent, node, acceptsChildren, empty } = target
@ -201,17 +204,17 @@
parent: id,
index: idx,
})
}
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
}, ThrottleRate)
const handleEvent = e => {
e.preventDefault()
throttledProcessEvent(e.clientX, e.clientY)
e.stopPropagation()
processEvent(e.clientX, e.clientY)
}
// Callback when on top of a component
// Callback when on top of a component.
const onDragOver = e => {
if (!source || !target) {
if (!source || !target || insideGrid(e)) {
return
}
handleEvent(e)
@ -219,7 +222,7 @@
// Callback when entering a potential drop target
const onDragEnter = e => {
if (!source) {
if (!source || insideGrid(e)) {
return
}
@ -241,8 +244,8 @@
}
// Callback when dropping a drag on top of some component
const onDrop = () => {
if (!source || !drop?.parent || drop?.index == null) {
const onDrop = e => {
if (!source || !drop?.parent || drop?.index == null || insideGrid(e)) {
return
}
@ -326,6 +329,6 @@
prefix="Inside"
/>
{#if $dndIsDragging}
{#if $builderStore.dragging}
<DNDPlaceholderOverlay />
{/if}

View File

@ -1,27 +1,17 @@
<script>
import { onMount, onDestroy } from "svelte"
import { get } from "svelte/store"
import IndicatorSet from "./IndicatorSet.svelte"
import {
builderStore,
screenStore,
dndStore,
dndParent,
dndIsDragging,
} from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { builderStore, componentStore } from "stores"
import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js"
import { DNDPlaceholderID } from "constants"
import { DNDModes } from "stores/dnd.js"
const ThrottleRate = 130
let dragInfo
let gridStyles
// Cache some dnd store state as local variables as it massively helps
// performance. It lets us avoid calling svelte getters on every DOM action.
$: source = $dndStore.source
$: target = $dndStore.target
$: drop = $dndStore.drop
$: dragNode = getDOMNode(dragInfo?.id)
$: applyStyles(dragNode, gridStyles)
const insideGrid = e => {
return e.target?.closest?.(".grid") || e.target.classList.contains("anchor")
}
// Util to get the inner DOM node by a component ID
const getDOMNode = id => {
@ -29,535 +19,195 @@
return [...component.children][0]
}
// Util to calculate the variance of a set of data
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
const applyStyles = (dragNode, gridStyles) => {
if (!dragNode || !gridStyles) {
return
}
Object.entries(gridStyles).forEach(([style, value]) => {
dragNode.style[style] = value
})
return squareSum / arr.length
}
// Callback when drag stops (whether dropped or not)
const stopDragging = () => {
// Save changes
if (gridStyles) {
builderStore.actions.updateStyles(gridStyles)
}
// Reset listener
if (source?.id) {
const component = document.getElementsByClassName(source?.id)[0]
if (component) {
component.removeEventListener("dragend", stopDragging)
}
if (dragInfo?.domTarget) {
dragInfo.domTarget.removeEventListener("dragend", stopDragging)
}
// Reset state
dndStore.actions.reset()
}
const calculatePointDelta = (point1, point2) => {
const deltaX = Math.abs(point1[0] - point2[0])
const deltaY = Math.abs(point1[1] - point2[1])
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
dragInfo = null
gridStyles = null
builderStore.actions.setDragging(false)
}
// Callback when initially starting a drag on a draggable component
const onDragStart = e => {
// New stuff
// var img = new Image()
// img.src =
// "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="
// e.dataTransfer.setDragImage(img, 0, 0)
//
// // Resize component
// if (e.target.classList.contains("anchor")) {
// dragInfo = {
// target: e.target.dataset.id,
// side: e.target.dataset.side,
// mode: "resize",
// }
// } else {
// // Drag component
// const parent = e.target.closest(".component")
// if (!parent?.classList.contains("draggable")) {
// return
// }
// dragInfo = {
// target: parent.dataset.id,
// parent: parent.dataset.parent,
// mode: "move",
// }
// }
// if (!dragInfo) {
// return
// }
//
// builderStore.actions.selectComponent(dragInfo.target)
// builderStore.actions.setDragging(true)
// Current stuff
const component = e.target.closest(".component")
if (!component?.classList.contains("draggable")) {
if (!insideGrid(e)) {
return
}
// Hide drag ghost image
e.dataTransfer.setDragImage(new Image(), 0, 0)
// Add event handler to clear all drag state when dragging ends
component.addEventListener("dragend", stopDragging)
//
// Update state
const id = component.dataset.id
const parentId = component.dataset.parent
const parent = findComponentById(
get(screenStore).activeScreen?.props,
parentId
)
const index = parent._children.findIndex(
x => x._id === component.dataset.id
)
dndStore.actions.startDraggingExistingComponent({
id,
bounds: component.children[0].getBoundingClientRect(),
parent: parentId,
index,
})
builderStore.actions.selectComponent(id)
// Set initial drop info to show placeholder exactly where the dragged
// component is.
// Execute this asynchronously to prevent bugs caused by updating state in
// the same handler as selecting a new component (which causes a client
// re-initialisation).
setTimeout(() => {
dndStore.actions.updateDrop({
parent: parentId,
index,
})
}, 0)
}
// Core logic for handling drop events and determining where to render the
// drop target placeholder
const processEvent = (mouseX, mouseY) => {
if (!target) {
return null
}
let { id, parent, node, acceptsChildren, empty } = target
// If we're over something that does not accept children then we go up a
// level and consider the mouse position relative to the parent
if (!acceptsChildren) {
id = parent
empty = false
node = getDOMNode(parent)
// Extract state
let mode, id, side
if (e.target.classList.contains("anchor")) {
// Handle resize
mode = "resize"
id = e.target.dataset.id
side = e.target.dataset.side
} else {
// Handle move
mode = "move"
const component = e.target.closest(".component")
id = component.dataset.id
}
// We're now hovering over something which does accept children.
// If it is empty, just go inside it.
if (empty) {
dndStore.actions.updateDrop({
parent: id,
index: 0,
})
// Find grid parent
const domComponent = getDOMNode(id)
const gridId = domComponent?.closest(".grid")?.parentNode.dataset.id
if (!gridId) {
return
}
// As the first DOM node in a component may not necessarily contain the
// child components, we can find to try the parent of the first child
// component and use that as the real parent DOM node
const childNode = node.getElementsByClassName("component")[0]
if (childNode?.parentNode) {
node = childNode.parentNode
// Update state
dragInfo = {
domTarget: e.target,
id,
gridId,
mode,
side,
}
// builderStore.actions.selectComponent(dragInfo.id)
// builderStore.actions.setDragging(true)
// Append an ephemeral div to allow us to determine layout if only one
// child exists
let ephemeralDiv
if (node.children.length === 1) {
ephemeralDiv = document.createElement("div")
ephemeralDiv.dataset.id = DNDPlaceholderID
node.appendChild(ephemeralDiv)
}
// We're now hovering over something which accepts children and is not
// empty, so we need to work out where to inside the placeholder
// Calculate the coordinates of various locations on each child.
const childCoords = [...(node.children || [])].map(node => {
const child = node.children?.[0] || node
const bounds = child.getBoundingClientRect()
return {
placeholder: node.dataset.id === DNDPlaceholderID,
centerX: bounds.left + bounds.width / 2,
centerY: bounds.top + bounds.height / 2,
left: bounds.left,
right: bounds.right,
top: bounds.top,
bottom: bounds.bottom,
}
})
// Now that we've calculated the position of the children, we no longer need
// the ephemeral div
if (ephemeralDiv) {
node.removeChild(ephemeralDiv)
}
// Calculate the variance between each set of positions on the children
const variances = Object.keys(childCoords[0])
.filter(x => x !== "placeholder")
.map(key => {
const coords = childCoords.map(x => x[key])
return {
variance: variance(coords),
side: key,
}
})
// Sort by variance. The lowest variance position indicates whether we are
// in a row or column layout
variances.sort((a, b) => {
return a.variance < b.variance ? -1 : 1
})
const column = ["centerX", "left", "right"].includes(variances[0].side)
// Calculate breakpoints between child components so we can determine the
// index to drop the component in.
// We want to ignore the placeholder from this calculation as it should not
// be considered a real child of the parent.
let breakpoints = childCoords
.filter(x => !x.placeholder)
.map(x => {
return column ? x.centerY : x.centerX
})
// Determine the index to drop the component in
const mousePosition = column ? mouseY : mouseX
let idx = 0
while (idx < breakpoints.length && breakpoints[idx] < mousePosition) {
idx++
}
dndStore.actions.updateDrop({
parent: id,
index: idx,
})
// Add event handler to clear all drag state when dragging ends
dragInfo.domTarget.addEventListener("dragend", stopDragging)
}
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
// Callback when entering a potential drop target
const onDragEnter = e => {
// Skip if we aren't validly dragging currently
if (!dragInfo || dragInfo.grid || !insideGrid(e)) {
return
}
const compDef = $componentStore.selectedComponent
const domGrid = getDOMNode(dragInfo.gridId)
if (domGrid) {
const getStyle = x => parseInt(compDef._styles.normal?.[x] || "0")
dragInfo.grid = {
startX: e.clientX,
startY: e.clientY,
rowStart: getStyle("grid-row-start"),
rowEnd: getStyle("grid-row-end"),
colStart: getStyle("grid-column-start"),
colEnd: getStyle("grid-column-end"),
rowDeltaMin: 1 - getStyle("grid-row-start"),
rowDeltaMax:
parseInt(domGrid.dataset.cols) + 1 - getStyle("grid-row-end"),
colDeltaMin: 1 - getStyle("grid-column-start"),
colDeltaMax:
parseInt(domGrid.dataset.cols) + 1 - getStyle("grid-column-end"),
}
handleEvent(e)
}
}
const processEvent = Utils.throttle((mouseX, mouseY) => {
if (!dragInfo?.grid) {
return
}
const { mode, side, gridId, grid } = dragInfo
const {
startX,
startY,
rowStart,
rowEnd,
colStart,
colEnd,
rowDeltaMin,
rowDeltaMax,
colDeltaMin,
colDeltaMax,
} = grid
const domGrid = getDOMNode(gridId)
const cols = parseInt(domGrid.dataset.cols)
const { width, height } = domGrid.getBoundingClientRect()
const colWidth = width / cols
const diffX = mouseX - startX
let deltaX = Math.round(diffX / colWidth)
const rowHeight = height / cols
const diffY = mouseY - startY
let deltaY = Math.round(diffY / rowHeight)
if (mode === "move") {
deltaY = Math.min(Math.max(deltaY, rowDeltaMin), rowDeltaMax)
deltaX = Math.min(Math.max(deltaX, colDeltaMin), colDeltaMax)
gridStyles = {
"grid-row-start": rowStart + deltaY,
"grid-row-end": rowEnd + deltaY,
"grid-column-start": colStart + deltaX,
"grid-column-end": colEnd + deltaX,
}
} else if (mode === "resize") {
let newStyles = {}
if (side === "right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
} else if (side === "left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
} else if (side === "top") {
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "bottom") {
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
newStyles["grid-row-end"] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "top-right") {
newStyles["grid-column-end"] = Math.max(colEnd + deltaX, colStart + 1)
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "top-left") {
newStyles["grid-column-start"] = Math.min(colStart + deltaX, colEnd - 1)
newStyles["grid-row-start"] = Math.min(rowStart + deltaY, rowEnd - 1)
}
gridStyles = newStyles
}
}, 100)
const handleEvent = e => {
e.preventDefault()
throttledProcessEvent(e.clientX, e.clientY)
e.stopPropagation()
processEvent(e.clientX, e.clientY)
}
// Callback when on top of a component
const onDragOver = e => {
// New stuff
// // Skip if we aren't validly dragging currently
// if (!dragInfo) {
// return
// }
//
// e.preventDefault()
//
// // Set drag info for grids if not set
// if (!dragInfo.grid) {
// const coord = e.target.closest(".grid-coord")
// if (coord) {
// const row = parseInt(coord.dataset.row)
// const col = parseInt(coord.dataset.col)
// const component = $componentStore.selectedComponent
// const getStyle = x => parseInt(component._styles.normal?.[x] || "0")
// dragInfo.grid = {
// startRow: row,
// startCol: col,
// rowDeltaMin: 1 - getStyle("grid-row-start"),
// rowDeltaMax: 13 - getStyle("grid-row-end"),
// colDeltaMin: 1 - getStyle("grid-column-start"),
// colDeltaMax: 13 - getStyle("grid-column-end"),
// }
// }
// }
//
// if (!dropInfo) {
// return
// }
//
// const { droppableInside, bounds } = dropInfo
// const { top, left, height, width } = bounds
// const mouseY = e.clientY
// const mouseX = e.clientX
// const snapFactor = droppableInside ? 0.33 : 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
// 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)
// }
//
// // When no edges match, drop inside if possible
// if (!sides.length) {
// dropInfo.mode = droppableInside ? "inside" : null
// dropInfo.side = null
// return
// }
//
// // When one edge matches, use that edge
// if (sides.length === 1) {
// dropInfo.side = sides[0]
// if ([Sides.Top, Sides.Left].includes(sides[0])) {
// dropInfo.mode = "above"
// } else {
// dropInfo.mode = "below"
// }
// return
// }
//
// // 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)
// const edge = delta1 < delta2 ? sides[0] : sides[1]
// dropInfo.side = edge
// if ([Sides.Top, Sides.Left].includes(edge)) {
// dropInfo.mode = "above"
// } else {
// dropInfo.mode = "below"
// }
// Current stuff
if (!source || !target) {
if (!dragInfo?.grid || !insideGrid(e)) {
return
}
handleEvent(e)
}
// Callback when entering a potential drop target
const onDragEnter = e => {
// // New stuff
// const coord = e.target.closest(".grid-coord")
// if (coord && dragInfo.grid) {
// const row = parseInt(coord.dataset.row)
// const col = parseInt(coord.dataset.col)
// const { mode, side, grid } = dragInfo
// const {
// startRow,
// startCol,
// rowDeltaMin,
// rowDeltaMax,
// colDeltaMin,
// colDeltaMax,
// } = grid
//
// const component = $componentStore.selectedComponent
// const rowStart = parseInt(
// component._styles.normal?.["grid-row-start"] || 0
// )
// const rowEnd = parseInt(component._styles.normal?.["grid-row-end"] || 0)
// const colStart = parseInt(
// component._styles.normal?.["grid-column-start"] || 0
// )
// const colEnd = parseInt(
// component._styles.normal?.["grid-column-end"] || 0
// )
//
// let rowDelta = row - startRow
// let colDelta = col - startCol
//
// if (mode === "move") {
// rowDelta = Math.min(Math.max(rowDelta, rowDeltaMin), rowDeltaMax)
// colDelta = Math.min(Math.max(colDelta, colDeltaMin), colDeltaMax)
// builderStore.actions.setGridStyles({
// "grid-row-start": rowStart + rowDelta,
// "grid-row-end": rowEnd + rowDelta,
// "grid-column-start": colStart + colDelta,
// "grid-column-end": colEnd + colDelta,
// })
// } else if (mode === "resize") {
// let newStyles = {}
// if (side === "right") {
// newStyles["grid-column-end"] = colEnd + colDelta
// } else if (side === "left") {
// newStyles["grid-column-start"] = colStart + colDelta
// } else if (side === "top") {
// newStyles["grid-row-start"] = rowStart + rowDelta
// } else if (side === "bottom") {
// newStyles["grid-row-end"] = rowEnd + rowDelta
// } else if (side === "bottom-right") {
// newStyles["grid-column-end"] = colEnd + colDelta
// newStyles["grid-row-end"] = rowEnd + rowDelta
// } else if (side === "bottom-left") {
// newStyles["grid-column-start"] = colStart + colDelta
// newStyles["grid-row-end"] = rowEnd + rowDelta
// } else if (side === "top-right") {
// newStyles["grid-column-end"] = colEnd + colDelta
// newStyles["grid-row-start"] = rowStart + rowDelta
// } else if (side === "top-left") {
// newStyles["grid-column-start"] = colStart + colDelta
// newStyles["grid-row-start"] = rowStart + rowDelta
// }
// builderStore.actions.setGridStyles(newStyles)
// }
// }
// return
//
// const element = e.target.closest(".component:not(.block)")
// if (
// element &&
// element.classList.contains("droppable") &&
// element.dataset.id !== dragInfo.target
// ) {
// // Do nothing if this is the same target
// if (element.dataset.id === dropInfo?.target) {
// return
// }
//
// // Ensure the dragging flag is always set.
// // There's a bit of a race condition between the app reinitialisation
// // after selecting the DND component and setting this the first time
// if (!get(builderStore).isDragging) {
// builderStore.actions.setDragging(true)
// }
//
// // Store target ID
// const target = element.dataset.id
//
// // Precompute and store some info to avoid recalculating everything in
// // dragOver
// const child = getDOMNodeForComponent(e.target)
// const bounds = child.getBoundingClientRect()
// dropInfo = {
// target,
// name: element.dataset.name,
// icon: element.dataset.icon,
// droppableInside: element.classList.contains("empty"),
// bounds,
// }
// } else {
// dropInfo = null
// }
// Current stuff
if (!source) {
return
}
// Find the next valid component to consider dropping over, ignoring nested
// block components
const component = e.target?.closest?.(
`.component:not(.block):not(.${source.id})`
)
if (component && component.classList.contains("droppable")) {
dndStore.actions.updateTarget({
id: component.dataset.id,
parent: component.dataset.parent,
node: getDOMNode(component.dataset.id),
empty: component.classList.contains("empty"),
acceptsChildren: component.classList.contains("parent"),
})
handleEvent(e)
}
}
// Callback when dropping a drag on top of some component
const onDrop = () => {
if (!source || !drop?.parent || drop?.index == null) {
return
}
// Check if we're adding a new component rather than moving one
if (source.newComponentType) {
builderStore.actions.dropNewComponent(
source.newComponentType,
drop.parent,
drop.index
)
return
}
// Convert parent + index into target + mode
let legacyDropTarget, legacyDropMode
const parent = findComponentById(
get(screenStore).activeScreen?.props,
drop.parent
)
if (!parent) {
return
}
// Do nothing if we didn't change the location
if (source.parent === drop.parent && source.index === drop.index) {
return
}
// Filter out source component and placeholder from consideration
const children = parent._children?.filter(
x => x._id !== DNDPlaceholderID && x._id !== source.id
)
// Use inside if no existing children
if (!children?.length) {
legacyDropTarget = parent._id
legacyDropMode = "inside"
} else if (drop.index === 0) {
legacyDropTarget = children[0]?._id
legacyDropMode = "above"
} else {
legacyDropTarget = children[drop.index - 1]?._id
legacyDropMode = "below"
}
if (legacyDropTarget && legacyDropMode) {
builderStore.actions.moveComponent(
source.id,
legacyDropTarget,
legacyDropMode
)
}
}
onMount(() => {
// Events fired on the draggable target
document.addEventListener("dragstart", onDragStart, false)
// Events fired on the drop targets
document.addEventListener("dragover", onDragOver, false)
document.addEventListener("dragenter", onDragEnter, false)
document.addEventListener("drop", onDrop, false)
document.addEventListener("dragover", onDragOver, false)
})
onDestroy(() => {
// Events fired on the draggable target
document.removeEventListener("dragstart", onDragStart, false)
// Events fired on the drop targets
document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("dragenter", onDragEnter, false)
document.removeEventListener("drop", onDrop, false)
document.removeEventListener("dragover", onDragOver, false)
})
</script>
<IndicatorSet
componentId={$dndParent}
color="var(--spectrum-global-color-static-green-500)"
zIndex="930"
transition
prefix="Inside"
/>
{#if $dndIsDragging}
<DNDPlaceholderOverlay />
{/if}

View File

@ -1,7 +1,7 @@
<script>
import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, dndIsDragging } from "stores"
import { builderStore } from "stores"
let componentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
@ -30,7 +30,7 @@
</script>
<IndicatorSet
componentId={$dndIsDragging ? null : componentId}
componentId={$builderStore.dragging ? null : componentId}
color="var(--spectrum-global-color-static-blue-200)"
transition
{zIndex}

View File

@ -10,10 +10,22 @@
export let icon
export let color
export let zIndex
export let componentId
export let transition = false
export let line = false
export let alignRight = false
export let componentId
export let showResizeAnchors = false
const AnchorSides = [
"right",
"left",
"top",
"bottom",
"bottom-right",
"bottom-left",
"top-right",
"top-left",
]
$: flipped = top < 24
</script>
@ -41,54 +53,16 @@
{/if}
</div>
{/if}
<div
draggable={true}
class="anchor right"
data-side="right"
data-id={componentId}
/>
<div
draggable={true}
class="anchor top"
data-side="top"
data-id={componentId}
/>
<div
draggable={true}
class="anchor left"
data-side="left"
data-id={componentId}
/>
<div
draggable={true}
class="anchor bottom"
data-side="bottom"
data-id={componentId}
/>
<div
draggable={true}
class="anchor bottom-right"
data-side="bottom-right"
data-id={componentId}
/>
<div
draggable={true}
class="anchor bottom-left"
data-side="bottom-left"
data-id={componentId}
/>
<div
draggable={true}
class="anchor top-right"
data-side="top-right"
data-id={componentId}
/>
<div
draggable={true}
class="anchor top-left"
data-side="top-left"
data-id={componentId}
/>
{#if showResizeAnchors}
{#each AnchorSides as side}
<div
draggable="true"
class="anchor {side}"
data-side={side}
data-id={componentId}
/>
{/each}
{/if}
</div>
<style>

View File

@ -9,11 +9,13 @@
export let transition
export let zIndex
export let prefix = null
export let allowResizeAnchors = false
let indicators = []
let interval
let text
let icon
let insideGrid = false
$: visibleIndicators = indicators.filter(x => x.visible)
$: offset = $builderStore.inBuilder ? 0 : 2
@ -23,6 +25,19 @@
let callbackCount = 0
let nextIndicators = []
const checkInsideGrid = id => {
const component = document.getElementsByClassName(id)[0]
const domNode = component?.children[0]
// Ignore grid itself
if (domNode?.classList.contains("grid")) {
return false
}
// Check if we're a descendent of a grid
return domNode?.closest(".grid") != null
}
const createIntersectionCallback = idx => entries => {
if (callbackCount >= observers.length) {
return
@ -52,6 +67,11 @@
observers = []
nextIndicators = []
// Check if we're inside a grid
if (allowResizeAnchors) {
insideGrid = checkInsideGrid(componentId)
}
// Determine next set of indicators
const parents = document.getElementsByClassName(componentId)
if (parents.length) {
@ -127,6 +147,7 @@
height={indicator.height}
text={idx === 0 ? text : null}
icon={idx === 0 ? icon : null}
showResizeAnchors={allowResizeAnchors && insideGrid}
{componentId}
{transition}
{zIndex}

View File

@ -1,5 +1,5 @@
<script>
import { builderStore, dndIsDragging } from "stores"
import { builderStore } from "stores"
import IndicatorSet from "./IndicatorSet.svelte"
$: color = $builderStore.editMode
@ -8,8 +8,9 @@
</script>
<IndicatorSet
componentId={$dndIsDragging ? null : $builderStore.selectedComponentId}
componentId={$builderStore.selectedComponentId}
{color}
zIndex="910"
transition
allowResizeAnchors
/>

View File

@ -3,7 +3,7 @@
import SettingsButton from "./SettingsButton.svelte"
import SettingsColorPicker from "./SettingsColorPicker.svelte"
import SettingsPicker from "./SettingsPicker.svelte"
import { builderStore, componentStore, dndIsDragging } from "stores"
import { builderStore, componentStore } from "stores"
import { domDebounce } from "utils/domDebounce"
const verticalOffset = 36
@ -16,7 +16,7 @@
let measured = false
$: definition = $componentStore.selectedComponentDefinition
$: showBar = definition?.showSettingsBar && !$dndIsDragging
$: showBar = definition?.showSettingsBar && !$builderStore.dragging
$: settings = getBarSettings(definition)
const getBarSettings = definition => {

View File

@ -41,8 +41,6 @@ const loadBudibase = async () => {
previewDevice: window["##BUDIBASE_PREVIEW_DEVICE##"],
navigation: window["##BUDIBASE_PREVIEW_NAVIGATION##"],
hiddenComponentIds: window["##BUDIBASE_HIDDEN_COMPONENT_IDS##"],
gridStyles: get(builderStore).gridStyles,
isDragging: get(builderStore).isDragging,
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
location: window["##BUDIBASE_LOCATION##"],
})

View File

@ -19,6 +19,7 @@ const createBuilderStore = () => {
navigation: null,
hiddenComponentIds: [],
usedPlugins: null,
dragging: false,
// Legacy - allow the builder to specify a layout
layout: null,
@ -111,6 +112,12 @@ const createBuilderStore = () => {
// Notify the builder so we can reload component definitions
dispatchEvent("reload-plugin")
},
setDragging: dragging => {
store.update(state => {
state.dragging = dragging
return state
})
},
}
return {
...store,

View File

@ -10,9 +10,6 @@ const createDndStore = () => {
// Info about where the component would be dropped
drop: null,
// Grid info
gridStyles: null,
}
const store = writable(initialState)
@ -62,13 +59,6 @@ const createDndStore = () => {
store.set(initialState)
}
const setGridStyles = styles => {
store.update(state => {
state.gridStyles = styles
return state
})
}
return {
subscribe: store.subscribe,
actions: {
@ -77,7 +67,6 @@ const createDndStore = () => {
updateTarget,
updateDrop,
reset,
setGridStyles,
},
}
}
@ -88,7 +77,6 @@ export const dndStore = createDndStore()
// performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change.
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
export const dndBounds = derived(

View File

@ -21,7 +21,6 @@ export {
dndParent,
dndBounds,
dndIsNewComponent,
dndIsDragging,
} from "./dnd"
// Context stores are layered and duplicated, so it is not a singleton