dropdown search allow new value
This commit is contained in:
parent
d19dbe77eb
commit
be558f775b
|
|
@ -22,8 +22,11 @@ export const TypeDropdown: React.FC<any> = ({
|
|||
? [fm.data?.[name]]
|
||||
: []
|
||||
}
|
||||
allowNew={true}
|
||||
unique={false}
|
||||
disabledSearch={false}
|
||||
// popupClassName={}
|
||||
fitur="search-add"
|
||||
required={required}
|
||||
onSelect={({ search, item }) => {
|
||||
if (item) {
|
||||
|
|
@ -42,10 +45,11 @@ export const TypeDropdown: React.FC<any> = ({
|
|||
if (typeof onChange === "function" && item) {
|
||||
onChange(item);
|
||||
}
|
||||
console.log(fm.data[name]);
|
||||
return item?.value || search;
|
||||
}}
|
||||
disabled={disabled}
|
||||
allowNew={false}
|
||||
// allowNew={false}
|
||||
autoPopupWidth={true}
|
||||
focusOpen={true}
|
||||
mode={mode ? mode : "single"}
|
||||
|
|
|
|||
|
|
@ -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 { TypeaheadOptions } from "./typeahead-opt";
|
||||
|
|
@ -11,6 +18,7 @@ import uniqBy from "lodash.uniqby";
|
|||
type OptItem = { value: string; label: string; tag?: string };
|
||||
|
||||
export const Typeahead: FC<{
|
||||
fitur?: "search-add";
|
||||
value?: string[] | null;
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
|
|
@ -34,6 +42,7 @@ export const Typeahead: FC<{
|
|||
onInit?: (e: any) => void;
|
||||
}> = ({
|
||||
value,
|
||||
fitur,
|
||||
note,
|
||||
options: options_fn,
|
||||
onSelect,
|
||||
|
|
@ -51,6 +60,8 @@ export const Typeahead: FC<{
|
|||
disabledSearch,
|
||||
onInit,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedTerm, setDebouncedTerm] = useState("");
|
||||
const local = useLocal({
|
||||
value: [] as string[],
|
||||
open: false,
|
||||
|
|
@ -133,7 +144,7 @@ export const Typeahead: FC<{
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(local.unique);
|
||||
if (local.unique) {
|
||||
let found = local.value.find((e) => {
|
||||
return e === arg.item?.value || arg.search === e;
|
||||
|
|
@ -194,7 +205,6 @@ export const Typeahead: FC<{
|
|||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const selected = select({
|
||||
search: local.search.input,
|
||||
item: local.select,
|
||||
|
|
@ -217,6 +227,7 @@ export const Typeahead: FC<{
|
|||
return;
|
||||
}
|
||||
if (options.length > 0) {
|
||||
console.log("HALOOO");
|
||||
local.open = true;
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
|
|
@ -268,9 +279,11 @@ export const Typeahead: FC<{
|
|||
search: local.search.input,
|
||||
existing: options,
|
||||
});
|
||||
console.log({ res });
|
||||
|
||||
if (res) {
|
||||
const applyOptions = (result: (string | OptItem)[]) => {
|
||||
console.log({ result });
|
||||
local.options = result.map((item) => {
|
||||
if (typeof item === "string") return { value: item, label: 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 = () => {
|
||||
local.search.searching = false;
|
||||
local.search.input = "";
|
||||
|
|
@ -372,6 +425,7 @@ export const Typeahead: FC<{
|
|||
);
|
||||
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) {
|
||||
const found = options.find((e) => e.value === local.value[0]);
|
||||
if (found) {
|
||||
|
|
@ -380,11 +434,23 @@ export const Typeahead: FC<{
|
|||
inputval = local.value[0];
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (allow_new && local.open) {
|
||||
console.log(local);
|
||||
local.search.input = local.value[0];
|
||||
local.render();
|
||||
}
|
||||
}, [local.open]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-grow w-full relative">
|
||||
<div
|
||||
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 ",
|
||||
className
|
||||
)}
|
||||
|
|
@ -441,8 +507,10 @@ export const Typeahead: FC<{
|
|||
<></>
|
||||
)}
|
||||
<TypeaheadOptions
|
||||
fitur={fitur}
|
||||
popup={true}
|
||||
onOpenChange={(open) => {
|
||||
console.log("OPEN");
|
||||
if (!open) {
|
||||
local.select = null;
|
||||
}
|
||||
|
|
@ -491,7 +559,10 @@ export const Typeahead: FC<{
|
|||
}}
|
||||
>
|
||||
<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) => {
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
|
|
@ -500,9 +571,12 @@ export const Typeahead: FC<{
|
|||
loadOptions();
|
||||
local.open = true;
|
||||
local.render();
|
||||
// if (allow_new) {
|
||||
// local.search.input = inputval;
|
||||
// local.render();
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
if (local.mode === "single") {
|
||||
if (input && input.current) input.current.select();
|
||||
}
|
||||
|
|
@ -531,7 +605,9 @@ export const Typeahead: FC<{
|
|||
|
||||
local.search.searching = true;
|
||||
local.render();
|
||||
|
||||
if (allow_new) {
|
||||
setSearchTerm(val);
|
||||
}
|
||||
if (local.search.searching) {
|
||||
if (local.local_search) {
|
||||
if (!local.loaded) {
|
||||
|
|
@ -612,7 +688,7 @@ export const Typeahead: FC<{
|
|||
</TypeaheadOptions>
|
||||
</div>
|
||||
|
||||
{local.mode === "single" && (
|
||||
{local.mode === "single" && fitur !== "search-add" && (
|
||||
<>
|
||||
<div
|
||||
className={cx(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { FC } from "react";
|
||||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { Popover } from "../../Popover/Popover";
|
||||
import { ButtonBetter } from "../../ui/button";
|
||||
|
||||
export type OptionItem = { value: string; label: string };
|
||||
export const TypeaheadOptions: FC<{
|
||||
|
|
@ -20,7 +21,8 @@ export const TypeaheadOptions: FC<{
|
|||
searching?: boolean;
|
||||
searchText?: string;
|
||||
width?: number;
|
||||
isMulti?: boolean
|
||||
isMulti?: boolean;
|
||||
fitur?: "search-add";
|
||||
}> = ({
|
||||
popup,
|
||||
children,
|
||||
|
|
@ -33,7 +35,9 @@ export const TypeaheadOptions: FC<{
|
|||
searching,
|
||||
searchText,
|
||||
showEmpty,
|
||||
width,isMulti
|
||||
width,
|
||||
isMulti,
|
||||
fitur,
|
||||
}) => {
|
||||
if (!popup) return children;
|
||||
const local = useLocal({
|
||||
|
|
@ -53,61 +57,97 @@ export const TypeaheadOptions: FC<{
|
|||
`,
|
||||
css`
|
||||
max-height: 400px;
|
||||
overflow: auto
|
||||
overflow: auto;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{options.map((item, idx) => {
|
||||
const is_selected = selected?.({ item, options, idx });
|
||||
{options.map((item, idx) => {
|
||||
const is_selected = selected?.({ item, options, idx });
|
||||
|
||||
if (is_selected) {
|
||||
local.selectedIdx = idx;
|
||||
}
|
||||
if (is_selected) {
|
||||
local.selectedIdx = idx;
|
||||
}
|
||||
|
||||
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 || <> </>}
|
||||
</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 || <> </>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{searching ? (
|
||||
<div className="px-4 w-full text-slate-400">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{options.length === 0 && (
|
||||
<div className="p-4 w-full text-center text-md text-slate-400">
|
||||
{!searchText ? (
|
||||
<>— Empty —</>
|
||||
) : (
|
||||
<>
|
||||
Search
|
||||
{searching ? (
|
||||
<div className="px-4 w-full text-slate-400">Loading...</div>
|
||||
) : (
|
||||
<>
|
||||
{options.length === 0 && (
|
||||
<div className="p-4 w-full text-center text-md text-slate-400">
|
||||
{fitur === "search-add" ? (
|
||||
<ButtonBetter
|
||||
variant={"outline"}
|
||||
className="flex flex-row gap-x-2"
|
||||
>
|
||||
<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
|
||||
className={css`
|
||||
font-style: italic;
|
||||
padding: 0px 5px;
|
||||
`}
|
||||
>
|
||||
"{searchText}"
|
||||
</span>
|
||||
not found
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</ButtonBetter>
|
||||
) : (
|
||||
<>
|
||||
{!searchText ? (
|
||||
<>— Empty —</>
|
||||
) : (
|
||||
<>
|
||||
Search
|
||||
<span
|
||||
className={css`
|
||||
font-style: italic;
|
||||
padding: 0px 5px;
|
||||
`}
|
||||
>
|
||||
"{searchText}"
|
||||
</span>
|
||||
not found
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue