mirror of
https://github.com/community-scripts/ProxmoxVE
synced 2025-02-15 12:19:17 +00:00
Remove MultiSelect component
This commit is contained in:
parent
a3b2a476c1
commit
64fd9a5dcd
@ -1,387 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Check, X as RemoveIcon } from "lucide-react";
|
||||
import React, {
|
||||
KeyboardEvent,
|
||||
createContext,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface MultiSelectorProps
|
||||
extends React.ComponentPropsWithoutRef<typeof CommandPrimitive> {
|
||||
values: string[];
|
||||
onValuesChange: (value: string[]) => void;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
interface MultiSelectContextProps {
|
||||
value: string[];
|
||||
onValueChange: (value: any) => void;
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
inputValue: string;
|
||||
setInputValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
activeIndex: number;
|
||||
setActiveIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||
ref: React.RefObject<HTMLInputElement>;
|
||||
handleSelect: (e: React.SyntheticEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const MultiSelectContext = createContext<MultiSelectContextProps | null>(null);
|
||||
|
||||
const useMultiSelect = () => {
|
||||
const context = useContext(MultiSelectContext);
|
||||
if (!context) {
|
||||
throw new Error("useMultiSelect must be used within MultiSelectProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* MultiSelect Docs: {@link: https://shadcn-extension.vercel.app/docs/multi-select}
|
||||
*/
|
||||
|
||||
// TODO : expose the visibility of the popup
|
||||
|
||||
const MultiSelector = ({
|
||||
values: value,
|
||||
onValuesChange: onValueChange,
|
||||
loop = false,
|
||||
className,
|
||||
children,
|
||||
dir,
|
||||
...props
|
||||
}: MultiSelectorProps) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [isValueSelected, setIsValueSelected] = React.useState(false);
|
||||
const [selectedValue, setSelectedValue] = React.useState("");
|
||||
|
||||
const onValueChangeHandler = useCallback(
|
||||
(val: string) => {
|
||||
if (value.includes(val)) {
|
||||
onValueChange(value.filter((item) => item !== val));
|
||||
} else {
|
||||
onValueChange([...value, val]);
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[value],
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(e: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const target = e.currentTarget;
|
||||
const selection = target.value.substring(
|
||||
target.selectionStart ?? 0,
|
||||
target.selectionEnd ?? 0,
|
||||
);
|
||||
|
||||
setSelectedValue(selection);
|
||||
setIsValueSelected(selection === inputValue);
|
||||
},
|
||||
[inputValue],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
const target = inputRef.current;
|
||||
|
||||
if (!target) return;
|
||||
|
||||
const moveNext = () => {
|
||||
const nextIndex = activeIndex + 1;
|
||||
setActiveIndex(
|
||||
nextIndex > value.length - 1 ? (loop ? 0 : -1) : nextIndex,
|
||||
);
|
||||
};
|
||||
|
||||
const movePrev = () => {
|
||||
const prevIndex = activeIndex - 1;
|
||||
setActiveIndex(prevIndex < 0 ? value.length - 1 : prevIndex);
|
||||
};
|
||||
|
||||
const moveCurrent = () => {
|
||||
const newIndex =
|
||||
activeIndex - 1 <= 0
|
||||
? value.length - 1 === 0
|
||||
? -1
|
||||
: 0
|
||||
: activeIndex - 1;
|
||||
setActiveIndex(newIndex);
|
||||
};
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowLeft":
|
||||
if (dir === "rtl") {
|
||||
if (value.length > 0 && (activeIndex !== -1 || loop)) {
|
||||
moveNext();
|
||||
}
|
||||
} else {
|
||||
if (value.length > 0 && target.selectionStart === 0) {
|
||||
movePrev();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
if (dir === "rtl") {
|
||||
if (value.length > 0 && target.selectionStart === 0) {
|
||||
movePrev();
|
||||
}
|
||||
} else {
|
||||
if (value.length > 0 && (activeIndex !== -1 || loop)) {
|
||||
moveNext();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Backspace":
|
||||
case "Delete":
|
||||
if (value.length > 0) {
|
||||
if (activeIndex !== -1 && activeIndex < value.length) {
|
||||
onValueChangeHandler(value[activeIndex]);
|
||||
moveCurrent();
|
||||
} else {
|
||||
if (target.selectionStart === 0) {
|
||||
if (selectedValue === inputValue || isValueSelected) {
|
||||
onValueChangeHandler(value[value.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "Enter":
|
||||
setOpen(true);
|
||||
break;
|
||||
|
||||
case "Escape":
|
||||
if (activeIndex !== -1) {
|
||||
setActiveIndex(-1);
|
||||
} else if (open) {
|
||||
setOpen(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[value, inputValue, activeIndex, loop],
|
||||
);
|
||||
|
||||
return (
|
||||
<MultiSelectContext.Provider
|
||||
value={{
|
||||
value,
|
||||
onValueChange: onValueChangeHandler,
|
||||
open,
|
||||
setOpen,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
ref: inputRef,
|
||||
handleSelect,
|
||||
}}
|
||||
>
|
||||
<Command
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
"overflow-visible bg-transparent flex flex-col space-y-2",
|
||||
className,
|
||||
)}
|
||||
dir={dir}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
</MultiSelectContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiSelectorTrigger = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { value, onValueChange, activeIndex } = useMultiSelect();
|
||||
|
||||
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap gap-1 p-1 py-2 ring-1 ring-muted rounded-lg bg-background",
|
||||
{
|
||||
"ring-1 focus-within:ring-ring": activeIndex === -1,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{value.map((item, index) => (
|
||||
<Badge
|
||||
key={item}
|
||||
className={cn(
|
||||
"px-1 rounded-xl flex items-center gap-1",
|
||||
activeIndex === index && "ring-2 ring-muted-foreground ",
|
||||
)}
|
||||
variant={"secondary"}
|
||||
>
|
||||
<span className="text-xs">{item}</span>
|
||||
<button
|
||||
aria-label={`Remove ${item} option`}
|
||||
aria-roledescription="button to remove option"
|
||||
type="button"
|
||||
onMouseDown={mousePreventDefault}
|
||||
onClick={() => onValueChange(item)}
|
||||
>
|
||||
<span className="sr-only">Remove {item} option</span>
|
||||
<RemoveIcon className="h-4 w-4 hover:stroke-destructive" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectorTrigger.displayName = "MultiSelectorTrigger";
|
||||
|
||||
const MultiSelectorInput = forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const {
|
||||
setOpen,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
activeIndex,
|
||||
setActiveIndex,
|
||||
handleSelect,
|
||||
ref: inputRef,
|
||||
} = useMultiSelect();
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Input
|
||||
{...props}
|
||||
tabIndex={0}
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onValueChange={activeIndex === -1 ? setInputValue : undefined}
|
||||
onSelect={handleSelect}
|
||||
onBlur={() => setOpen(false)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onClick={() => setActiveIndex(-1)}
|
||||
className={cn(
|
||||
"ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1",
|
||||
className,
|
||||
activeIndex !== -1 && "caret-transparent",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectorInput.displayName = "MultiSelectorInput";
|
||||
|
||||
const MultiSelectorContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ children }, ref) => {
|
||||
const { open } = useMultiSelect();
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
{open && children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectorContent.displayName = "MultiSelectorContent";
|
||||
|
||||
const MultiSelectorList = forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, children }, ref) => {
|
||||
return (
|
||||
<CommandList
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-2 flex flex-col gap-2 rounded-md scrollbar-thin scrollbar-track-transparent transition-colors scrollbar-thumb-muted-foreground dark:scrollbar-thumb-muted scrollbar-thumb-rounded-lg w-full absolute bg-background shadow-md z-10 border border-muted top-0",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground">No results found</span>
|
||||
</CommandEmpty>
|
||||
</CommandList>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectorList.displayName = "MultiSelectorList";
|
||||
|
||||
const MultiSelectorItem = forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
{ value: string } & React.ComponentPropsWithoutRef<
|
||||
typeof CommandPrimitive.Item
|
||||
>
|
||||
>(({ className, value, children, ...props }, ref) => {
|
||||
const { value: Options, onValueChange, setInputValue } = useMultiSelect();
|
||||
|
||||
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const isIncluded = Options.includes(value);
|
||||
return (
|
||||
<CommandItem
|
||||
ref={ref}
|
||||
{...props}
|
||||
onSelect={() => {
|
||||
onValueChange(value);
|
||||
setInputValue("");
|
||||
}}
|
||||
className={cn(
|
||||
"rounded-md cursor-pointer px-2 py-1 transition-colors flex justify-between ",
|
||||
className,
|
||||
isIncluded && "opacity-50 cursor-default",
|
||||
props.disabled && "opacity-50 cursor-not-allowed",
|
||||
)}
|
||||
onMouseDown={mousePreventDefault}
|
||||
>
|
||||
{children}
|
||||
{isIncluded && <Check className="h-4 w-4" />}
|
||||
</CommandItem>
|
||||
);
|
||||
});
|
||||
|
||||
MultiSelectorItem.displayName = "MultiSelectorItem";
|
||||
|
||||
export {
|
||||
MultiSelector,
|
||||
MultiSelectorContent,
|
||||
MultiSelectorInput,
|
||||
MultiSelectorItem,
|
||||
MultiSelectorList,
|
||||
MultiSelectorTrigger,
|
||||
};
|
Loading…
Reference in New Issue
Block a user