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) hoverStore.hover(data.id, false)
} else if (type === "update-prop") { } else if (type === "update-prop") {
await componentStore.updateSetting(data.prop, data.value) await componentStore.updateSetting(data.prop, data.value)
} else if (type === "update-styles") { } else if (type === "update-meta-styles") {
await componentStore.updateStyles(data.styles, data.id) await componentStore.updateMetaStyles(data.styles, data.id)
} else if (type === "delete-component" && data.id) { } else if (type === "delete-component" && data.id) {
// Legacy type, can be deleted in future // Legacy type, can be deleted in future
confirmDeleteComponent(data.id) confirmDeleteComponent(data.id)

View File

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

View File

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

View File

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

View File

@ -8,10 +8,16 @@
export let cols = 12 export let cols = 12
export let rows = 12 export let rows = 12
let width
let height
$: cols = cols || 12 $: cols = cols || 12
$: rows = rows || 12 $: rows = rows || 12
$: coords = generateCoords(rows, cols) $: coords = generateCoords(rows, cols)
$: mobile = $context.device.mobile $: mobile = $context.device.mobile
$: empty = $component.empty
$: colSize = width / cols
$: rowSize = height / rows
const generateCoords = (rows, cols) => { const generateCoords = (rows, cols) => {
let grid = [] let grid = []
@ -27,14 +33,18 @@
<div <div
class="grid" class="grid"
class:mobile class:mobile
bind:clientWidth={width}
bind:clientHeight={height}
use:styleable={{ use:styleable={{
...$component.styles, ...$component.styles,
normal: { normal: {
...$component.styles?.normal, ...$component.styles?.normal,
"--cols": cols, "--cols": cols,
"--rows": rows, "--rows": rows,
gap: "0 !important", "--col-size": `${colSize}px`,
"--row-size": `${rowSize}px`,
}, },
empty: false,
}} }}
data-rows={rows} data-rows={rows}
data-cols={cols} data-cols={cols}
@ -46,55 +56,18 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<slot />
<!-- Only render the slot if not empty, as we don't want the placeholder -->
{#if !empty}
<slot />
{/if}
</div> </div>
<style> <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 { .grid {
position: relative; position: relative;
height: 400px; height: 400px;
gap: 0;
} }
.grid, .grid,
.underlay { .underlay {
@ -118,4 +91,36 @@
.placeholder { .placeholder {
background-color: var(--spectrum-global-color-gray-100); 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> </style>

View File

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

View File

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

View File

@ -40,8 +40,11 @@ const createBuilderStore = () => {
updateProp: (prop, value) => { updateProp: (prop, value) => {
eventStore.actions.dispatchEvent("update-prop", { prop, value }) eventStore.actions.dispatchEvent("update-prop", { prop, value })
}, },
updateStyles: async (styles, id) => { updateMetaStyles: async (styles, id) => {
await eventStore.actions.dispatchEvent("update-styles", { styles, id }) await eventStore.actions.dispatchEvent("update-meta-styles", {
styles,
id,
})
}, },
keyDown: (key, ctrlKey) => { keyDown: (key, ctrlKey) => {
eventStore.actions.dispatchEvent("key-down", { 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. * Helper to build a CSS string from a style object.
*/ */
const buildStyleString = (styleObject, customStyles) => { export const buildStyleString = (styleObject, customStyles) => {
let str = "" let str = ""
Object.entries(styleObject || {}).forEach(([style, value]) => { Object.entries(styleObject || {}).forEach(([style, value]) => {
if (style && value != null) { if (style && value != null) {