Large refactor of grid css variable handling to simplify everything

This commit is contained in:
Andrew Kingston 2024-07-31 10:35:57 +01:00
parent de183d5c78
commit cb3c667859
No known key found for this signature in database
11 changed files with 306 additions and 117 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-meta-styles") {
await componentStore.updateMetaStyles(data.styles, data.id)
} else if (type === "update-styles") {
await componentStore.updateStyles(data.styles, data.id)
} else if (type === "delete-component" && data.id) {
// Legacy type, can be deleted in future
confirmDeleteComponent(data.id)

View File

@ -64,7 +64,6 @@ 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)
@ -979,16 +978,6 @@ 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

@ -118,7 +118,9 @@
"height": 200
},
"grid": {
"fill": true
"hAlign": "stretch",
"vAlign": "stretch",
"showControls": false
},
"styles": ["padding", "size", "background", "border", "shadow"],
"settings": [
@ -2790,6 +2792,10 @@
"width": 400,
"height": 400
},
"grid": {
"hAlign": "stretch",
"vAlign": "stretch"
},
"settings": [
{
"type": "select",
@ -6915,6 +6921,10 @@
"width": 400,
"height": 400
},
"grid": {
"hAlign": "stretch",
"vAlign": "start"
},
"settings": [
{
"type": "table",
@ -7256,6 +7266,11 @@
"width": 600,
"height": 400
},
"grid": {
"hAlign": "stretch",
"vAlign": "stretch",
"showControls": false
},
"settings": [
{
"type": "dataSource",

View File

@ -40,6 +40,7 @@
getActionDependentContextKeys,
} from "../utils/buttonActions.js"
import { buildStyleString } from "utils/styleable.js"
import { getBaseGridVars } from "utils/grid.js"
export let instance = {}
export let isLayout = false
@ -103,7 +104,7 @@
let settingsDefinitionMap
let missingRequiredSettings = false
// Temporary meta styles which can be added in the app preview for things like
// Temporary styles which can be added in the app preview for things like
// DND. We clear these whenever a new instance is received.
let ephemeralStyles
@ -197,14 +198,14 @@
$: currentTheme = $context?.device?.theme
$: darkMode = !currentTheme?.includes("light")
// Build meta styles and stringify to apply to the wrapper node
$: definitionMetaStyles = getDefinitonMetaStyles(definition)
$: metaStyles = {
...definitionMetaStyles,
...instance._styles?.meta,
// Build up full styles and split them into variables and non-variables
$: baseStyles = getBaseStyles(definition)
$: parsedStyles = parseStyles({
...baseStyles,
...instance._styles?.normal,
...ephemeralStyles,
}
$: metaCSS = buildStyleString(metaStyles)
})
$: wrapperCSS = buildStyleString(parsedStyles.variables)
// Update component context
$: store.set({
@ -212,7 +213,7 @@
children: children.length,
styles: {
...instance._styles,
meta: metaStyles,
normal: parsedStyles.nonVariables,
custom: customCSS,
id,
empty: emptyState,
@ -612,7 +613,7 @@
}
}
const select = e => {
const handleWrapperClick = e => {
if (isBlock) {
return
}
@ -620,19 +621,21 @@
builderStore.actions.selectComponent(id)
}
// Util to generate meta styles based on component definition
const alignToStyleMap = {
start: "flex-start",
center: "center",
end: "flex-end",
stretch: "stretch",
// Splits component styles into variables and non-variables
const parseStyles = styles => {
let variables = {}
let nonVariables = {}
for (let style of Object.keys(styles || {})) {
const group = style.startsWith("--") ? variables : nonVariables
group[style] = styles[style]
}
return { variables, nonVariables }
}
const getDefinitonMetaStyles = definition => {
const gridHAlign = definition.grid?.hAlign || "stretch"
const gridVAlign = definition.grid?.vAlign || "center"
// Generates any required base styles based on the component definition
const getBaseStyles = definition => {
return {
["align-items"]: alignToStyleMap[gridHAlign],
["justify-content"]: alignToStyleMap[gridVAlign],
...getBaseGridVars(definition),
}
}
@ -684,9 +687,9 @@
data-name={name}
data-icon={icon}
data-parent={$component.id}
style={metaCSS}
style={wrapperCSS}
{draggable}
on:click={select}
on:click={handleWrapperClick}
>
{#if errorState}
<ComponentErrorState

View File

@ -96,16 +96,22 @@
.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 */
/* Position vars */
--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));
/* Flex vars */
--h-align: var(--grid-desktop-h-align, var(--grid-mobile-h-align, stretch));
--v-align: var(--grid-desktop-v-align, var(--grid-mobile-v-align, center));
--child-flex: var(
--grid-desktop-child-flex,
var(--grid-mobile-child-flex, 0 0 auto)
);
/* Ensure grid metadata falls within limits */
grid-column-start: min(max(1, var(--col-start)), var(--cols)) !important;
grid-column-end: min(
@ -114,20 +120,32 @@
) !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;
/* Flex container styles */
flex-direction: column;
align-items: var(--h-align);
justify-content: var(--v-align);
}
/* On mobile, use mobile metadata and fall back to desktop */
.grid.mobile :global(> .component) {
/* Position vars */
--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));
/* Flex vars */
--h-align: var(--grid-mobile-h-align, var(--grid-desktop-h-align, stretch));
--v-align: var(--grid-mobile-v-align, var(--grid-desktop-v-align, center));
--child-flex: var(
--grid-mobile-child-flex,
var(--grid-desktop-child-flex, 0 0 auto)
);
}
/* Handle grid children which need to fill the outer component wrapper */
.grid :global(> .component.fill > *) {
width: 100%;
height: 100%;
flex: 1 1 0;
.grid :global(> .component > *) {
flex: var(--child-flex) !important;
}
</style>

View File

@ -2,53 +2,20 @@
import { onMount, onDestroy } from "svelte"
import { builderStore, componentStore } from "stores"
import { Utils, memo } from "@budibase/frontend-core"
import { isGridEvent, getGridParentID } from "utils/grid"
import {
isGridEvent,
getGridParentID,
getGridVar,
getDefaultGridVarValue,
getOtherDeviceGridVar,
} from "utils/grid"
// Enum for device preview type, included in CSS variables
const Devices = {
Desktop: "desktop",
Mobile: "mobile",
}
// Generates the CSS variable for a certain grid param suffix, for the current
// device
const getCSSVar = suffix => {
const device =
$builderStore.previewDevice === Devices.Mobile
? Devices.Mobile
: Devices.Desktop
return `--grid-${device}-${suffix}`
}
// Generates the CSS variable for a certain grid param suffix, for the other
// device variant than the one included in this variable
const getOtherDeviceCSSVar = cssVar => {
if (cssVar.includes(Devices.Desktop)) {
return cssVar.replace(Devices.Desktop, Devices.Mobile)
} else {
return cssVar.replace(Devices.Mobile, Devices.Desktop)
}
}
// Gets the default value for a certain grid CSS variable
const getDefaultValue = cssVar => {
return cssVar.endsWith("-start") ? 1 : 2
}
// Enums for our grid CSS variables, for the current device
const Vars = {
get ColStart() {
return getCSSVar("col-start")
},
get ColEnd() {
return getCSSVar("col-end")
},
get RowStart() {
return getCSSVar("row-start")
},
get RowEnd() {
return getCSSVar("row-end")
},
// Grid CSS variables
$: vars = {
colStart: $getGridVar("col-start"),
colEnd: $getGridVar("col-end"),
rowStart: $getGridVar("row-start"),
rowEnd: $getGridVar("row-end"),
}
let dragInfo
@ -97,34 +64,34 @@
deltaX = minMax(deltaX, 1 - colStart, cols + 1 - colEnd)
deltaY = minMax(deltaY, 1 - rowStart, rows + 1 - rowEnd)
const newStyles = {
[Vars.ColStart]: colStart + deltaX,
[Vars.ColEnd]: colEnd + deltaX,
[Vars.RowStart]: rowStart + deltaY,
[Vars.RowEnd]: rowEnd + deltaY,
[vars.colStart]: colStart + deltaX,
[vars.colEnd]: colEnd + deltaX,
[vars.rowStart]: rowStart + deltaY,
[vars.rowEnd]: rowEnd + deltaY,
}
gridStyles.set(newStyles)
} else if (mode === "resize") {
let newStyles = {}
if (side === "right") {
newStyles[Vars.ColEnd] = Math.max(colEnd + deltaX, colStart + 1)
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
} else if (side === "left") {
newStyles[Vars.ColStart] = Math.min(colStart + deltaX, colEnd - 1)
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
} else if (side === "top") {
newStyles[Vars.RowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "bottom") {
newStyles[Vars.RowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-right") {
newStyles[Vars.ColEnd] = Math.max(colEnd + deltaX, colStart + 1)
newStyles[Vars.RowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "bottom-left") {
newStyles[Vars.ColStart] = Math.min(colStart + deltaX, colEnd - 1)
newStyles[Vars.RowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
newStyles[vars.rowEnd] = Math.max(rowEnd + deltaY, rowStart + 1)
} else if (side === "top-right") {
newStyles[Vars.ColEnd] = Math.max(colEnd + deltaX, colStart + 1)
newStyles[Vars.RowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
newStyles[vars.colEnd] = Math.max(colEnd + deltaX, colStart + 1)
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
} else if (side === "top-left") {
newStyles[Vars.ColStart] = Math.min(colStart + deltaX, colEnd - 1)
newStyles[Vars.RowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
newStyles[vars.colStart] = Math.min(colStart + deltaX, colEnd - 1)
newStyles[vars.rowStart] = Math.min(rowStart + deltaY, rowEnd - 1)
}
gridStyles.set(newStyles)
}
@ -197,20 +164,21 @@
const getCurrent = cssVar => {
let style = styles?.getPropertyValue(cssVar)
if (!style) {
style = styles?.getPropertyValue(getOtherDeviceCSSVar(cssVar))
style = styles?.getPropertyValue(getOtherDeviceGridVar(cssVar))
}
return parseInt(style || getDefaultValue(cssVar))
return parseInt(style || getDefaultGridVarValue(cssVar))
}
dragInfo.grid = {
startX: e.clientX,
startY: e.clientY,
// Ensure things are within limits
rowStart: minMax(getCurrent(Vars.RowStart), 1, gridRows),
rowEnd: minMax(getCurrent(Vars.RowEnd), 2, gridRows + 1),
colStart: minMax(getCurrent(Vars.ColStart), 1, gridCols),
colEnd: minMax(getCurrent(Vars.ColEnd), 2, gridCols + 1),
rowStart: minMax(getCurrent(vars.rowStart), 1, gridRows),
rowEnd: minMax(getCurrent(vars.rowEnd), 2, gridRows + 1),
colStart: minMax(getCurrent(vars.colStart), 1, gridCols),
colEnd: minMax(getCurrent(vars.colEnd), 2, gridCols + 1),
}
console.log(dragInfo.grid)
handleEvent(e)
}
}
@ -226,7 +194,7 @@
const stopDragging = async () => {
// Save changes
if ($gridStyles) {
await builderStore.actions.updateMetaStyles($gridStyles, dragInfo.id)
await builderStore.actions.updateStyles($gridStyles, dragInfo.id)
}
// Reset listener

View File

@ -0,0 +1,48 @@
<script>
import { Icon } from "@budibase/bbui"
import { builderStore, componentStore } from "stores"
import { getGridVarValue } from "utils/grid"
export let style
export let value
export let icon
export let title
$: currentValue = getGridVarValue($componentStore.selectedComponent, style)
$: console.log(style, currentValue)
$: active = currentValue === value
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
{title}
class:active
on:click={() => {
builderStore.actions.updateStyles(
{ [style]: value },
$componentStore.selectedComponent._id
)
}}
>
<Icon name={icon} size="S" />
</div>
<style>
div {
padding: 6px;
border-radius: 2px;
color: var(--spectrum-global-color-gray-700);
display: flex;
transition: color 0.13s ease-in-out, background-color 0.13s ease-in-out;
}
div:hover {
background-color: var(--spectrum-global-color-gray-200);
cursor: pointer;
}
.active,
.active:hover {
background-color: rgba(13, 102, 208, 0.1);
color: var(--spectrum-global-color-blue-600);
}
</style>

View File

@ -1,11 +1,13 @@
<script>
import { onMount, onDestroy } from "svelte"
import SettingsButton from "./SettingsButton.svelte"
import GridStylesButton from "./GridStylesButton.svelte"
import SettingsColorPicker from "./SettingsColorPicker.svelte"
import SettingsPicker from "./SettingsPicker.svelte"
import { builderStore, componentStore, dndIsDragging } from "stores"
import { Utils } from "@budibase/frontend-core"
import { isGridChild } from "utils/grid"
import { findComponentParent } from "utils/components"
import { getGridVar } from "utils/grid"
const verticalOffset = 36
const horizontalOffset = 2
@ -16,7 +18,10 @@
let self
let measured = false
// TODO: respect dependsOn keys
$: id = $builderStore.selectedComponentId
$: parent = findComponentParent($builderStore.screen.props, id)
$: instance = componentStore.actions.getComponentInstance(id)
$: state = $instance?.state
$: definition = $componentStore.selectedComponentDefinition
@ -32,6 +37,11 @@
}
$: settings = getBarSettings(definition)
$: isRoot = id === $builderStore.screen?.props?._id
$: insideGrid =
parent?._component.endsWith("/container") && parent.layout === "grid"
$: showGridStyles = insideGrid && definition?.grid?.showControls !== false
$: gridHAlignVar = $getGridVar("h-align")
$: gridVAlignVar = $getGridVar("v-align")
const getBarSettings = definition => {
let allSettings = []
@ -51,7 +61,7 @@
}
const id = $builderStore.selectedComponentId
let element = document.getElementsByClassName(id)?.[0]
if (!isGridChild(element)) {
if (!insideGrid) {
element = element?.children?.[0]
}
@ -135,6 +145,58 @@
bind:this={self}
class:visible={measured}
>
{#if showGridStyles}
<GridStylesButton
style={gridHAlignVar}
value="start"
icon="AlignLeft"
title="Align left"
/>
<GridStylesButton
style={gridHAlignVar}
value="center"
icon="AlignCenter"
title="Align center"
/>
<GridStylesButton
style={gridHAlignVar}
value="end"
icon="AlignRight"
title="Align right"
/>
<GridStylesButton
style={gridHAlignVar}
value="stretch"
icon="MoveLeftRight"
title="Stretch horizontally"
/>
<div class="divider" />
<GridStylesButton
style={gridVAlignVar}
value="start"
icon="AlignTop"
title="Align top"
/>
<GridStylesButton
style={gridVAlignVar}
value="center"
icon="AlignMiddle"
title="Align middle"
/>
<GridStylesButton
style={gridVAlignVar}
value="end"
icon="AlignBottom"
title="Align bottom"
/>
<GridStylesButton
style={gridVAlignVar}
value="stretch"
icon="MoveUpDown"
title="Stretch vertically"
/>
<div class="divider" />
{/if}
{#each settings as setting, idx}
{#if setting.type === "select"}
{#if setting.barStyle === "buttons"}

View File

@ -9,6 +9,7 @@
export let title
export let rotate = false
export let bool = false
export let active = false
const dispatch = createEventDispatcher()
$: currentValue = $componentStore.selectedComponent?.[prop]

View File

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

View File

@ -1,3 +1,51 @@
import { builderStore } from "stores"
import { derived, get } from "svelte/store"
/**
* We use CSS variables on components to control positioning and layout of
* components inside grids.
* --grid-[mobile/desktop]-[row/col]-[start-end]: for positioning
* --grid-[mobile/desktop]-[h/v]-align: for layout of inner components within
* the components grid bounds
*
* Component definitions define their default layout preference via the
* `grid.hAlign` and `grid.vAlign` keys in the manifest.
*/
// Enum for device preview type, included in grid CSS variables
const Devices = {
Desktop: "desktop",
Mobile: "mobile",
}
// Generates the CSS variable for a certain grid param suffix, for the current
// device
const previewDevice = derived(builderStore, $store => $store.device)
export const getGridVar = derived(previewDevice, device => suffix => {
const prefix = device === Devices.Mobile ? Devices.Mobile : Devices.Desktop
return `--grid-${prefix}-${suffix}`
})
// Generates the CSS variable for a certain grid param suffix, for the other
// device variant than the one included in this variable
export const getOtherDeviceGridVar = cssVar => {
if (cssVar.includes(Devices.Desktop)) {
return cssVar.replace(Devices.Desktop, Devices.Mobile)
} else {
return cssVar.replace(Devices.Mobile, Devices.Desktop)
}
}
// Gets the default value for a certain grid CSS variable
export const getDefaultGridVarValue = cssVar => {
if (cssVar.includes("align")) {
return cssVar.includes("-h-") ? "stretch" : "center"
} else {
return cssVar.endsWith("-start") ? 1 : 2
}
}
// Determines whether a JS event originated from immediately within a grid
export const isGridEvent = e => {
return (
e.target
@ -8,6 +56,7 @@ export const isGridEvent = e => {
)
}
// Determines whether a DOM element is an immediate child of a grid
export const isGridChild = node => {
return node
?.closest(".component")
@ -15,6 +64,42 @@ export const isGridChild = node => {
?.childNodes[0]?.classList?.contains("grid")
}
// Gets the component ID of the closest parent grid
export const getGridParentID = node => {
return node?.closest(".grid")?.parentNode.dataset.id
}
// Generates the base set of grid CSS vars from a component definition
const alignmentToStyleMap = {
start: "flex-start",
center: "center",
end: "flex-end",
stretch: "stretch",
}
export const getBaseGridVars = definition => {
const gridHAlign = definition.grid?.hAlign || "stretch"
const gridVAlign = definition.grid?.vAlign || "center"
const flexStyles = gridVAlign === "stretch" ? "1 1 0" : "0 0 auto"
return {
["--grid-desktop-h-align"]: alignmentToStyleMap[gridHAlign],
["--grid-mobile-h-align"]: alignmentToStyleMap[gridHAlign],
["--grid-desktop-v-align"]: alignmentToStyleMap[gridVAlign],
["--grid-mobile-v-align"]: alignmentToStyleMap[gridVAlign],
["--grid-desktop-child-flex"]: flexStyles,
["--grid-mobile-child-flex"]: flexStyles,
}
}
// Gets the current value of a certain grid CSS variable for a component
export const getGridVarValue = (component, variable) => {
// Try the desired variable
let val = component?._styles?.normal?.[variable]
// Otherwise try the other device variables
if (!val) {
val = component?._styles?.normal?.[getOtherDeviceGridVar(variable)]
}
// Otherwise use the default
return val ? val : getDefaultGridVarValue(variable)
}