-
-
Save orblazer/d5d307444a2607c64744bbf56ebbc35b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { deepEqual } from 'fast-equals' | |
/* Types */ | |
interface GroupBase<Option> { | |
readonly options: readonly Option[] | |
readonly label: string | |
} | |
type OptionsOrGroups<Option, Group extends GroupBase<Option> = GroupBase<Option>> = readonly (Option | Group)[] | |
type Options<Option> = readonly Option[] | |
type SingleValue<Option> = Option | null | |
type MultiValue<Option> = readonly Option[] | |
type PropsValue<Option, IsMulti extends boolean> = IsMulti extends true ? MultiValue<Option> : SingleValue<Option> | |
// Internal | |
interface CategorizedOption<Option> { | |
type: 'option' | |
data: Option | |
isDisabled: boolean | |
isSelected: boolean | |
label: string | |
value: string | |
index: number | |
} | |
interface CategorizedGroup<Option, Group extends GroupBase<Option>> { | |
type: 'group' | |
data: Group | |
options: readonly CategorizedOption<Option>[] | |
index: number | |
} | |
type CategorizedGroupOrOption<Option, Group extends GroupBase<Option>> = | |
| CategorizedGroup<Option, Group> | |
| CategorizedOption<Option> | |
// Svelte | |
type Option = $$Generic | |
type IsMulti = $$Generic<boolean> | |
type Group = GroupBase<Option> // $$Generic<GroupBase<Option>> | |
interface $$Props { | |
/** Array of options that populate the select menu */ | |
options: OptionsOrGroups<Option, Group> | |
/** Support multiple selected options */ | |
isMulti?: IsMulti | |
value: PropsValue<Option, IsMulti> | |
} | |
interface $$Slots { | |
default: never | |
group?: { | |
group: Group | |
} | |
option?: { | |
option: Option | |
} | |
} | |
/* Props */ | |
export let value: $$Props['value'] | |
export let options: $$Props['options'] | |
export let isMulti: $$Props['isMulti'] = false as IsMulti // FIXME: @typescript-eslint/no-unsafe-assignment | |
/* State */ | |
// Clean value (updated on `value`) | |
let _value: Options<Option> | |
$: { | |
const val = ( | |
Array.isArray(value) ? value.filter(Boolean) : typeof value === 'object' && value !== null ? [value] : [] | |
) as Options<Option> | |
if (!deepEqual(_value, val)) _value = val | |
} | |
// Check if option is selected (updated on `isOptionSelected`) | |
let _isOptionSelected: (option: Option, selectValue: Options<Option>) => boolean | |
$: _isOptionSelected = (option, selectValue) => { | |
if (selectValue.indexOf(option) > -1) return true | |
return _value.some((option) => (option as { value?: string }).value === (selectValue as { value?: string }).value) | |
} | |
// Convert option to categorized option (updated on `isOptionDisabled`, `_isOptionSelected`, `getOptionLabel`, `getOptionValue`) | |
let toCategorizedOption: (option: Option, index: number, selectValue: Options<Option>) => CategorizedOption<Option> | |
$: toCategorizedOption = (option, index, selectValue) => ({ | |
type: 'option', | |
data: option, | |
isDisabled: (option as { isDisabled?: boolean }).isDisabled, | |
isSelected: _isOptionSelected(option, selectValue), | |
label: (option as { label?: string }).label, | |
value: (option as { value?: string }).value, | |
index | |
}) | |
// Categorized options (updated on `options`, `_value`, `inputValue`, `isMulti`, `filterOption`, `toCategorizedOption`) | |
let categorizedOptions: CategorizedGroupOrOption<Option, Group>[] | |
$: categorizedOptions = options.reduce<CategorizedGroupOrOption<Option, Group>[]>( | |
(acc, groupOrOption, groupOrOptionIndex) => { | |
// Process group | |
if ('options' in groupOrOption) { | |
const categorizedOptions = groupOrOption.options | |
// FIXME: nex-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-caall, @typescript-eslint/no-unsafe-argument | |
.map((option, index) => toCategorizedOption(option, index, _value)) | |
.filter(({ isSelected }) => !isMulti || !isSelected) | |
return acc.concat( | |
categorizedOptions.length > 0 | |
? [ | |
{ | |
type: 'group', | |
data: groupOrOption, // FIXME: @typescript-eslint/no-unsafe-assignment | |
options: categorizedOptions, // FIXME: @typescript-eslint/no-unsafe-assignment | |
index: groupOrOptionIndex | |
} | |
] | |
: [] | |
) | |
} | |
// Process option | |
const categorizedOption = toCategorizedOption(groupOrOption, groupOrOptionIndex, _value) | |
if (!isMulti || !categorizedOption.isSelected) { | |
acc.push(categorizedOption) | |
} | |
return acc | |
}, | |
[] | |
) | |
</script> | |
<ul> | |
{#each categorizedOptions as item (item.index)} | |
<!-- FIXME: nex-line @typescript-eslint/restrict-template-expressions --> | |
{@const id = `${item.index}`} | |
{#if item.type === 'group'} | |
<ul> | |
<li id="item-{item.index}"> | |
<slot name="group" group={item.data}> | |
<span>{item.data}</span> | |
</slot> | |
</li> | |
{#each item.options as groupItem (`${item.index}-${groupItem.index}`)} | |
<!-- FIXME: nex-line @typescript-eslint/restrict-template-expressions --> | |
{@const id = `${item.index}-${groupItem.index}`} | |
<li {id}> | |
<slot name="option" option={groupItem.data}> | |
{groupItem.data} | |
</slot> | |
</li> | |
{/each} | |
</ul> | |
{:else} | |
<li {id}> | |
<slot name="option" option={item.data}> | |
{item.data} | |
</slot> | |
</li> | |
{/if} | |
{/each} | |
</ul> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment