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

View File

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

View File

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

View File

@ -1,27 +1,17 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { get } from "svelte/store" import { builderStore, componentStore } from "stores"
import IndicatorSet from "./IndicatorSet.svelte"
import {
builderStore,
screenStore,
dndStore,
dndParent,
dndIsDragging,
} from "stores"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core" 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 $: dragNode = getDOMNode(dragInfo?.id)
// performance. It lets us avoid calling svelte getters on every DOM action. $: applyStyles(dragNode, gridStyles)
$: source = $dndStore.source
$: target = $dndStore.target const insideGrid = e => {
$: drop = $dndStore.drop return e.target?.closest?.(".grid") || e.target.classList.contains("anchor")
}
// Util to get the inner DOM node by a component ID // Util to get the inner DOM node by a component ID
const getDOMNode = id => { const getDOMNode = id => {
@ -29,535 +19,195 @@
return [...component.children][0] return [...component.children][0]
} }
// Util to calculate the variance of a set of data const applyStyles = (dragNode, gridStyles) => {
const variance = arr => { if (!dragNode || !gridStyles) {
const mean = arr.reduce((a, b) => a + b, 0) / arr.length return
let squareSum = 0 }
arr.forEach(value => { Object.entries(gridStyles).forEach(([style, value]) => {
const delta = value - mean dragNode.style[style] = value
squareSum += delta * delta
}) })
return squareSum / arr.length
} }
// Callback when drag stops (whether dropped or not) // Callback when drag stops (whether dropped or not)
const stopDragging = () => { const stopDragging = () => {
// Reset listener // Save changes
if (source?.id) { if (gridStyles) {
const component = document.getElementsByClassName(source?.id)[0] builderStore.actions.updateStyles(gridStyles)
if (component) {
component.removeEventListener("dragend", stopDragging)
} }
// Reset listener
if (dragInfo?.domTarget) {
dragInfo.domTarget.removeEventListener("dragend", stopDragging)
} }
// Reset state // Reset state
dndStore.actions.reset() dragInfo = null
} gridStyles = null
builderStore.actions.setDragging(false)
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)
} }
// 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 => {
// New stuff if (!insideGrid(e)) {
// 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")) {
return return
} }
// Hide drag ghost image // Hide drag ghost image
e.dataTransfer.setDragImage(new Image(), 0, 0) e.dataTransfer.setDragImage(new Image(), 0, 0)
// Add event handler to clear all drag state when dragging ends // Extract state
component.addEventListener("dragend", stopDragging) 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
}
// // Find grid parent
const domComponent = getDOMNode(id)
const gridId = domComponent?.closest(".grid")?.parentNode.dataset.id
if (!gridId) {
return
}
// Update state // Update state
const id = component.dataset.id dragInfo = {
const parentId = component.dataset.parent domTarget: e.target,
const parent = findComponentById(
get(screenStore).activeScreen?.props,
parentId
)
const index = parent._children.findIndex(
x => x._id === component.dataset.id
)
dndStore.actions.startDraggingExistingComponent({
id, id,
bounds: component.children[0].getBoundingClientRect(), gridId,
parent: parentId, mode,
index, side,
})
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)
} }
// builderStore.actions.selectComponent(dragInfo.id)
// builderStore.actions.setDragging(true)
// Core logic for handling drop events and determining where to render the // Add event handler to clear all drag state when dragging ends
// drop target placeholder dragInfo.domTarget.addEventListener("dragend", stopDragging)
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)
}
// 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,
})
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
}
// 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,
})
}
const throttledProcessEvent = Utils.throttle(processEvent, ThrottleRate)
const handleEvent = e => {
e.preventDefault()
throttledProcessEvent(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) {
return
}
handleEvent(e)
} }
// Callback when entering a potential drop target // Callback when entering a potential drop target
const onDragEnter = e => { const onDragEnter = e => {
// // New stuff // Skip if we aren't validly dragging currently
// const coord = e.target.closest(".grid-coord") if (!dragInfo || dragInfo.grid || !insideGrid(e)) {
// 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 return
} }
// Find the next valid component to consider dropping over, ignoring nested const compDef = $componentStore.selectedComponent
// block components const domGrid = getDOMNode(dragInfo.gridId)
const component = e.target?.closest?.( if (domGrid) {
`.component:not(.block):not(.${source.id})` const getStyle = x => parseInt(compDef._styles.normal?.[x] || "0")
) dragInfo.grid = {
if (component && component.classList.contains("droppable")) { startX: e.clientX,
dndStore.actions.updateTarget({ startY: e.clientY,
id: component.dataset.id, rowStart: getStyle("grid-row-start"),
parent: component.dataset.parent, rowEnd: getStyle("grid-row-end"),
node: getDOMNode(component.dataset.id), colStart: getStyle("grid-column-start"),
empty: component.classList.contains("empty"), colEnd: getStyle("grid-column-end"),
acceptsChildren: component.classList.contains("parent"), 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) handleEvent(e)
} }
} }
// Callback when dropping a drag on top of some component const processEvent = Utils.throttle((mouseX, mouseY) => {
const onDrop = () => { if (!dragInfo?.grid) {
if (!source || !drop?.parent || drop?.index == null) {
return return
} }
// Check if we're adding a new component rather than moving one const { mode, side, gridId, grid } = dragInfo
if (source.newComponentType) { const {
builderStore.actions.dropNewComponent( startX,
source.newComponentType, startY,
drop.parent, rowStart,
drop.index 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()
e.stopPropagation()
processEvent(e.clientX, e.clientY)
}
const onDragOver = e => {
if (!dragInfo?.grid || !insideGrid(e)) {
return return
} }
handleEvent(e)
// 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(() => { onMount(() => {
// Events fired on the draggable target
document.addEventListener("dragstart", onDragStart, false) document.addEventListener("dragstart", onDragStart, false)
// Events fired on the drop targets
document.addEventListener("dragover", onDragOver, false)
document.addEventListener("dragenter", onDragEnter, false) document.addEventListener("dragenter", onDragEnter, false)
document.addEventListener("drop", onDrop, false) document.addEventListener("dragover", onDragOver, false)
}) })
onDestroy(() => { onDestroy(() => {
// Events fired on the draggable target
document.removeEventListener("dragstart", onDragStart, false) document.removeEventListener("dragstart", onDragStart, false)
// Events fired on the drop targets
document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("dragenter", onDragEnter, false) document.removeEventListener("dragenter", onDragEnter, false)
document.removeEventListener("drop", onDrop, false) document.removeEventListener("dragover", onDragOver, false)
}) })
</script> </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> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, dndIsDragging } from "stores" import { builderStore } from "stores"
let componentId let componentId
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920 $: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
@ -30,7 +30,7 @@
</script> </script>
<IndicatorSet <IndicatorSet
componentId={$dndIsDragging ? null : componentId} componentId={$builderStore.dragging ? null : componentId}
color="var(--spectrum-global-color-static-blue-200)" color="var(--spectrum-global-color-static-blue-200)"
transition transition
{zIndex} {zIndex}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ const createBuilderStore = () => {
navigation: null, navigation: null,
hiddenComponentIds: [], hiddenComponentIds: [],
usedPlugins: null, usedPlugins: null,
dragging: false,
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
layout: null, layout: null,
@ -111,6 +112,12 @@ 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")
}, },
setDragging: dragging => {
store.update(state => {
state.dragging = dragging
return state
})
},
} }
return { return {
...store, ...store,

View File

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

View File

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