Add tags and categories
This commit is contained in:
@ -77,6 +77,12 @@ interface MultipleSelectorProps {
|
||||
>;
|
||||
/** hide the clear all button. */
|
||||
hideClearAllButton?: boolean;
|
||||
|
||||
/** Show selected items inside dropdown as disabled items (useful for "Selected" section). */
|
||||
showSelectedInDropdown?: boolean;
|
||||
|
||||
/** Optional explicit group ordering (top to bottom). */
|
||||
groupOrder?: string[];
|
||||
}
|
||||
|
||||
export interface MultipleSelectorRef {
|
||||
@ -192,6 +198,8 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
commandProps,
|
||||
inputProps,
|
||||
hideClearAllButton = false,
|
||||
showSelectedInDropdown = false,
|
||||
groupOrder,
|
||||
}: MultipleSelectorProps,
|
||||
ref: React.Ref<MultipleSelectorRef>,
|
||||
) => {
|
||||
@ -404,10 +412,13 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
return <CommandEmpty>{emptyIndicator}</CommandEmpty>;
|
||||
}, [creatable, emptyIndicator, onSearch, options]);
|
||||
|
||||
const selectables = React.useMemo<GroupOption>(
|
||||
() => removePickedOption(options, selected),
|
||||
[options, selected],
|
||||
);
|
||||
const selectables = React.useMemo<GroupOption>(() => {
|
||||
if (showSelectedInDropdown) {
|
||||
// keep all options; selected will be rendered disabled (see below)
|
||||
return options;
|
||||
}
|
||||
return removePickedOption(options, selected);
|
||||
}, [options, selected, showSelectedInDropdown]);
|
||||
|
||||
/** Avoid Creatable Selector freezing or lagging when paste a long string. */
|
||||
const commandFilter = React.useCallback(() => {
|
||||
@ -424,6 +435,30 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
return undefined;
|
||||
}, [creatable, commandProps?.filter]);
|
||||
|
||||
const orderedGroupEntries = React.useMemo(() => {
|
||||
const entries = Object.entries(selectables);
|
||||
|
||||
if (!groupOrder || groupOrder.length === 0) {
|
||||
// default: existing behavior
|
||||
return entries;
|
||||
}
|
||||
|
||||
const map = new Map(entries);
|
||||
const ordered: Array<[string, Option[]]> = [];
|
||||
|
||||
for (const key of groupOrder) {
|
||||
const v = map.get(key);
|
||||
if (v) {
|
||||
ordered.push([key, v]);
|
||||
map.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// any remaining groups not specified in groupOrder (alphabetical)
|
||||
const rest = Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||
return [...ordered, ...rest];
|
||||
}, [selectables, groupOrder]);
|
||||
|
||||
return (
|
||||
<Command
|
||||
ref={dropdownRef}
|
||||
@ -458,8 +493,8 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
<Badge
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground',
|
||||
'data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground',
|
||||
'data-disabled:bg-muted-foreground data-disabled:text-muted data-disabled:hover:bg-muted-foreground',
|
||||
'data-fixed:bg-muted-foreground data-fixed:text-muted data-fixed:hover:bg-muted-foreground',
|
||||
badgeClassName,
|
||||
)}
|
||||
data-fixed={option.fixed}
|
||||
@ -559,32 +594,42 @@ const MultipleSelector = React.forwardRef<MultipleSelectorRef, MultipleSelectorP
|
||||
{EmptyItem()}
|
||||
{CreatableItem()}
|
||||
{!selectFirstItem && <CommandItem value="-" className="hidden" />}
|
||||
{Object.entries(selectables).map(([key, dropdowns]) => (
|
||||
{orderedGroupEntries.map(([key, dropdowns]) => (
|
||||
<CommandGroup key={key} heading={key} className="h-full overflow-auto">
|
||||
<>
|
||||
{dropdowns.map((option) => {
|
||||
const alreadySelected = selected.some((s) => s.value === option.value);
|
||||
const disabledItem = option.disable || (showSelectedInDropdown && alreadySelected);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
value={option.label}
|
||||
disabled={option.disable}
|
||||
disabled={disabledItem}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onSelect={() => {
|
||||
if (disabledItem) return;
|
||||
|
||||
if (selected.length >= maxSelected) {
|
||||
onMaxSelected?.(selected.length);
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
|
||||
// Guard against duplicates (safety)
|
||||
if (selected.some((s) => s.value === option.value)) return;
|
||||
|
||||
const newOptions = [...selected, option];
|
||||
setSelected(newOptions);
|
||||
onChange?.(newOptions);
|
||||
}}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
option.disable && 'cursor-default text-muted-foreground',
|
||||
disabledItem && 'cursor-default text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{option.label}
|
||||
|
||||
Reference in New Issue
Block a user