Multi Select

A rich multi-select editor.

Loading...
Files
components/select-editor-demo.tsx
'use client';

import React from 'react';
import { useForm, useWatch } from 'react-hook-form';

import { zodResolver } from '@hookform/resolvers/zod';
import { CheckIcon, PlusIcon } from 'lucide-react';
import * as z from 'zod';

import { Button } from '@/components/plate-ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from '@/components/plate-ui/form';
import {
  type SelectItem,
  SelectEditor,
  SelectEditorCombobox,
  SelectEditorContent,
  SelectEditorInput,
} from '@/components/plate-ui/select-editor';

const LABELS = [
  { url: '/docs/components/editor', value: 'Editor' },
  { url: '/docs/components/select-editor', value: 'Select Editor' },
  { url: '/docs/components/block-selection', value: 'Block Selection' },
  { url: '/docs/components/button', value: 'Button' },
  { url: '/docs/components/command', value: 'Command' },
  { url: '/docs/components/dialog', value: 'Dialog' },
  { url: '/docs/components/form', value: 'Form' },
  { url: '/docs/components/input', value: 'Input' },
  { url: '/docs/components/label', value: 'Label' },
  { url: '/docs/components/plate-element', value: 'Plate Element' },
  { url: '/docs/components/popover', value: 'Popover' },
  { url: '/docs/components/tag-element', value: 'Tag Element' },
] satisfies (SelectItem & { url: string })[];

const formSchema = z.object({
  labels: z
    .array(
      z.object({
        value: z.string(),
      })
    )
    .min(1, 'Select at least one label')
    .max(10, 'Select up to 10 labels'),
});

type FormValues = z.infer<typeof formSchema>;

export default function EditorSelectForm() {
  const [readOnly, setReadOnly] = React.useState(false);
  const form = useForm<FormValues>({
    defaultValues: {
      labels: [LABELS[0]],
    },
    resolver: zodResolver(formSchema),
  });

  const labels = useWatch({ control: form.control, name: 'labels' });

  return (
    <div className="mx-auto w-full max-w-2xl space-y-8 p-11 pl-2 pt-24">
      <Form {...form}>
        <div className="space-y-6">
          <FormField
            name="labels"
            control={form.control}
            render={({ field }) => (
              <FormItem>
                <div className="flex items-start gap-2">
                  <Button
                    variant="ghost"
                    className="h-10"
                    onClick={() => setReadOnly(!readOnly)}
                    type="button"
                  >
                    {readOnly ? (
                      <PlusIcon className="size-4" />
                    ) : (
                      <CheckIcon className="size-4" />
                    )}
                  </Button>

                  {readOnly && labels.length === 0 ? (
                    <Button
                      size="lg"
                      variant="ghost"
                      className="h-10"
                      onClick={() => {
                        setReadOnly(false);
                      }}
                      type="button"
                    >
                      Add labels
                    </Button>
                  ) : (
                    <FormControl>
                      <SelectEditor
                        value={field.value}
                        onValueChange={readOnly ? undefined : field.onChange}
                        items={LABELS}
                      >
                        <SelectEditorContent>
                          <SelectEditorInput
                            readOnly={readOnly}
                            placeholder={
                              readOnly ? 'Empty' : 'Select labels...'
                            }
                          />
                          {!readOnly && <SelectEditorCombobox />}
                        </SelectEditorContent>
                      </SelectEditor>
                    </FormControl>
                  )}
                </div>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>
      </Form>
    </div>
  );
}

功能

与传统的基于输入框的多选组件不同,该组件基于 Plate 编辑器构建,提供以下功能:

  • 完整的历史记录支持(撤销/重做)
  • 标签之间和标签内的原生光标导航
  • 支持选择一个或多个标签
  • 复制/粘贴标签
  • 拖放重新排序标签
  • 只读模式
  • 防止重复标签
  • 创建新标签,不区分大小写
  • 搜索文本清理
  • 空格修剪
  • 使用 cmdk 的模糊搜索

安装

npm install @udecode/plate-tag

用法

import { MultiSelectPlugin } from '@udecode/plate-tag/react';
import { TagElement } from '@/components/plate-ui/tag-element';
import {
  SelectEditor,
  SelectEditorContent,
  SelectEditorInput,
  SelectEditorCombobox,
  type SelectItem,
} from '@/components/plate-ui/select-editor';
 
// Define your items
const ITEMS: SelectItem[] = [
  { value: 'React' },
  { value: 'TypeScript' },
  { value: 'JavaScript' },
];
 
export default function MySelectEditor() {
  const [value, setValue] = React.useState<SelectItem[]>([ITEMS[0]]);
 
  return (
    <SelectEditor
      value={value}
      onValueChange={setValue}
      items={ITEMS}
    >
      <SelectEditorContent>
        <SelectEditorInput placeholder="Select items..." />
        <SelectEditorCombobox />
      </SelectEditorContent>
    </SelectEditor>
  );
}

插件

TagPlugin

内联空元素插件。

MultiSelectPlugin

继承 TagPlugin

API

editor.tf.insert.tag

在当前选择位置插入一个新的多选元素。

Parameters

Collapse all

    多选元素的属性:

Hooks

useSelectedItems

获取编辑器中当前选中的标签项。

Returns

Collapse all

    当前选中的标签项数组,每个标签项包含一个值和任何其他属性。

getSelectedItems

获取编辑器中所有标签项。

Parameters

Collapse all

    编辑器实例。

Returns

Collapse all

    编辑器中的标签项数组。

isEqualTags

比较两个标签集是否相等,忽略顺序。

Parameters

Collapse all

    编辑器实例。

    要与当前编辑器标签进行比较的新标签集。

Returns

Collapse all

    true 如果两个集合包含相同的值,false 否则。

useSelectableItems

获取可以被选择的项目,通过搜索并排除已经选中的项目。

Parameters

Collapse all

Returns

Collapse all

    过滤后的选择项目数组。

useSelectEditorCombobox

处理组合框行为,包括文本清理和项目选择。

Parameters

Collapse all

Types

TTagElement

type TTagElement = TElement & {
  value: string;
  [key: string]: unknown;
};

TagLike

type TagLike = {
  value: string;
  [key: string]: unknown;
};