评论

PreviousNext

Add comments to text as marks.

Loading…

功能特性

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

Kit 用法

安装

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

'use client';
 
import type { ExtendConfig, Path } from 'platejs';
 
import {
  type BaseCommentConfig,
  BaseCommentPlugin,
  getDraftCommentKey,
} from '@platejs/comment';
import { toTPlatePlugin } from 'platejs/react';
 
import { CommentLeaf } from '@/components/ui/comment-node';
import { getDiscussionClickTarget } from './discussion-kit';
 
type CommentConfig = ExtendConfig<
  BaseCommentConfig,
  {
    activeId: string | null;
    commentingBlock: Path | null;
    hoverId: string | null;
  }
>;
 
export const commentPlugin = toTPlatePlugin<CommentConfig>(BaseCommentPlugin, {
  handlers: {
    onClick: ({ api, event, setOption, type }) => {
      const activeTarget = getDiscussionClickTarget({
        selector: `.slate-${type}`,
        target: event.target,
      });
 
      if (!activeTarget) {
        setOption('activeId', null);
        return;
      }
 
      const commentEntry = api.comment?.node();
 
      setOption(
        'activeId',
        commentEntry ? (api.comment?.nodeId(commentEntry[0]) ?? null) : null
      );
    },
  },
  options: {
    activeId: null,
    commentingBlock: null,
    hoverId: null,
  },
})
  .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];
'use client';
 
import type { ExtendConfig, Path } from 'platejs';
 
import {
  type BaseCommentConfig,
  BaseCommentPlugin,
  getDraftCommentKey,
} from '@platejs/comment';
import { toTPlatePlugin } from 'platejs/react';
 
import { CommentLeaf } from '@/components/ui/comment-node';
import { getDiscussionClickTarget } from './discussion-kit';
 
type CommentConfig = ExtendConfig<
  BaseCommentConfig,
  {
    activeId: string | null;
    commentingBlock: Path | null;
    hoverId: string | null;
  }
>;
 
export const commentPlugin = toTPlatePlugin<CommentConfig>(BaseCommentPlugin, {
  handlers: {
    onClick: ({ api, event, setOption, type }) => {
      const activeTarget = getDiscussionClickTarget({
        selector: `.slate-${type}`,
        target: event.target,
      });
 
      if (!activeTarget) {
        setOption('activeId', null);
        return;
      }
 
      const commentEntry = api.comment?.node();
 
      setOption(
        'activeId',
        commentEntry ? (api.comment?.nodeId(commentEntry[0]) ?? null) : null
      );
    },
  },
  options: {
    activeId: null,
    commentingBlock: null,
    hoverId: null,
  },
})
  .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];

添加 Kit

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

手动用法

安装

pnpm add @platejs/comment
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;
  }
>;
 
export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    options: {
      activeId: null,
      commentingBlock: null,
      hoverId: null,
    },
    render: {
      node: CommentLeaf,
    },
  })
);
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;
  }
>;
 
export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    options: {
      activeId: null,
      commentingBlock: null,
      hoverId: null,
    },
    render: {
      node: CommentLeaf,
    },
  })
);
  • options.activeId: 当前激活的评论 ID,用于视觉高亮
  • options.commentingBlock: 当前正在评论的块的路径
  • options.hoverId: 当前悬停的评论 ID,用于悬停效果
  • render.node: 指定 CommentLeaf 来渲染评论文本标记

添加点击处理器

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

export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    handlers: {
      // Set active comment when clicking on comment marks
      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();
      },
    },
    // ... previous options and render
  })
);
export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    handlers: {
      // Set active comment when clicking on comment marks
      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();
      },
    },
    // ... previous options and render
  })
);

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

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

扩展 Transforms

扩展 setDraft transform 以增强功能:

export const commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    // ... previous configuration
  })
)
  .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 commentPlugin = toTPlatePlugin<CommentConfig>(
  BaseCommentPlugin,
  ({ editor }) => ({
    // ... previous configuration
  })
)
  .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: [
    // ...otherPlugins,
    commentPlugin,
  ],
});
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    commentPlugin,
  ],
});

讨论集成

评论插件与讨论插件配合使用,实现完整的协作功能:

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

键盘快捷键

KeyDescription
Cmd + Shift + M

在选中文本上添加评论。

Plate Plus

  • Full stack example for Suggestion and Comment
  • Floating comments & suggestions UI with better user experience
  • Comment rendered with Plate editor
  • Discussion list in the sidebar

插件

CommentPlugin

用于创建和管理文本评论的插件,支持状态追踪和讨论集成。

Options

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

    当前正在评论的块的路径。

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

API

api.comment.has

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

Parameters

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

Returnsboolean

    评论是否存在。

api.comment.node

获取评论节点条目。

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

    查找节点的选项。

ReturnsNodeEntry<TCommentText> | undefined

    如果找到则返回评论节点条目。

api.comment.nodeId

从叶子节点获取评论的 ID。

Parameters

    评论叶子节点。

Returnsstring | undefined

    如果找到则返回评论 ID。

api.comment.nodes

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

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

    查找节点的选项。

ReturnsNodeEntry<TCommentText>[]

    评论节点条目数组。

Transforms

tf.comment.removeMark

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

tf.comment.setDraft

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

OptionsSetNodesOptions

    设置草稿评论的选项。

tf.comment.unsetMark

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

Parameters

    取消评论标记的选项。

Optionsobject

    要取消的评论 ID。

    当为 true 时,一次性移除所有 AI 评论。

    • 默认值: false

工具函数

getCommentCount

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

Parameters

    评论节点。

Returnsnumber

    评论数量。

getCommentKey

根据提供的 ID 生成评论键。

Parameters

    评论的 ID。

Returnsstring

    生成的评论键。

getCommentKeyId

从评论键中提取评论 ID。

Parameters

    评论键。

Returnsstring

    提取的评论 ID。

getCommentKeys

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

Parameters

    要检查评论键的节点。

Returnsstring[]

    评论键数组。

getDraftCommentKey

获取用于草稿评论的键。

Returnsstring

    草稿评论键。

isCommentKey

检查给定的键是否是评论键。

Parameters

    要检查的键。

Returnsboolean

    是否是评论键。

isCommentNodeById

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

Parameters

    要检查的节点。

    评论的 ID。

Returnsboolean

    节点是否是具有指定 ID 的评论。

类型

TCommentText

可以包含评论的文本节点。

Attributes

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

    按评论 ID 键入的评论数据。一个文本节点中可以存在多个评论。