Initial commit for new panel
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState, useId } from "react";
|
||||
import {
|
||||
type FieldErrors,
|
||||
type FieldValues,
|
||||
type Path,
|
||||
type UseFormRegisterReturn,
|
||||
} from "react-hook-form";
|
||||
|
||||
interface Option {
|
||||
value: string;
|
||||
text: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
type MultiSelectProps<T extends FieldValues> = {
|
||||
label: string;
|
||||
items: { value: string; text: string }[];
|
||||
errors?: FieldErrors<T>;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
} & UseFormRegisterReturn<Path<T>>;
|
||||
|
||||
function MultiSelect<T extends FieldValues>({
|
||||
name,
|
||||
label,
|
||||
items,
|
||||
errors,
|
||||
required,
|
||||
placeholder = "Select an option",
|
||||
className,
|
||||
onChange,
|
||||
onBlur,
|
||||
ref,
|
||||
}: MultiSelectProps<T>) {
|
||||
const id = useId();
|
||||
const error = errors && errors[name];
|
||||
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const [show, setShow] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const trigger = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(
|
||||
items.map((item) => ({
|
||||
...item,
|
||||
selected: false,
|
||||
})),
|
||||
);
|
||||
}, [items]);
|
||||
|
||||
const open = () => {
|
||||
setShow(true);
|
||||
};
|
||||
|
||||
const isOpen = () => {
|
||||
return show === true;
|
||||
};
|
||||
|
||||
const combinedRef = (el: HTMLDivElement) => {
|
||||
trigger.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (index: number) => {
|
||||
const newOptions = [...options];
|
||||
let newSelected = [...selected];
|
||||
|
||||
if (!newOptions[index].selected) {
|
||||
newOptions[index].selected = true;
|
||||
newSelected.push(index);
|
||||
} else {
|
||||
newOptions[index].selected = false;
|
||||
newSelected = newSelected.filter((i) => i !== index);
|
||||
}
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
const remove = (index: number, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
const newOptions = [...options];
|
||||
const newSelected = selected.filter((i) => i !== index);
|
||||
newOptions[index].selected = false;
|
||||
|
||||
setOptions(newOptions);
|
||||
setSelected(newSelected);
|
||||
|
||||
const selectedValues = newSelected.map((i) => options[i].value);
|
||||
onChange?.({ target: { name, value: selectedValues } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const clickHandler = ({ target }: MouseEvent) => {
|
||||
if (!dropdownRef.current) return;
|
||||
if (
|
||||
!show ||
|
||||
dropdownRef.current.contains(target as Node) ||
|
||||
trigger.current?.contains(target as Node)
|
||||
)
|
||||
return;
|
||||
setShow(false);
|
||||
};
|
||||
document.addEventListener("click", clickHandler);
|
||||
return () => document.removeEventListener("click", clickHandler);
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<div className={`relative z-50 ${className}`}>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="mb-3 block text-body-sm font-medium text-dark dark:text-white"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={combinedRef}
|
||||
onClick={open}
|
||||
onBlur={onBlur}
|
||||
tabIndex={0}
|
||||
className={`flex min-h-[46px] w-full cursor-pointer items-center rounded-[7px] border-[1.5px] bg-transparent py-2 pl-3 pr-3 outline-none transition focus:border-primary active:border-primary dark:bg-dark-2 ${
|
||||
error
|
||||
? "border-red focus:border-red dark:border-red"
|
||||
: "border-stroke dark:border-dark-3"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-auto flex-wrap gap-2">
|
||||
{selected.map((index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-center rounded-[5px] border-[.5px] border-stroke bg-gray-2 px-2.5 py-1 text-body-sm font-medium dark:border-dark-3 dark:bg-dark"
|
||||
>
|
||||
<div className="max-w-full flex-initial">
|
||||
{options[index]?.text}
|
||||
</div>
|
||||
<div className="flex flex-auto flex-row-reverse">
|
||||
<div
|
||||
onClick={(e) => remove(index, e)}
|
||||
className="hover:text-red cursor-pointer pl-2"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
role="button"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.35355 3.35355C9.54882 3.15829 9.54882 2.84171 9.35355 2.64645C9.15829 2.45118 8.84171 2.45118 8.64645 2.64645L6 5.29289L3.35355 2.64645C3.15829 2.45118 2.84171 2.45118 2.64645 2.64645C2.45118 2.84171 2.45118 3.15829 2.64645 3.35355L5.29289 6L2.64645 8.64645C2.45118 8.84171 2.45118 9.15829 2.64645 9.35355C2.84171 9.54882 3.15829 9.54882 3.35355 9.35355L6 6.70711L8.64645 9.35355C8.84171 9.54882 9.15829 9.54882 9.35355 9.35355C9.54882 9.15829 9.54882 8.84171 9.35355 8.64645L6.70711 6L9.35355 3.35355Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{selected.length === 0 && (
|
||||
<div className="flex-1">
|
||||
<span className="h-full w-full bg-transparent p-1 px-2 text-dark-5 outline-none dark:text-dark-6">
|
||||
{placeholder}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center py-1 pl-1 pr-1">
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer text-dark-4 outline-none focus:outline-none dark:text-dark-6"
|
||||
>
|
||||
<svg
|
||||
className={`fill-current transition-transform duration-200 ${
|
||||
isOpen() ? "rotate-180" : ""
|
||||
}`}
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.69149 7.09327C3.91613 6.83119 4.31069 6.80084 4.57277 7.02548L9.99936 11.6768L15.4259 7.02548C15.688 6.80084 16.0826 6.83119 16.3072 7.09327C16.5319 7.35535 16.5015 7.74991 16.2394 7.97455L10.4061 12.9745C10.172 13.1752 9.82667 13.1752 9.59261 12.9745L3.75928 7.97455C3.4972 7.74991 3.46685 7.35535 3.69149 7.09327Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`max-h-select absolute left-0 top-full z-40 w-full overflow-y-auto rounded bg-white shadow-1 dark:bg-dark-2 dark:shadow-card ${
|
||||
isOpen() ? "" : "hidden"
|
||||
}`}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div className="flex w-full flex-col">
|
||||
{options.map((option, index) => (
|
||||
<div key={index}>
|
||||
<div
|
||||
className="w-full cursor-pointer rounded-t border-b border-stroke hover:bg-primary/5 dark:border-dark-3"
|
||||
onClick={() => handleSelect(index)}
|
||||
>
|
||||
<div
|
||||
className={`relative flex w-full items-center border-l-2 p-2 pl-2 ${
|
||||
option.selected ? "border-primary" : "border-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mx-2 leading-6">{option.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="text-red mt-1.5 text-sm">{error.message as string}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default MultiSelect;
|
||||
Reference in New Issue
Block a user