Comments

Show, add, reply to, edit, resolve, and delete slide comments.

Register reviewModule() to read and edit the comment threads of each slide. Render PptxEditorReview from @pptx-editor.dev/pro/react or @pptx-editor.dev/pro/vue to show them in a comments pane. The editor saves comments in the format that the deck already uses. For the feature status, see CommentsPartial.

Show the comments pane

Pass the review module and the name of the current user to the editor root. The author value signs new comments and replies. Then give the pane to the Comments tab of the side panel:

import { PptxEditor } from '@pptx-editor.dev/react';
import { PptxEditorReview, reviewModule } from '@pptx-editor.dev/pro/react';
import '@pptx-editor.dev/core/styles/editor.css';

const MODULES = [reviewModule()];
const PANES = { comments: <PptxEditorReview /> };

export function Presentation({ bytes, user }: { bytes: Uint8Array; user: string }) {
  return (
    <PptxEditor.Root document={bytes} modules={MODULES} author={user}>
      <PptxEditor.Viewport style={{ display: 'flex', height: '100vh' }}>
        <PptxEditor.Content />
        <PptxEditor.SidePanel tab="comments" panes={PANES} />
      </PptxEditor.Viewport>
    </PptxEditor.Root>
  );
}

The pane lists the threads of the current slide. Users add a comment to the slide, to the selected text, or to the selected element, reply, edit, delete, and resolve or reopen a thread. In a comment field, Enter posts, Shift+Enter starts a new line, and Escape cancels.

Right-click text, a shape, a picture, or a table on the slide and pick New comment to start a comment on that element, or on the selected text. The row shows while the pane is mounted, or while a side panel or your host can show the Comments tab on request (listenPaneRequests). It opens the tab when it is closed and focuses the new draft. In view mode, the row is disabled. Your own control runs the same action with useEditorCommand('review.addComment').

A marker on the canvas shows each thread. A click on a marker selects its thread and turns the side panel to the Comments tab. The markers are pointer shortcuts: keyboard and screen reader users reach the same threads in the pane.

The slide also shows what each thread annotates, in the color of the author who started it. A comment on selected text draws a band over that text. The band is light at rest and stronger while its thread is selected or the caret is in it. A comment on a selected picture or shape draws a ring around it. A comment on text follows its text through edits and undo, and collaborators receive the moved range with the edit. When an edit removes all of its text, the thread points at the element. See Review colors and styling to change the colors.

Comment markers, bands, and rings are editing chrome. They do not print or export.

Without the review module, PptxEditorReview and the packaged Comments tab say that comments are kept when you save. The packaged PptxEditor component has no side panel, so compose the editor to show the pane. For the other parts of a composed layout, see Layout.

A change of author applies to later comments without a remount. Existing comments keep their authors. Without author, a collaboration session signs comments with the name of its identity. With no name at all, the pane shows Set an author to comment., and the editor refuses to add a comment with disabled.noCommentAuthor.

Configure the pane and the markers

Pass pane options to reviewModule:

import { reviewModule } from '@pptx-editor.dev/pro';

const modules = [reviewModule({ pane: { opening: 'manual', commentMarkers: 'initials' } })];
OptionValuesEffect
pane.opening'auto' (default), 'manual'With auto, a click on a marker turns the side panel to the Comments tab. With manual, the click only selects the thread.
pane.commentMarkers'icon' (default), 'initials'icon draws a comment icon on a small card, with a check mark on a resolved thread. initials draws a speech bubble in the author's color, with the author's initials and the reply count.

reviewModule throws a TypeError for an unknown value.

Customize the pane

PptxEditorReview is a compound component. Change it one step at a time, and stop at the first step that does what you need:

  1. Restyle the packaged parts with className, card.className, and the data-* attributes. See Review colors and styling.
  2. Change the icon of an action with its icon prop.
  3. Put a part's behavior on your own element with asChild.
  4. Replace a part with your own child, or remove it with hidden. Set preset={false} to render only the root parts that you pass.
  5. Build your own pane with useReview.

This pane hides the Delete action, shows only open threads, and labels the New comment button Ask a question:

import { PptxEditorReview } from '@pptx-editor.dev/pro/react';

export function OpenThreads() {
  return (
    <PptxEditorReview filter={(item) => !item.resolved} card={{ className: 'deck-card' }}>
      <PptxEditorReview.AddComment>Ask a question</PptxEditorReview.AddComment>
      <PptxEditorReview.Delete hidden />
    </PptxEditorReview>
  );
}

Root parts (List, AddComment, and Draft) replace the packaged ones when you pass them as direct children. Other children become the card template of the list. To render each thread yourself, pass a function child in React or the #card scoped slot in Vue. It receives one ReviewItemView.

PartDefault
ListThe threads of the slide, one card each, or Empty when there is none
AddCommentThe New comment button
DraftThe compose box of a new comment. nodeId sets its target.
EmptyThe text for a slide with no comments
CardOne thread: its head, text, replies, and reply line
Avatar, Author, TimeThe head of a comment
AnchorThe name of the element that the thread points at
SummaryThe comment text, or its edit box
Resolve, Reopen, Edit, DeleteThe actions of a comment. Each takes icon.
RepliesThe replies of a thread
ReplyThe reply line of the selected open thread

Each part takes className, asChild, and hidden. The root also takes t, a label resolver. A key that it does not resolve falls back to the bundled catalog.

Build your own pane

useReview() returns the comments of the current slide and the actions on them. Call it inside the editor root. Outside a root, call useReviewOf(editor) with your own instance. In Vue, the data fields are computed refs.

FieldContent
itemsEach thread, then its replies, as ReviewItemView values
activeKey, setActiveThe selected thread, and a function that selects one. Pass { identity } to refuse the pick when the thread under the key has another identity.
comment, reply, edit, removeAdd, reply to, edit, and delete comments
resolve, reopenChange the state of a thread
author, setAuthorThe name that signs new comments
commentTargetThe element that a new comment points at, or null for the slide
readOnly, readyWhether the pane cannot edit, and whether a deck is open

Each action returns true when the editor applies the change. Inside a card, useReviewItem() returns its ReviewItemView. useReviewAuthor(name) returns the color slot of an author.

To open your own draft from the New comment row, call registerCommentDraft(editor, open) from @pptx-editor.dev/core/editor when your pane mounts, and call the function that it returns when the pane unmounts. The row calls the open function of the latest registration.

Edit comments with commands

Run comment commands on an open PptxEditorInstance. This helper adds a comment to the selected element, or to the slide when nothing is selected, and then replies to it:

import type { PptxEditorInstance } from '@pptx-editor.dev/core/editor';

export function askForReview(editor: PptxEditorInstance) {
  const added = editor.exec({
    type: 'addComment',
    text: 'Check the totals',
    author: 'Ada Lovelace',
  });
  if (!added.ok) throw new Error(added.reason);
  const thread = editor.snapshot().comments?.at(-1);
  if (!thread) return;
  const reply = editor.exec({
    type: 'replyToComment',
    commentId: thread.id,
    text: 'Done',
  });
  if (!reply.ok) throw new Error(reply.reason);
}
CommandFieldsResult
addCommenttext, optional nodeId and rangeA new thread on the current slide
replyToCommentcommentId, textA reply at the end of the thread
editCommentcommentId, textNew text for a comment or a reply
deleteCommentcommentIdDeletes a reply, or a thread with all of its replies
resolveCommentcommentIdMarks the thread as resolved
reopenCommentcommentIdMarks the thread as open

Every command except addComment also takes expect: the author, date, text, and identity of the comment as your view shows it. The editor then refuses the command with errors.commentChanged when the comment at commentId is not that comment. It compares the author, the date, and the identity.

A twin shares its GUID, author, and creation time with another comment of the slide, or, in the legacy format, its author and index. Its id holds its place among its twins. For such an id, the editor also compares the text, and it refuses the command with errors.noComment when no twin is left. After a delete of a twin, an old id with a place can name no comment, or it can reach the remaining twin, so take ids from the current snapshot. So an edit that a collaborator makes to a comment does not refuse yours, unless the comment has a twin.

An expect without identity can still fail for a twin, even with ids from the current snapshot:

  • When another twin has the same text, the editor cannot tell the two apart, and refuses with errors.commentAmbiguous.
  • When the id named another twin before, for example after a delete and an undo, the editor refuses with errors.commentChanged until your next call to editor.snapshot().

Send identity whenever you act on a comment. Without it, after your next call to editor.snapshot(), an old id can reach another twin with the same author, date, and text. In this sample, thread is a thread of editor.snapshot().comments:

const result = editor.exec({
  type: 'editComment',
  commentId: thread.id,
  text: 'Check the totals again',
  expect: {
    author: thread.author,
    date: thread.date,
    text: thread.text,
    identity: thread.identity,
  },
});
if (!result.ok && result.reason === 'errors.commentChanged') {
  // Keep the user's text, and show the comment again.
}

The comments pane sends expect with each action. When the editor refuses a new comment, a save, or a reply, the field keeps its text and shows the reason. After a notice that the comment changed, the edit box shows what the comment reads now, and a second save or reply applies to the comment as it shows now. The pane sends the identity, so it acts on the copy of an exact duplicate that it shows. A reply item of useReview reports its thread as parent, and the reply, resolve, and reopen actions on a reply item send it as expect.

Without nodeId, addComment points the thread at the selected text of a shape, else at the one selected element, else at the slide. With nodeId and range, it points at that range of the element's text. range has start and end points, as a text selection does. In this sample, shape is an entry of editor.snapshot().shapes:

editor.exec({
  type: 'addComment',
  text: 'Is this the right figure?',
  nodeId: shape.nodeId,
  range: { start: { paragraph: 0, offset: 4 }, end: { paragraph: 0, offset: 9 } },
});

The editor refuses a range without nodeId, or with no text in it, with disabled.noCommentTarget. A deck with only legacy comments has no text ranges: there, the thread points at the element.

addComment and replyToComment also take author, initials, and date. They override the editor's author for that comment. The date is an ISO 8601 date and time. Without a date, the editor uses the current time in UTC. Each command is one undo step.

Read the threads

editor.snapshot().comments lists the threads of the current slide. It is null without the review module. activeComment is the id of the selected thread. Each thread has id, identity, format (modern or legacy), author, initials, date, text, textEditable, resolved, anchor, and replies. anchor is { kind: 'text', nodeId, start, end } for a thread on text, { kind: 'shape', nodeId } for a thread on an element, { kind: 'point', x, y } for a place on the slide, or { kind: 'slide' }. textEditable is false for a comment whose content editComment cannot write back, such as a mention. Pass id to the comment commands. Key your own UI state, such as an open reply field, by identity. When a file repeats a GUID, a delete of another comment can change id. Once shown, an identity never names another comment, also after the slide changes and comes back, or the editor opens another deck:

  • A comment that never had a twin keeps its identity until it is deleted. A twin shares the GUID, author, and creation time of another comment of the slide.
  • Twins keep their identity when one change explains their group: an edit or a resolve that changes at most one text, or one twin more or less at one place.
  • After any other change, every twin of the group takes a new identity, so state that you keyed by the old one ends. The comments pane closes such a field without saving.
  • A comment that a slide shows after its first read takes a new identity.

readCommentThreads reads identity from the file alone, without this carry.

To read the threads of any slide, call readCommentThreads with a package and a slide part name:

import { readFile } from 'node:fs/promises';
import { readCommentThreads } from '@pptx-editor.dev/core/editor';
import { presentationInfo, readPptx } from '@pptx-editor.dev/core/store';

const opened = readPptx(await readFile('deck.pptx'));
if (!opened.ok) throw new Error('Cannot open the presentation');
for (const slide of presentationInfo(opened.pkg).slides) {
  for (const thread of readCommentThreads(opened.pkg, slide.partName)) {
    console.log(slide.partName, thread.author, thread.text, thread.replies.length);
  }
}

Comment text and author names come from an untrusted file. Render them as text, such as React expressions or Vue interpolation. Use your application's access controls before you show them.

Understand the formats

A deck can store threaded comments or legacy comments (ECMA-376 Part 1, 19.4). The editor adds threaded comments to a deck that has none or that already uses them. In a deck with only legacy comments, new comments are legacy comments. Legacy comments have no resolved state, so resolveComment and reopenComment are refused with disabled.commentNoStatus.

The editor writes the author list, the comment lists, their relationships, and their content types. A new author gets a new id. Two collaborators who add comments at the same time keep both comments, and the save joins the lists that they made.

Limits

  • A comment points at the slide, at an element on the top level of the slide, or at a range of the text of such a shape. Other targets are refused with disabled.noCommentTarget.
  • Only threaded comments hold text ranges. In a deck with only legacy comments, a comment on text points at its shape.
  • Each editor moves a text range for its own edits. When two collaborators edit the text of a commented shape at the same time, the range can end a few characters off.
  • Mentions, reactions, and other content of threaded comments are kept but not shown. An edit of a comment with such content is refused with disabled.commentNotEditable.
  • The pane lists threads in file order, not by their position on the slide. It shows the threads of the current slide only.
  • The import of a slide with threaded comments from another presentation is refused.

See also