Skip to content

Instantly share code, notes, and snippets.

@orblazer
Created April 13, 2022 02:14
Show Gist options
  • Save orblazer/d5d307444a2607c64744bbf56ebbc35b to your computer and use it in GitHub Desktop.
Save orblazer/d5d307444a2607c64744bbf56ebbc35b to your computer and use it in GitHub Desktop.
<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