Update builder preview to be interactive and improve builder preview experience

This commit is contained in:
Andrew Kingston 2021-01-27 15:52:12 +00:00
parent 6034102fd0
commit bfebf0226a
13 changed files with 81 additions and 166 deletions

View File

@ -11,9 +11,6 @@
*, *:before, *:after {
box-sizing: border-box;
}
* {
pointer-events: none;
}
</style>
<script src='/assets/budibase-client.js'></script>
<script>

View File

@ -120,7 +120,7 @@
value={componentInstance[setting.key] ?? componentInstance[setting.key]?.defaultValue}
{componentInstance}
onChange={val => onChange(setting.key, val)}
props={{ options: setting.options }} />
props={{ options: setting.options, placeholder: setting.placeholder }} />
{/if}
{/each}
{:else}

View File

@ -9,7 +9,6 @@ export const layout = [
key: "display",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Block", value: "block" },
{ label: "Inline Block", value: "inline-block" },
{ label: "Flex", value: "flex" },
@ -37,7 +36,6 @@ export const layout = [
key: "justify-content",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -51,7 +49,6 @@ export const layout = [
key: "align-items",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -64,7 +61,6 @@ export const layout = [
key: "flex-wrap",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Wrap", value: "wrap" },
{ label: "No wrap", value: "nowrap" },
],
@ -74,7 +70,6 @@ export const layout = [
key: "gap",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -93,7 +88,6 @@ export const margin = [
key: "margin",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -113,7 +107,6 @@ export const margin = [
key: "margin-top",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -133,7 +126,6 @@ export const margin = [
key: "margin-right",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -153,7 +145,6 @@ export const margin = [
key: "margin-bottom",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -173,7 +164,6 @@ export const margin = [
key: "margin-left",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -196,7 +186,6 @@ export const padding = [
key: "padding",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -214,7 +203,6 @@ export const padding = [
key: "padding-top",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -232,7 +220,6 @@ export const padding = [
key: "padding-right",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -250,7 +237,6 @@ export const padding = [
key: "padding-bottom",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -268,7 +254,6 @@ export const padding = [
key: "padding-left",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" },
{ label: "4px", value: "4px" },
{ label: "8px", value: "8px" },
@ -289,7 +274,6 @@ export const size = [
key: "flex",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Shrink", value: "0 1 auto" },
{ label: "Grow", value: "1 1 auto" },
],
@ -338,7 +322,6 @@ export const position = [
key: "position",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Static", value: "static" },
{ label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" },
@ -375,7 +358,6 @@ export const position = [
key: "z-index",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "-9999", value: "-9999" },
{ label: "-3", value: "-3" },
{ label: "-2", value: "-2" },
@ -395,7 +377,6 @@ export const typography = [
key: "font-family",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Arial", value: "Arial" },
{ label: "Arial Black", value: "Arial Black" },
{ label: "Cursive", value: "Cursive" },
@ -418,7 +399,6 @@ export const typography = [
key: "font-weight",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "200", value: "200" },
{ label: "300", value: "300" },
{ label: "400", value: "400" },
@ -434,7 +414,6 @@ export const typography = [
key: "font-size",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "8px", value: "8px" },
{ label: "10px", value: "10px" },
{ label: "12px", value: "12px" },
@ -454,7 +433,6 @@ export const typography = [
key: "line-height",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "1", value: "1" },
{ label: "1.25", value: "1.25" },
{ label: "1.5", value: "1.5" },
@ -496,7 +474,6 @@ export const typography = [
key: "text-decoration-line",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" },
@ -516,7 +493,6 @@ export const background = [
key: "background-image",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{
label: "Warm Flame",
@ -603,7 +579,6 @@ export const border = [
key: "border-radius",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "X Small", value: "0.125rem" },
{ label: "Small", value: "0.25rem" },
@ -619,7 +594,6 @@ export const border = [
key: "border-width",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "X Small", value: "0.5px" },
{ label: "Small", value: "1px" },
@ -638,7 +612,6 @@ export const border = [
key: "border-style",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "Hidden", value: "hidden" },
{ label: "Dotted", value: "dotted" },
@ -659,7 +632,6 @@ export const effects = [
key: "opacity",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "0", value: "0" },
{ label: "0.2", value: "0.2" },
{ label: "0.4", value: "0.4" },
@ -673,7 +645,6 @@ export const effects = [
key: "transform",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" },
{ label: "45 deg", value: "rotate(45deg)" },
{ label: "90 deg", value: "rotate(90deg)" },
@ -690,7 +661,6 @@ export const effects = [
key: "box-shadow",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{
@ -723,7 +693,6 @@ export const transitions = [
key: "transition-property",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ label: "All", value: "all" },
{ label: "Background Color", value: "background color" },
@ -745,7 +714,6 @@ export const transitions = [
control: OptionSelect,
placeholder: "sec",
options: [
{ label: "Choose option", value: "" },
{ label: "0.4s", value: "0.4s" },
{ label: "0.6s", value: "0.6s" },
{ label: "0.8s", value: "0.8s" },
@ -759,7 +727,6 @@ export const transitions = [
key: "transition-timing-function",
control: OptionSelect,
options: [
{ label: "Choose option", value: "" },
{ label: "Linear", value: "linear" },
{ label: "Ease", value: "ease" },
{ label: "Ease in", value: "ease-in" },

View File

@ -2,9 +2,14 @@
import { writable } from "svelte/store"
import { setContext, onMount } from "svelte"
import Component from "./Component.svelte"
import NotificationDisplay from './NotificationDisplay.svelte'
import NotificationDisplay from "./NotificationDisplay.svelte"
import SDK from "../sdk"
import { createDataStore, initialise, screenStore, notificationStore } from "../store"
import {
createDataStore,
initialise,
screenStore,
builderStore,
} from "../store"
// Provide contexts
setContext("sdk", SDK)
@ -23,5 +28,5 @@
{#if loaded && $screenStore.activeLayout}
<Component definition={$screenStore.activeLayout.props} />
<NotificationDisplay />
{/if}
<NotificationDisplay />

View File

@ -1,19 +1,23 @@
<script>
import { getContext, setContext } from "svelte"
import { writable } from "svelte/store"
import { writable, get } from "svelte/store"
import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "../utils/componentProps"
import { bindingStore, builderStore } from "../store"
import { hashString } from "../utils/hash"
export let definition = {}
let enrichedProps
let componentProps
// Props are hashed when inside the builder preview and used as a key, so that
// components fully remount whenever any props change
let propsHash = 0
// Get contexts
const dataContext = getContext("data")
const screenslotContext = getContext("screenslot")
// Create component context
const componentStore = writable({})
@ -27,16 +31,11 @@
$: updateProps(enrichedProps)
$: styles = definition._styles
// Allow component selection in the builder preview if we're previewing a
// layout, or we're preview a screen and we're inside the screenslot
$: allowSelection =
$builderStore.previewType === "layout" || screenslotContext
// Update component context
$: componentStore.set({
id,
children: children.length,
styles: { ...styles, id, allowSelection },
styles: { ...styles, id },
})
// Updates the component props.
@ -46,14 +45,20 @@
if (!props) {
return
}
let propsChanged = false
if (!componentProps) {
componentProps = {}
propsChanged = true
}
Object.keys(props).forEach(key => {
if (!propsAreSame(props[key], componentProps[key])) {
propsChanged = true
componentProps[key] = props[key]
}
})
if (get(builderStore).inBuilder && propsChanged) {
propsHash = hashString(JSON.stringify(componentProps))
}
}
// Gets the component constructor for the specified component
@ -70,22 +75,16 @@
const enrichComponentProps = async (definition, context, bindingStore) => {
enrichedProps = await enrichProps(definition, context, bindingStore)
}
// Returns a unique key to let svelte know when to remount components.
// If a component is selected we want to remount it every time any props
// change.
const getChildKey = childId => {
const selected = childId === $builderStore.selectedComponentId
return selected ? `${childId}-${$builderStore.previewId}` : childId
}
</script>
{#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}>
{#if children.length}
{#each children as child (getChildKey(child._id))}
<svelte:self definition={child} />
{/each}
{/if}
</svelte:component>
{#key propsHash}
<svelte:component this={constructor} {...componentProps}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self definition={child} />
{/each}
{/if}
</svelte:component>
{/key}
{/if}

View File

@ -7,9 +7,6 @@
const { styleable } = getContext("sdk")
const component = getContext("component")
// Set context flag so components know that we're now inside the screenslot
setContext("screenslot", true)
// Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config
// changes

View File

@ -0,0 +1,12 @@
export const hashString = str => {
if (!str) {
return 0
}
let hash = 0
for (let i = 0; i < str.length; i++) {
let char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash // Convert to 32bit integer
}
return hash
}

View File

@ -1,9 +1,6 @@
import { get } from "svelte/store"
import { builderStore } from "../store"
const selectedComponentWidth = 2
const selectedComponentColor = "#4285f4"
/**
* Helper to build a CSS string from a style object.
*/
@ -23,24 +20,14 @@ const buildStyleString = (styleObject, customStyles) => {
* events for any selectable components (overriding the blanket ban on pointer
* events in the iframe HTML).
*/
const addBuilderPreviewStyles = (styleString, componentId, selectable) => {
let str = styleString
// Apply extra styles if we're in the builder preview
const state = get(builderStore)
if (state.inBuilder) {
// Allow pointer events and always enable cursor
if (selectable) {
str += ";pointer-events: all !important; cursor: pointer !important;"
}
// Highlighted selected element
if (componentId === state.selectedComponentId) {
str += `;border: ${selectedComponentWidth}px solid ${selectedComponentColor} !important;`
}
const addBuilderPreviewStyles = (node, styleString, componentId) => {
if (componentId === get(builderStore).selectedComponentId) {
const style = window.getComputedStyle(node)
const property = style?.display === "table-row" ? "outline" : "border"
return styleString + `;${property}: 2px solid #4285f4 !important;`
} else {
return styleString
}
return str
}
/**
@ -52,17 +39,9 @@ export const styleable = (node, styles = {}) => {
let applyHoverStyles
let selectComponent
// Kill JS even bubbling
const blockEvent = event => {
event.preventDefault()
event.stopPropagation()
return false
}
// Creates event listeners and applies initial styles
const setupStyles = (newStyles = {}) => {
const componentId = newStyles.id
const selectable = !!newStyles.allowSelection
const customStyles = newStyles.custom || ""
const normalStyles = newStyles.normal || {}
const hoverStyles = {
@ -70,10 +49,9 @@ export const styleable = (node, styles = {}) => {
...(newStyles.hover || {}),
}
// Applies a style string to a DOM node, enriching it for the builder
// preview
// Applies a style string to a DOM node
const applyStyles = styleString => {
node.style = addBuilderPreviewStyles(styleString, componentId, selectable)
node.style = addBuilderPreviewStyles(node, styleString, componentId)
node.dataset.componentId = componentId
}
@ -91,7 +69,9 @@ export const styleable = (node, styles = {}) => {
// builder preview
selectComponent = event => {
builderStore.actions.selectComponent(componentId)
return blockEvent(event)
event.preventDefault()
event.stopPropagation()
return false
}
// Add listeners to toggle hover styles
@ -101,10 +81,6 @@ export const styleable = (node, styles = {}) => {
// Add builder preview click listener
if (get(builderStore).inBuilder) {
node.addEventListener("click", selectComponent, false)
// Kill other interaction events
node.addEventListener("mousedown", blockEvent)
node.addEventListener("mouseup", blockEvent)
}
// Apply initial normal styles
@ -119,8 +95,6 @@ export const styleable = (node, styles = {}) => {
// Remove builder preview click listener
if (get(builderStore).inBuilder) {
node.removeEventListener("click", selectComponent)
node.removeEventListener("mousedown", blockEvent)
node.removeEventListener("mouseup", blockEvent)
}
}

View File

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
const { authStore, styleable } = getContext("sdk")
const { authStore, styleable, builderStore } = getContext("sdk")
const component = getContext("component")
export let buttonText = "Log In"
@ -23,6 +23,9 @@
}
const login = async () => {
if ($builderStore.inBuilder) {
return
}
loading = true
await authStore.actions.logIn({ email, password })
loading = false

View File

@ -1,12 +1,15 @@
<script>
import { getContext } from "svelte"
const { authStore, linkable, styleable } = getContext("sdk")
const { authStore, linkable, styleable, builderStore } = getContext("sdk")
const component = getContext("component")
export let logoUrl
const logOut = async () => {
if ($builderStore.inBuilder) {
return
}
await authStore.actions.logOut()
}
</script>

View File

@ -92,16 +92,4 @@
{/each}
</ul>
</div>
{#if $fieldState.error}
<div class="error">{$fieldState.error}</div>
{/if}
</SpectrumField>
<style>
.error {
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
) !important;
}
</style>

View File

@ -10,7 +10,7 @@
const component = getContext("component")
const { labelPosition, formApi } = formContext || {}
const formField = formApi?.registerField(field) ?? {}
const { fieldId } = formField
const { fieldId, fieldState } = formField
$: labelPositionClass =
labelPosition === "top" ? "" : `spectrum-FieldLabel--${labelPosition}`
@ -31,6 +31,20 @@
{/if}
<div class="spectrum-Form-itemField">
<slot />
{#if $fieldState.error}
<div class="error">{$fieldState.error}</div>
{/if}
</div>
</div>
{/if}
<style>
.error {
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
);
font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spectrum-global-dimension-size-75);
}
</style>

View File

@ -1,7 +1,5 @@
<script>
import "@spectrum-css/textfield/dist/index-vars.css"
import "@spectrum-css/actionbutton/dist/index-vars.css"
import "@spectrum-css/stepper/dist/index-vars.css"
import { getContext } from "svelte"
import SpectrumField from "./SpectrumField.svelte"
@ -15,8 +13,6 @@
const formField = formApi?.registerField(field) ?? {}
const { fieldApi, fieldState } = formField
$: numeric = type === "number"
// Update value on blur only
const onBlur = event => {
fieldApi.setValue(event.target.value)
@ -24,11 +20,8 @@
</script>
<SpectrumField {label} {field}>
<div class:spectrum-Stepper={type === 'number'}>
<div
class="spectrum-Textfield"
class:spectrum-Stepper-textfield={numeric}
class:is-invalid={!$fieldState.valid}>
<div>
<div class="spectrum-Textfield" class:is-invalid={!$fieldState.valid}>
{#if !$fieldState.valid}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
@ -43,44 +36,7 @@
placeholder={placeholder || ''}
on:blur={onBlur}
{type}
class="spectrum-Textfield-input"
class:spectrum-Stepper-input={numeric} />
class="spectrum-Textfield-input" />
</div>
{#if numeric}
<span class="spectrum-Stepper-buttons">
<button
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepUp"
tabindex="-1">
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronUp75 spectrum-Stepper-stepUpIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron75" />
</svg>
</button>
<button
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepDown"
tabindex="-1">
<svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown75 spectrum-Stepper-stepDownIcon"
focusable="false"
aria-hidden="true">
<use xlink:href="#spectrum-css-icon-Chevron75" />
</svg>
</button>
</span>
{/if}
{#if $fieldState.error}
<div class="error">{$fieldState.error}</div>
{/if}
</div>
</SpectrumField>
<style>
.error {
color: var(
--spectrum-semantic-negative-color-default,
var(--spectrum-global-color-red-500)
) !important;
}
</style>