dropdown search allow new value

This commit is contained in:
faisolavolut 2025-01-22 22:38:57 +07:00
parent d19dbe77eb
commit be558f775b
3 changed files with 173 additions and 53 deletions

View File

@ -22,8 +22,11 @@ export const TypeDropdown: React.FC<any> = ({
? [fm.data?.[name]] ? [fm.data?.[name]]
: [] : []
} }
allowNew={true}
unique={false}
disabledSearch={false} disabledSearch={false}
// popupClassName={} // popupClassName={}
fitur="search-add"
required={required} required={required}
onSelect={({ search, item }) => { onSelect={({ search, item }) => {
if (item) { if (item) {
@ -42,10 +45,11 @@ export const TypeDropdown: React.FC<any> = ({
if (typeof onChange === "function" && item) { if (typeof onChange === "function" && item) {
onChange(item); onChange(item);
} }
console.log(fm.data[name]);
return item?.value || search; return item?.value || search;
}} }}
disabled={disabled} disabled={disabled}
allowNew={false} // allowNew={false}
autoPopupWidth={true} autoPopupWidth={true}
focusOpen={true} focusOpen={true}
mode={mode ? mode : "single"} mode={mode ? mode : "single"}

View File

@ -1,4 +1,11 @@
import { FC, KeyboardEvent, useCallback, useEffect, useRef } from "react"; import {
FC,
KeyboardEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { useLocal } from "@/lib/utils/use-local"; import { useLocal } from "@/lib/utils/use-local";
import { TypeaheadOptions } from "./typeahead-opt"; import { TypeaheadOptions } from "./typeahead-opt";
@ -11,6 +18,7 @@ import uniqBy from "lodash.uniqby";
type OptItem = { value: string; label: string; tag?: string }; type OptItem = { value: string; label: string; tag?: string };
export const Typeahead: FC<{ export const Typeahead: FC<{
fitur?: "search-add";
value?: string[] | null; value?: string[] | null;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
@ -34,6 +42,7 @@ export const Typeahead: FC<{
onInit?: (e: any) => void; onInit?: (e: any) => void;
}> = ({ }> = ({
value, value,
fitur,
note, note,
options: options_fn, options: options_fn,
onSelect, onSelect,
@ -51,6 +60,8 @@ export const Typeahead: FC<{
disabledSearch, disabledSearch,
onInit, onInit,
}) => { }) => {
const [searchTerm, setSearchTerm] = useState("");
const [debouncedTerm, setDebouncedTerm] = useState("");
const local = useLocal({ const local = useLocal({
value: [] as string[], value: [] as string[],
open: false, open: false,
@ -133,7 +144,7 @@ export const Typeahead: FC<{
return false; return false;
} }
} }
console.log(local.unique);
if (local.unique) { if (local.unique) {
let found = local.value.find((e) => { let found = local.value.find((e) => {
return e === arg.item?.value || arg.search === e; return e === arg.item?.value || arg.search === e;
@ -194,7 +205,6 @@ export const Typeahead: FC<{
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const selected = select({ const selected = select({
search: local.search.input, search: local.search.input,
item: local.select, item: local.select,
@ -217,6 +227,7 @@ export const Typeahead: FC<{
return; return;
} }
if (options.length > 0) { if (options.length > 0) {
console.log("HALOOO");
local.open = true; local.open = true;
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
e.preventDefault(); e.preventDefault();
@ -268,9 +279,11 @@ export const Typeahead: FC<{
search: local.search.input, search: local.search.input,
existing: options, existing: options,
}); });
console.log({ res });
if (res) { if (res) {
const applyOptions = (result: (string | OptItem)[]) => { const applyOptions = (result: (string | OptItem)[]) => {
console.log({ result });
local.options = result.map((item) => { local.options = result.map((item) => {
if (typeof item === "string") return { value: item, label: item }; if (typeof item === "string") return { value: item, label: item };
return item; return item;
@ -328,7 +341,47 @@ export const Typeahead: FC<{
}); });
} }
}, []); }, []);
// Debounce effect
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedTerm(searchTerm); // Update debounced term after 1 second
}, 100);
return () => clearTimeout(timer); // Clear timeout if user types again
}, [searchTerm]);
// Function to handle search
useEffect(() => {
if (debouncedTerm) {
performSearch(debouncedTerm);
}
}, [debouncedTerm]);
const performSearch = (value: any) => {
console.log("Searching for:", value);
if (typeof onSelect === "function") {
const result = onSelect({
search: value,
item: {
label: value,
value: value,
},
});
if (result) {
local.value.push(result);
local.render();
if (typeof onChange === "function") {
onChange(local.value);
}
return result;
} else {
return false;
}
}
// Lakukan pencarian, panggil API, atau filter data di sini
};
const resetSearch = () => { const resetSearch = () => {
local.search.searching = false; local.search.searching = false;
local.search.input = ""; local.search.input = "";
@ -372,6 +425,7 @@ export const Typeahead: FC<{
); );
let inputval = local.search.input; let inputval = local.search.input;
// console.log("search-add -> ", value?.[0], local.open, local.value?.length);
if (!local.open && local.mode === "single" && local.value?.length > 0) { if (!local.open && local.mode === "single" && local.value?.length > 0) {
const found = options.find((e) => e.value === local.value[0]); const found = options.find((e) => e.value === local.value[0]);
if (found) { if (found) {
@ -380,11 +434,23 @@ export const Typeahead: FC<{
inputval = local.value[0]; inputval = local.value[0];
} }
} }
useEffect(() => {
if (allow_new && local.open) {
console.log(local);
local.search.input = local.value[0];
local.render();
}
}, [local.open]);
return ( return (
<div className="flex flex-row flex-grow w-full relative"> <div className="flex flex-row flex-grow w-full relative">
<div <div
className={cx( className={cx(
local.mode === "single" ? "cursor-pointer" : "cursor-text", allow_new
? "cursor-text"
: local.mode === "single"
? "cursor-pointer"
: "cursor-text",
"text-black flex relative flex-wrap py-0 items-center w-full h-full flex-1 ", "text-black flex relative flex-wrap py-0 items-center w-full h-full flex-1 ",
className className
)} )}
@ -441,8 +507,10 @@ export const Typeahead: FC<{
<></> <></>
)} )}
<TypeaheadOptions <TypeaheadOptions
fitur={fitur}
popup={true} popup={true}
onOpenChange={(open) => { onOpenChange={(open) => {
console.log("OPEN");
if (!open) { if (!open) {
local.select = null; local.select = null;
} }
@ -491,7 +559,10 @@ export const Typeahead: FC<{
}} }}
> >
<div <div
className="single flex-1 flex-grow flex flex-row cursor-pointer" className={cx(
allow_new ? "cursor-text" : "cursor-pointer",
"single flex-1 flex-grow flex flex-row"
)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!disabled) { if (!disabled) {
@ -500,9 +571,12 @@ export const Typeahead: FC<{
loadOptions(); loadOptions();
local.open = true; local.open = true;
local.render(); local.render();
// if (allow_new) {
// local.search.input = inputval;
// local.render();
// }
} }
} }
if (local.mode === "single") { if (local.mode === "single") {
if (input && input.current) input.current.select(); if (input && input.current) input.current.select();
} }
@ -531,7 +605,9 @@ export const Typeahead: FC<{
local.search.searching = true; local.search.searching = true;
local.render(); local.render();
if (allow_new) {
setSearchTerm(val);
}
if (local.search.searching) { if (local.search.searching) {
if (local.local_search) { if (local.local_search) {
if (!local.loaded) { if (!local.loaded) {
@ -612,7 +688,7 @@ export const Typeahead: FC<{
</TypeaheadOptions> </TypeaheadOptions>
</div> </div>
{local.mode === "single" && ( {local.mode === "single" && fitur !== "search-add" && (
<> <>
<div <div
className={cx( className={cx(

View File

@ -1,6 +1,7 @@
import { FC } from "react"; import { FC } from "react";
import { useLocal } from "@/lib/utils/use-local"; import { useLocal } from "@/lib/utils/use-local";
import { Popover } from "../../Popover/Popover"; import { Popover } from "../../Popover/Popover";
import { ButtonBetter } from "../../ui/button";
export type OptionItem = { value: string; label: string }; export type OptionItem = { value: string; label: string };
export const TypeaheadOptions: FC<{ export const TypeaheadOptions: FC<{
@ -20,7 +21,8 @@ export const TypeaheadOptions: FC<{
searching?: boolean; searching?: boolean;
searchText?: string; searchText?: string;
width?: number; width?: number;
isMulti?: boolean isMulti?: boolean;
fitur?: "search-add";
}> = ({ }> = ({
popup, popup,
children, children,
@ -33,7 +35,9 @@ export const TypeaheadOptions: FC<{
searching, searching,
searchText, searchText,
showEmpty, showEmpty,
width,isMulti width,
isMulti,
fitur,
}) => { }) => {
if (!popup) return children; if (!popup) return children;
const local = useLocal({ const local = useLocal({
@ -53,61 +57,97 @@ export const TypeaheadOptions: FC<{
`, `,
css` css`
max-height: 400px; max-height: 400px;
overflow: auto overflow: auto;
` `
)} )}
> >
{options.map((item, idx) => { {options.map((item, idx) => {
const is_selected = selected?.({ item, options, idx }); const is_selected = selected?.({ item, options, idx });
if (is_selected) { if (is_selected) {
local.selectedIdx = idx; local.selectedIdx = idx;
} }
return ( return (
<div <div
tabIndex={0} tabIndex={0}
key={item.value + "_" + idx} key={item.value + "_" + idx}
className={cx( className={cx(
"opt-item px-3 py-1 cursor-pointer option-item text-sm", "opt-item px-3 py-1 cursor-pointer option-item text-sm",
is_selected ? "bg-blue-600 text-white" : "hover:bg-blue-50", is_selected ? "bg-blue-600 text-white" : "hover:bg-blue-50",
idx > 0 && "border-t" idx > 0 && "border-t"
)} )}
onClick={() => { onClick={() => {
onSelect?.(item.value); onSelect?.(item.value);
}} }}
> >
{item.label || <>&nbsp;</>} {item.label || <>&nbsp;</>}
</div> </div>
); );
})} })}
{searching ? ( {searching ? (
<div className="px-4 w-full text-slate-400">Loading...</div> <div className="px-4 w-full text-slate-400">Loading...</div>
) : ( ) : (
<> <>
{options.length === 0 && ( {options.length === 0 && (
<div className="p-4 w-full text-center text-md text-slate-400"> <div className="p-4 w-full text-center text-md text-slate-400">
{!searchText ? ( {fitur === "search-add" ? (
<>&mdash; Empty &mdash;</> <ButtonBetter
) : ( variant={"outline"}
<> className="flex flex-row gap-x-2"
Search >
<svg
xmlns="http://www.w3.org/2000/svg"
width={20}
height={20}
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit={10}
strokeWidth={1.5}
d="M6 12h12m-6 6V6"
></path>
</svg>{" "}
<span>
Add{" "}
<span <span
className={css` className={css`
font-style: italic; font-style: italic;
padding: 0px 5px;
`} `}
> >
"{searchText}" "{searchText}"
</span> </span>
not found </span>
</> </ButtonBetter>
)} ) : (
</div> <>
)} {!searchText ? (
</> <>&mdash; Empty &mdash;</>
)} ) : (
<>
Search
<span
className={css`
font-style: italic;
padding: 0px 5px;
`}
>
"{searchText}"
</span>
not found
</>
)}
</>
)}
</div>
)}
</>
)}
</div> </div>
); );