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

import * as 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/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from '@/components/ui/form';
import {
  type SelectItem,
  SelectEditor,
  SelectEditorCombobox,
  SelectEditorContent,
  SelectEditorInput,
} from '@/components/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/popover', value: 'Popover' },
  { url: '/docs/components/tag-node', 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 pt-24 pl-2">
      <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>
  );
}
A form with a select editor component for managing labels.
select-editor-demo
select-editor-demo

特性

与传统的基于输入的多选不同,该组件构建在 Plate editor 之上,提供:

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

手动使用

安装

pnpm add @platejs/tag

添加插件

import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    MultiSelectPlugin, // 具有标签功能的多选编辑器
  ],
});

配置插件

import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
import { TagElement } from '@/components/ui/tag-node';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    MultiSelectPlugin.withComponent(TagElement),
  ],
});
  • MultiSelectPlugin:扩展 TagPlugin 并将编辑器限制为仅包含标签元素
  • withComponent:分配 TagElement 来渲染标签组件

添加 SelectEditor

基本示例

import { MultiSelectPlugin } from '@platejs/tag/react';
import { TagElement } from '@/components/ui/tag-node';
import {
  SelectEditor,
  SelectEditorContent,
  SelectEditorInput,
  SelectEditorCombobox,
  type SelectItem,
} from '@/components/ui/select-editor';
 
// 定义你的项目
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="选择项目..." />
        <SelectEditorCombobox />
      </SelectEditorContent>
    </SelectEditor>
  );
}

表单示例

'use client';

import * as 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/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from '@/components/ui/form';
import {
  type SelectItem,
  SelectEditor,
  SelectEditorCombobox,
  SelectEditorContent,
  SelectEditorInput,
} from '@/components/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/popover', value: 'Popover' },
  { url: '/docs/components/tag-node', 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 pt-24 pl-2">
      <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>
  );
}

插件

TagPlugin

用于单个标签功能的内联 void 元素插件。

MultiSelectPlugin

TagPlugin 的扩展,将编辑器限制为仅包含标签元素,启用多选行为,具有自动文本清理和重复预防功能。

API

tf.insert.tag

在当前选择处插入新的多选元素。

Parameters

Collapse all

    多选元素的属性。

OptionsTTagProps

Collapse all

    多选元素的唯一值。

getSelectedItems

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

ReturnsTTagProps[]

Collapse all

    编辑器中的标签项目数组。

isEqualTags

比较两组标签是否相等的工具函数,忽略顺序。

Parameters

Collapse all

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

Returnsboolean

Collapse all

    两组是否包含相同的值。

Hooks

useSelectedItems

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

ReturnsTTagProps[]

Collapse all

    具有值和属性的选中标签项目数组。

useSelectableItems

获取可选择的可用项目的 Hook,通过搜索过滤并排除已选中的项目。

Optionsoptions

Collapse all

    是否允许创建新项目。

    • 默认值: true

    项目的自定义过滤函数。

    可用项目数组。

    新项目的过滤函数。

    新项目在列表中的位置。

    • 默认值: 'end'

ReturnsT[]

Collapse all

    过滤后的可选项目数组。

useSelectEditorCombobox

处理编辑器中组合框行为的 Hook,包括文本清理和项目选择。

Optionsoptions

Collapse all

    组合框是否打开。

    选择第一个组合框项目的函数。

    选中项目更改时的回调。

类型

TTagElement

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

TTagProps

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