import type { Accessor, JSX } from 'solid-js'; import { createComponent, createContext, createMemo, createSignal, untrack, useContext, } from 'solid-js'; import assert from '../utils/assert'; import isEqual from '../utils/is-equal'; import type { Ref } from '../utils/types'; export interface SelectStateProperties { isSelected(value: T): boolean; select(value: T): void; hasSelected(): boolean; isActive(value: T): boolean; hasActive(): boolean; focus(value: T): void; blur(): void; disabled(): boolean; } export interface SingleSelectStateControlledOptions { multiple?: false; toggleable?: boolean; value: T; onChange?: (value?: T) => void; disabled?: boolean; by?: (a: T, b: T) => boolean; } export interface SingleSelectStateUncontrolledOptions { multiple?: false; toggleable?: boolean; defaultValue: T; onChange?: (value?: T) => void; disabled?: boolean; by?: (a: T, b: T) => boolean; } export type SingleSelectStateOptions = | SingleSelectStateControlledOptions | SingleSelectStateUncontrolledOptions; export function createSingleSelectState( options: SingleSelectStateOptions, ): SelectStateProperties { const [active, setActive] = createSignal>(); let selectedValue: Accessor; let setSelectedValue: (value: T | undefined) => void; const equals = options.by || isEqual; if ('defaultValue' in options) { const [selected, setSelected] = createSignal( options.defaultValue, ); selectedValue = selected; setSelectedValue = (value): void => { setSelected(() => value); if (options.onChange) { options.onChange(value); } }; } else { selectedValue = createMemo(() => options.value); setSelectedValue = (value): void => { if (options.onChange) { options.onChange(value); } }; } const isDisabled = createMemo(() => !!options.disabled); return { isSelected(value): boolean { return isEqual(value, selectedValue()); }, select(value): void { if (!untrack(isDisabled)) { if (options.toggleable && equals(untrack(selectedValue) as T, value)) { setSelectedValue(undefined); } else { setSelectedValue(value); } } }, hasSelected(): boolean { return selectedValue() != null; }, disabled: isDisabled, hasActive(): boolean { return !!active(); }, isActive(value): boolean { const ref = active(); return ref ? equals(value, ref.value) : false; }, focus(value): void { if (!untrack(isDisabled)) { setActive({ value, }); } }, blur(): void { if (!untrack(isDisabled)) { setActive(undefined); } }, }; } export interface MultipleSelectStateControlledOptions { multiple: true; toggleable?: boolean; value: T[]; onChange?: (value: T[]) => void; disabled?: boolean; by?: (a: T, b: T) => boolean; } export interface MultipleSelectStateUncontrolledOptions { multiple: true; toggleable?: boolean; defaultValue: T[]; onChange?: (value: T[]) => void; disabled?: boolean; by?: (a: T, b: T) => boolean; } export type MultipleSelectStateOptions = | MultipleSelectStateControlledOptions | MultipleSelectStateUncontrolledOptions; export function createMultipleSelectState( options: MultipleSelectStateOptions, ): SelectStateProperties { const [active, setActive] = createSignal>(); let selectedValues: Accessor; let setSelectedValues: (value: T[]) => void; const equals = options.by || isEqual; if ('defaultValue' in options) { const [selected, setSelected] = createSignal(options.defaultValue); selectedValues = selected; setSelectedValues = (value): void => { setSelected(() => value); if (options.onChange) { options.onChange(value); } }; } else { selectedValues = createMemo(() => options.value); setSelectedValues = (value): void => { if (options.onChange) { options.onChange(value); } }; } const isDisabled = createMemo(() => !!options.disabled); return { isSelected(value): boolean { const values = selectedValues(); // Looks up for the value for (let i = 0, len = values.length; i < len; i += 1) { if (equals(value, values[i])) { return true; } } return false; }, select(value): void { if (!untrack(isDisabled)) { const newValues: T[] = []; const currentValues = untrack(selectedValues); let hasValue = false; for (let i = 0, len = currentValues.length; i < len; i += 1) { const item = currentValues[i]; // Compare ahead const isSame = equals(item, value); // If it's the same then we mark the the target value // as already existing in the array if (isSame) { hasValue = true; } // If it's the same and the list is toggleable // don't push the item if (!(options.toggleable && isSame)) { newValues.push(item); } } // The value doesn't exist, push the new value if (!hasValue) { newValues.push(value); } setSelectedValues(newValues); } }, hasSelected: createMemo(() => selectedValues().length > 0), disabled: isDisabled, hasActive: createMemo(() => !!active()), isActive(value): boolean { const ref = active(); if (ref) { return equals(value, ref.value); } return false; }, focus(value): void { if (!untrack(isDisabled)) { setActive({ value, }); } }, blur(): void { if (!untrack(isDisabled)) { setActive(undefined); } }, }; } export interface SelectStateRenderProps { children?: JSX.Element | ((state: SelectStateProperties) => JSX.Element); } export interface SelectStateProviderProps extends SelectStateRenderProps { state: SelectStateProperties; } const SelectStateContext = createContext>(); export function SelectStateProvider( props: SelectStateProviderProps, ): JSX.Element { return createComponent(SelectStateContext.Provider, { value: props.state, get children() { const current = props.children; if (typeof current === 'function') { return current(props.state); } return current; }, }); } export function useSelectState(): SelectStateProperties { const ctx = useContext(SelectStateContext); assert(ctx, new Error('Missing ')); return ctx; } export function SelectStateChild( props: SelectStateRenderProps, ): JSX.Element { const state = useSelectState(); return createMemo(() => { const current = props.children; if (typeof current === 'function' && current.length === 1) { return createMemo(() => current(state)); } return current; }) as unknown as JSX.Element; }