Loading…
Kit 使用
安装
添加讨论功能最快的方法是使用 DiscussionKit,它包含预配置的 discussionPlugin 及其 Plate UI 组件。
'use client';
import type { TComment } from '@/components/ui/comment';
import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
export type TDiscussion = {
id: string;
comments: TComment[];
createdAt: Date;
isResolved: boolean;
userId: string;
documentContent?: string;
};
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
const getTargetElement = (target: EventTarget | null) => {
if (target instanceof HTMLElement) return target;
if (target instanceof Node) return target.parentElement;
return null;
};
export const getDiscussionClickTarget = ({
selector,
target,
}: {
selector: string;
target: EventTarget | null;
}) => {
const element = getTargetElement(target);
if (!element) return null;
return element.closest(selector) as HTMLElement | null;
};
export const getDiscussionBlockClickTarget = ({
selector = BLOCK_SUGGESTION_SELECTOR,
target,
}: {
selector?: string;
target: EventTarget | null;
}) =>
getDiscussionClickTarget({
selector,
target,
});
const discussionsData: TDiscussion[] = [
{
id: 'discussion1',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Comments are a great way to provide feedback and discuss changes.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 600_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'charlie',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'Agreed! The link to the docs makes it easy to learn more.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 500_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'bob',
},
],
createdAt: new Date(),
documentContent: 'comments',
isResolved: false,
userId: 'charlie',
},
{
id: 'discussion2',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 300_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'bob',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'This helps users understand how powerful the editor can be.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 200_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'charlie',
},
],
createdAt: new Date(),
documentContent: 'overlapping',
isResolved: false,
userId: 'bob',
},
];
const avatarUrl = (seed: string) =>
`https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
const usersData: Record<
string,
{ id: string; avatarUrl: string; name: string; hue?: number }
> = {
alice: {
id: 'alice',
avatarUrl: avatarUrl('alice6'),
name: 'Alice',
},
bob: {
id: 'bob',
avatarUrl: avatarUrl('bob4'),
name: 'Bob',
},
charlie: {
id: 'charlie',
avatarUrl: avatarUrl('charlie2'),
name: 'Charlie',
},
};
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = createPlatePlugin({
key: 'discussion',
options: {
currentUserId: 'alice',
discussions: discussionsData,
users: usersData,
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
})
.extendSelectors(({ getOption }) => ({
currentUser: () => getOption('users')[getOption('currentUserId')],
user: (id: string) => getOption('users')[id],
}));
export const DiscussionKit = [discussionPlugin];'use client';
import type { TComment } from '@/components/ui/comment';
import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
export type TDiscussion = {
id: string;
comments: TComment[];
createdAt: Date;
isResolved: boolean;
userId: string;
documentContent?: string;
};
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
const getTargetElement = (target: EventTarget | null) => {
if (target instanceof HTMLElement) return target;
if (target instanceof Node) return target.parentElement;
return null;
};
export const getDiscussionClickTarget = ({
selector,
target,
}: {
selector: string;
target: EventTarget | null;
}) => {
const element = getTargetElement(target);
if (!element) return null;
return element.closest(selector) as HTMLElement | null;
};
export const getDiscussionBlockClickTarget = ({
selector = BLOCK_SUGGESTION_SELECTOR,
target,
}: {
selector?: string;
target: EventTarget | null;
}) =>
getDiscussionClickTarget({
selector,
target,
});
const discussionsData: TDiscussion[] = [
{
id: 'discussion1',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Comments are a great way to provide feedback and discuss changes.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 600_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'charlie',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'Agreed! The link to the docs makes it easy to learn more.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 500_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'bob',
},
],
createdAt: new Date(),
documentContent: 'comments',
isResolved: false,
userId: 'charlie',
},
{
id: 'discussion2',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 300_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'bob',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'This helps users understand how powerful the editor can be.',
},
],
type: 'p',
},
],
createdAt: new Date(Date.now() - 200_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'charlie',
},
],
createdAt: new Date(),
documentContent: 'overlapping',
isResolved: false,
userId: 'bob',
},
];
const avatarUrl = (seed: string) =>
`https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
const usersData: Record<
string,
{ id: string; avatarUrl: string; name: string; hue?: number }
> = {
alice: {
id: 'alice',
avatarUrl: avatarUrl('alice6'),
name: 'Alice',
},
bob: {
id: 'bob',
avatarUrl: avatarUrl('bob4'),
name: 'Bob',
},
charlie: {
id: 'charlie',
avatarUrl: avatarUrl('charlie2'),
name: 'Charlie',
},
};
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = createPlatePlugin({
key: 'discussion',
options: {
currentUserId: 'alice',
discussions: discussionsData,
users: usersData,
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
})
.extendSelectors(({ getOption }) => ({
currentUser: () => getOption('users')[getOption('currentUserId')],
user: (id: string) => getOption('users')[id],
}));
export const DiscussionKit = [discussionPlugin];BlockDiscussion: 在节点上方渲染讨论 UI
添加 Kit
import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/plugins/discussion-kit';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
...DiscussionKit,
],
});import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/plugins/discussion-kit';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
...DiscussionKit,
],
});手动使用
安装
pnpm add @platejs/comment @platejs/suggestionpnpm add @platejs/comment @platejs/suggestion创建插件
import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
export interface TDiscussion {
id: string;
comments: TComment[];
createdAt: Date;
isResolved: boolean;
userId: string;
documentContent?: string;
}
const usersData = {
alice: {
id: 'alice',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
name: 'Alice',
},
bob: {
id: 'bob',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
name: 'Bob',
},
};
export const discussionPlugin = createPlatePlugin({
key: 'discussion',
options: {
currentUserId: 'alice',
discussions: [],
users: usersData,
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
})
.extendSelectors(({ getOption }) => ({
currentUser: () => getOption('users')[getOption('currentUserId')],
user: (id: string) => getOption('users')[id],
}));import { createPlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/ui/block-discussion';
export interface TDiscussion {
id: string;
comments: TComment[];
createdAt: Date;
isResolved: boolean;
userId: string;
documentContent?: string;
}
const usersData = {
alice: {
id: 'alice',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
name: 'Alice',
},
bob: {
id: 'bob',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
name: 'Bob',
},
};
export const discussionPlugin = createPlatePlugin({
key: 'discussion',
options: {
currentUserId: 'alice',
discussions: [],
users: usersData,
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
})
.extendSelectors(({ getOption }) => ({
currentUser: () => getOption('users')[getOption('currentUserId')],
user: (id: string) => getOption('users')[id],
}));options.currentUserId: 当前活跃用户的 IDoptions.discussions: 讨论数据结构数组options.users: 将用户 ID 映射到用户数据的对象render.aboveNodes: 在节点上方渲染BlockDiscussion用于讨论 UIselectors.currentUser: 获取当前用户数据selectors.user: 通过 ID 获取用户数据
添加插件
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
discussionPlugin,
],
});import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...其他插件,
discussionPlugin,
],
});插件
discussionPlugin
用于管理包括用户和讨论数据在内的协作状态的纯 UI 插件。
选择器
currentUser
获取当前用户数据。
user
通过 ID 获取用户数据。
类型
TDiscussion
包含评论和元数据的讨论数据结构。
UserData
用于协作的用户信息结构。