update Field.tsx, TypeDropdown.tsx, Typeahead.tsx and typeahead-opt.tsx

This commit is contained in:
faisolavolut 2025-02-10 08:31:34 +07:00
parent 624df96b1e
commit 4460c2bfcb
4 changed files with 371 additions and 120 deletions

View File

@ -14,6 +14,7 @@ export const Field: React.FC<{
fm: any;
label: string;
name: string;
isBetter?: boolean;
onLoad?: () => Promise<any> | any;
type?:
| "rating"
@ -40,7 +41,6 @@ export const Field: React.FC<{
disabled?: boolean;
required?: boolean;
hidden_label?: boolean;
onChange?: ({ data }: any) => Promise<void> | 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) ? (

View File

@ -10,6 +10,8 @@ export const TypeDropdown: React.FC<any> = ({
disabled,
mode,
allowNew = false,
unique = true,
isBetter = false,
}) => {
return (
<>
@ -22,7 +24,7 @@ export const TypeDropdown: React.FC<any> = ({
: []
}
allowNew={allowNew}
unique={mode === "multi" ? true : false}
unique={mode === "multi" ? (isBetter ? false : true) : false}
disabledSearch={false}
// popupClassName={}
required={required}

View File

@ -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<{
}
}}
>
<input
placeholder={
local.mode === "multi"
? placeholder
: valueLabel[0]?.label || placeholder
}
type="text"
ref={input}
value={inputval}
onChange={async (e) => {
const val = e.currentTarget.value;
if (!local.open) {
local.open = true;
{isBetter ? (
<div className="h-9 flex-grow flex flex-row items-start">
<div className="flex flex-grow"></div>
<div className="h-9 flex flex-row items-center px-2">
<GoChevronDown size={14} />
</div>
</div>
) : (
<input
placeholder={
local.mode === "multi"
? placeholder
: valueLabel[0]?.label || placeholder
}
type="text"
ref={input}
value={inputval}
onChange={async (e) => {
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}
/>
)}
</div>
</TypeaheadOptions>
</div>

View File

@ -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<{
<div
className={cx(
className,
width
"flex flex-col",
isBetter
? css`
min-width: 350px;
height: 450px;
`
: width
? css`
min-width: ${width}px;
height: 450px;
`
: css`
min-width: 150px;
height: 450px;
`,
css`
max-height: 400px;
@ -63,45 +86,74 @@ export const TypeaheadOptions: FC<{
`
)}
>
{!loading ? (
{isBetter ? (
<>
{options.map((item, idx) => {
const is_selected = selected?.({ item, options, idx });
if (is_selected) {
local.selectedIdx = idx;
}
return (
<div className="flex flex-row w-full p-1">
<div className="flex-grow flex flex-row relative">
<div
tabIndex={0}
key={item.value + "_" + idx}
className={cx(
"opt-item px-3 py-1 cursor-pointer option-item text-sm",
is_selected ? "bg-blue-600 text-white" : "hover:bg-blue-50",
idx > 0 && "border-t"
"absolute left-0 px-1.5",
css`
top: 50%;
transform: translateY(-50%);
`
)}
onClick={() => {
onSelect?.(item.value);
}}
>
{item.label || <>&nbsp;</>}
<IoSearchOutline />
</div>
);
})}
<input
placeholder={"Search"}
type="text"
spellCheck={false}
onChange={onSearch}
className={cx(
"pl-6 pr-3 py-1 rounded-md text-black flex h-9 flex-grow border border-gray-200 bg-white text-base 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"
)}
/>
</div>
</div>
{init.search.input === "" || !init.search.input ? (
<div className="flex flex-row px-3 py-1 gap-x-2 items-center cursor-pointer">
<Checkbox
id="terms"
className="border border-primary"
checked={init?.selectBetter?.all ? true : false}
onClick={(e) => {
init.selectBetter.all = !init.selectBetter.all;
init.render();
if (typeof onSelectAll === "function")
onSelectAll(init.selectBetter.all);
}}
/>
Select All
</div>
) : (
<></>
)}
</>
) : (
<></>
)}
{loading || searching ? (
<div className="px-4 w-full text-slate-400 text-sm py-2">
<div
className={cx(
isBetter && "flex-grow flex flex-row items-center justify-center",
"px-4 w-full text-slate-400 text-sm py-2"
)}
>
Loading...
</div>
) : (
<>
{options.length === 0 && (
<div className="p-4 w-full text-center text-md text-slate-400">
{options.length === 0 ? (
<div
className={cx(
isBetter &&
"flex-grow flex flex-row items-center justify-center",
"p-4 w-full text-center text-md text-slate-400"
)}
>
{fitur === "search-add" ? (
<ButtonBetter
variant={"outline"}
@ -155,9 +207,73 @@ export const TypeaheadOptions: FC<{
</>
)}
</div>
) : (
<ScrollArea className="w-full flex-grow flex flex-col gap-y-2">
{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 (
<div
className="flex flex-row px-3 py-1 gap-x-2 items-center cursor-pointer"
key={item.value + "_" + idx}
>
<Checkbox
id="terms"
className="border border-primary"
checked={is_selected}
onClick={() => {
if (is_selected) {
if (typeof onRemove === "function") onRemove(item);
} else {
onSelect?.(item.value);
}
}}
/>
{item.label || <>&nbsp;</>}
</div>
);
}
return (
<div
tabIndex={0}
key={item.value + "_" + idx}
className={cx(
"opt-item px-3 py-1 cursor-pointer option-item text-sm",
is_selected
? "bg-blue-600 text-white"
: "hover:bg-blue-50",
idx > 0 && "border-t"
)}
onClick={() => {
onSelect?.(item.value);
}}
>
{item.label || <>&nbsp;</>}
</div>
);
})}
</ScrollArea>
)}
</>
)}
{isBetter ? (
<div className="w-full flex flex-row items-center justify-end p-1 border-t border-gray-200">
<ButtonBetter className="rounded-md text-xs flex flex-row items-center gap-x-1">
<IoCheckmark />
OK
</ButtonBetter>
</div>
) : (
<></>
)}
</div>
);
@ -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}