forked from tsai/budibase
1 changed files with 0 additions and 354 deletions
@ -1,354 +0,0 @@ |
|||||
<script> |
|
||||
import { onMount, afterUpdate } from "svelte" |
|
||||
import { fly } from "svelte/transition" |
|
||||
import { Label } from "@budibase/bbui" |
|
||||
import buildStyle from "@budibase/bbui/src/utils/buildStyle" |
|
||||
const xPath = |
|
||||
"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" |
|
||||
|
|
||||
export let value = [] |
|
||||
export let label = undefined |
|
||||
export let secondary = false |
|
||||
export let outline = false |
|
||||
export let disabled = false |
|
||||
export let placeholder = undefined |
|
||||
|
|
||||
let options = [] |
|
||||
let optionsVisible = false |
|
||||
let selected = {} |
|
||||
let first = true |
|
||||
let slot |
|
||||
let anchor |
|
||||
let dimensions = { top: 0, left: 0, minWidth: 0, maxHeight: 0 } |
|
||||
|
|
||||
onMount(() => { |
|
||||
const domOptions = Array.from(slot.querySelectorAll("option")) |
|
||||
options = domOptions.map(option => ({ |
|
||||
value: option.value, |
|
||||
name: option.textContent, |
|
||||
})) |
|
||||
if (value) { |
|
||||
options.forEach(option => { |
|
||||
if (value.includes(option.value)) { |
|
||||
selected[option.value] = option |
|
||||
} |
|
||||
}) |
|
||||
} |
|
||||
first = false |
|
||||
}) |
|
||||
|
|
||||
// Update dimensions after each dom update |
|
||||
afterUpdate(() => { |
|
||||
if (anchor) { |
|
||||
updateDimensions() |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
// Keep value up to date with selected options |
|
||||
$: { |
|
||||
if (!first) { |
|
||||
value = Object.values(selected).map(option => option.value) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Build CSS styles for options dropdown |
|
||||
$: menuStyle = buildStyle({ |
|
||||
"max-height": `${dimensions.maxHeight.toFixed(0)}px`, |
|
||||
"min-width": `${dimensions.minWidth.toFixed(0)}px`, |
|
||||
top: `${dimensions.top}px`, |
|
||||
left: `${dimensions.left}px`, |
|
||||
}) |
|
||||
|
|
||||
function updateDimensions() { |
|
||||
const { bottom, left, width } = anchor.getBoundingClientRect() |
|
||||
dimensions = { |
|
||||
top: bottom, |
|
||||
left, |
|
||||
minWidth: width - 4, |
|
||||
maxHeight: window.innerHeight - bottom - 30, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function add(token) { |
|
||||
selected[token.value] = token |
|
||||
} |
|
||||
|
|
||||
function remove(value) { |
|
||||
const { [value]: val, ...rest } = selected |
|
||||
selected = rest |
|
||||
} |
|
||||
|
|
||||
function showOptions(show) { |
|
||||
optionsVisible = show |
|
||||
} |
|
||||
|
|
||||
function handleClick() { |
|
||||
showOptions(!optionsVisible) |
|
||||
} |
|
||||
|
|
||||
function handleOptionMousedown(e) { |
|
||||
const value = e.target.dataset.value |
|
||||
if (value == null) { |
|
||||
return |
|
||||
} |
|
||||
if (selected[value]) { |
|
||||
remove(value) |
|
||||
} else { |
|
||||
add(options.filter(option => option.value === value)[0]) |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div> |
|
||||
{#if label} |
|
||||
<Label extraSmall grey>{label}</Label> |
|
||||
{/if} |
|
||||
<div class="multiselect"> |
|
||||
<div class="tokens-wrapper"> |
|
||||
<div |
|
||||
bind:this={anchor} |
|
||||
class="tokens" |
|
||||
class:outline |
|
||||
class:disabled |
|
||||
class:secondary |
|
||||
class:optionsVisible |
|
||||
on:click|self={handleClick} |
|
||||
class:empty={!value || !value.length}> |
|
||||
{#each Object.values(selected) as option} |
|
||||
<div |
|
||||
class="token" |
|
||||
class:secondary |
|
||||
data-id={option.value} |
|
||||
on:click|self={handleClick}> |
|
||||
<span>{option.name}</span> |
|
||||
<div |
|
||||
class="token-remove" |
|
||||
title="Remove {option.name}" |
|
||||
on:click={() => remove(option.value)}> |
|
||||
<svg |
|
||||
class="icon-clear" |
|
||||
xmlns="http://www.w3.org/2000/svg" |
|
||||
width="18" |
|
||||
height="18" |
|
||||
viewBox="0 0 24 24"> |
|
||||
<path d={xPath} /> |
|
||||
</svg> |
|
||||
</div> |
|
||||
</div> |
|
||||
{/each} |
|
||||
{#if !value || !value.length} |
|
||||
{#if placeholder && placeholder.length} |
|
||||
<div class:disabled class="placeholder">{placeholder}</div> |
|
||||
{:else} |
|
||||
<div class="placeholder"> </div> |
|
||||
{/if} |
|
||||
{/if} |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<select bind:this={slot} type="multiple" class="hidden"> |
|
||||
<slot /> |
|
||||
</select> |
|
||||
|
|
||||
{#if optionsVisible} |
|
||||
<div class="options-overlay" on:mousedown|self={() => showOptions(false)}> |
|
||||
<ul |
|
||||
class="options" |
|
||||
style={menuStyle} |
|
||||
transition:fly={{ duration: 200, y: 5 }} |
|
||||
on:mousedown|preventDefault={handleOptionMousedown}> |
|
||||
{#each options as option} |
|
||||
<li |
|
||||
class:selected={selected[option.value]} |
|
||||
data-value={option.value}> |
|
||||
{option.name} |
|
||||
</li> |
|
||||
{/each} |
|
||||
{#if !options.length} |
|
||||
<li class="no-results">No results</li> |
|
||||
{/if} |
|
||||
</ul> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.multiselect { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: flex-start; |
|
||||
align-items: stretch; |
|
||||
font-family: var(--font-sans); |
|
||||
} |
|
||||
.multiselect:hover { |
|
||||
border-bottom-color: hsl(0, 0%, 50%); |
|
||||
} |
|
||||
|
|
||||
.tokens-wrapper { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: flex-start; |
|
||||
align-items: center; |
|
||||
flex: 0 1 auto; |
|
||||
} |
|
||||
|
|
||||
.tokens { |
|
||||
align-items: center; |
|
||||
display: flex; |
|
||||
flex-wrap: wrap; |
|
||||
position: relative; |
|
||||
width: 0; |
|
||||
flex: 1 1 auto; |
|
||||
background-color: white; |
|
||||
border-radius: var(--border-radius-m); |
|
||||
padding: 0 var(--spacing-m) calc(var(--spacing-m) - var(--spacing-xs)) |
|
||||
calc(var(--spacing-m) / 2); |
|
||||
border: var(--border-transparent); |
|
||||
} |
|
||||
.tokens.disabled { |
|
||||
background-color: var(--grey-4); |
|
||||
pointer-events: none; |
|
||||
} |
|
||||
.tokens.outline { |
|
||||
border: var(--border-dark); |
|
||||
} |
|
||||
.tokens.secondary { |
|
||||
background-color: var(--grey-2); |
|
||||
} |
|
||||
.tokens:hover { |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
.tokens::after { |
|
||||
background: none repeat scroll 0 0 transparent; |
|
||||
bottom: -1px; |
|
||||
content: ""; |
|
||||
display: block; |
|
||||
height: 2px; |
|
||||
left: 50%; |
|
||||
position: absolute; |
|
||||
transition: width 0.3s ease 0s, left 0.3s ease 0s; |
|
||||
width: 0; |
|
||||
} |
|
||||
.tokens.optionsVisible { |
|
||||
border: var(--border-blue); |
|
||||
} |
|
||||
.tokens.empty { |
|
||||
padding: var(--spacing-m); |
|
||||
font-size: var(--font-size-xs); |
|
||||
user-select: none; |
|
||||
} |
|
||||
.tokens::after { |
|
||||
width: 100%; |
|
||||
left: 0; |
|
||||
} |
|
||||
|
|
||||
.token { |
|
||||
font-size: var(--font-size-xs); |
|
||||
background-color: var(--grey-2); |
|
||||
border-radius: var(--border-radius-l); |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
align-items: center; |
|
||||
margin: calc(var(--spacing-m) - var(--spacing-xs)) 0 0 |
|
||||
calc(var(--spacing-m) / 2); |
|
||||
max-height: 1.3rem; |
|
||||
padding: var(--spacing-xs) var(--spacing-s); |
|
||||
transition: background-color 0.3s; |
|
||||
white-space: nowrap; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
.token.secondary { |
|
||||
background-color: white; |
|
||||
} |
|
||||
.token span { |
|
||||
pointer-events: none; |
|
||||
user-select: none; |
|
||||
flex: 1 1 auto; |
|
||||
overflow: hidden; |
|
||||
white-space: nowrap; |
|
||||
text-overflow: ellipsis; |
|
||||
} |
|
||||
.token .token-remove { |
|
||||
align-items: center; |
|
||||
background-color: var(--grey-4); |
|
||||
border-radius: 50%; |
|
||||
color: var(--white); |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
height: 1rem; |
|
||||
width: 1rem; |
|
||||
margin: calc(-1 * var(--spacing-xs)) 0 calc(-1 * var(--spacing-xs)) |
|
||||
var(--spacing-xs); |
|
||||
flex: 0 0 auto; |
|
||||
} |
|
||||
.token .token-remove:hover { |
|
||||
background-color: var(--grey-5); |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.placeholder { |
|
||||
pointer-events: none; |
|
||||
} |
|
||||
.placeholder.disabled { |
|
||||
color: var(--grey-6); |
|
||||
} |
|
||||
|
|
||||
.icon-clear path { |
|
||||
fill: white; |
|
||||
} |
|
||||
|
|
||||
.options-overlay { |
|
||||
position: fixed; |
|
||||
top: 0; |
|
||||
left: 0; |
|
||||
width: 100vw; |
|
||||
height: 100vh; |
|
||||
z-index: 999; |
|
||||
} |
|
||||
|
|
||||
.options { |
|
||||
left: 0; |
|
||||
list-style: none; |
|
||||
margin-block-end: 0; |
|
||||
margin-block-start: 0; |
|
||||
overflow-y: auto; |
|
||||
padding-inline-start: 0; |
|
||||
position: absolute; |
|
||||
border: var(--border-dark); |
|
||||
border-radius: var(--border-radius-m); |
|
||||
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.15); |
|
||||
margin: var(--spacing-xs) 0; |
|
||||
padding: var(--spacing-s) 0; |
|
||||
background-color: white; |
|
||||
max-height: 200px; |
|
||||
} |
|
||||
li { |
|
||||
background-color: white; |
|
||||
cursor: pointer; |
|
||||
padding: var(--spacing-s) var(--spacing-m); |
|
||||
font-size: var(--font-size-xs); |
|
||||
} |
|
||||
li.selected { |
|
||||
background-color: var(--blue); |
|
||||
color: white; |
|
||||
} |
|
||||
li:not(.selected):hover { |
|
||||
background-color: var(--grey-1); |
|
||||
} |
|
||||
li.no-results:hover { |
|
||||
background-color: white; |
|
||||
cursor: initial; |
|
||||
} |
|
||||
|
|
||||
.hidden { |
|
||||
height: 0; |
|
||||
overflow: hidden; |
|
||||
visibility: hidden; |
|
||||
padding: 0; |
|
||||
margin: 0; |
|
||||
border: 0; |
|
||||
outline: 0; |
|
||||
} |
|
||||
</style> |
|
||||
Loading…
Reference in new issue