Rework grid layouts to automatically grow as required
This commit is contained in:
parent
2c9b8ac941
commit
aaa33acc1c
|
@ -1,50 +1,136 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { getContext, onMount } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { GridRowHeight, GridColumns } from "constants"
|
||||
|
||||
const component = getContext("component")
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
|
||||
const cols = 12
|
||||
const rowHeight = 24
|
||||
|
||||
let width
|
||||
let height
|
||||
let ref
|
||||
let rows = 1
|
||||
let children = writable({})
|
||||
let mounted = false
|
||||
|
||||
$: rows = calculateRequiredRows($children, mobile)
|
||||
$: mobile = $context.device.mobile
|
||||
$: empty = $component.empty
|
||||
$: rows = Math.max(1, Math.floor(height / rowHeight))
|
||||
$: colSize = width / cols
|
||||
$: rowSize = height / rows
|
||||
$: colSize = width / GridColumns
|
||||
$: height = rows * GridRowHeight
|
||||
|
||||
// Calculates the minimum number of rows required to render all child
|
||||
// components, on a certain device type
|
||||
const calculateRequiredRows = (children, mobile) => {
|
||||
const key = mobile ? "mobileRowEnd" : "desktopRowEnd"
|
||||
let max = 2
|
||||
for (let id of Object.keys(children)) {
|
||||
if (children[id][key] > max) {
|
||||
max = children[id][key]
|
||||
}
|
||||
}
|
||||
return max - 1
|
||||
}
|
||||
|
||||
// Stores metadata about a child node as constraints for determining grid size
|
||||
const storeChild = node => {
|
||||
children.update(state => ({
|
||||
...state,
|
||||
[node.dataset.id]: {
|
||||
desktopRowEnd: parseInt(node.dataset.gridDesktopRowEnd),
|
||||
mobileRowEnd: parseInt(node.dataset.gridMobileRowEnd),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// Removes constraint metadata for a certain child node
|
||||
const removeChild = node => {
|
||||
children.update(state => {
|
||||
delete state[node.dataset.id]
|
||||
return { ...state }
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let observer
|
||||
if ($builderStore.inBuilder) {
|
||||
// Set up an observer to watch for changes in metadata attributes of child
|
||||
// components, as well as child addition and deletion
|
||||
observer = new MutationObserver(mutations => {
|
||||
for (let mutation of mutations) {
|
||||
const { target, type, addedNodes, removedNodes } = mutation
|
||||
if (target === ref) {
|
||||
if (addedNodes[0]?.classList?.contains("component")) {
|
||||
// We've added a new child component inside the grid, so we need
|
||||
// to consider it when determining required rows
|
||||
storeChild(addedNodes[0])
|
||||
} else if (removedNodes[0]?.classList?.contains("component")) {
|
||||
// We've removed a child component inside the grid, so we need
|
||||
// to stop considering it when determining required rows
|
||||
removeChild(removedNodes[0])
|
||||
}
|
||||
} else if (
|
||||
type === "attributes" &&
|
||||
target.parentNode === ref &&
|
||||
target.classList.contains("component")
|
||||
) {
|
||||
// We've updated the size or position of a child
|
||||
storeChild(target)
|
||||
}
|
||||
}
|
||||
})
|
||||
observer.observe(ref, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: [
|
||||
"data-grid-desktop-row-end",
|
||||
"data-grid-mobile-row-end",
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
// Now that the observer is set up, we mark the grid as mounted to mount
|
||||
// our child components
|
||||
mounted = true
|
||||
|
||||
// Cleanup our observer
|
||||
return () => {
|
||||
observer?.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class="grid"
|
||||
class:mobile
|
||||
bind:clientWidth={width}
|
||||
bind:clientHeight={height}
|
||||
use:styleable={{
|
||||
...$component.styles,
|
||||
normal: {
|
||||
...$component.styles?.normal,
|
||||
"--cols": cols,
|
||||
"--height": `${height}px`,
|
||||
"--cols": GridColumns,
|
||||
"--rows": rows,
|
||||
"--col-size": colSize,
|
||||
"--row-size": rowSize,
|
||||
"--row-size": GridRowHeight,
|
||||
},
|
||||
empty: false,
|
||||
}}
|
||||
data-rows={rows}
|
||||
data-cols={cols}
|
||||
data-cols={GridColumns}
|
||||
data-col-size={colSize}
|
||||
>
|
||||
<div class="underlay">
|
||||
{#each { length: cols * rows } as _}
|
||||
<div class="placeholder" />
|
||||
{/each}
|
||||
</div>
|
||||
{#if $builderStore.inBuilder}
|
||||
<div class="underlay">
|
||||
{#each { length: GridColumns * rows } as _, idx}
|
||||
<div class="placeholder" class:first-col={idx % GridColumns === 0} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Only render the slot if not empty, as we don't want the placeholder -->
|
||||
{#if !empty}
|
||||
{#if !empty && mounted}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -52,7 +138,6 @@
|
|||
<style>
|
||||
.grid {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
|
||||
/*
|
||||
Prevent cross-grid variable inheritance. The other variables for alignment
|
||||
|
@ -71,8 +156,11 @@
|
|||
|
||||
.grid,
|
||||
.underlay {
|
||||
height: var(--height) !important;
|
||||
min-height: none !important;
|
||||
max-height: none !important;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(var(--rows), 1fr);
|
||||
grid-template-rows: repeat(var(--rows), calc(var(--row-size) * 1px));
|
||||
grid-template-columns: repeat(var(--cols), 1fr);
|
||||
gap: 0;
|
||||
}
|
||||
|
@ -84,7 +172,6 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: 1px solid var(--spectrum-global-color-gray-900);
|
||||
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -95,6 +182,9 @@
|
|||
border-bottom: 1px solid var(--spectrum-global-color-gray-900);
|
||||
border-right: 1px solid var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.placeholder.first-col {
|
||||
border-left: 1px solid var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
|
||||
/* Highlight grid lines when resizing children */
|
||||
:global(.grid.highlight > .underlay) {
|
||||
|
@ -131,20 +221,10 @@
|
|||
)
|
||||
)
|
||||
);
|
||||
|
||||
/* Row end is always provided by the gridLayout action */
|
||||
--row-start: var(--grid-desktop-row-start, var(--grid-mobile-row-start, 1));
|
||||
--row-end: var(
|
||||
--grid-desktop-row-end,
|
||||
var(
|
||||
--grid-mobile-row-end,
|
||||
round(
|
||||
up,
|
||||
calc(
|
||||
(var(--grid-spacing) * 2 + var(--default-height)) / var(--row-size) +
|
||||
1
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
--row-end: var(--grid-desktop-row-end, var(--grid-mobile-row-end));
|
||||
|
||||
/* Flex vars */
|
||||
--h-align: var(--grid-desktop-h-align, var(--grid-mobile-h-align));
|
||||
|
@ -156,8 +236,8 @@
|
|||
max(2, var(--col-end)),
|
||||
calc(var(--cols) + 1)
|
||||
) !important;
|
||||
grid-row-start: min(max(1, var(--row-start)), var(--rows)) !important;
|
||||
grid-row-end: min(max(2, var(--row-end)), calc(var(--rows) + 1)) !important;
|
||||
grid-row-start: max(1, var(--row-start)) !important;
|
||||
grid-row-end: max(2, var(--row-end)) !important;
|
||||
|
||||
/* Flex container styles */
|
||||
flex-direction: column;
|
||||
|
@ -182,18 +262,7 @@
|
|||
)
|
||||
);
|
||||
--row-start: var(--grid-mobile-row-start, var(--grid-desktop-row-start, 1));
|
||||
--row-end: var(
|
||||
--grid-mobile-row-end,
|
||||
var(
|
||||
--grid-desktop-row-end,
|
||||
round(
|
||||
up,
|
||||
calc(
|
||||
(var(--spacing) * 2 + var(--default-height)) / var(--row-size) + 1
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
--row-end: var(--grid-mobile-row-end, var(--grid-desktop-row-end));
|
||||
|
||||
/* Flex vars */
|
||||
--h-align: var(--grid-mobile-h-align, var(--grid-desktop-h-align));
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { onMount, onDestroy } from "svelte"
|
||||
import { builderStore, componentStore } from "stores"
|
||||
import { Utils, memo } from "@budibase/frontend-core"
|
||||
import { GridRowHeight } from "constants"
|
||||
import {
|
||||
isGridEvent,
|
||||
getGridParent,
|
||||
|
@ -15,8 +16,8 @@
|
|||
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
|
||||
|
||||
let dragInfo
|
||||
let styles = memo()
|
||||
let gridStyles = memo()
|
||||
let id
|
||||
|
||||
// Grid CSS variables
|
||||
$: device = $builderStore.previewDevice
|
||||
|
@ -28,12 +29,14 @@
|
|||
}
|
||||
|
||||
// Some memoisation of primitive types for performance
|
||||
$: id = dragInfo?.id || id
|
||||
$: id = dragInfo?.id
|
||||
$: gridId = dragInfo?.gridId
|
||||
|
||||
// Set ephemeral grid styles on the dragged component
|
||||
// Set ephemeral styles
|
||||
$: instance = componentStore.actions.getComponentInstance(id)
|
||||
$: componentStyles = getComponentStyles($gridStyles)
|
||||
$: $instance?.setEphemeralStyles(componentStyles)
|
||||
$: gridInstance = componentStore.actions.getComponentInstance(gridId)
|
||||
$: $instance?.setEphemeralStyles(enrichComponentStyles($styles))
|
||||
$: $gridInstance?.setEphemeralStyles($gridStyles)
|
||||
|
||||
// Sugar for a combination of both min and max
|
||||
const minMax = (value, min, max) => Math.min(max, Math.max(min, value))
|
||||
|
@ -44,47 +47,41 @@
|
|||
return Array.from(component?.children || [])[0]
|
||||
}
|
||||
|
||||
const getComponentStyles = gridStyles => {
|
||||
let styles = { ...gridStyles }
|
||||
if (gridStyles) {
|
||||
styles["z-index"] = 999
|
||||
styles["pointer-events"] = "none"
|
||||
const enrichComponentStyles = styles => {
|
||||
let clone = { ...styles }
|
||||
if (styles) {
|
||||
clone["z-index"] = 999
|
||||
clone["pointer-events"] = "none"
|
||||
}
|
||||
return styles
|
||||
return clone
|
||||
}
|
||||
|
||||
const processEvent = Utils.throttle((mouseX, mouseY) => {
|
||||
if (!dragInfo?.grid) {
|
||||
return
|
||||
}
|
||||
|
||||
const { mode, side, gridId, grid } = dragInfo
|
||||
const { startX, startY, rowStart, rowEnd, colStart, colEnd } = grid
|
||||
|
||||
const domGrid = getDOMNode(gridId)
|
||||
if (!domGrid) {
|
||||
return
|
||||
}
|
||||
const cols = parseInt(domGrid.dataset.cols)
|
||||
const rows = parseInt(domGrid.dataset.rows)
|
||||
const { width, height } = domGrid.getBoundingClientRect()
|
||||
|
||||
const colWidth = width / cols
|
||||
const colSize = parseInt(domGrid.dataset.colSize)
|
||||
const diffX = mouseX - startX
|
||||
let deltaX = Math.round(diffX / colWidth)
|
||||
const rowHeight = height / rows
|
||||
let deltaX = Math.round(diffX / colSize)
|
||||
const diffY = mouseY - startY
|
||||
let deltaY = Math.round(diffY / rowHeight)
|
||||
let deltaY = Math.round(diffY / GridRowHeight)
|
||||
if (mode === "move") {
|
||||
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
|
||||
deltaY = minMax(deltaY, 1 - rowStart, rows + 1 - rowEnd)
|
||||
deltaY = Math.max(deltaY, 1 - rowStart)
|
||||
const newStyles = {
|
||||
[vars.colStart]: colStart + deltaX,
|
||||
[vars.colEnd]: colEnd + deltaX,
|
||||
[vars.rowStart]: rowStart + deltaY,
|
||||
[vars.rowEnd]: rowEnd + deltaY,
|
||||
}
|
||||
gridStyles.set(newStyles)
|
||||
styles.set(newStyles)
|
||||
} else if (mode === "resize") {
|
||||
let newStyles = {}
|
||||
if (side === "right") {
|
||||
|
@ -108,7 +105,7 @@
|
|||
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
|
||||
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
|
||||
}
|
||||
gridStyles.set(newStyles)
|
||||
styles.set(newStyles)
|
||||
}
|
||||
}, 10)
|
||||
|
||||
|
@ -180,15 +177,14 @@
|
|||
return
|
||||
}
|
||||
const gridCols = parseInt(domGrid.dataset.cols)
|
||||
const gridRows = parseInt(domGrid.dataset.rows)
|
||||
const styles = getComputedStyle(domComponent.parentNode)
|
||||
dragInfo.grid = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
|
||||
// Ensure things are within limits
|
||||
rowStart: minMax(styles["grid-row-start"], 1, gridRows),
|
||||
rowEnd: minMax(styles["grid-row-end"], 2, gridRows + 1),
|
||||
rowStart: Math.max(styles["grid-row-start"], 1),
|
||||
rowEnd: Math.max(styles["grid-row-end"], 2),
|
||||
colStart: minMax(styles["grid-column-start"], 1, gridCols),
|
||||
colEnd: minMax(styles["grid-column-end"], 2, gridCols + 1),
|
||||
}
|
||||
|
@ -210,8 +206,11 @@
|
|||
const { id, gridId, domTarget } = dragInfo
|
||||
|
||||
// Save changes
|
||||
if ($styles) {
|
||||
await builderStore.actions.updateStyles($styles, id)
|
||||
}
|
||||
if ($gridStyles) {
|
||||
await builderStore.actions.updateStyles($gridStyles, id)
|
||||
await builderStore.actions.updateStyles($gridStyles, gridId)
|
||||
}
|
||||
|
||||
// Reset DOM
|
||||
|
@ -227,9 +226,22 @@
|
|||
|
||||
// Reset state
|
||||
dragInfo = null
|
||||
styles.set(null)
|
||||
gridStyles.set(null)
|
||||
}
|
||||
|
||||
const calculateRequiredRows = () => {
|
||||
let required = 1
|
||||
const children = document.querySelectorAll(`.${gridId}-dom > .component`)
|
||||
for (let child of children) {
|
||||
const rowEnd = child.dataset.grid_desktop_col_end
|
||||
if (rowEnd > required) {
|
||||
required = rowEnd
|
||||
}
|
||||
}
|
||||
return required - 1
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("dragstart", onDragStart, false)
|
||||
document.addEventListener("dragenter", onDragEnter, false)
|
||||
|
|
|
@ -15,3 +15,6 @@ export const ActionTypes = {
|
|||
|
||||
export const DNDPlaceholderID = "dnd-placeholder"
|
||||
export const ScreenslotType = "screenslot"
|
||||
export const GridRowHeight = 24
|
||||
export const GridColumns = 12
|
||||
export const GridSpacing = 4
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { GridSpacing } from "constants"
|
||||
import { GridRowHeight } from "constants"
|
||||
import { builderStore } from "stores"
|
||||
import { buildStyleString } from "utils/styleable.js"
|
||||
|
||||
|
@ -84,15 +86,23 @@ export const gridLayout = (node, metadata) => {
|
|||
}
|
||||
|
||||
// Generate base set of grid CSS vars based for this component
|
||||
let width = errored ? 500 : definition.size?.width || 200
|
||||
let height = errored ? 60 : definition.size?.height || 200
|
||||
width += 2 * GridSpacing
|
||||
height += 2 * GridSpacing
|
||||
const hAlign = errored ? "stretch" : definition?.grid?.hAlign || "stretch"
|
||||
const vAlign = errored ? "stretch" : definition?.grid?.vAlign || "center"
|
||||
const vars = {
|
||||
"--default-width": errored ? 500 : definition.size?.width || 200,
|
||||
"--default-height": errored ? 60 : definition.size?.height || 200,
|
||||
"--default-width": width,
|
||||
"--default-height": height,
|
||||
"--grid-desktop-h-align": hAlign,
|
||||
"--grid-mobile-h-align": hAlign,
|
||||
"--grid-desktop-v-align": vAlign,
|
||||
"--grid-mobile-v-align": vAlign,
|
||||
|
||||
// Variables for automatically determining grid height
|
||||
"--grid-desktop-row-end": Math.ceil(height / GridRowHeight) + 1,
|
||||
"--grid-mobile-row-end": Math.ceil(height / GridRowHeight) + 1,
|
||||
}
|
||||
|
||||
// Extract any other CSS variables from the saved component styles
|
||||
|
@ -103,6 +113,16 @@ export const gridLayout = (node, metadata) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply some metadata to data attributes to speed up lookups
|
||||
const desktopRowEnd = `${vars["--grid-desktop-row-end"]}`
|
||||
const mobileRowEnd = `${vars["--grid-mobile-row-end"]}`
|
||||
if (node.dataset.gridDesktopRowEnd !== desktopRowEnd) {
|
||||
node.dataset.gridDesktopRowEnd = desktopRowEnd
|
||||
}
|
||||
if (node.dataset.gridMobileRowEnd !== mobileRowEnd) {
|
||||
node.dataset.gridMobileRowEnd = mobileRowEnd
|
||||
}
|
||||
|
||||
// Apply all CSS variables to the wrapper
|
||||
node.style = buildStyleString(vars)
|
||||
|
||||
|
|
Loading…
Reference in New Issue