import React from 'react';
import { useCommandActions } from '@udecode/cmdk';
import { isHotkey } from '@udecode/plate';
import { isEqualTags } from '@udecode/plate-tag';
import {
MultiSelectPlugin,
TagPlugin,
useSelectableItems,
useSelectEditorCombobox,
} from '@udecode/plate-tag/react';
import {
Plate,
useEditorContainerRef,
useEditorRef,
usePlateEditor,
} from '@udecode/plate/react';
import { Fzf } from 'fzf';
import { PlusIcon } from 'lucide-react';
import { Command, CommandGroup, CommandItem, CommandList } from './command';
import { Editor, EditorContainer } from './editor';
import { Popover, PopoverAnchor, PopoverContent } from './popover';
import { TagElement } from './tag-element';
export type SelectItem = {
value: string;
isNew?: boolean;
};
type SelectEditorContextValue = {
items: SelectItem[];
open: boolean;
setOpen: (open: boolean) => void;
defaultValue?: SelectItem[];
value?: SelectItem[];
onValueChange?: (items: SelectItem[]) => void;
};
const SelectEditorContext = React.createContext<
SelectEditorContextValue | undefined
>(undefined);
const useSelectEditorContext = () => {
const context = React.useContext(SelectEditorContext);
if (!context) {
throw new Error('useSelectEditor must be used within SelectEditor');
}
return context;
};
export function SelectEditor({
children,
defaultValue,
items = [],
value,
onValueChange,
}: {
children: React.ReactNode;
defaultValue?: SelectItem[];
items?: SelectItem[];
value?: SelectItem[];
onValueChange?: (items: SelectItem[]) => void;
}) {
const [open, setOpen] = React.useState(false);
const [internalValue] = React.useState(defaultValue);
return (
{children}
);
}
export function SelectEditorContent({
children,
}: {
children: React.ReactNode;
}) {
const { value } = useSelectEditorContext();
const { setSearch } = useCommandActions();
const editor = usePlateEditor(
{
plugins: [MultiSelectPlugin.withComponent(TagElement)],
value: createEditorValue(value),
},
[],
);
React.useEffect(() => {
if (!isEqualTags(editor, value)) {
editor.tf.replaceNodes(createEditorValue(value), {
at: [],
children: true,
});
}
}, [editor, value]);
return (
{
setSearch(editor.api.string([]));
}}
editor={editor}
>
{children}
);
}
export const SelectEditorInput = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef
>((props, ref) => {
const editor = useEditorRef();
const { setOpen } = useSelectEditorContext();
const { selectCurrentItem, selectFirstItem } = useCommandActions();
return (
setOpen(false)}
onFocusCapture={() => {
setOpen(true);
selectFirstItem();
}}
onKeyDown={(e) => {
if (isHotkey('enter', e)) {
e.preventDefault();
selectCurrentItem();
editor.tf.removeNodes({ at: [], empty: false, text: true });
}
if (isHotkey('escape', e) || isHotkey('mod+enter', e)) {
e.preventDefault();
e.currentTarget.blur();
}
}}
autoFocusOnEditable
{...props}
/>
);
});
SelectEditorInput.displayName = 'SelectEditorInput';
export function SelectEditorCombobox() {
const editor = useEditorRef();
const containerRef = useEditorContainerRef();
const { items, open, onValueChange } = useSelectEditorContext();
const selectableItems = useSelectableItems({
filter: fzfFilter,
items,
});
const { selectFirstItem } = useCommandActions();
useSelectEditorCombobox({ open, selectFirstItem, onValueChange });
if (!open || selectableItems.length === 0) return null;
return (
e.preventDefault()}
onOpenAutoFocus={(e) => e.preventDefault()}
align="start"
alignOffset={-4}
animate={false}
sideOffset={8}
>
{selectableItems.map((item) => (
e.preventDefault()}
onSelect={() => {
editor.getTransforms(TagPlugin).insert.tag(item);
}}
>
{item.isNew ? (
Create new label:
"{item.value}"
) : (
item.value
)}
))}
);
}
const createEditorValue = (value?: SelectItem[]) => [
{
children: [
{ text: '' },
...(value?.flatMap((item) => [
{
children: [{ text: '' }],
type: TagPlugin.key,
...item,
},
{
text: '',
},
]) ?? []),
],
type: 'p',
},
];
const fzfFilter = (value: string, search: string): boolean => {
if (!search) return true;
const fzf = new Fzf([value], {
casing: 'case-insensitive',
selector: (v: string) => v,
});
return fzf.find(search).length > 0;
};