forked from tsai/budibase
15 changed files with 143 additions and 384 deletions
@ -1,25 +1,41 @@ |
|||||
<script> |
<script> |
||||
import OptionSelect from "./OptionSelect.svelte" |
import { Select } from "@budibase/bbui" |
||||
import MultiOptionSelect from "./MultiOptionSelect.svelte" |
|
||||
import { |
import { |
||||
getDatasourceForProvider, |
getDatasourceForProvider, |
||||
getSchemaForDatasource, |
getSchemaForDatasource, |
||||
} from "builderStore/dataBinding" |
} from "builderStore/dataBinding" |
||||
import { currentAsset } from "builderStore" |
import { currentAsset } from "builderStore" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
export let componentInstance = {} |
export let componentInstance = {} |
||||
export let value = "" |
export let value = "" |
||||
export let onChange = () => {} |
|
||||
export let multiselect = false |
export let multiselect = false |
||||
export let placeholder |
export let placeholder |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) |
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) |
||||
$: schema = getSchemaForDatasource(datasource).schema |
$: schema = getSchemaForDatasource(datasource).schema |
||||
$: options = Object.keys(schema || {}) |
$: options = Object.keys(schema || {}) |
||||
|
$: boundValue = getValidValue(value, options) |
||||
|
|
||||
|
const getValidValue = (value, options) => { |
||||
|
// Reset value if there aren't any options |
||||
|
if (!Array.isArray(options)) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
// Reset value if the value isn't found in the options |
||||
|
if (options.indexOf(value) === -1) { |
||||
|
return null |
||||
|
} |
||||
|
|
||||
|
return value |
||||
|
} |
||||
|
|
||||
|
const onChange = value => { |
||||
|
boundValue = getValidValue(value.detail, options) |
||||
|
dispatch("change", boundValue) |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
{#if multiselect} |
<Select {placeholder} value={boundValue} on:change={onChange} {options} /> |
||||
<MultiOptionSelect {value} {onChange} {options} {placeholder} /> |
|
||||
{:else} |
|
||||
<OptionSelect {value} {onChange} {options} {placeholder} /> |
|
||||
{/if} |
|
||||
|
|||||
@ -1,5 +1,35 @@ |
|||||
<script> |
<script> |
||||
import FieldSelect from "./FieldSelect.svelte" |
import { Multiselect } from "@budibase/bbui" |
||||
|
import { |
||||
|
getDatasourceForProvider, |
||||
|
getSchemaForDatasource, |
||||
|
} from "builderStore/dataBinding" |
||||
|
import { currentAsset } from "builderStore" |
||||
|
import { createEventDispatcher } from "svelte" |
||||
|
|
||||
|
export let componentInstance = {} |
||||
|
export let value = "" |
||||
|
export let multiselect = false |
||||
|
export let placeholder |
||||
|
|
||||
|
const dispatch = createEventDispatcher() |
||||
|
$: datasource = getDatasourceForProvider($currentAsset, componentInstance) |
||||
|
$: schema = getSchemaForDatasource(datasource).schema |
||||
|
$: options = Object.keys(schema || {}) |
||||
|
$: boundValue = getValidOptions(value, options) |
||||
|
|
||||
|
const getValidOptions = (selectedOptions, allOptions) => { |
||||
|
// Fix the hardcoded default string value |
||||
|
if (!Array.isArray(selectedOptions)) { |
||||
|
selectedOptions = [] |
||||
|
} |
||||
|
return selectedOptions.filter(val => allOptions.indexOf(val) !== -1) |
||||
|
} |
||||
|
|
||||
|
const setValue = value => { |
||||
|
boundValue = getValidOptions(value.detail, options) |
||||
|
dispatch("change", boundValue) |
||||
|
} |
||||
</script> |
</script> |
||||
|
|
||||
<FieldSelect {...$$props} multiselect /> |
<Multiselect {placeholder} value={boundValue} on:change={setValue} {options} /> |
||||
|
|||||
@ -1,43 +0,0 @@ |
|||||
<script> |
|
||||
import { Multiselect } from "@budibase/bbui" |
|
||||
|
|
||||
export let options = [] |
|
||||
export let value = [] |
|
||||
export let onChange = () => {} |
|
||||
export let placeholder |
|
||||
|
|
||||
let boundValue = getValidOptions(value, options) |
|
||||
|
|
||||
function getValidOptions(selectedOptions, allOptions) { |
|
||||
// Fix the hardcoded default string value |
|
||||
if (!Array.isArray(selectedOptions)) { |
|
||||
selectedOptions = [] |
|
||||
} |
|
||||
return selectedOptions.filter(val => allOptions.indexOf(val) !== -1) |
|
||||
} |
|
||||
|
|
||||
function setValue(value) { |
|
||||
boundValue = getValidOptions(value.detail, options) |
|
||||
onChange(boundValue) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div> |
|
||||
<Multiselect |
|
||||
align="right" |
|
||||
extraThin |
|
||||
secondary |
|
||||
{placeholder} |
|
||||
value={boundValue} |
|
||||
on:change={setValue}> |
|
||||
{#each options as option} |
|
||||
<option value={option}>{option}</option> |
|
||||
{/each} |
|
||||
</Multiselect> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
div { |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,257 +0,0 @@ |
|||||
<script> |
|
||||
import { onMount } from "svelte" |
|
||||
import Portal from "svelte-portal" |
|
||||
import { buildStyle } from "../../../../helpers.js" |
|
||||
|
|
||||
export let options = [] |
|
||||
export let value = "" |
|
||||
export let styleBindingProperty |
|
||||
export let onChange = () => {} |
|
||||
export let placeholder |
|
||||
|
|
||||
let open = null |
|
||||
let rotate = "" |
|
||||
let select |
|
||||
let selectMenu |
|
||||
let icon |
|
||||
let width = 0 |
|
||||
|
|
||||
let selectAnchor = null |
|
||||
let dimensions = { top: 0, bottom: 0, left: 0 } |
|
||||
|
|
||||
let positionSide = "top" |
|
||||
let maxHeight = 0 |
|
||||
let scrollTop = 0 |
|
||||
let containerEl = null |
|
||||
|
|
||||
const handleStyleBind = value => |
|
||||
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {} |
|
||||
|
|
||||
onMount(() => { |
|
||||
if (select) { |
|
||||
select.addEventListener("keydown", handleEnter) |
|
||||
} |
|
||||
|
|
||||
return () => { |
|
||||
select.removeEventListener("keydown", handleEnter) |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
function handleEscape(e) { |
|
||||
if (open && e.key === "Escape") { |
|
||||
toggleSelect(false) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function getDimensions() { |
|
||||
const { |
|
||||
bottom, |
|
||||
top: spaceAbove, |
|
||||
left, |
|
||||
} = selectAnchor.getBoundingClientRect() |
|
||||
const spaceBelow = window.innerHeight - bottom |
|
||||
|
|
||||
let y |
|
||||
|
|
||||
if (spaceAbove > spaceBelow) { |
|
||||
positionSide = "bottom" |
|
||||
maxHeight = spaceAbove - 20 |
|
||||
y = window.innerHeight - spaceAbove |
|
||||
} else { |
|
||||
positionSide = "top" |
|
||||
y = bottom |
|
||||
maxHeight = spaceBelow - 20 |
|
||||
} |
|
||||
|
|
||||
dimensions = { [positionSide]: y, left } |
|
||||
} |
|
||||
|
|
||||
function handleEnter(e) { |
|
||||
if (!open && e.key === "Enter") { |
|
||||
toggleSelect(true) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function toggleSelect(isOpen) { |
|
||||
getDimensions() |
|
||||
if (isOpen) { |
|
||||
icon.style.transform = "rotate(180deg)" |
|
||||
} else { |
|
||||
icon.style.transform = "rotate(0deg)" |
|
||||
} |
|
||||
open = isOpen |
|
||||
} |
|
||||
|
|
||||
function handleClick(val) { |
|
||||
value = val |
|
||||
onChange(value) |
|
||||
toggleSelect(false) |
|
||||
} |
|
||||
|
|
||||
$: menuStyle = buildStyle({ |
|
||||
"max-height": `${maxHeight.toFixed(0)}px`, |
|
||||
"transform-origin": `center ${positionSide}`, |
|
||||
[positionSide]: `${dimensions[positionSide]}px`, |
|
||||
left: `${dimensions.left.toFixed(0)}px`, |
|
||||
width: `${width}px`, |
|
||||
}) |
|
||||
|
|
||||
$: isOptionsObject = options.every(o => typeof o === "object") |
|
||||
|
|
||||
$: selectedOption = isOptionsObject |
|
||||
? options.find(o => o.value === value || (o.value === "" && value == null)) |
|
||||
: {} |
|
||||
|
|
||||
$: if (open && selectMenu) { |
|
||||
selectMenu.focus() |
|
||||
} |
|
||||
|
|
||||
$: displayLabel = |
|
||||
selectedOption && selectedOption.label |
|
||||
? selectedOption.label |
|
||||
: value || placeholder || "Choose option" |
|
||||
</script> |
|
||||
|
|
||||
<div |
|
||||
bind:clientWidth={width} |
|
||||
tabindex="0" |
|
||||
bind:this={select} |
|
||||
class="bb-select-container" |
|
||||
on:click={() => toggleSelect(!open)}> |
|
||||
<div bind:this={selectAnchor} title={value} class="bb-select-anchor selected"> |
|
||||
<span>{displayLabel}</span> |
|
||||
<i bind:this={icon} class="ri-arrow-down-s-fill" /> |
|
||||
</div> |
|
||||
{#if open} |
|
||||
<Portal> |
|
||||
<div |
|
||||
tabindex="0" |
|
||||
class:open |
|
||||
bind:this={selectMenu} |
|
||||
style={menuStyle} |
|
||||
on:keydown={handleEscape} |
|
||||
class="bb-select-menu"> |
|
||||
<ul> |
|
||||
<li |
|
||||
on:click|self={() => handleClick(null)} |
|
||||
class:selected={value == null || value === ''}> |
|
||||
Choose option |
|
||||
</li> |
|
||||
{#if isOptionsObject} |
|
||||
{#each options as { value: v, label }} |
|
||||
<li |
|
||||
{...handleStyleBind(v)} |
|
||||
on:click|self={() => handleClick(v)} |
|
||||
class:selected={value === v}> |
|
||||
{label} |
|
||||
</li> |
|
||||
{/each} |
|
||||
{:else} |
|
||||
{#each options as v} |
|
||||
<li |
|
||||
{...handleStyleBind(v)} |
|
||||
on:click|self={() => handleClick(v)} |
|
||||
class:selected={value === v}> |
|
||||
{v} |
|
||||
</li> |
|
||||
{/each} |
|
||||
{/if} |
|
||||
</ul> |
|
||||
</div> |
|
||||
<div on:click|self={() => toggleSelect(false)} class="overlay" /> |
|
||||
</Portal> |
|
||||
{/if} |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.overlay { |
|
||||
position: fixed; |
|
||||
top: 0; |
|
||||
bottom: 0; |
|
||||
right: 0; |
|
||||
left: 0; |
|
||||
z-index: 1; |
|
||||
} |
|
||||
|
|
||||
.bb-select-container { |
|
||||
outline: none; |
|
||||
cursor: pointer; |
|
||||
overflow: hidden; |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
|
|
||||
.bb-select-anchor { |
|
||||
cursor: pointer; |
|
||||
display: flex; |
|
||||
padding: var(--spacing-s) var(--spacing-m); |
|
||||
background-color: var(--grey-2); |
|
||||
border-radius: var(--border-radius-m); |
|
||||
align-items: center; |
|
||||
white-space: nowrap; |
|
||||
} |
|
||||
|
|
||||
.bb-select-anchor > span { |
|
||||
color: var(--ink); |
|
||||
font-weight: 400; |
|
||||
overflow-x: hidden; |
|
||||
text-overflow: ellipsis; |
|
||||
white-space: nowrap; |
|
||||
font-size: var(--font-size-xs); |
|
||||
flex: 1 1 auto; |
|
||||
} |
|
||||
|
|
||||
.bb-select-anchor > i { |
|
||||
transition: transform 0.13s ease; |
|
||||
transform-origin: center; |
|
||||
width: 20px; |
|
||||
height: 20px; |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.selected { |
|
||||
color: var(--ink); |
|
||||
font-weight: 400; |
|
||||
} |
|
||||
|
|
||||
.bb-select-menu { |
|
||||
position: absolute; |
|
||||
display: flex; |
|
||||
outline: none; |
|
||||
box-sizing: border-box; |
|
||||
flex-direction: column; |
|
||||
opacity: 0; |
|
||||
z-index: 2; |
|
||||
color: var(--ink); |
|
||||
font-weight: 400; |
|
||||
height: fit-content !important; |
|
||||
border-bottom-left-radius: 2px; |
|
||||
border-bottom-right-radius: 2px; |
|
||||
background-color: var(--grey-2); |
|
||||
transform: scale(0); |
|
||||
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1); |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
|
|
||||
.open { |
|
||||
transform: scale(1); |
|
||||
opacity: 1; |
|
||||
} |
|
||||
|
|
||||
ul { |
|
||||
list-style-type: none; |
|
||||
margin: 0; |
|
||||
padding: 5px 0px; |
|
||||
} |
|
||||
|
|
||||
li { |
|
||||
height: auto; |
|
||||
padding: 5px 0px; |
|
||||
cursor: pointer; |
|
||||
padding-left: 10px; |
|
||||
font-size: var(--font-size-xs); |
|
||||
} |
|
||||
|
|
||||
li:hover { |
|
||||
background-color: var(--grey-3); |
|
||||
} |
|
||||
</style> |
|
||||
Loading…
Reference in new issue