Update grid layout to include nested flex wrappers for more layout control

This commit is contained in:
Andrew Kingston 2024-07-30 15:20:59 +01:00
parent 0d6e7bd5d3
commit 29ddeab0d4
No known key found for this signature in database
9 changed files with 99 additions and 63 deletions

View File

@ -132,8 +132,8 @@
hoverStore.hover(data.id, false)
} else if (type === "update-prop") {
await componentStore.updateSetting(data.prop, data.value)
} else if (type === "update-styles") {
await componentStore.updateStyles(data.styles, data.id)
} else if (type === "update-meta-styles") {
await componentStore.updateMetaStyles(data.styles, data.id)
} else if (type === "delete-component" && data.id) {
// Legacy type, can be deleted in future
confirmDeleteComponent(data.id)

View File

@ -64,6 +64,7 @@ export class ComponentStore extends BudiStore {
this.moveDown = this.moveDown.bind(this)
this.updateStyle = this.updateStyle.bind(this)
this.updateStyles = this.updateStyles.bind(this)
this.updateMetaStyles = this.updateMetaStyles.bind(this)
this.updateCustomStyle = this.updateCustomStyle.bind(this)
this.updateConditions = this.updateConditions.bind(this)
this.requestEjectBlock = this.requestEjectBlock.bind(this)
@ -978,6 +979,16 @@ export class ComponentStore extends BudiStore {
await this.patch(patchFn, id)
}
async updateMetaStyles(styles, id) {
const patchFn = component => {
component._styles.meta = {
...component._styles.meta,
...styles,
}
}
await this.patch(patchFn, id)
}
async updateCustomStyle(style) {
await this.patch(component => {
component._styles.custom = style

View File

@ -39,6 +39,7 @@
getActionContextKey,
getActionDependentContextKeys,
} from "../utils/buttonActions.js"
import { buildStyleString } from "utils/styleable.js"
export let instance = {}
export let isLayout = false
@ -102,8 +103,8 @@
let settingsDefinitionMap
let missingRequiredSettings = false
// Temporary styles which can be added in the app preview for things like DND.
// We clear these whenever a new instance is received.
// Temporary meta styles which can be added in the app preview for things like
// DND. We clear these whenever a new instance is received.
let ephemeralStyles
// Single string of all HBS blocks, used to check if we use a certain binding
@ -196,16 +197,20 @@
$: currentTheme = $context?.device?.theme
$: darkMode = !currentTheme?.includes("light")
// Build meta styles and stringify to apply to the wrapper node
$: metaStyles = {
...instance._styles?.meta,
...ephemeralStyles,
}
$: metaCSS = buildStyleString(metaStyles)
// Update component context
$: store.set({
id,
children: children.length,
styles: {
...instance._styles,
normal: {
...instance._styles?.normal,
...ephemeralStyles,
},
meta: metaStyles,
custom: customCSS,
id,
empty: emptyState,
@ -605,6 +610,14 @@
}
}
const select = e => {
if (isBlock) {
return
}
e.stopPropagation()
builderStore.actions.selectComponent(id)
}
onMount(() => {
// Register this component instance for external access
if ($appStore.isDevApp) {
@ -635,6 +648,8 @@
{#if constructor && initialSettings && (visible || inSelectedPath) && !builderHidden}
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
<!-- and the performance matters for the selection indicators -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class={`component ${id}`}
class:draggable
@ -650,6 +665,9 @@
data-name={name}
data-icon={icon}
data-parent={$component.id}
style={metaCSS}
{draggable}
on:click={select}
>
{#if errorState}
<ComponentErrorState

View File

@ -12,7 +12,7 @@
export let wrap
export let onClick
$: directionClass = direction ? `valid-container direction-${direction}` : ""
$: directionClass = direction ? `flex-container direction-${direction}` : ""
$: hAlignClass = hAlign ? `hAlign-${hAlign}` : ""
$: vAlignClass = vAlign ? `vAlign-${vAlign}` : ""
$: sizeClass = size ? `size-${size}` : ""
@ -39,11 +39,11 @@
</div>
<style>
.valid-container {
.flex-container {
display: flex;
max-width: 100%;
}
.valid-container :global(.component > *) {
.flex-container :global(.component > *) {
max-width: 100%;
}
.direction-row {

View File

@ -8,10 +8,16 @@
export let cols = 12
export let rows = 12
let width
let height
$: cols = cols || 12
$: rows = rows || 12
$: coords = generateCoords(rows, cols)
$: mobile = $context.device.mobile
$: empty = $component.empty
$: colSize = width / cols
$: rowSize = height / rows
const generateCoords = (rows, cols) => {
let grid = []
@ -27,14 +33,18 @@
<div
class="grid"
class:mobile
bind:clientWidth={width}
bind:clientHeight={height}
use:styleable={{
...$component.styles,
normal: {
...$component.styles?.normal,
"--cols": cols,
"--rows": rows,
gap: "0 !important",
"--col-size": `${colSize}px`,
"--row-size": `${rowSize}px`,
},
empty: false,
}}
data-rows={rows}
data-cols={cols}
@ -46,55 +56,18 @@
{/each}
</div>
{/if}
<slot />
<!-- Only render the slot if not empty, as we don't want the placeholder -->
{#if !empty}
<slot />
{/if}
</div>
<style>
/*
Ensure all children of containers which are top level children of
grids do not overflow
*/
:global(.grid > .component > .valid-container > .component > *) {
max-height: 100%;
max-width: 100%;
}
/* Ensure all top level children have some grid styles set */
:global(.grid > .component > *) {
overflow: hidden;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
min-width: 0;
/* On desktop, use desktop metadata and fall back to mobile */
--col-start: var(--grid-desktop-col-start, var(--grid-mobile-col-start, 1));
--col-end: var(--grid-desktop-col-end, var(--grid-mobile-col-end, 2));
--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, 2));
/* Ensure grid metadata falls within limits */
grid-column-start: min(max(1, var(--col-start)), var(--cols)) !important;
grid-column-end: min(
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;
}
/* On mobile, use mobile metadata and fall back to desktop */
:global(.grid.mobile > .component > *) {
--col-start: var(--grid-mobile-col-start, var(--grid-desktop-col-start, 1));
--col-end: var(--grid-mobile-col-end, var(--grid-desktop-col-end, 2));
--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, 2));
}
.grid {
position: relative;
height: 400px;
gap: 0;
}
.grid,
.underlay {
@ -118,4 +91,36 @@
.placeholder {
background-color: var(--spectrum-global-color-gray-100);
}
/* Ensure all top level children have grid styles applied */
.grid :global(> .component) {
display: flex;
overflow: hidden;
flex-direction: column;
justify-content: center;
align-items: stretch;
/* On desktop, use desktop metadata and fall back to mobile */
--col-start: var(--grid-desktop-col-start, var(--grid-mobile-col-start, 1));
--col-end: var(--grid-desktop-col-end, var(--grid-mobile-col-end, 2));
--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, 2));
/* Ensure grid metadata falls within limits */
grid-column-start: min(max(1, var(--col-start)), var(--cols)) !important;
grid-column-end: min(
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;
}
/* On mobile, use mobile metadata and fall back to desktop */
.grid.mobile :global(> .component) {
--col-start: var(--grid-mobile-col-start, var(--grid-desktop-col-start, 1));
--col-end: var(--grid-mobile-col-end, var(--grid-desktop-col-end, 2));
--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, 2));
}
</style>

View File

@ -200,7 +200,7 @@
const domGrid = getDOMNode(dragInfo.gridId)
const gridCols = parseInt(domGrid.dataset.cols)
const gridRows = parseInt(domGrid.dataset.rows)
const domNode = getDOMNode(dragInfo.id)
const domNode = getDOMNode(dragInfo.id)?.parentNode
const styles = window.getComputedStyle(domNode)
if (domGrid) {
// Util to get the current grid CSS variable for this device. If unset,
@ -237,7 +237,7 @@
const stopDragging = async () => {
// Save changes
if ($gridStyles) {
await builderStore.actions.updateStyles($gridStyles, dragInfo.id)
await builderStore.actions.updateMetaStyles($gridStyles, dragInfo.id)
}
// Reset listener

View File

@ -124,9 +124,8 @@
// Extract valid children
// Sanity limit of 100 active indicators
const children = Array.from(
document.getElementsByClassName(`${componentId}-dom`)
)
const className = nextState.insideGrid ? componentId : `${componentId}-dom`
const children = Array.from(document.getElementsByClassName(className))
.filter(x => x != null)
.slice(0, 100)

View File

@ -40,8 +40,11 @@ const createBuilderStore = () => {
updateProp: (prop, value) => {
eventStore.actions.dispatchEvent("update-prop", { prop, value })
},
updateStyles: async (styles, id) => {
await eventStore.actions.dispatchEvent("update-styles", { styles, id })
updateMetaStyles: async (styles, id) => {
await eventStore.actions.dispatchEvent("update-meta-styles", {
styles,
id,
})
},
keyDown: (key, ctrlKey) => {
eventStore.actions.dispatchEvent("key-down", { key, ctrlKey })

View File

@ -3,7 +3,7 @@ import { builderStore } from "stores"
/**
* Helper to build a CSS string from a style object.
*/
const buildStyleString = (styleObject, customStyles) => {
export const buildStyleString = (styleObject, customStyles) => {
let str = ""
Object.entries(styleObject || {}).forEach(([style, value]) => {
if (style && value != null) {