diff --git a/comps/ui/typeahead.tsx b/comps/ui/typeahead.tsx index 2af03ef..2bb4e5e 100755 --- a/comps/ui/typeahead.tsx +++ b/comps/ui/typeahead.tsx @@ -39,405 +39,384 @@ export const Typeahead: FC<{ disabled, onChange, }) => { - const local = useLocal({ - value: [] as string[], - open: false, - options: [] as { value: string; label: string }[], - loaded: false, - search: { - input: "", - timeout: null as any, - searching: false, - promise: null as any, - result: null as null | { value: string; label: string }[], - }, - unique: typeof unique === "undefined" ? true : unique, - allow_new: typeof allow_new === "undefined" ? false : allow_new, - on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open, - local_search: typeof local_search === "undefined" ? true : local_search, - mode: typeof mode === "undefined" ? "multi" : mode, - auto_popup_width: - typeof auto_popup_width === "undefined" ? false : auto_popup_width, - select: null as null | { value: string; label: string }, - }); - const input = useRef(null); - - let select_found = false; - let options = [...(local.search.result || local.options)]; - if (local.allow_new && local.search.input) { - options.push({ value: local.search.input, label: local.search.input }); - } - const added = new Set(); - if (local.mode === "multi") { - options = options.filter((e) => { - if (!added.has(e.value)) added.add(e.value); - else return false; - if (local.select && local.select.value === e.value) select_found = true; - if (local.unique) { - if (local.value.includes(e.value)) { - return false; - } - } - return true; + const local = useLocal({ + value: [] as string[], + open: false, + options: [] as { value: string; label: string }[], + loaded: false, + loading: false, + search: { + input: "", + timeout: null as any, + searching: false, + promise: null as any, + result: null as null | { value: string; label: string }[], + }, + unique: typeof unique === "undefined" ? true : unique, + allow_new: typeof allow_new === "undefined" ? false : allow_new, + on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open, + local_search: typeof local_search === "undefined" ? true : local_search, + mode: typeof mode === "undefined" ? "multi" : mode, + auto_popup_width: + typeof auto_popup_width === "undefined" ? false : auto_popup_width, + select: null as null | { value: string; label: string }, }); + const input = useRef(null); - if (!select_found) { - local.select = options[0]; + let select_found = false; + let options = [...(local.search.result || local.options)]; + if (local.allow_new && local.search.input) { + options.push({ value: local.search.input, label: local.search.input }); } - } - - useEffect(() => { - if (!isEditor) { - if (typeof value === "object" && value) { - local.value = value; - local.render(); - } - } - }, [value]); - - const select = useCallback( - (arg: { - search: string; - item?: null | { value: string; label: string }; - }) => { - if (!local.allow_new) { - let found = null; - if (!arg.item) { - found = options.find((e) => e.value === arg.search); - } else { - found = options.find((e) => e.value === arg.item?.value); - } - if (!found) { - return false; - } - } - - if (local.unique) { - let found = local.value.find((e) => { - return e === arg.item?.value || arg.search === e; - }); - if (found) { - return false; - } - } - - if (typeof onSelect === "function") { - const result = onSelect(arg); - - if (result) { - local.value.push(result); - local.render(); - return result; - } else { - return false; - } - } else { - let val = false as any; - if (arg.item) { - local.value.push(arg.item.value); - val = arg.item.value; - } else { - if (!arg.search) return false; - local.value.push(arg.search); - val = arg.search; - } - - if (typeof onChange === "function") { - onChange(local.value); - } - local.render(); - return val; - } - return true; - }, - [onSelect, local.value, options] - ); - - const keydown = useCallback( - (e: KeyboardEvent) => { - if (!local.open) { - e.preventDefault(); - e.stopPropagation(); - local.open = true; - local.render(); - return; - } - - if (e.key === "Backspace") { - if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { - local.value.pop(); - local.render(); - } - } - if (e.key === "Enter") { - e.preventDefault(); - e.stopPropagation(); - - const selected = select({ - search: local.search.input, - item: local.select, - }); - - if (local.mode === "single") { - local.open = false; - } - if (typeof selected === "string") { - resetSearch(); - if (local.mode === "single") { - const item = local.options.find((item) => item.value === selected); - if (item) { - local.search.input = item.label; - } + const added = new Set(); + if (local.mode === "multi") { + options = options.filter((e) => { + if (!added.has(e.value)) added.add(e.value); + else return false; + if (local.select && local.select.value === e.value) select_found = true; + if (local.unique) { + if (local.value.includes(e.value)) { + return false; } } - local.render(); + return true; + }); - return; + if (!select_found) { + local.select = options[0]; } - if (options.length > 0) { - local.open = true; - if (e.key === "ArrowDown") { - e.preventDefault(); - const idx = options.findIndex((item) => { - if (item.value === local.select?.value) return true; + } + + useEffect(() => { + if (!isEditor) { + if (local.options.length === 0) { + loadOptions().then(() => { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } }); - if (idx >= 0) { - if (idx + 1 <= options.length) { - local.select = options[idx + 1]; + } else { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } + } + } + }, [value]); + + const select = useCallback( + (arg: { + search: string; + item?: null | { value: string; label: string }; + }) => { + if (!local.allow_new) { + let found = null; + if (!arg.item) { + found = options.find((e) => e.value === arg.search); + } else { + found = options.find((e) => e.value === arg.item?.value); + } + if (!found) { + return false; + } + } + + if (local.unique) { + let found = local.value.find((e) => { + return e === arg.item?.value || arg.search === e; + }); + if (found) { + return false; + } + } + + if (typeof onSelect === "function") { + const result = onSelect(arg); + + if (result) { + local.value.push(result); + local.render(); + return result; + } else { + return false; + } + } else { + let val = false as any; + if (arg.item) { + local.value.push(arg.item.value); + val = arg.item.value; + } else { + if (!arg.search) return false; + local.value.push(arg.search); + val = arg.search; + } + + if (typeof onChange === "function") { + onChange(local.value); + } + local.render(); + return val; + } + return true; + }, + [onSelect, local.value, options] + ); + + const keydown = useCallback( + (e: KeyboardEvent) => { + if (!local.open) { + e.preventDefault(); + e.stopPropagation(); + local.open = true; + local.render(); + return; + } + + if (e.key === "Backspace") { + if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { + local.value.pop(); + local.render(); + } + } + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + + const selected = select({ + search: local.search.input, + item: local.select, + }); + + if (local.mode === "single") { + local.open = false; + } + if (typeof selected === "string") { + resetSearch(); + if (local.mode === "single") { + const item = local.options.find((item) => item.value === selected); + if (item) { + local.search.input = item.label; + } + } + } + local.render(); + + return; + } + if (options.length > 0) { + local.open = true; + if (e.key === "ArrowDown") { + e.preventDefault(); + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx + 1 <= options.length) { + local.select = options[idx + 1]; + } else { + local.select = options[0]; + } } else { local.select = options[0]; } - } else { - local.select = options[0]; + local.render(); } - local.render(); - } - if (e.key === "ArrowUp") { - e.preventDefault(); + if (e.key === "ArrowUp") { + e.preventDefault(); - const idx = options.findIndex((item) => { - if (item.value === local.select?.value) return true; - }); - if (idx >= 0) { - if (idx - 1 >= 0) { - local.select = options[idx - 1]; + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx - 1 >= 0) { + local.select = options[idx - 1]; + } else { + local.select = options[options.length - 1]; + } } else { - local.select = options[options.length - 1]; + local.select = options[0]; } + local.render(); + } + } + }, + [local.value, local.select, select, options, local.search.input] + ); + + const loadOptions = useCallback(async () => { + if (typeof options_fn === "function" && !local.loading) { + local.loading = true; + local.loaded = false; + local.render(); + const res = options_fn({ + search: local.search.input, + existing: local.options, + }); + if (res) { + const applyOptions = ( + result: (string | { value: string; label: string })[] + ) => { + local.options = result.map((item) => { + if (typeof item === "string") return { value: item, label: item }; + return item; + }); + local.render(); + }; + if (res instanceof Promise) { + applyOptions(await res); } else { - local.select = options[0]; + applyOptions(res); } + local.loaded = true; + local.loading = false; local.render(); } } - }, - [local.value, local.select, select, options, local.search.input] - ); + }, [options_fn]); - const openOptions = useCallback(async () => { - if (typeof options_fn === "function") { - local.loaded = true; - const res = options_fn({ - search: local.search.input, - existing: local.options, - }); - if (res) { - const applyOptions = ( - result: (string | { value: string; label: string })[] - ) => { - local.options = result.map((item) => { - if (typeof item === "string") return { value: item, label: item }; - return item; - }); - local.render(); - }; - if (res instanceof Promise) { - applyOptions(await res); - } else { - applyOptions(res); + const resetSearch = () => { + local.search.searching = false; + local.search.input = ""; + local.search.promise = null; + local.search.result = null; + local.select = null; + clearTimeout(local.search.timeout); + }; + + if (local.mode === "single" && local.value.length > 1) { + local.value = [local.value.pop() || ""]; + } + const valueLabel = local.value.map((value) => { + const item = local.options.find((item) => item.value === value); + + if (local.mode === "single") { + if (!local.open) { + local.select = item || null; + local.search.input = item?.label || ""; } } - } - }, [options_fn]); + return item; + }); - const resetSearch = () => { - local.search.searching = false; - local.search.input = ""; - local.search.promise = null; - local.search.result = null; - local.select = null; - clearTimeout(local.search.timeout); - }; - - if (local.mode === "single" && local.value.length > 1) { - local.value = [local.value.pop() || ""]; - } - const valueLabel = local.value.map((value) => { - const item = local.options.find((item) => item.value === value); - - if (local.mode === "single") { - if (!local.open) { - local.select = item || null; - local.search.input = item?.label || ""; - } - } - return item; - }); - - return ( -
{ - input.current?.focus(); - }} - > - {local.mode === "multi" ? ( - <> - {valueLabel.map((e, idx) => { - return ( - { - ev.stopPropagation(); - ev.preventDefault(); - local.value = local.value.filter((val) => e?.value !== val); - local.render(); - input.current?.focus(); - }} - > -
{e?.label}
- -
- ); - })} - - ) : ( - <> - )} - - { - if (!open) { - local.select = null; - } - local.open = open; - local.render(); + return ( +
{ + input.current?.focus(); }} - open={local.open} - options={options} - searching={local.search.searching} - onSelect={(value) => { - local.open = false; + > + {local.mode === "multi" ? ( + <> + {valueLabel.map((e, idx) => { + return ( + { + ev.stopPropagation(); + ev.preventDefault(); + local.value = local.value.filter((val) => e?.value !== val); + local.render(); + input.current?.focus(); + }} + > +
{e?.label}
+ +
+ ); + })} + + ) : ( + <> + )} - resetSearch(); - if (local.mode === "single") { + { + if (!open) { + local.select = null; + } + local.open = open; + local.render(); + }} + open={local.open} + options={options} + searching={local.search.searching} + onSelect={(value) => { + local.open = false; + + resetSearch(); const item = local.options.find((item) => item.value === value); if (item) { - local.search.input = item.label; - + let search = local.search.input; + if (local.mode === "single") { + local.search.input = item.label; + } else { + local.search.input = ""; + } + select({ - search: local.search.input, + search, item, }); } - } - local.render(); - }} - width={local.auto_popup_width ? input.current?.offsetWidth : undefined} - selected={({ item, options, idx }) => { - if (item.value === local.select?.value) { - return true; - } - return false; - }} - > - { - e.stopPropagation(); - - if (!local.open) { - if (local.on_focus_open) { - openOptions(); - local.open = true; - local.render(); - } - } + local.render(); }} - onChange={async (e) => { - const val = e.currentTarget.value; - if (!local.open) { - local.open = true; + width={local.auto_popup_width ? input.current?.offsetWidth : undefined} + selected={({ item, options, idx }) => { + if (item.value === local.select?.value) { + return true; } - local.search.input = val; - local.render(); + return false; + }} + > + { + e.stopPropagation(); - if (local.search.promise) { - await local.search.promise; - } - - local.search.searching = true; - local.render(); - - if (local.search.searching) { - if (local.local_search) { - if (!local.loaded) { - await openOptions(); + if (!local.open) { + if (local.on_focus_open) { + loadOptions(); + local.open = true; + local.render(); } - const search = local.search.input.toLowerCase(); - if (search) { - local.search.result = local.options.filter((e) => - e.label.toLowerCase().includes(search) - ); + } + }} + onChange={async (e) => { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } + local.search.input = val; + local.render(); - if ( - local.search.result.length > 0 && - !local.search.result.find( - (e) => e.value === local.select?.value - ) - ) { - local.select = local.search.result[0]; + if (local.search.promise) { + await local.search.promise; + } + + local.search.searching = true; + local.render(); + + if (local.search.searching) { + if (local.local_search) { + if (!local.loaded) { + await loadOptions(); } - } else { - local.search.result = null; - } - local.search.searching = false; - local.render(); - } else { - clearTimeout(local.search.timeout); - local.search.timeout = setTimeout(async () => { - const result = options_fn?.({ - search: local.search.input, - existing: local.options, - }); - if (result) { - if (result instanceof Promise) { - local.search.promise = result; - local.search.result = (await result).map((item) => { - if (typeof item === "string") - return { value: item, label: item }; - return item; - }); - local.search.searching = false; - local.search.promise = null; - } else { - local.search.result = result.map((item) => { - if (typeof item === "string") - return { value: item, label: item }; - return item; - }); - local.search.searching = false; - } + const search = local.search.input.toLowerCase(); + if (search) { + local.search.result = local.options.filter((e) => + e.label.toLowerCase().includes(search) + ); if ( local.search.result.length > 0 && @@ -447,33 +426,72 @@ export const Typeahead: FC<{ ) { local.select = local.search.result[0]; } - - local.render(); + } else { + local.search.result = null; } - }, 100); - } - } - }} - spellCheck={false} - className={cx( - "c-flex-1 c-mb-2 c-text-sm c-outline-none", - local.mode === "single" ? "c-cursor-pointer" : "" - )} - onKeyDown={keydown} - /> - + local.search.searching = false; + local.render(); + } else { + clearTimeout(local.search.timeout); + local.search.timeout = setTimeout(async () => { + const result = options_fn?.({ + search: local.search.input, + existing: local.options, + }); + if (result) { + if (result instanceof Promise) { + local.search.promise = result; + local.search.result = (await result).map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + local.search.promise = null; + } else { + local.search.result = result.map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + } - {local.mode === "single" && ( -
- -
- )} -
- ); -}; + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + local.select = local.search.result[0]; + } + + local.render(); + } + }, 100); + } + } + }} + spellCheck={false} + className={cx( + "c-flex-1 c-mb-2 c-text-sm c-outline-none", + local.mode === "single" ? "c-cursor-pointer" : "" + )} + onKeyDown={keydown} + /> +
+ + {local.mode === "single" && ( +
+ +
+ )} +
+ ); + };