2025-08-04 17:45:44 +02:00
|
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { type JSX, useRef, useState } from "react";
|
2025-08-04 17:45:44 +02:00
|
|
|
import ReactSelect, {
|
2025-08-28 18:27:07 +02:00
|
|
|
type CSSObjectWithLabel,
|
|
|
|
|
type GroupBase,
|
|
|
|
|
type MenuListProps,
|
2025-08-04 17:45:44 +02:00
|
|
|
} from "react-select";
|
|
|
|
|
import { cn } from "~/lib/utils";
|
|
|
|
|
|
|
|
|
|
export type ListOption = {
|
2025-08-28 18:27:07 +02:00
|
|
|
label: string;
|
|
|
|
|
value: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type BaseProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
elementId: string;
|
|
|
|
|
elementName: string;
|
|
|
|
|
options: ListOption[];
|
|
|
|
|
placeholder: string;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
menuPlacement?: "auto" | "bottom" | "top";
|
|
|
|
|
defaultValue?: ListOption | ListOption[];
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MultiProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
isMulti: true;
|
|
|
|
|
onValueChange: (value: ListOption[]) => void;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
type SigletProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
isMulti: false;
|
|
|
|
|
onValueChange: (value: ListOption) => void;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
type MultiSelectProps = BaseProps & (MultiProps | SigletProps);
|
|
|
|
|
|
|
|
|
|
const VirtualizedMenuList = (
|
2025-08-28 18:27:07 +02:00
|
|
|
props: MenuListProps<
|
|
|
|
|
ListOption,
|
|
|
|
|
MultiSelectProps["isMulti"],
|
|
|
|
|
GroupBase<ListOption>
|
|
|
|
|
>,
|
2025-08-04 17:45:44 +02:00
|
|
|
) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const rows = props.children as JSX.Element[];
|
|
|
|
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const count = rows.length;
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
const virtualizer = useVirtualizer({
|
|
|
|
|
count,
|
|
|
|
|
getScrollElement: () => parentRef.current,
|
|
|
|
|
estimateSize: () => 45,
|
|
|
|
|
});
|
|
|
|
|
const items = virtualizer.getVirtualItems();
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={parentRef}
|
|
|
|
|
className="List"
|
|
|
|
|
style={{
|
|
|
|
|
height: `${virtualizer.getTotalSize()}px`,
|
|
|
|
|
maxHeight: "300px",
|
|
|
|
|
width: "auto",
|
|
|
|
|
overflowY: "auto",
|
|
|
|
|
contain: "strict",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
height: virtualizer.getTotalSize(),
|
|
|
|
|
width: "100%",
|
|
|
|
|
position: "relative",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
width: "100%",
|
|
|
|
|
transform: `translateY(${items[0]?.start ?? 0}px)`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{items.map((virtualRow) => (
|
|
|
|
|
<div
|
|
|
|
|
key={virtualRow.key}
|
|
|
|
|
data-index={virtualRow.index}
|
|
|
|
|
ref={virtualizer.measureElement}
|
|
|
|
|
className={virtualRow.index % 2 ? "ListItemOdd" : "ListItemEven"}
|
|
|
|
|
>
|
|
|
|
|
{rows[virtualRow.index]}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @example
|
|
|
|
|
* <MultiSelect
|
|
|
|
|
* elementId="select-naz"
|
|
|
|
|
* elementName="nazioneNascita"
|
|
|
|
|
* options={UnpackOptions({ options: nazioni_options })}
|
|
|
|
|
* defaultValue={RepackValues({
|
|
|
|
|
* options: nazioni_options,
|
|
|
|
|
* values: field.value,
|
|
|
|
|
* })}
|
|
|
|
|
* onValueChange={(value) => {
|
|
|
|
|
* field.onChange(value.value);
|
|
|
|
|
* //if isMulti
|
|
|
|
|
* field.onChange(value.map((v) => v.value));
|
|
|
|
|
* }}
|
|
|
|
|
* placeholder="Seleziona..."
|
|
|
|
|
* isMulti={false}
|
|
|
|
|
* />
|
|
|
|
|
*/
|
|
|
|
|
export const MultiSelect = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
elementId,
|
|
|
|
|
defaultValue,
|
|
|
|
|
elementName,
|
|
|
|
|
isMulti,
|
|
|
|
|
onValueChange,
|
|
|
|
|
options,
|
|
|
|
|
placeholder,
|
|
|
|
|
disabled,
|
|
|
|
|
menuPlacement,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: MultiSelectProps) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
return (
|
|
|
|
|
<ReactSelect
|
|
|
|
|
menuPlacement={menuPlacement}
|
|
|
|
|
components={{ MenuList: VirtualizedMenuList }}
|
|
|
|
|
inputId={elementId}
|
|
|
|
|
isMulti={isMulti}
|
|
|
|
|
isDisabled={disabled}
|
|
|
|
|
instanceId={elementId}
|
|
|
|
|
name={elementName}
|
|
|
|
|
options={options}
|
|
|
|
|
onChange={(values) => {
|
|
|
|
|
if (isMulti) {
|
|
|
|
|
const valore = values as ListOption[];
|
|
|
|
|
onValueChange(valore);
|
|
|
|
|
} else {
|
|
|
|
|
const valore = values as ListOption;
|
|
|
|
|
onValueChange(valore);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
defaultValue={defaultValue}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
onMenuOpen={() => setMenuOpen(true)}
|
|
|
|
|
onMenuClose={() => setMenuOpen(false)}
|
|
|
|
|
styles={{
|
|
|
|
|
container: (base, state) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
":hover .select__indicator-separator": {
|
|
|
|
|
backgroundColor: state.isFocused
|
|
|
|
|
? "rgb(161,161,170)"
|
|
|
|
|
: "rgb(161,161,170)",
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
":hover .select__dropdown-indicator": {
|
|
|
|
|
color: state.isFocused ? "rgb(161,161,170)" : "rgb(161,161,170)",
|
|
|
|
|
},
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
control: (base, state) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
|
|
|
|
minHeight: "42px",
|
|
|
|
|
borderRadius: "0.5rem",
|
|
|
|
|
boxShadow: undefined,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
border: state.isFocused
|
|
|
|
|
? "1px solid rgb(161,161,170)"
|
|
|
|
|
: "1px solid rgb(212 ,212 ,216)",
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"&:hover": {
|
|
|
|
|
borderColor: state.isFocused
|
|
|
|
|
? "rgb(161,161,170)"
|
|
|
|
|
: "rgb(161 ,161 ,170)",
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"& .select__indicator-separator": {
|
|
|
|
|
backgroundColor: state.isFocused
|
|
|
|
|
? "rgb(161,161,170)"
|
|
|
|
|
: "rgb(212 ,212 ,216)",
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"& .select__dropdown-indicator": {
|
|
|
|
|
color: state.isFocused
|
|
|
|
|
? "rgb(161,161,170)"
|
|
|
|
|
: "rgb(212 ,212 ,216)",
|
|
|
|
|
transition: "none",
|
|
|
|
|
},
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
multiValue: (base) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
|
|
|
|
backgroundColor: undefined,
|
|
|
|
|
borderRadius: "0.5rem",
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
multiValueLabel: (base) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
|
|
|
|
borderRadius: "0.5rem",
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
menu: (base) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
|
|
|
|
borderRadius: "0.5rem",
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
option: (base, state) =>
|
|
|
|
|
({
|
|
|
|
|
...base,
|
|
|
|
|
backgroundColor: undefined,
|
|
|
|
|
color: state.isSelected ? undefined : undefined,
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"&:hover": {
|
|
|
|
|
backgroundColor: undefined,
|
|
|
|
|
color: state.isSelected ? undefined : undefined,
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"&:active": {
|
|
|
|
|
backgroundColor: "rgb(161, 161, 170)",
|
|
|
|
|
color: "rgb(255, 255, 255)",
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
"&:focus": {
|
|
|
|
|
backgroundColor: undefined,
|
|
|
|
|
},
|
|
|
|
|
}) as CSSObjectWithLabel,
|
|
|
|
|
}}
|
|
|
|
|
classNames={{
|
|
|
|
|
valueContainer: () => cn("min-h-[38px]"),
|
|
|
|
|
control: () =>
|
|
|
|
|
cn(
|
|
|
|
|
"mt-2 w-full rounded-lg h-min-[42px] dark:bg-primary bg-white shadow-xs outline-hidden text-foreground dark:text-white",
|
|
|
|
|
),
|
|
|
|
|
option: () =>
|
|
|
|
|
cn(
|
|
|
|
|
"dark:bg-black text-foreground bg-white dark:text-white border-b border-neutral-300 dark:border-foreground shadow-xs outline-hidden hover:text-primary hover:bg-neutral-200 dark:hover:bg-neutral-700",
|
|
|
|
|
),
|
|
|
|
|
menu: () =>
|
|
|
|
|
cn(
|
|
|
|
|
`dark:bg-black text-foreground bg-white dark:text-white border shadow-xs outline-hidden
|
2025-08-04 17:45:44 +02:00
|
|
|
dark:border-foreground border-neutral-300 overflow-hidden`,
|
2025-08-28 18:27:07 +02:00
|
|
|
menuOpen
|
|
|
|
|
? "animate-in fade-in-0 zoom-in-90"
|
|
|
|
|
: "animate-out fade-out-0 zoom-out-90",
|
|
|
|
|
),
|
|
|
|
|
multiValueLabel: () =>
|
|
|
|
|
cn(
|
|
|
|
|
"dark:bg-black rounded-lg text-primary bg-white dark:text-white outline-hidden",
|
|
|
|
|
),
|
|
|
|
|
multiValue: () =>
|
|
|
|
|
cn(
|
|
|
|
|
"overflow-hidden dark:bg-black rounded-lg text-primary bg-white dark:text-white border border-neutral-300 dark:border-foreground shadow-xs outline-hidden",
|
|
|
|
|
),
|
|
|
|
|
multiValueRemove: () => cn("rounded-s-none rounded-e-md"),
|
|
|
|
|
input: () => cn("*:ring-0 focus:*:ring-0"),
|
|
|
|
|
singleValue: () => cn("text-primary font-medium"),
|
|
|
|
|
}}
|
|
|
|
|
classNamePrefix="select"
|
|
|
|
|
/>
|
|
|
|
|
);
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type UnpackOptions = {
|
2025-08-28 18:27:07 +02:00
|
|
|
options: string[];
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
export const UnpackOptions = ({ options }: UnpackOptions) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
return options.map((v, idx) => ({ value: idx.toString(), label: v }));
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type RepackValues = {
|
2025-08-28 18:27:07 +02:00
|
|
|
values: string | string[];
|
|
|
|
|
options: string[];
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
export const RepackValues = ({ values, options }: RepackValues) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
if (Array.isArray(values)) {
|
|
|
|
|
return values.map((v) => ({
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <intended>
|
|
|
|
|
label: options[Number.parseInt(v)]!,
|
|
|
|
|
value: v,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
// biome-ignore lint/style/noNonNullAssertion: <intended>
|
|
|
|
|
label: options[Number.parseInt(values)]!,
|
|
|
|
|
value: values,
|
|
|
|
|
};
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|