mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
54 changed files with 915 additions and 294 deletions
@ -0,0 +1,172 @@ |
|||
<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 { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = null |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let error = null |
|||
export let id = null |
|||
export let readonly = false |
|||
export let updateOnChange = true |
|||
export let quiet = false |
|||
export let min |
|||
export let max |
|||
export let step |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let focus = false |
|||
|
|||
// We need to keep the field value bound to a different variable in order |
|||
// to properly handle erroneous values. If we don't do this then it is |
|||
// possible for the field to show stale text which does not represent the |
|||
// real value. The reactive statement is to ensure that external changes to |
|||
// the value prop are reflected. |
|||
let fieldValue = value |
|||
$: fieldValue = value |
|||
|
|||
// Ensure step is always a numeric value defaulting to 1 |
|||
$: step = step == null || isNaN(step) ? 1 : step |
|||
|
|||
const updateValue = value => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
const float = parseFloat(value) |
|||
value = isNaN(float) ? null : float |
|||
if (value != null) { |
|||
if (min != null && value < min) { |
|||
value = min |
|||
} else if (max != null && value > max) { |
|||
value = max |
|||
} |
|||
} |
|||
dispatch("change", value) |
|||
fieldValue = value |
|||
} |
|||
|
|||
const onFocus = () => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
focus = true |
|||
} |
|||
|
|||
const onBlur = event => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
focus = false |
|||
updateValue(event.target.value) |
|||
} |
|||
|
|||
const onInput = event => { |
|||
if (readonly || !updateOnChange) { |
|||
return |
|||
} |
|||
updateValue(event.target.value) |
|||
} |
|||
|
|||
const updateValueOnEnter = event => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
if (event.key === "Enter") { |
|||
updateValue(event.target.value) |
|||
} |
|||
} |
|||
|
|||
const stepUp = () => { |
|||
if (value == null || isNaN(value)) { |
|||
updateValue(step) |
|||
} else { |
|||
updateValue(value + step) |
|||
} |
|||
} |
|||
|
|||
const stepDown = () => { |
|||
if (value == null || isNaN(value)) { |
|||
updateValue(step) |
|||
} else { |
|||
updateValue(value - step) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
class="spectrum-Stepper" |
|||
class:spectrum-Stepper--quiet={quiet} |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled} |
|||
class:is-focused={focus} |
|||
> |
|||
{#if error} |
|||
<svg |
|||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-Alert" /> |
|||
</svg> |
|||
{/if} |
|||
|
|||
<div class="spectrum-Textfield spectrum-Stepper-textfield"> |
|||
<input |
|||
{disabled} |
|||
{readonly} |
|||
{id} |
|||
bind:value={fieldValue} |
|||
placeholder={placeholder || ""} |
|||
type="number" |
|||
class="spectrum-Textfield-input spectrum-Stepper-input" |
|||
on:click |
|||
on:blur |
|||
on:focus |
|||
on:input |
|||
on:keyup |
|||
on:blur={onBlur} |
|||
on:focus={onFocus} |
|||
on:input={onInput} |
|||
on:keyup={updateValueOnEnter} |
|||
/> |
|||
</div> |
|||
<span class="spectrum-Stepper-buttons"> |
|||
<button |
|||
class="spectrum-ActionButton spectrum-ActionButton--sizeM spectrum-Stepper-stepUp" |
|||
tabindex="-1" |
|||
on:click={stepUp} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronUp75" |
|||
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" |
|||
on:click={stepDown} |
|||
> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronDown75" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron75" /> |
|||
</svg> |
|||
</button> |
|||
</span> |
|||
</div> |
|||
|
|||
<style> |
|||
.spectrum-Stepper { |
|||
width: 100%; |
|||
} |
|||
.spectrum-Stepper::before { |
|||
display: none; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,45 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import Stepper from "./Core/Stepper.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let value = null |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let readonly = false |
|||
export let error = null |
|||
export let updateOnChange = true |
|||
export let quiet = false |
|||
export let min = null |
|||
export let max = null |
|||
export let step = 1 |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
const onChange = e => { |
|||
value = e.detail |
|||
dispatch("change", e.detail) |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {labelPosition} {error}> |
|||
<Stepper |
|||
{updateOnChange} |
|||
{error} |
|||
{disabled} |
|||
{readonly} |
|||
{value} |
|||
{placeholder} |
|||
{quiet} |
|||
{min} |
|||
{max} |
|||
{step} |
|||
on:change={onChange} |
|||
on:click |
|||
on:input |
|||
on:blur |
|||
on:focus |
|||
on:keyup |
|||
/> |
|||
</Field> |
|||
@ -0,0 +1,68 @@ |
|||
<script> |
|||
import { Select, Label, Stepper } from "@budibase/bbui" |
|||
import { currentAsset, store } from "builderStore" |
|||
import { getActionProviderComponents } from "builderStore/dataBinding" |
|||
import { onMount } from "svelte" |
|||
|
|||
export let parameters |
|||
|
|||
$: actionProviders = getActionProviderComponents( |
|||
$currentAsset, |
|||
$store.selectedComponentId, |
|||
"ChangeFormStep" |
|||
) |
|||
|
|||
const typeOptions = [ |
|||
{ |
|||
label: "Next step", |
|||
value: "next", |
|||
}, |
|||
{ |
|||
label: "Previous step", |
|||
value: "prev", |
|||
}, |
|||
{ |
|||
label: "First step", |
|||
value: "first", |
|||
}, |
|||
{ |
|||
label: "Specific step", |
|||
value: "specific", |
|||
}, |
|||
] |
|||
|
|||
onMount(() => { |
|||
if (!parameters.type) { |
|||
parameters.type = "next" |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label small>Form</Label> |
|||
<Select |
|||
placeholder={null} |
|||
bind:value={parameters.componentId} |
|||
options={actionProviders} |
|||
getOptionLabel={x => x._instanceName} |
|||
getOptionValue={x => x._id} |
|||
/> |
|||
<Label small>Step</Label> |
|||
<Select bind:value={parameters.type} options={typeOptions} /> |
|||
{#if parameters.type === "specific"} |
|||
<Label small>Number</Label> |
|||
<Stepper bind:value={parameters.number} /> |
|||
{/if} |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
grid-template-columns: 60px 1fr; |
|||
align-items: center; |
|||
max-width: 400px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,62 @@ |
|||
/** |
|||
* Finds a component instance by ID |
|||
*/ |
|||
export const findComponentById = (component, componentId) => { |
|||
if (!component || !componentId) { |
|||
return null |
|||
} |
|||
if (component._id === componentId) { |
|||
return component |
|||
} |
|||
if (!component._children?.length) { |
|||
return null |
|||
} |
|||
for (let child of component._children) { |
|||
const result = findComponentById(child, componentId) |
|||
if (result) { |
|||
return result |
|||
} |
|||
} |
|||
return null |
|||
} |
|||
|
|||
/** |
|||
* Finds the component path to a component |
|||
*/ |
|||
export const findComponentPathById = (component, componentId, path = []) => { |
|||
if (!component || !componentId) { |
|||
return null |
|||
} |
|||
path = [...path, component] |
|||
if (component._id === componentId) { |
|||
return path |
|||
} |
|||
if (!component._children?.length) { |
|||
return null |
|||
} |
|||
for (let child of component._children) { |
|||
const result = findComponentPathById(child, componentId, path) |
|||
if (result) { |
|||
return result |
|||
} |
|||
} |
|||
return null |
|||
} |
|||
|
|||
/** |
|||
* Finds all children instances of a certain component type of a given component |
|||
*/ |
|||
export const findChildrenByType = (component, type, children = []) => { |
|||
if (!component) { |
|||
return |
|||
} |
|||
if (component._component.endsWith(`/${type}`)) { |
|||
children.push(component) |
|||
} |
|||
if (!component._children?.length) { |
|||
return |
|||
} |
|||
component._children.forEach(child => { |
|||
findChildrenByType(child, type, children) |
|||
}) |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<script> |
|||
import { getContext, setContext } from "svelte" |
|||
import Placeholder from "../Placeholder.svelte" |
|||
|
|||
export let step = 1 |
|||
|
|||
const { styleable, builderStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
const formContext = getContext("form") |
|||
|
|||
// Set form step context so fields know what step they are within |
|||
setContext("form-step", step || 1) |
|||
|
|||
$: formState = formContext?.formState |
|||
$: currentStep = $formState?.currentStep |
|||
|
|||
// If in the builder preview, show this step if a child is selected |
|||
$: { |
|||
if ( |
|||
formContext && |
|||
$builderStore.inBuilder && |
|||
$builderStore.selectedComponentPath?.includes($component.id) |
|||
) { |
|||
formContext.formApi.setStep(step) |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
{#if !formContext} |
|||
<Placeholder text="Form steps need to be wrapped in a form" /> |
|||
{:else if step === currentStep} |
|||
<div use:styleable={$component.styles}> |
|||
<slot /> |
|||
</div> |
|||
{/if} |
|||
Loading…
Reference in new issue