import { Accessor, Component, createEffect, createMemo, createSignal, For, JSX, Show } from "solid-js"; import { useI18n } from "../i18n"; import { CommandType } from "./command"; import { useCommands } from "./context"; import css from "./palette.module.css"; import { split_by_filter } from "~/utilities"; export interface CommandPaletteApi { readonly open: Accessor; show(): void; hide(): void; } export const CommandPalette: Component<{ api?: (api: CommandPaletteApi) => any, onSubmit?: SubmitHandler }> = (props) => { const [open, setOpen] = createSignal(false); const [root, setRoot] = createSignal(); const [search, setSearch] = createSignal>(); const context = useCommands(); const { t } = useI18n(); if (!context) { console.log('context is missing...'); } const api = { open, show() { setOpen(true); }, hide() { setOpen(false); }, }; createEffect(() => { props.api?.(api); }); createEffect(() => { const isOpen = open(); if (isOpen) { search()?.clear(); root()?.showModal(); } else { root()?.close(); } }); const onSubmit = (command: CommandType) => { setOpen(false); props.onSubmit?.(command); command(); }; return setOpen(false)}> t(item.label) as string} context={setSearch} onSubmit={onSubmit}>{ (item, ctx) => { const label = t(item.label) as string; const filter = ctx.filter().toLowerCase(); return { ([is_hit, part]) => {part} }; } } ; }; interface SubmitHandler { (item: T): any; } interface SearchContext { readonly filter: Accessor; readonly results: Accessor; readonly value: Accessor; searchFor(term: string): void; clear(): void; } interface SearchableListProps { items: T[]; title?: string; keySelector(item: T): string; filter?: (item: T, search: string) => boolean; children(item: T, context: SearchContext): JSX.Element; context?: (context: SearchContext) => any, onSubmit?: SubmitHandler; } function SearchableList(props: SearchableListProps): JSX.Element { const [term, setTerm] = createSignal(''); const [selected, setSelected] = createSignal(0); const id = createUniqueId(); const results = createMemo(() => { const search = term(); if (search === '') { return []; } return props.items.filter(item => props.filter ? props.filter(item, search) : props.keySelector(item).toLowerCase().includes(search.toLowerCase())); }); const value = createMemo(() => results().at(selected())); const ctx = { filter: term, results, value, searchFor(term: string) { setTerm(term); }, clear() { setTerm(''); setSelected(0); }, }; createEffect(() => { props.context?.(ctx); }); createEffect(() => { const length = results().length - 1; setSelected(current => Math.min(current, length)); }); const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowUp') { setSelected(current => Math.max(0, current - 1)); e.preventDefault(); } if (e.key === 'ArrowDown') { setSelected(current => Math.min(results().length - 1, current + 1)); e.preventDefault(); } }; const onSubmit = (e: SubmitEvent) => { e.preventDefault(); const v = value(); if (v === undefined) { return; } ctx.clear(); props.onSubmit?.(v); }; return
setTerm(e.target.value)} placeholder="start typing for command" autofocus autocomplete="off" enterkeyhint="go" /> { (result, index) =>
{props.children(result, ctx)}
}
; }; let keyCounter = 0; const createUniqueId = () => `key-${keyCounter++}`;