From 4460c2bfcbf333ef0cd5880e74130c9c53dee704 Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Mon, 10 Feb 2025 08:31:34 +0700 Subject: [PATCH] update Field.tsx, TypeDropdown.tsx, Typeahead.tsx and typeahead-opt.tsx --- components/form/Field.tsx | 7 +- components/form/field/TypeDropdown.tsx | 4 +- components/form/field/Typeahead.tsx | 312 +++++++++++++++++------- components/form/field/typeahead-opt.tsx | 168 +++++++++++-- 4 files changed, 371 insertions(+), 120 deletions(-) diff --git a/components/form/Field.tsx b/components/form/Field.tsx index 4b89584..0255d7c 100644 --- a/components/form/Field.tsx +++ b/components/form/Field.tsx @@ -14,6 +14,7 @@ export const Field: React.FC<{ fm: any; label: string; name: string; + isBetter?: boolean; onLoad?: () => Promise | any; type?: | "rating" @@ -40,7 +41,6 @@ export const Field: React.FC<{ disabled?: boolean; required?: boolean; hidden_label?: boolean; - onChange?: ({ data }: any) => Promise | void; className?: string; classField?: string; @@ -48,9 +48,11 @@ export const Field: React.FC<{ prefix?: string | any | (() => any); suffix?: string | any | (() => any); allowNew?: boolean; + unique?: boolean; }> = ({ fm, label, + isBetter, name, onLoad, type = "text", @@ -65,6 +67,7 @@ export const Field: React.FC<{ prefix, suffix, allowNew, + unique = true, }) => { let result = null; const field = useLocal({ @@ -247,6 +250,8 @@ export const Field: React.FC<{ disabled={is_disable} onChange={onChange} mode="multi" + unique={unique} + isBetter={isBetter} /> ) : ["checkbox"].includes(type) ? ( diff --git a/components/form/field/TypeDropdown.tsx b/components/form/field/TypeDropdown.tsx index ae03021..e5b445d 100644 --- a/components/form/field/TypeDropdown.tsx +++ b/components/form/field/TypeDropdown.tsx @@ -10,6 +10,8 @@ export const TypeDropdown: React.FC = ({ disabled, mode, allowNew = false, + unique = true, + isBetter = false, }) => { return ( <> @@ -22,7 +24,7 @@ export const TypeDropdown: React.FC = ({ : [] } allowNew={allowNew} - unique={mode === "multi" ? true : false} + unique={mode === "multi" ? (isBetter ? false : true) : false} disabledSearch={false} // popupClassName={} required={required} diff --git a/components/form/field/Typeahead.tsx b/components/form/field/Typeahead.tsx index 3f416b7..020be1a 100644 --- a/components/form/field/Typeahead.tsx +++ b/components/form/field/Typeahead.tsx @@ -40,6 +40,7 @@ export const Typeahead: FC<{ note?: string; disabledSearch?: boolean; onInit?: (e: any) => void; + isBetter?: boolean; }> = ({ value, fitur, @@ -59,7 +60,9 @@ export const Typeahead: FC<{ popupClassName, disabledSearch, onInit, + isBetter = false, }) => { + const maxLength = 4; const [searchTerm, setSearchTerm] = useState(""); const [debouncedTerm, setDebouncedTerm] = useState(""); const local = useLocal({ @@ -68,6 +71,10 @@ export const Typeahead: FC<{ options: [] as OptItem[], loaded: false, loading: false, + selectBetter: { + all: false, + partial: [] as any[], + }, search: { input: "", timeout: null as any, @@ -102,9 +109,10 @@ export const Typeahead: FC<{ } return true; }); - - if (!select_found) { - local.select = options[0]; + if (Array.isArray(value) && value?.length) { + if (!select_found) { + local.select = options[0]; + } } } @@ -513,6 +521,25 @@ export const Typeahead: FC<{ resetSearch(); } }} + onRemove={(data) => { + local.value = local.value.filter((val) => data?.value !== val); + local.render(); + input.current?.focus(); + + if (typeof onChange === "function") { + onChange(local.value); + } + }} + onSelectAll={(data: boolean) => { + local.value = data ? options.map((e) => e?.value) : []; + local.render(); + input.current?.focus(); + if (typeof onChange === "function") { + onChange(local.value); + } + }} + init={local} + isBetter={isBetter} loading={local.loading} showEmpty={!allow_new} className={popupClassName} @@ -520,8 +547,89 @@ export const Typeahead: FC<{ options={options} searching={local.search.searching} searchText={local.search.input} + onSearch={async (e) => { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } + + local.search.input = val; + local.render(); + + if (local.search.promise) { + await local.search.promise; + } + + local.search.searching = true; + local.render(); + if (allow_new) { + setSearchTerm(val); + } + if (local.search.searching) { + if (local.local_search) { + if (!local.loaded) { + await loadOptions(); + } + const search = local.search.input.toLowerCase(); + if (search) { + local.search.result = options.filter((e) => + e.label.toLowerCase().includes(search) + ); + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + } + } 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: 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; + } + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + } + + local.render(); + } + }, 100); + } + } + }} onSelect={(value) => { - local.open = false; + if (!isBetter) local.open = false; resetSearch(); const item = options.find((item) => item.value === value); if (item) { @@ -545,9 +653,20 @@ export const Typeahead: FC<{ } isMulti={local.mode === "multi"} selected={({ item, options, idx }) => { - if (item.value === local.select?.value) { + // console.log(local.select); + if (isBetter) { + const val = local.value?.length ? local.value : []; + let isSelect = options.find((e) => { + return ( + e?.value === item?.value && + val.find((ex) => ex === item?.value) + ); + }); + return isSelect ? true : false; + } else if (item.value === local.select?.value) { return true; } + return false; }} > @@ -576,82 +695,51 @@ export const Typeahead: FC<{ } }} > - { - const val = e.currentTarget.value; - if (!local.open) { - local.open = true; + {isBetter ? ( +
+
+
+ +
+
+ ) : ( + { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } - local.search.input = val; - local.render(); + local.search.input = val; + local.render(); - if (local.search.promise) { - await local.search.promise; - } + if (local.search.promise) { + await local.search.promise; + } - local.search.searching = true; - local.render(); - if (allow_new) { - setSearchTerm(val); - } - if (local.search.searching) { - if (local.local_search) { - if (!local.loaded) { - await loadOptions(); - } - const search = local.search.input.toLowerCase(); - if (search) { - local.search.result = options.filter((e) => - e.label.toLowerCase().includes(search) - ); - - if ( - local.search.result.length > 0 && - !local.search.result.find( - (e) => e.value === local.select?.value - ) - ) { - local.select = local.search.result[0]; + local.search.searching = true; + local.render(); + if (allow_new) { + setSearchTerm(val); + } + 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: 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 = options.filter((e) => + e.label.toLowerCase().includes(search) + ); if ( local.search.result.length > 0 && @@ -661,24 +749,64 @@ export const Typeahead: FC<{ ) { local.select = local.search.result[0]; } - - local.render(); + } else { + local.search.result = null; } - }, 100); + 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: 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; + } + + 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); + } } - } - }} - disabled={!disabled ? disabledSearch : disabled} - spellCheck={false} - className={cx( - "text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0", - local.mode === "single" ? "cursor-pointer" : "" - )} - style={{ - pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled - }} - onKeyDown={keydown} - /> + }} + disabled={!disabled ? disabledSearch : disabled} + spellCheck={false} + className={cx( + "text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0", + local.mode === "single" ? "cursor-pointer" : "" + )} + style={{ + pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled + }} + onKeyDown={keydown} + /> + )} diff --git a/components/form/field/typeahead-opt.tsx b/components/form/field/typeahead-opt.tsx index 6ac28b8..f9252e0 100644 --- a/components/form/field/typeahead-opt.tsx +++ b/components/form/field/typeahead-opt.tsx @@ -2,10 +2,16 @@ import { FC } from "react"; import { useLocal } from "@/lib/utils/use-local"; import { Popover } from "../../Popover/Popover"; import { ButtonBetter } from "../../ui/button"; +import { Checkbox } from "../../ui/checkbox"; +import { ScrollArea } from "../../ui/scroll-area"; +import { IoCheckmark, IoSearchOutline } from "react-icons/io5"; export type OptionItem = { value: string; label: string }; export const TypeaheadOptions: FC<{ popup?: boolean; + onRemove?: (data: any) => void; + onSelectAll?: (data: boolean) => void; + init?: any; loading?: boolean; open?: boolean; children: any; @@ -24,6 +30,9 @@ export const TypeaheadOptions: FC<{ width?: number; isMulti?: boolean; fitur?: "search-add"; + isBetter?: boolean; + onSearch?: (event: any) => void; + search?: boolean; }> = ({ popup, loading, @@ -40,6 +49,12 @@ export const TypeaheadOptions: FC<{ width, isMulti, fitur, + isBetter, + init, + onSearch, + search, + onRemove, + onSelectAll, }) => { if (!popup) return children; const local = useLocal({ @@ -50,12 +65,20 @@ export const TypeaheadOptions: FC<{
- {!loading ? ( + {isBetter ? ( <> - {options.map((item, idx) => { - const is_selected = selected?.({ item, options, idx }); - - if (is_selected) { - local.selectedIdx = idx; - } - - return ( +
+
0 && "border-t" + "absolute left-0 px-1.5", + css` + top: 50%; + transform: translateY(-50%); + ` )} - onClick={() => { - onSelect?.(item.value); - }} > - {item.label || <> } +
- ); - })} + + +
+
+ {init.search.input === "" || !init.search.input ? ( +
+ { + init.selectBetter.all = !init.selectBetter.all; + init.render(); + if (typeof onSelectAll === "function") + onSelectAll(init.selectBetter.all); + }} + /> + Select All +
+ ) : ( + <> + )} ) : ( <> )} - {loading || searching ? ( -
+
Loading...
) : ( <> - {options.length === 0 && ( -
+ {options.length === 0 ? ( +
{fitur === "search-add" ? ( )}
+ ) : ( + + {options.map((item, idx) => { + const is_selected = isBetter + ? init.selectBetter.all + ? true + : selected?.({ item, options, idx }) + : selected?.({ item, options, idx }); + + if (is_selected) { + local.selectedIdx = idx; + } + if (isBetter) { + return ( +
+ { + if (is_selected) { + if (typeof onRemove === "function") onRemove(item); + } else { + onSelect?.(item.value); + } + }} + /> + {item.label || <> } +
+ ); + } + return ( +
0 && "border-t" + )} + onClick={() => { + onSelect?.(item.value); + }} + > + {item.label || <> } +
+ ); + })} +
)} )} + {isBetter ? ( +
+ + + OK + +
+ ) : ( + <> + )}
); @@ -169,7 +285,7 @@ export const TypeaheadOptions: FC<{ arrow={false} onOpenChange={onOpenChange} backdrop={false} - classNameTrigger={!isMulti ? "w-full" : ""} + classNameTrigger={!isMulti ? "w-full" : "flex-grow"} placement="bottom-start" className="flex-1 rounded-md overflow-hidden" content={content}