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; };