Files
windmill/frontend/src/lib/components/select/Select.svelte
T
Diego Imbert fa8d1b47db fix: new MultiSelect component (#5979)
* rename all multiselects to Legacy

* move Select

* Separate SelectDropdown

* fix click outside for select with portal

* multiselect v0

* multiselect clear btn

* move filterText logic to SelectDropdown

* ui nits

* console.log

* draggable

* Draggable multiselect

* multiselect search

* nit refacto

* autofocus multiselect input

* Replace in AppMultiSelectV2

* search icon

* app multi select nits

* arginput update multiselect

* fix autofocus scrolling up

* replace High priority tags multiselect

* autoscaling config editor multiselect replace

* fix clear btn not in border

* replace multiselect in cron input

* replace multiselect in savedinputs

* replace EventHandlerItem multiselect

* select dropdown shadow

* more multiselect migration

* hover opacity on drag

* TokensTable UI fixes + replace multiselect

* ai settings replace multiselect

* DefaultTags Multiselect replace

* prevent multiselect from opening on drag

* nit

* app multiselect css + simplify

* console log

* safeSelectItems cleanup

* Remove svelte-multiselect

* clip when wrap not allowed

* hide duplicate app components

* CSS works better  with multiselect in app editor

* allowOverflow

* allowClear

* fix custom createText messed up with search
2025-06-19 14:37:17 +02:00

157 lines
3.9 KiB
Svelte

<script lang="ts" generics="Item extends { label?: string; value: any; }">
import { clickOutside } from '$lib/utils'
import { twMerge } from 'tailwind-merge'
import CloseButton from '../common/CloseButton.svelte'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { processItems, type ProcessedItem } from './utils.svelte'
import SelectDropdown from './SelectDropdown.svelte'
import { deepEqual } from 'fast-equals'
type Value = Item['value']
let {
items,
placeholder = 'Please select',
value = $bindable(),
filterText: _filterTextBind = $bindable(undefined),
class: className = '',
clearable = false,
listAutoWidth = true,
disabled: _disabled = false,
containerStyle = '',
inputClass = '',
disablePortal = false,
loading = false,
autofocus,
RightIcon,
createText,
noItemsMsg,
groupBy,
sortBy,
onFocus,
onBlur,
onClear,
onCreateItem
}: {
items?: Item[]
value: Value | undefined
placeholder?: string
class?: string
clearable?: boolean
filterText?: string
disabled?: boolean
listAutoWidth?: boolean
containerStyle?: string
inputClass?: string
disablePortal?: boolean
loading?: boolean
autofocus?: boolean
RightIcon?: any
createText?: string
noItemsMsg?: string
groupBy?: (item: Item) => string
sortBy?: (a: Item, b: Item) => number
onFocus?: () => void
onBlur?: () => void
onClear?: () => void
onCreateItem?: (value: string) => void
} = $props()
let disabled = $derived(_disabled || loading)
let filterText = $state<string>('')
let open = $state<boolean>(false)
let inputEl: HTMLInputElement | undefined = $state()
let processedItems: ProcessedItem<Value>[] = $derived.by(() => {
let args = { items, createText, filterText, groupBy, onCreateItem, sortBy }
return untrack(() => processItems(args))
})
$effect(() => {
if (_filterTextBind !== undefined) filterText = _filterTextBind
})
$effect(() => {
if (_filterTextBind !== undefined) _filterTextBind = filterText
})
$effect(() => {
if (filterText) open = true
})
$effect(() => {
if (!open) filterText = ''
})
let valueEntry = $derived(value && processedItems?.find((item) => deepEqual(item.value, value)))
function setValue(item: ProcessedItem<Value>) {
if (item.__is_create && onCreateItem) {
onCreateItem(item.value)
} else {
value = item.value
}
filterText = ''
open = false
}
function clearValue() {
filterText = ''
if (onClear) onClear()
else value = undefined
}
</script>
<div
class={`relative ${className}`}
use:clickOutside={{ onClickOutside: () => (open = false) }}
onpointerdown={() => onFocus?.()}
onfocus={() => onFocus?.()}
onblur={() => onBlur?.()}
>
{#if loading}
<div class="absolute z-10 right-2 h-full flex items-center">
<Loader2 size={18} class="animate-spin" />
</div>
{:else if clearable && !disabled && value}
<div class="absolute z-10 right-2 h-full flex items-center">
<CloseButton noBg small on:close={clearValue} />
</div>
{:else if RightIcon}
<div class="absolute z-10 right-2 h-full flex items-center">
<RightIcon size={18} class="text-tertiary/35" />
</div>
{/if}
<!-- svelte-ignore a11y_autofocus -->
<input
{autofocus}
{disabled}
type="text"
bind:value={() => filterText, (v) => (filterText = v)}
placeholder={loading ? 'Loading...' : (valueEntry?.label ?? placeholder)}
style={containerStyle}
class={twMerge(
'!bg-surface text-ellipsis',
open ? '' : 'cursor-pointer',
valueEntry && !loading ? '!placeholder-primary' : '',
(clearable || RightIcon) && !disabled && value ? '!pr-8' : '',
inputClass ?? ''
)}
autocomplete="off"
onpointerdown={() => (open = true)}
bind:this={inputEl}
/>
<SelectDropdown
{disablePortal}
onSelectValue={setValue}
{open}
{processedItems}
{value}
{disabled}
{filterText}
getInputRect={inputEl && (() => inputEl!.getBoundingClientRect())}
{listAutoWidth}
{noItemsMsg}
/>
</div>