Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
Adding and displaying comments within content.
discussion-demo
discussion-demo

功能特性

  • 文本评论: 以内联标注形式添加文本评论标记
  • 重叠评论: 支持同一段文本上的多个评论
  • 草稿评论: 在最终确定前创建评论草稿
  • 状态追踪: 跟踪评论状态和用户交互
  • 讨论集成: 与讨论插件配合实现完整协作功能

套件使用

安装

最快捷的添加评论功能方式是使用 CommentKit,它包含预配置的 commentPlugin 和相关组件以及它们的 Plate UI 组件。

'use client';

import type { ExtendConfig, Path } from 'platejs';

import {
  type BaseCommentConfig,
  BaseCommentPlugin,
  getDraftCommentKey,
} from '@platejs/comment';
import { isSlateString } from 'platejs';
import { toTPlatePlugin } from 'platejs/react';

import { CommentLeaf } from '@/components/ui/comment-node';

type CommentConfig = ExtendConfig<
  BaseCommentConfig,
  {
    activeId: string | null;
    commentingBlock: Path | null;
    hoverId: string | null;
    uniquePathMap: Map<string, Path>;
  }
>;

export const commentPlugin = toTPlatePlugin<CommentConfig>(BaseCommentPlugin, {
  handlers: {
    onClick: ({ api, event, setOption, type }) => {
      let leaf = event.target as HTMLElement;
      let isSet = false;

      const unsetActiveSuggestion = () => {
        setOption('activeId', null);
        isSet = true;
      };

      if (!isSlateString(leaf)) unsetActiveSuggestion();

      while (leaf.parentElement) {
        if (leaf.classList.contains(`slate-${type}`)) {
          const commentsEntry = api.comment!.node();

          if (!commentsEntry) {
            unsetActiveSuggestion();

            break;
          }

          const id = api.comment!.nodeId(commentsEntry[0]);

          setOption('activeId', id ?? null);
          isSet = true;

          break;
        }

        leaf = leaf.parentElement;
      }

      if (!isSet) unsetActiveSuggestion();
    },
  },
  options: {
    activeId: null,
    commentingBlock: null,
    hoverId: null,
    uniquePathMap: new Map(),
  },
})
  .extendTransforms(
    ({
      editor,
      setOption,
      tf: {
        comment: { setDraft },
      },
    }) => ({
      setDraft: () => {
        if (editor.api.isCollapsed()) {
          editor.tf.select(editor.api.block()![1]);
        }

        setDraft();

        editor.tf.collapse();
        setOption('activeId', getDraftCommentKey());
        setOption('commentingBlock', editor.selection!.focus.path.slice(0, 1));
      },
    })
  )
  .configure({
    node: { component: CommentLeaf },
    shortcuts: {
      setDraft: { keys: 'mod+shift+m' },
    },
  });

export const CommentKit = [commentPlugin];

添加套件

import { createPlateEditor } from 'platejs/react';
import { CommentKit } from '@/components/editor/plugins/comment-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    ...CommentKit,
  ],
});

手动配置

安装

pnpm add @platejs/comment

扩展评论插件

创建带有状态管理扩展配置的评论插件:

import {
  type ExtendConfig,
  type Path,
  isSlateString,
} from 'platejs';
import {
  type BaseCommentConfig,
  BaseCommentPlugin,
  getDraftCommentKey,
} from '@platejs/comment';
import { toTPlatePlugin } from 'platejs/react';
import { CommentLeaf } from '@/components/ui/comment-node';
 
type CommentConfig = ExtendConfig<
  BaseCommentConfig,
  {
    activeId: string | null;
    commentingBlock: Path | null;
    hoverId: string | null;
    uniquePathMap: Map<string, Path>;
  }
>;
 
export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    options: {
      activeId: null,
      commentingBlock: null,
      hoverId: null,
      uniquePathMap: new Map(),
    },
    render: {
      node: CommentLeaf,
    },
  })
);
  • options.activeId: 当前激活评论ID,用于视觉高亮
  • options.commentingBlock: 当前被评论区块的路径
  • options.hoverId: 当前悬停评论ID,用于悬停效果
  • options.uniquePathMap: 追踪评论解析唯一路径的映射表
  • render.node: 指定 CommentLeaf 来渲染评论文本标记

添加点击处理

添加点击处理来管理激活评论状态:

export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    handlers: {
      // 点击评论标记时设置激活评论
      onClick: ({ api, event, setOption, type }) => {
        let leaf = event.target as HTMLElement;
        let isSet = false;
 
        const unsetActiveComment = () => {
          setOption('activeId', null);
          isSet = true;
        };
 
        if (!isSlateString(leaf)) unsetActiveComment();
 
        while (leaf.parentElement) {
          if (leaf.classList.contains(`slate-${type}`)) {
            const commentsEntry = api.comment.node();
 
            if (!commentsEntry) {
              unsetActiveComment();
              break;
            }
 
            const id = api.comment.nodeId(commentsEntry[0]);
            setOption('activeId', id ?? null);
            isSet = true;
            break;
          }
 
          leaf = leaf.parentElement;
        }
 
        if (!isSet) unsetActiveComment();
      },
    },
    // ... 之前的options和render配置
  })
);

点击处理器追踪当前激活的评论:

  • 检测评论点击: 遍历DOM查找评论元素
  • 设置激活状态: 点击评论时更新activeId
  • 清除状态: 点击评论外部时取消activeId
  • 视觉反馈: 在评论组件中启用悬停/激活样式

扩展转换

扩展setDraft转换以增强功能:

export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    // ... 之前的配置
  })
)
  .extendTransforms(
    ({
      editor,
      setOption,
      tf: {
        comment: { setDraft },
      },
    }) => ({
      setDraft: () => {
        if (editor.api.isCollapsed()) {
          editor.tf.select(editor.api.block()![1]);
        }
 
        setDraft();
 
        editor.tf.collapse();
        setOption('activeId', getDraftCommentKey());
        setOption('commentingBlock', editor.selection!.focus.path.slice(0, 1));
      },
    })
  )
  .configure({
    node: { component: CommentLeaf },
    shortcuts: {
      setDraft: { keys: 'mod+shift+m' },
    },
  });

添加工具栏按钮

您可以在工具栏中添加CommentToolbarButton来为选中文本添加评论。

添加插件

import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    commentPlugin,
  ],
});

讨论集成

评论插件可与讨论插件配合实现完整协作:

import { discussionPlugin } from '@/components/editor/plugins/discussion-kit';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    discussionPlugin,
    commentPlugin,
  ],
});

键盘快捷键

KeyDescription
Cmd + Shift + M

在选中文本上添加评论

Plate Plus

comment
comment

插件

CommentPlugin

用于创建和管理文本评论的插件,具有状态追踪和讨论集成功能。

Options

Collapse all

    当前激活评论ID,用于视觉高亮。内部用于状态追踪。

    当前被评论区块的路径。

    当前悬停评论ID,用于悬停效果。

    追踪评论解析唯一路径的映射表。

API

api.comment.has

检查编辑器中是否存在指定ID的评论。

Parameters

Collapse all

    包含要检查评论ID的选项。

Returnsboolean

Collapse all

    评论是否存在。

api.comment.node

获取评论节点entry。

OptionsEditorNodesOptions & { id?: string; isDraft?: boolean }

Collapse all

    查找节点的选项。

ReturnsNodeEntry<TCommentText> | undefined

Collapse all

    找到的评论节点entry。

api.comment.nodeId

从叶子节点获取评论ID。

Parameters

Collapse all

    评论叶子节点。

Returnsstring | undefined

Collapse all

    找到的评论ID。

api.comment.nodes

获取所有匹配选项的评论节点entry。

OptionsEditorNodesOptions & { id?: string; isDraft?: boolean }

Collapse all

    查找节点的选项。

ReturnsNodeEntry<TCommentText>[]

Collapse all

    评论节点entry数组。

转换

tf.comment.removeMark

从当前选区或指定位置移除评论标记。

tf.comment.setDraft

在当前选区设置草稿评论标记。

OptionsSetNodesOptions

Collapse all

    设置草稿评论的选项。

tf.comment.unsetMark

从编辑器中取消设置指定ID的评论节点。

Parameters

Collapse all

    包含要取消评论ID的选项。

工具函数

getCommentCount

获取评论节点中非草稿评论的数量。

Parameters

Collapse all

    评论节点。

Returnsnumber

Collapse all

    评论数量。

getCommentKey

基于提供的ID生成评论key。

Parameters

Collapse all

    评论ID。

Returnsstring

Collapse all

    生成的评论key。

getCommentKeyId

从评论key中提取评论ID。

Parameters

Collapse all

    评论key。

Returnsstring

Collapse all

    提取的评论ID。

getCommentKeys

返回给定节点中存在的评论key数组。

Parameters

Collapse all

    要检查评论key的节点。

Returnsstring[]

Collapse all

    评论key数组。

getDraftCommentKey

获取草稿评论使用的key。

Returnsstring

Collapse all

    草稿评论key。

isCommentKey

检查给定key是否为评论key。

Parameters

Collapse all

    要检查的key。

Returnsboolean

Collapse all

    是否为评论key。

isCommentNodeById

检查给定节点是否为指定ID的评论。

Parameters

Collapse all

    要检查的节点。

    评论ID。

Returnsboolean

Collapse all

    节点是否为指定ID的评论。

类型

TCommentText

可包含评论的文本节点。

Attributes

Collapse all

    该文本节点是否包含评论。

    按评论ID索引的评论数据。一个文本节点可包含多个评论。