tiptop-editor 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/components/comment/CommentButton.d.ts +7 -0
- package/dist/components/comment/CommentSelectionMenu.d.ts +10 -0
- package/dist/components/comment/CommentsContext.d.ts +11 -0
- package/dist/components/comment/context.d.ts +2 -0
- package/dist/components/comment/useCommentActions.d.ts +13 -0
- package/dist/components/comment/useComments.d.ts +7 -0
- package/dist/components/editor/TiptopEditor.stories.d.ts +1 -0
- package/dist/extensions/comment/CommentMark.d.ts +23 -0
- package/dist/extensions/comment/NodeCommentExtension.d.ts +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/tiptop-editor.css +1 -1
- package/dist/tiptop-editor.es.js +4627 -4363
- package/dist/tiptop-editor.umd.js +19 -20
- package/dist/types.d.ts +47 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -412,6 +412,229 @@ Example:
|
|
|
412
412
|
}
|
|
413
413
|
```
|
|
414
414
|
|
|
415
|
+
## Comments
|
|
416
|
+
|
|
417
|
+
The package ships a full inline and block comment system designed for **review / view mode**. Comments are stored entirely in the calling app — the package exposes hooks and callbacks so you can persist and sync with your own API.
|
|
418
|
+
|
|
419
|
+
### Comment types
|
|
420
|
+
|
|
421
|
+
| Type | What it annotates | How it looks |
|
|
422
|
+
|------|------------------|--------------|
|
|
423
|
+
| `inline` | A selected text range | Yellow underline highlight on the text |
|
|
424
|
+
| `node` | An entire block (paragraph, heading, …) | Left amber border on the block |
|
|
425
|
+
|
|
426
|
+
### Quick setup
|
|
427
|
+
|
|
428
|
+
Wrap the editor in `CommentsProvider`, pass `showCommentMenu: true` and `editable: false` to put the editor into view mode, then render your own comment drawer next to it.
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
import { TiptopEditor, CommentsProvider, useComments, useCommentActions } from 'tiptop-editor'
|
|
432
|
+
import { Dropdown, Label } from '@heroui/react'
|
|
433
|
+
import { MessageSquarePlus } from 'lucide-react'
|
|
434
|
+
import { useTiptopEditor } from 'tiptop-editor'
|
|
435
|
+
|
|
436
|
+
// Optional: lets users add block-level comments from the drag-handle menu
|
|
437
|
+
function NodeCommentButton() {
|
|
438
|
+
const editor = useTiptopEditor()
|
|
439
|
+
const comments = useComments()
|
|
440
|
+
|
|
441
|
+
if (!editor || !comments) return null
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<Dropdown.Section>
|
|
445
|
+
<Dropdown.Item
|
|
446
|
+
id="add_node_comment"
|
|
447
|
+
textValue="Add comment"
|
|
448
|
+
onPress={() => {
|
|
449
|
+
comments.setPendingComment({
|
|
450
|
+
id: crypto.randomUUID(),
|
|
451
|
+
type: 'node',
|
|
452
|
+
nodePos: editor.state.selection.from,
|
|
453
|
+
})
|
|
454
|
+
}}
|
|
455
|
+
>
|
|
456
|
+
<MessageSquarePlus size={16} />
|
|
457
|
+
<Label>Add comment</Label>
|
|
458
|
+
</Dropdown.Item>
|
|
459
|
+
</Dropdown.Section>
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function ReviewPage() {
|
|
464
|
+
return (
|
|
465
|
+
<CommentsProvider>
|
|
466
|
+
<div style={{ display: 'flex' }}>
|
|
467
|
+
<TiptopEditor
|
|
468
|
+
editorOptions={{
|
|
469
|
+
content: '<p>Document content…</p>',
|
|
470
|
+
immediatelyRender: false,
|
|
471
|
+
editable: false,
|
|
472
|
+
showCommentMenu: true,
|
|
473
|
+
}}
|
|
474
|
+
slots={{
|
|
475
|
+
dragHandleDropdown: <NodeCommentButton />,
|
|
476
|
+
}}
|
|
477
|
+
/>
|
|
478
|
+
{/* Your own drawer component here — see "Building a custom comment drawer" below */}
|
|
479
|
+
</div>
|
|
480
|
+
</CommentsProvider>
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
> `showCommentMenu: true` registers the `CommentMark` and `NodeCommentExtension` Tiptap extensions and renders the floating "Add comment" button that appears on text selection.
|
|
486
|
+
|
|
487
|
+
### Persisting comments with your API
|
|
488
|
+
|
|
489
|
+
`CommentsProvider` accepts an `onCommentsChange` callback that fires whenever the comment list changes. Use it to sync with your backend.
|
|
490
|
+
|
|
491
|
+
```tsx
|
|
492
|
+
import { useState } from 'react'
|
|
493
|
+
import { CommentsProvider, TiptopEditor } from 'tiptop-editor'
|
|
494
|
+
import type { TiptopComment } from 'tiptop-editor'
|
|
495
|
+
|
|
496
|
+
export function ReviewPage() {
|
|
497
|
+
const [initialComments] = useState<TiptopComment[]>([
|
|
498
|
+
// Comments loaded from your API on mount
|
|
499
|
+
])
|
|
500
|
+
|
|
501
|
+
const handleCommentsChange = async (comments: TiptopComment[]) => {
|
|
502
|
+
await fetch('/api/documents/123/comments', {
|
|
503
|
+
method: 'PUT',
|
|
504
|
+
body: JSON.stringify(comments),
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<CommentsProvider
|
|
510
|
+
initialComments={initialComments}
|
|
511
|
+
onCommentsChange={handleCommentsChange}
|
|
512
|
+
>
|
|
513
|
+
<div style={{ display: 'flex' }}>
|
|
514
|
+
<TiptopEditor
|
|
515
|
+
editorOptions={{
|
|
516
|
+
content: '<p>…</p>',
|
|
517
|
+
editable: false,
|
|
518
|
+
showCommentMenu: true,
|
|
519
|
+
immediatelyRender: false,
|
|
520
|
+
}}
|
|
521
|
+
/>
|
|
522
|
+
{/* Your own drawer here */}
|
|
523
|
+
</div>
|
|
524
|
+
</CommentsProvider>
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`onCommentsChange` receives the full updated array after every add, resolve, reply, or delete. It fires synchronously after the state update — debounce or batch API calls on your side as needed.
|
|
530
|
+
|
|
531
|
+
### Loading existing comments
|
|
532
|
+
|
|
533
|
+
Pass your persisted comments as `initialComments` to `CommentsProvider`. The comment marks in the editor HTML already carry a `data-comment-id` attribute, so the sidebar threads reconnect automatically as long as the IDs match.
|
|
534
|
+
|
|
535
|
+
> The editor content (HTML string with mark attributes) and the comments array are two separate things to persist. Store both and restore both — the HTML goes into `editorOptions.content`, the comments go into `initialComments`.
|
|
536
|
+
|
|
537
|
+
### Reading comment state programmatically
|
|
538
|
+
|
|
539
|
+
Use `useComments()` anywhere inside `CommentsProvider` to read or mutate the comment state directly.
|
|
540
|
+
|
|
541
|
+
```tsx
|
|
542
|
+
import { useComments } from 'tiptop-editor'
|
|
543
|
+
|
|
544
|
+
function CommentCount() {
|
|
545
|
+
const comments = useComments()
|
|
546
|
+
if (!comments) return null
|
|
547
|
+
|
|
548
|
+
const open = comments.comments.filter(c => !c.resolved).length
|
|
549
|
+
return <span>{open} open comment{open !== 1 ? 's' : ''}</span>
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Available values and actions from `useComments()`:
|
|
554
|
+
|
|
555
|
+
| Name | Type | Description |
|
|
556
|
+
|------|------|-------------|
|
|
557
|
+
| `comments` | `TiptopComment[]` | All comments (open and resolved) |
|
|
558
|
+
| `activeCommentId` | `string \| null` | ID of the currently focused comment |
|
|
559
|
+
| `pendingComment` | `PendingComment \| null` | Comment being drafted (not yet submitted) |
|
|
560
|
+
| `addComment` | `(id, type, content, author?) => void` | Finalize a pending comment |
|
|
561
|
+
| `removeComment` | `(id) => void` | Delete a comment from the context (use `useCommentActions().remove` to also strip the editor mark) |
|
|
562
|
+
| `resolveComment` | `(id) => void` | Mark as resolved in the context (use `useCommentActions().resolve` to also strip the editor mark) |
|
|
563
|
+
| `replyToComment` | `(commentId, content, author?) => void` | Append a reply |
|
|
564
|
+
| `setActiveCommentId` | `(id \| null) => void` | Highlight a comment thread |
|
|
565
|
+
| `setPendingComment` | `(PendingComment \| null) => void` | Open the new-comment form |
|
|
566
|
+
| `getComment` | `(id) => TiptopComment \| undefined` | Look up a single comment |
|
|
567
|
+
|
|
568
|
+
### Building a custom comment drawer
|
|
569
|
+
|
|
570
|
+
The package does not ship a styled sidebar — you build your own. The only non-obvious part is that resolving or deleting a comment requires two coordinated steps: removing the mark from the editor **and** updating the context. The `useCommentActions` hook handles both so you don't have to.
|
|
571
|
+
|
|
572
|
+
```tsx
|
|
573
|
+
import {
|
|
574
|
+
TiptopEditor,
|
|
575
|
+
CommentsProvider,
|
|
576
|
+
useComments,
|
|
577
|
+
useCommentActions,
|
|
578
|
+
} from 'tiptop-editor'
|
|
579
|
+
|
|
580
|
+
function MyCommentDrawer() {
|
|
581
|
+
const ctx = useComments()
|
|
582
|
+
const { submit, resolve, remove } = useCommentActions()
|
|
583
|
+
|
|
584
|
+
if (!ctx) return null
|
|
585
|
+
const { comments, pendingComment, activeCommentId, setActiveCommentId } = ctx
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
<aside>
|
|
589
|
+
{/* New comment form — shown when the user clicks "Add comment" */}
|
|
590
|
+
{pendingComment && (
|
|
591
|
+
<form onSubmit={e => {
|
|
592
|
+
e.preventDefault()
|
|
593
|
+
const text = new FormData(e.currentTarget).get('comment') as string
|
|
594
|
+
submit(pendingComment, text)
|
|
595
|
+
}}>
|
|
596
|
+
<textarea name="comment" placeholder="Add a comment…" autoFocus />
|
|
597
|
+
<button type="submit">Save</button>
|
|
598
|
+
<button type="button" onClick={() => ctx.setPendingComment(null)}>Cancel</button>
|
|
599
|
+
</form>
|
|
600
|
+
)}
|
|
601
|
+
|
|
602
|
+
{/* Comment threads */}
|
|
603
|
+
{comments.map(comment => (
|
|
604
|
+
<div
|
|
605
|
+
key={comment.id}
|
|
606
|
+
data-active={activeCommentId === comment.id || undefined}
|
|
607
|
+
onClick={() => setActiveCommentId(comment.id)}
|
|
608
|
+
>
|
|
609
|
+
<p>{comment.content}</p>
|
|
610
|
+
<button onClick={e => { e.stopPropagation(); resolve(comment.id) }}>Resolve</button>
|
|
611
|
+
<button onClick={e => { e.stopPropagation(); remove(comment.id) }}>Delete</button>
|
|
612
|
+
</div>
|
|
613
|
+
))}
|
|
614
|
+
</aside>
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function ReviewPage() {
|
|
619
|
+
return (
|
|
620
|
+
<CommentsProvider onCommentsChange={comments => saveToApi(comments)}>
|
|
621
|
+
<div style={{ display: 'flex' }}>
|
|
622
|
+
<TiptopEditor editorOptions={{ editable: false, showCommentMenu: true, immediatelyRender: false }} />
|
|
623
|
+
<MyCommentDrawer />
|
|
624
|
+
</div>
|
|
625
|
+
</CommentsProvider>
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
`useCommentActions` provides three methods:
|
|
631
|
+
|
|
632
|
+
| Method | Description |
|
|
633
|
+
|--------|-------------|
|
|
634
|
+
| `submit(pending, content, author?)` | Applies the pending comment to the editor (adds the mark/attribute) and registers it in the context |
|
|
635
|
+
| `resolve(commentId)` | Removes the mark/attribute from the editor and marks the comment as resolved |
|
|
636
|
+
| `remove(commentId)` | Removes the mark/attribute from the editor and deletes the comment from the context |
|
|
637
|
+
|
|
415
638
|
## Notes
|
|
416
639
|
|
|
417
640
|
- If you use SSR, keep `immediatelyRender: false`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A button meant to be placed in the `selectionMenuAppend` slot.
|
|
3
|
+
* When pressed it snapshots the current text selection and opens
|
|
4
|
+
* the comment sidebar so the user can type a new inline comment.
|
|
5
|
+
*/
|
|
6
|
+
declare const CommentButton: () => import("react/jsx-runtime").JSX.Element | null;
|
|
7
|
+
export default CommentButton;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Editor } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* Bubble menu that appears on text selection only when the editor is in
|
|
4
|
+
* view / review mode (`editable: false`). Rendered automatically by
|
|
5
|
+
* `TiptopEditor` when `showCommentMenu: true` is set.
|
|
6
|
+
*/
|
|
7
|
+
declare const CommentSelectionMenu: ({ editor }: {
|
|
8
|
+
editor: Editor;
|
|
9
|
+
}) => import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export default CommentSelectionMenu;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { TiptopComment } from '../../types';
|
|
3
|
+
interface CommentsProviderProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Pre-populate the comment list (e.g. data fetched from your API on mount). */
|
|
6
|
+
initialComments?: TiptopComment[];
|
|
7
|
+
/** Called whenever the comments array changes. Use this to persist or sync externally. */
|
|
8
|
+
onCommentsChange?: (comments: TiptopComment[]) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const CommentsProvider: ({ children, initialComments, onCommentsChange }: CommentsProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PendingComment } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns coordinated comment actions that keep the editor marks and the
|
|
4
|
+
* CommentsContext in sync. Use this when building a custom comment UI instead
|
|
5
|
+
* of reimplementing the editor↔context coordination yourself.
|
|
6
|
+
*
|
|
7
|
+
* Must be called inside both a `CommentsProvider` and a `TiptopEditor` tree.
|
|
8
|
+
*/
|
|
9
|
+
export declare const useCommentActions: () => {
|
|
10
|
+
submit: (pending: PendingComment, content: string, author?: string) => void;
|
|
11
|
+
resolve: (commentId: string) => void;
|
|
12
|
+
remove: (commentId: string) => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommentsContextValue } from '../../types';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the comments context value, or `null` if no `CommentsProvider`
|
|
4
|
+
* is present in the tree. Components that depend on comments should
|
|
5
|
+
* guard against `null` and render nothing when it's absent.
|
|
6
|
+
*/
|
|
7
|
+
export declare const useComments: () => CommentsContextValue | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Mark } from '@tiptap/core';
|
|
2
|
+
declare module '@tiptap/core' {
|
|
3
|
+
interface Commands<ReturnType> {
|
|
4
|
+
comment: {
|
|
5
|
+
/**
|
|
6
|
+
* Apply a comment mark to a specific text range.
|
|
7
|
+
* The range must be provided explicitly so the mark can be applied
|
|
8
|
+
* even after the editor selection has moved (e.g. after the user
|
|
9
|
+
* has focused a sidebar input).
|
|
10
|
+
*/
|
|
11
|
+
setComment: (commentId: string, range: {
|
|
12
|
+
from: number;
|
|
13
|
+
to: number;
|
|
14
|
+
}) => ReturnType;
|
|
15
|
+
/**
|
|
16
|
+
* Remove a comment mark by its commentId, searching the whole document.
|
|
17
|
+
*/
|
|
18
|
+
unsetComment: (commentId: string) => ReturnType;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
declare const CommentMark: Mark<any, any>;
|
|
23
|
+
export default CommentMark;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
declare module '@tiptap/core' {
|
|
3
|
+
interface Commands<ReturnType> {
|
|
4
|
+
nodeComment: {
|
|
5
|
+
/**
|
|
6
|
+
* Set a comment on the block node at the given ProseMirror position.
|
|
7
|
+
*/
|
|
8
|
+
setNodeComment: (commentId: string, nodePos: number) => ReturnType;
|
|
9
|
+
/**
|
|
10
|
+
* Remove the comment from every block node that carries the given commentId.
|
|
11
|
+
*/
|
|
12
|
+
unsetNodeComment: (commentId: string) => ReturnType;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
declare const NodeCommentExtension: Extension<any, any>;
|
|
17
|
+
export default NodeCommentExtension;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,3 +2,9 @@ export { default as TiptopEditor } from './components/editor/TiptopEditor';
|
|
|
2
2
|
export { TiptopEditorContext, useTiptopEditor } from './components/editor/TiptopEditorContext';
|
|
3
3
|
export { getDocumentMap, applyTargetedUpdate, applyTargetedUpdates } from './helpers';
|
|
4
4
|
export * from './types';
|
|
5
|
+
export { CommentsProvider } from './components/comment/CommentsContext';
|
|
6
|
+
export { useComments } from './components/comment/useComments';
|
|
7
|
+
export { useCommentActions } from './components/comment/useCommentActions';
|
|
8
|
+
export { default as CommentSelectionMenu } from './components/comment/CommentSelectionMenu';
|
|
9
|
+
export { default as CommentMark } from './extensions/comment/CommentMark';
|
|
10
|
+
export { default as NodeCommentExtension } from './extensions/comment/NodeCommentExtension';
|