Editor
Files
'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


特性
与传统的基于输入的多选不同,该组件构建在 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
在当前选择处插入新的多选元素。
getSelectedItems
获取编辑器中的所有标签项目。
isEqualTags
比较两组标签是否相等的工具函数,忽略顺序。
Hooks
useSelectedItems
获取编辑器中当前选中的标签项目的 Hook。
useSelectableItems
获取可选择的可用项目的 Hook,通过搜索过滤并排除已选中的项目。
useSelectEditorCombobox
处理编辑器中组合框行为的 Hook,包括文本清理和项目选择。
类型
TTagElement
type TTagElement = TElement & {
value: string;
[key: string]: unknown;
};TTagProps
type TTagProps = {
value: string;
[key: string]: unknown;
};