forked from tsai/budibase
2 changed files with 535 additions and 0 deletions
@ -0,0 +1,406 @@ |
|||
<script> |
|||
import "@spectrum-css/inputgroup/dist/index-vars.css" |
|||
import "@spectrum-css/popover/dist/index-vars.css" |
|||
import "@spectrum-css/menu/dist/index-vars.css" |
|||
import { fly } from "svelte/transition" |
|||
import { createEventDispatcher } from "svelte" |
|||
import clickOutside from "../../Actions/click_outside" |
|||
import Search from "./Search.svelte" |
|||
import Icon from "../../Icon/Icon.svelte" |
|||
import StatusLight from "../../StatusLight/StatusLight.svelte" |
|||
import Detail from "../../Typography/Detail.svelte" |
|||
|
|||
export let primaryLabel = "" |
|||
export let id = null |
|||
export let placeholder = "Choose an option or type" |
|||
export let disabled = false |
|||
export let readonly = false |
|||
export let updateOnChange = true |
|||
export let error = null |
|||
export let secondaryOptions = [] |
|||
export let primaryOptions = [] |
|||
export let secondaryFieldText = "" |
|||
export let secondaryFieldIcon = "" |
|||
export let secondaryFieldColour = "" |
|||
export let getPrimaryOptionLabel = option => option |
|||
export let getPrimaryOptionValue = option => option |
|||
export let getPrimaryOptionColour = () => null |
|||
export let getPrimaryOptionIcon = () => null |
|||
export let getSecondaryOptionLabel = option => option |
|||
export let getSecondaryOptionValue = option => option |
|||
export let getSecondaryOptionColour = () => null |
|||
export let onSelectOption = () => {} |
|||
export let autoWidth = false |
|||
export let autocomplete = false |
|||
export let isOptionSelected = () => false |
|||
export let isPlaceholder = false |
|||
export let placeholderOption = null |
|||
|
|||
const dispatch = createEventDispatcher() |
|||
let primaryOpen = false |
|||
let secondaryOpen = false |
|||
let focus = false |
|||
let searchTerm = null |
|||
|
|||
$: groupTitles = Object.keys(primaryOptions) |
|||
$: filteredOptions = getFilteredOptions( |
|||
primaryOptions, |
|||
searchTerm, |
|||
getPrimaryOptionLabel |
|||
) |
|||
let iconData |
|||
/* |
|||
$: iconData = primaryOptions?.find(x => { |
|||
return x.name === primaryFieldText |
|||
}) |
|||
*/ |
|||
const updateValue = newValue => { |
|||
if (readonly) { |
|||
return |
|||
} |
|||
dispatch("change", newValue) |
|||
} |
|||
|
|||
const onClickPrimary = () => { |
|||
dispatch("click") |
|||
if (readonly) { |
|||
return |
|||
} |
|||
primaryOpen = true |
|||
} |
|||
|
|||
const onClickSecondary = () => { |
|||
dispatch("click") |
|||
if (readonly) { |
|||
return |
|||
} |
|||
secondaryOpen = true |
|||
} |
|||
|
|||
const onPickPrimary = newValue => { |
|||
dispatch("pickprimary", newValue) |
|||
primaryOpen = false |
|||
} |
|||
|
|||
const onPickSecondary = newValue => { |
|||
console.log(newValue) |
|||
dispatch("picksecondary", newValue) |
|||
secondaryOpen = false |
|||
} |
|||
|
|||
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 getFilteredOptions = (options, term, getLabel) => { |
|||
if (autocomplete && term) { |
|||
const lowerCaseTerm = term.toLowerCase() |
|||
return options.filter(option => { |
|||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm) |
|||
}) |
|||
} |
|||
return options |
|||
} |
|||
</script> |
|||
|
|||
<div |
|||
class="spectrum-InputGroup" |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled} |
|||
> |
|||
<div |
|||
class="spectrum-Textfield spectrum-InputGroup-textfield" |
|||
class:is-invalid={!!error} |
|||
class:is-disabled={disabled} |
|||
class:is-focused={focus} |
|||
style="width: 70%" |
|||
> |
|||
{#if iconData} |
|||
<svg |
|||
width="16px" |
|||
height="16px" |
|||
class="spectrum-Icon iconPadding" |
|||
style="color: {iconData?.color}" |
|||
focusable="false" |
|||
> |
|||
<use xlink:href="#spectrum-icon-18-{iconData?.icon}" /> |
|||
</svg> |
|||
{/if} |
|||
<input |
|||
{id} |
|||
on:click={() => (primaryOpen = true)} |
|||
on:blur |
|||
on:focus |
|||
on:input |
|||
on:keyup |
|||
on:blur={onBlur} |
|||
on:input={onInput} |
|||
on:keyup={updateValueOnEnter} |
|||
value={primaryLabel || ""} |
|||
placeholder={placeholder || ""} |
|||
{disabled} |
|||
{readonly} |
|||
class="spectrum-Textfield-input spectrum-InputGroup-input" |
|||
class:labelPadding={iconData} |
|||
/> |
|||
</div> |
|||
{#if primaryOpen} |
|||
<div |
|||
use:clickOutside={() => (primaryOpen = false)} |
|||
transition:fly|local={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
|||
class:auto-width={autoWidth} |
|||
style="width: 70%" |
|||
> |
|||
<ul class="spectrum-Menu" role="listbox"> |
|||
{#if placeholderOption} |
|||
<li |
|||
class="spectrum-Menu-item placeholder" |
|||
class:is-selected={isPlaceholder} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => onSelectOption(null)} |
|||
> |
|||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
</li> |
|||
{/if} |
|||
{#each groupTitles as title} |
|||
<div class="spectrum-Menu-item"> |
|||
<Detail>{title}</Detail> |
|||
</div> |
|||
{#if primaryOptions} |
|||
{#each primaryOptions[title].data as option, idx} |
|||
<li |
|||
class="spectrum-Menu-item" |
|||
class:is-selected={isOptionSelected( |
|||
getPrimaryOptionValue(option, idx) |
|||
)} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => |
|||
onPickPrimary({ |
|||
value: primaryOptions[title].getValue(option), |
|||
label: primaryOptions[title].getLabel(option), |
|||
})} |
|||
> |
|||
{#if primaryOptions[title].getIcon(option)} |
|||
<div |
|||
style="background: {primaryOptions[title].getColour( |
|||
option |
|||
)};" |
|||
class="circle" |
|||
> |
|||
<div> |
|||
<Icon |
|||
size="S" |
|||
name={primaryOptions[title].getIcon(option)} |
|||
/> |
|||
</div> |
|||
</div> |
|||
{:else if getPrimaryOptionColour(option, idx)} |
|||
<span class="option-left"> |
|||
<StatusLight color={getPrimaryOptionColour(option, idx)} /> |
|||
</span> |
|||
{/if} |
|||
<span class="spectrum-Menu-itemLabel"> |
|||
<span |
|||
class:spacing-group={primaryOptions[title].getIcon(option)} |
|||
> |
|||
{primaryOptions[title].getLabel(option)} |
|||
<span /> |
|||
</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
{#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)} |
|||
<span class="option-right"> |
|||
<StatusLight |
|||
color={getPrimaryOptionColour(option, idx)} |
|||
/> |
|||
</span> |
|||
{/if} |
|||
</span> |
|||
</li> |
|||
{/each} |
|||
{/if} |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
{/if} |
|||
<div style="width: 30%"> |
|||
<button |
|||
{id} |
|||
class="spectrum-Picker spectrum-Picker--sizeM override-borders" |
|||
{disabled} |
|||
class:is-open={secondaryOpen} |
|||
aria-haspopup="listbox" |
|||
on:mousedown={onClickSecondary} |
|||
> |
|||
{#if secondaryFieldIcon} |
|||
<span class="option-left"> |
|||
<Icon name={secondaryFieldIcon} /> |
|||
</span> |
|||
{:else if secondaryFieldColour} |
|||
<span class="option-left"> |
|||
<StatusLight color={secondaryFieldColour} /> |
|||
</span> |
|||
{/if} |
|||
|
|||
<span class:auto-width={autoWidth} class="spectrum-Picker-label"> |
|||
{secondaryFieldText} |
|||
</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Chevron100" /> |
|||
</svg> |
|||
</button> |
|||
{#if secondaryOpen} |
|||
<div |
|||
use:clickOutside={() => (secondaryOpen = false)} |
|||
transition:fly|local={{ y: -20, duration: 200 }} |
|||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" |
|||
style="width: 30%" |
|||
> |
|||
<ul class="spectrum-Menu" role="listbox"> |
|||
{#each secondaryOptions as option, idx} |
|||
<li |
|||
class="spectrum-Menu-item" |
|||
class:is-selected={isOptionSelected( |
|||
getSecondaryOptionValue(option, idx) |
|||
)} |
|||
role="option" |
|||
aria-selected="true" |
|||
tabindex="0" |
|||
on:click={() => |
|||
onPickSecondary(getSecondaryOptionValue(option, idx))} |
|||
> |
|||
{#if getSecondaryOptionColour(option, idx)} |
|||
<span class="option-left"> |
|||
<StatusLight color={getSecondaryOptionColour(option, idx)} /> |
|||
</span> |
|||
{/if} |
|||
|
|||
<span class="spectrum-Menu-itemLabel"> |
|||
{getSecondaryOptionLabel(option, idx)} |
|||
</span> |
|||
<svg |
|||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" |
|||
focusable="false" |
|||
aria-hidden="true" |
|||
> |
|||
<use xlink:href="#spectrum-css-icon-Checkmark100" /> |
|||
</svg> |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.spacing-group { |
|||
margin-left: var(--spacing-m); |
|||
} |
|||
.spectrum-InputGroup { |
|||
min-width: 0; |
|||
width: 100%; |
|||
} |
|||
.override-borders { |
|||
border-top-left-radius: 0px; |
|||
border-bottom-left-radius: 0px; |
|||
} |
|||
|
|||
.override-borders-left { |
|||
border-top-right-radius: 0px; |
|||
border-bottom-right-radius: 0px; |
|||
border-right: 0px; |
|||
} |
|||
.spectrum-Popover { |
|||
max-height: 240px; |
|||
z-index: 999; |
|||
top: 100%; |
|||
} |
|||
|
|||
.option-left { |
|||
padding-right: 8px; |
|||
} |
|||
.option-right { |
|||
padding-left: 8px; |
|||
} |
|||
|
|||
.circle { |
|||
border-radius: 50%; |
|||
height: 28px; |
|||
color: white; |
|||
font-weight: bold; |
|||
line-height: 48px; |
|||
font-size: 1.2em; |
|||
width: 28px; |
|||
position: relative; |
|||
} |
|||
|
|||
.circle > div { |
|||
position: absolute; |
|||
text-decoration: none; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translateX(-50%) translateY(-50%); |
|||
} |
|||
|
|||
.iconPadding { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 10px; |
|||
transform: translateY(-50%); |
|||
color: silver; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.labelPadding { |
|||
padding-left: calc(1em + 10px + 8px); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,129 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import PickerDropdown from "./Core/PickerDropdown.svelte" |
|||
import { createEventDispatcher } from "svelte" |
|||
|
|||
export let primaryValue = null |
|||
export let secondaryValue = null |
|||
export let inputType = "text" |
|||
export let label = null |
|||
export let labelPosition = "above" |
|||
export let primaryPlaceholder = "test" |
|||
export let secondaryPlaceholder = null |
|||
export let autocomplete |
|||
export let placeholder = null |
|||
export let disabled = false |
|||
export let readonly = false |
|||
export let error = null |
|||
export let updateOnChange = true |
|||
export let getSecondaryOptionLabel = option => |
|||
extractProperty(option, "label") |
|||
export let getSecondaryOptionValue = option => |
|||
extractProperty(option, "value") |
|||
export let getSecondaryOptionColour = () => {} |
|||
export let getSecondaryOptionIcon = () => {} |
|||
export let quiet = false |
|||
export let dataCy |
|||
export let autofocus |
|||
export let primaryOptions = [] |
|||
export let secondaryOptions = [] |
|||
|
|||
let primaryLabel |
|||
let secondaryLabel |
|||
const dispatch = createEventDispatcher() |
|||
|
|||
$: secondaryFieldText = getSecondaryFieldText( |
|||
secondaryValue, |
|||
secondaryOptions, |
|||
secondaryPlaceholder |
|||
) |
|||
$: secondaryFieldIcon = getSecondaryFieldAttribute( |
|||
getSecondaryOptionIcon, |
|||
secondaryValue, |
|||
secondaryOptions |
|||
) |
|||
$: secondaryFieldColour = getSecondaryFieldAttribute( |
|||
getSecondaryOptionColour, |
|||
secondaryValue, |
|||
secondaryOptions |
|||
) |
|||
|
|||
const getSecondaryFieldAttribute = (getAttribute, value, options) => { |
|||
// Wait for options to load if there is a value but no options |
|||
|
|||
if (!options?.length) { |
|||
return "" |
|||
} |
|||
|
|||
const index = options.findIndex( |
|||
(option, idx) => getSecondaryOptionValue(option, idx) === value |
|||
) |
|||
|
|||
return index !== -1 ? getAttribute(options[index], index) : null |
|||
} |
|||
|
|||
const getSecondaryFieldText = (value, options, placeholder) => { |
|||
// Always use placeholder if no value |
|||
if (value == null || value === "") { |
|||
return placeholder || "Choose an option" |
|||
} |
|||
|
|||
return getSecondaryFieldAttribute(getSecondaryOptionLabel, value, options) |
|||
} |
|||
|
|||
const onPickPrimary = e => { |
|||
console.log("hello") |
|||
|
|||
primaryLabel = e.detail.label |
|||
primaryValue = e.detail.value |
|||
dispatch("pickprimary", e.detail.value) |
|||
} |
|||
|
|||
const onPickSecondary = e => { |
|||
console.log("hello222") |
|||
secondaryValue = e.detail |
|||
dispatch("picksecondary", e.detail) |
|||
} |
|||
|
|||
const extractProperty = (value, property) => { |
|||
if (value && typeof value === "object") { |
|||
return value[property] |
|||
} |
|||
return value |
|||
} |
|||
</script> |
|||
|
|||
<Field {label} {labelPosition} {error}> |
|||
<PickerDropdown |
|||
{autocomplete} |
|||
{dataCy} |
|||
{updateOnChange} |
|||
{error} |
|||
{disabled} |
|||
{readonly} |
|||
{placeholder} |
|||
{inputType} |
|||
{quiet} |
|||
{autofocus} |
|||
{primaryOptions} |
|||
{secondaryOptions} |
|||
{getSecondaryOptionLabel} |
|||
{getSecondaryOptionValue} |
|||
{getSecondaryOptionIcon} |
|||
{getSecondaryOptionColour} |
|||
{secondaryFieldText} |
|||
{secondaryFieldIcon} |
|||
{secondaryFieldColour} |
|||
{primaryValue} |
|||
{secondaryValue} |
|||
{primaryLabel} |
|||
{secondaryLabel} |
|||
on:pickprimary={onPickPrimary} |
|||
on:picksecondary={onPickSecondary} |
|||
on:click |
|||
on:input |
|||
on:blur |
|||
on:focus |
|||
on:keyup |
|||
/> |
|||
</Field> |
|||
Loading…
Reference in new issue