Update builder preview to be interactive and improve builder preview experience
This commit is contained in:
parent
f059739d3d
commit
6631fe2af8
|
@ -11,9 +11,6 @@
|
|||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<script src='/assets/budibase-client.js'></script>
|
||||
<script>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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} />
|
||||
{/if}
|
||||
<NotificationDisplay />
|
||||
{/if}
|
||||
|
|
|
@ -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}
|
||||
{#key propsHash}
|
||||
<svelte:component this={constructor} {...componentProps}>
|
||||
{#if children.length}
|
||||
{#each children as child (getChildKey(child._id))}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self definition={child} />
|
||||
{/each}
|
||||
{/if}
|
||||
</svelte:component>
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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;"
|
||||
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
|
||||
}
|
||||
|
||||
// Highlighted selected element
|
||||
if (componentId === state.selectedComponentId) {
|
||||
str += `;border: ${selectedComponentWidth}px solid ${selectedComponentColor} !important;`
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue