@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d
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 +0 -2
- package/commenting.css +1152 -350
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
- package/dist-cjs/canvas/canvas-events.js.map +7 -0
- package/dist-cjs/canvas/cluster-badge.js +121 -0
- package/dist-cjs/canvas/cluster-badge.js.map +7 -0
- package/dist-cjs/canvas/cluster-fade.js +85 -0
- package/dist-cjs/canvas/cluster-fade.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +53 -3
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/cluster-model.js +252 -0
- package/dist-cjs/canvas/cluster-model.js.map +7 -0
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +152 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +160 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +9 -6
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +63 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +17 -37
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +151 -989
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +49 -41
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +45 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +28 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/license.js.map +1 -1
- package/dist-cjs/canvas/mobile-placement.js +91 -0
- package/dist-cjs/canvas/mobile-placement.js.map +7 -0
- package/dist-cjs/canvas/options.js +57 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +134 -0
- package/dist-cjs/canvas/pending-composer.js.map +7 -0
- package/dist-cjs/canvas/pin-stacking.js +81 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +304 -0
- package/dist-cjs/canvas/thread-pin.js.map +7 -0
- package/dist-cjs/canvas/thread-preview.js +177 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +82 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +450 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +6 -6
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/replay.js +89 -22
- package/dist-cjs/clustering/replay.js.map +3 -3
- package/dist-cjs/clustering/runtime.js +126 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +904 -353
- package/dist-cjs/index.js +52 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +10 -6
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +15 -8
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +124 -39
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +11 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +9 -7
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +39 -40
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +16 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +115 -0
- package/dist-cjs/ui/icons.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +71 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +58 -6
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -7
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +13 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
- package/dist-cjs/ui/visual-viewport.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/canvas-events.mjs +15 -0
- package/dist-esm/canvas/canvas-events.mjs.map +7 -0
- package/dist-esm/canvas/cluster-badge.mjs +106 -0
- package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
- package/dist-esm/canvas/cluster-fade.mjs +65 -0
- package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +54 -4
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/cluster-model.mjs +236 -0
- package/dist-esm/canvas/cluster-model.mjs.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +132 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +145 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +10 -7
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +66 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +157 -1010
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +53 -46
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +35 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/license.mjs.map +1 -1
- package/dist-esm/canvas/mobile-placement.mjs +71 -0
- package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +59 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +126 -0
- package/dist-esm/canvas/pending-composer.mjs.map +7 -0
- package/dist-esm/canvas/pin-stacking.mjs +61 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +310 -0
- package/dist-esm/canvas/thread-pin.mjs.map +7 -0
- package/dist-esm/canvas/thread-preview.mjs +163 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +189 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +86 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +461 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +6 -6
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/replay.mjs +89 -22
- package/dist-esm/clustering/replay.mjs.map +3 -3
- package/dist-esm/clustering/runtime.mjs +126 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +904 -353
- package/dist-esm/index.mjs +102 -43
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +11 -7
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +15 -8
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +135 -39
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +11 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +9 -7
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +51 -50
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +16 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +95 -0
- package/dist-esm/ui/icons.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +59 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +59 -7
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -8
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +13 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/visual-viewport.mjs +12 -0
- package/dist-esm/ui/visual-viewport.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +191 -0
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +464 -55
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +178 -36
- package/src/canvas/cluster-input.ts +95 -9
- package/src/canvas/cluster-model.ts +378 -0
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +452 -0
- package/src/canvas/comment-mutations.ts +299 -0
- package/src/canvas/comment-reactions.test.ts +154 -0
- package/src/canvas/comment-reactions.tsx +246 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +17 -13
- package/src/canvas/comment-store.ts +68 -33
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +103 -25
- package/src/canvas/comments-filter-menu.tsx +20 -36
- package/src/canvas/comments-overflow-menu.tsx +17 -19
- package/src/canvas/comments-overlay.tsx +252 -1369
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +112 -73
- package/src/canvas/comments-visibility-toggle.tsx +25 -0
- package/src/canvas/context.ts +48 -0
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +66 -11
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +176 -4
- package/src/canvas/options.ts +276 -18
- package/src/canvas/pending-composer.tsx +157 -0
- package/src/canvas/pin-stacking.test.ts +224 -0
- package/src/canvas/pin-stacking.ts +103 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +88 -39
- package/src/canvas/thread-pin.tsx +418 -0
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +294 -0
- package/src/canvas/thread-stack.tsx +242 -0
- package/src/canvas/thread-state.test.ts +260 -0
- package/src/canvas/thread-state.ts +171 -37
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +614 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +17 -8
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +267 -13
- package/src/clustering/runtime.ts +176 -19
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +19 -9
- package/src/index.ts +91 -42
- package/src/ui/byline.tsx +21 -9
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +178 -56
- package/src/ui/comment-pin.tsx +13 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +82 -47
- package/src/ui/comments.css +577 -294
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +27 -2
- package/src/ui/icons.tsx +116 -0
- package/src/ui/reaction-picker.tsx +102 -0
- package/src/ui/reaction.tsx +125 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +13 -3
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -23
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -31
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'
|
|
2
|
+
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
createComment,
|
|
5
|
+
createCommentThread,
|
|
6
|
+
Editor,
|
|
7
|
+
EditorPortal,
|
|
8
|
+
TLRichText,
|
|
9
|
+
usePassThroughWheelEvents,
|
|
10
|
+
useTranslation,
|
|
11
|
+
useValue,
|
|
12
|
+
} from 'tldraw'
|
|
13
|
+
import { CommentComposer } from '../ui/comment-composer'
|
|
14
|
+
import { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'
|
|
15
|
+
import { CommentPin } from '../ui/comment-pin'
|
|
16
|
+
import {
|
|
17
|
+
clearCommentDraft,
|
|
18
|
+
getCommentDraft,
|
|
19
|
+
NEW_COMMENT_DRAFT,
|
|
20
|
+
saveCommentDraft,
|
|
21
|
+
} from './comment-drafts'
|
|
22
|
+
import { commitCommentMutation } from './comment-mutations'
|
|
23
|
+
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
24
|
+
import { PendingComment } from './comment-tool'
|
|
25
|
+
import { type CommentingContext } from './context'
|
|
26
|
+
import { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'
|
|
27
|
+
import { useCanComment, useCommentingOptions } from './options'
|
|
28
|
+
import { pendingComment } from './state'
|
|
29
|
+
|
|
30
|
+
const stop = (e: { stopPropagation(): void }) => e.stopPropagation()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The composer for a thread that doesn't exist yet: the comment tool has placed a point (or
|
|
34
|
+
* region) and is waiting on a first comment. Submitting creates the thread and its comment
|
|
35
|
+
* together; clicking away keeps the draft for the next placement.
|
|
36
|
+
*/
|
|
37
|
+
export function PendingComposer({
|
|
38
|
+
editor,
|
|
39
|
+
pending,
|
|
40
|
+
currentUserId,
|
|
41
|
+
resolveAuthor,
|
|
42
|
+
onPostComment,
|
|
43
|
+
getMentionSuggestions,
|
|
44
|
+
renderMentionSuggestion,
|
|
45
|
+
}: CommentingContext & { editor: Editor; pending: PendingComment }) {
|
|
46
|
+
const ComposerFallback = useCommentingOptions().components.ComposerFallback
|
|
47
|
+
const canComment = useCanComment(currentUserId)
|
|
48
|
+
const me = currentUserId ? resolveAuthor(currentUserId) : undefined
|
|
49
|
+
// The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.
|
|
50
|
+
const draftAvatar = (
|
|
51
|
+
<CommentPin>
|
|
52
|
+
<Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />
|
|
53
|
+
</CommentPin>
|
|
54
|
+
)
|
|
55
|
+
// Click-away keeps the draft (saved on every change) and the next placement composer
|
|
56
|
+
// restores it — the flip side of dismissing without a discard warning.
|
|
57
|
+
const [text, setText] = useState<TLRichText>(
|
|
58
|
+
() => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT
|
|
59
|
+
)
|
|
60
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
61
|
+
const msg = useTranslation()
|
|
62
|
+
// Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).
|
|
63
|
+
usePassThroughWheelEvents(ref)
|
|
64
|
+
|
|
65
|
+
const point = useValue('composer point', () => editor.pageToViewport(pending.point), [
|
|
66
|
+
editor,
|
|
67
|
+
pending.point,
|
|
68
|
+
])
|
|
69
|
+
// On mobile the composer floats free of the pin so it can clear the software keyboard; desktop
|
|
70
|
+
// keeps it pinned to the point.
|
|
71
|
+
const isMobile = useIsMobileCommenting()
|
|
72
|
+
const placed = useMobilePlacement(ref, point, isMobile)
|
|
73
|
+
|
|
74
|
+
// Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
77
|
+
const el = ref.current
|
|
78
|
+
const target = e.target as HTMLElement | null
|
|
79
|
+
if (!el || !target) return
|
|
80
|
+
// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is
|
|
81
|
+
// not "outside" — keep the draft open so the pick can insert.
|
|
82
|
+
if (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return
|
|
83
|
+
pendingComment.set(editor, null)
|
|
84
|
+
}
|
|
85
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
86
|
+
return () => document.removeEventListener('pointerdown', onPointerDown, true)
|
|
87
|
+
}, [editor])
|
|
88
|
+
|
|
89
|
+
const submit = () => {
|
|
90
|
+
if (isCommentEmpty(text) || !currentUserId) return
|
|
91
|
+
const comment = commitCommentMutation(editor, ({ put }) => {
|
|
92
|
+
const pageId = editor.getCurrentPageId()
|
|
93
|
+
const thread = createCommentThread({
|
|
94
|
+
pageId,
|
|
95
|
+
anchor: pending.anchor,
|
|
96
|
+
createdBy: currentUserId,
|
|
97
|
+
})
|
|
98
|
+
const comment = createComment({
|
|
99
|
+
threadId: thread.id,
|
|
100
|
+
pageId,
|
|
101
|
+
authorId: currentUserId,
|
|
102
|
+
body: text,
|
|
103
|
+
})
|
|
104
|
+
put([thread, comment])
|
|
105
|
+
return comment
|
|
106
|
+
})
|
|
107
|
+
setText(EMPTY_COMMENT)
|
|
108
|
+
clearCommentDraft(NEW_COMMENT_DRAFT)
|
|
109
|
+
pendingComment.set(editor, null)
|
|
110
|
+
// The host's callback is its own operation, not part of the post's history scope. It runs
|
|
111
|
+
// last so a throwing host can't strand the composer holding a draft of a posted comment.
|
|
112
|
+
onPostComment?.(comment)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<EditorPortal>
|
|
117
|
+
<div
|
|
118
|
+
ref={ref}
|
|
119
|
+
className={[
|
|
120
|
+
'tlui-cmt-canvas-composer',
|
|
121
|
+
pending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',
|
|
122
|
+
!canComment && 'tlui-cmt-canvas-composer--fallback',
|
|
123
|
+
]
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.join(' ')}
|
|
126
|
+
style={{ left: placed.left, top: placed.top }}
|
|
127
|
+
onPointerDown={stop}
|
|
128
|
+
onContextMenu={stop}
|
|
129
|
+
onKeyDown={(e) => {
|
|
130
|
+
if (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{canComment ? (
|
|
134
|
+
<CommentComposer
|
|
135
|
+
author={me ?? UNKNOWN_COMMENT_AUTHOR}
|
|
136
|
+
placeholder={msg('comments.add-placeholder')}
|
|
137
|
+
sendLabel={msg('comments.send')}
|
|
138
|
+
value={text}
|
|
139
|
+
onChange={(value) => {
|
|
140
|
+
setText(value)
|
|
141
|
+
saveCommentDraft(NEW_COMMENT_DRAFT, value)
|
|
142
|
+
}}
|
|
143
|
+
onSubmit={submit}
|
|
144
|
+
// No user, no author for the record — dead send button.
|
|
145
|
+
disabled={isCommentEmpty(text) || !currentUserId}
|
|
146
|
+
getMentionSuggestions={getMentionSuggestions}
|
|
147
|
+
renderMentionSuggestion={renderMentionSuggestion}
|
|
148
|
+
autoFocus
|
|
149
|
+
leading={draftAvatar}
|
|
150
|
+
/>
|
|
151
|
+
) : (
|
|
152
|
+
ComposerFallback && <ComposerFallback context="pending" />
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</EditorPortal>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Box, Mat, type Editor, type TLCommentAnchor, type TLCommentThread } from 'tldraw'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { computePinStacks, isOpenStackKeyLive, pinStackKey, pinStacksEqual } from './pin-stacking'
|
|
4
|
+
import { anchorPagePoint } from './thread-state'
|
|
5
|
+
|
|
6
|
+
const CURRENT_PAGE = 'page:one'
|
|
7
|
+
const OTHER_PAGE = 'page:two'
|
|
8
|
+
|
|
9
|
+
function thread(
|
|
10
|
+
id: string,
|
|
11
|
+
anchor: TLCommentAnchor,
|
|
12
|
+
opts: { pageId?: string; createdAt?: number } = {}
|
|
13
|
+
): TLCommentThread {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
typeName: 'comment-thread',
|
|
17
|
+
pageId: opts.pageId ?? CURRENT_PAGE,
|
|
18
|
+
anchor,
|
|
19
|
+
createdBy: 'user:1',
|
|
20
|
+
createdAt: opts.createdAt ?? 0,
|
|
21
|
+
resolved: null,
|
|
22
|
+
meta: {},
|
|
23
|
+
} as unknown as TLCommentThread
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unrotated shapes: local geometry sized by the box, placed by a translate-only page transform.
|
|
28
|
+
* No comment tool registered, so the commenting options `anchorPagePoint` reads are the defaults.
|
|
29
|
+
*/
|
|
30
|
+
function stubEditor(
|
|
31
|
+
shapes: Record<string, { minX: number; minY: number; maxX: number; maxY: number }> = {}
|
|
32
|
+
): Editor {
|
|
33
|
+
const shapeId = (shape: string | { id: string }) => (typeof shape === 'string' ? shape : shape.id)
|
|
34
|
+
return {
|
|
35
|
+
getCurrentPageId: () => CURRENT_PAGE,
|
|
36
|
+
getStateDescendant: () => undefined,
|
|
37
|
+
getShape: (id: string) => (shapes[id] ? { id } : undefined),
|
|
38
|
+
getShapeGeometry: (shape: string | { id: string }) => {
|
|
39
|
+
const bounds = shapes[shapeId(shape)]
|
|
40
|
+
return { bounds: new Box(0, 0, bounds.maxX - bounds.minX, bounds.maxY - bounds.minY) }
|
|
41
|
+
},
|
|
42
|
+
getShapePageTransform: (shape: string | { id: string }) => {
|
|
43
|
+
const bounds = shapes[shapeId(shape)]
|
|
44
|
+
return bounds ? Mat.Translate(bounds.minX, bounds.minY) : undefined
|
|
45
|
+
},
|
|
46
|
+
} as unknown as Editor
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const SHAPE = { 'shape:a': { minX: 0, minY: 0, maxX: 200, maxY: 100 } }
|
|
50
|
+
/** The same shape after someone moved it — same size, different page position. */
|
|
51
|
+
const SHAPE_MOVED = { 'shape:a': { minX: 500, minY: 300, maxX: 700, maxY: 400 } }
|
|
52
|
+
|
|
53
|
+
function impreciseAnchor(shapeId: string): TLCommentAnchor {
|
|
54
|
+
return { type: 'shape', shapeId, x: 0.2, y: 0.9, isPrecise: false } as TLCommentAnchor
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The stack key the pins on `shape:a` coincide at, for the given shape placement. */
|
|
58
|
+
function stackKeyOn(shapes: typeof SHAPE): string {
|
|
59
|
+
return pinStackKey(anchorPagePoint(stubEditor(shapes), impreciseAnchor('shape:a'))!)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe('computePinStacks', () => {
|
|
63
|
+
it('groups coincident imprecise pins on one shape, oldest first', () => {
|
|
64
|
+
const stacks = computePinStacks(stubEditor(SHAPE), [
|
|
65
|
+
thread('t2', impreciseAnchor('shape:a'), { createdAt: 20 }),
|
|
66
|
+
thread('t1', impreciseAnchor('shape:a'), { createdAt: 10 }),
|
|
67
|
+
])
|
|
68
|
+
expect(stacks.get('t1')).toEqual(['t1', 't2'])
|
|
69
|
+
expect(stacks.get('t2')).toEqual(['t1', 't2'])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('leaves separated pins ungrouped', () => {
|
|
73
|
+
const stacks = computePinStacks(stubEditor(), [
|
|
74
|
+
thread('t1', { type: 'point', x: 0, y: 0 }),
|
|
75
|
+
thread('t2', { type: 'point', x: 50, y: 0 }),
|
|
76
|
+
])
|
|
77
|
+
expect(stacks.size).toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('groups coincident point anchors', () => {
|
|
81
|
+
const stacks = computePinStacks(stubEditor(), [
|
|
82
|
+
thread('t1', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
|
|
83
|
+
thread('t2', { type: 'point', x: 5, y: 5 }, { createdAt: 2 }),
|
|
84
|
+
thread('t3', { type: 'point', x: 5, y: 5 }, { createdAt: 3 }),
|
|
85
|
+
])
|
|
86
|
+
expect(stacks.get('t2')).toEqual(['t1', 't2', 't3'])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('breaks creation-time ties by id so ordering is deterministic', () => {
|
|
90
|
+
const stacks = computePinStacks(stubEditor(), [
|
|
91
|
+
thread('t2', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
|
|
92
|
+
thread('t1', { type: 'point', x: 5, y: 5 }, { createdAt: 1 }),
|
|
93
|
+
])
|
|
94
|
+
expect(stacks.get('t1')).toEqual(['t1', 't2'])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('ignores threads on other pages and unresolvable anchors', () => {
|
|
98
|
+
const stacks = computePinStacks(stubEditor(SHAPE), [
|
|
99
|
+
thread('t1', impreciseAnchor('shape:a')),
|
|
100
|
+
thread('t2', impreciseAnchor('shape:a'), { pageId: OTHER_PAGE }),
|
|
101
|
+
thread('t3', impreciseAnchor('shape:gone')),
|
|
102
|
+
])
|
|
103
|
+
expect(stacks.size).toBe(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not group a precise pin sitting away from a coincident pair', () => {
|
|
107
|
+
const precise: TLCommentAnchor = {
|
|
108
|
+
type: 'shape',
|
|
109
|
+
shapeId: 'shape:a',
|
|
110
|
+
x: 0.5,
|
|
111
|
+
y: 0.5,
|
|
112
|
+
isPrecise: true,
|
|
113
|
+
} as TLCommentAnchor
|
|
114
|
+
const stacks = computePinStacks(stubEditor(SHAPE), [
|
|
115
|
+
thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
|
|
116
|
+
thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
|
|
117
|
+
thread('t3', precise),
|
|
118
|
+
])
|
|
119
|
+
expect(stacks.get('t1')).toEqual(['t1', 't2'])
|
|
120
|
+
expect(stacks.has('t3')).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('pinStacksEqual', () => {
|
|
125
|
+
const pair = () =>
|
|
126
|
+
new Map<string, readonly string[]>([
|
|
127
|
+
['t1', ['t1', 't2']],
|
|
128
|
+
['t2', ['t1', 't2']],
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
it('accepts equal groupings whatever the map or group identity', () => {
|
|
132
|
+
const a = pair()
|
|
133
|
+
expect(pinStacksEqual(a, a)).toBe(true)
|
|
134
|
+
expect(pinStacksEqual(a, pair())).toBe(true)
|
|
135
|
+
expect(pinStacksEqual(new Map(), new Map())).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('rejects a changed member set, a changed group, and a changed order', () => {
|
|
139
|
+
// the stack broke up
|
|
140
|
+
expect(pinStacksEqual(pair(), new Map())).toBe(false)
|
|
141
|
+
// a third pin joined
|
|
142
|
+
expect(
|
|
143
|
+
pinStacksEqual(
|
|
144
|
+
pair(),
|
|
145
|
+
new Map<string, readonly string[]>([
|
|
146
|
+
['t1', ['t1', 't2', 't3']],
|
|
147
|
+
['t2', ['t1', 't2', 't3']],
|
|
148
|
+
['t3', ['t1', 't2', 't3']],
|
|
149
|
+
])
|
|
150
|
+
)
|
|
151
|
+
).toBe(false)
|
|
152
|
+
// same size, different members
|
|
153
|
+
expect(
|
|
154
|
+
pinStacksEqual(
|
|
155
|
+
pair(),
|
|
156
|
+
new Map<string, readonly string[]>([
|
|
157
|
+
['t1', ['t1', 't3']],
|
|
158
|
+
['t3', ['t1', 't3']],
|
|
159
|
+
])
|
|
160
|
+
)
|
|
161
|
+
).toBe(false)
|
|
162
|
+
// same members, reordered within the group (the list renders in this order)
|
|
163
|
+
expect(
|
|
164
|
+
pinStacksEqual(
|
|
165
|
+
pair(),
|
|
166
|
+
new Map<string, readonly string[]>([
|
|
167
|
+
['t1', ['t2', 't1']],
|
|
168
|
+
['t2', ['t2', 't1']],
|
|
169
|
+
])
|
|
170
|
+
)
|
|
171
|
+
).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('is blind to position — equal groupings can sit at different page points', () => {
|
|
175
|
+
// The overlay holds the map's identity while this is true, which is why nothing keyed on a
|
|
176
|
+
// stack's *point* may key off it. See isOpenStackKeyLive.
|
|
177
|
+
const threads = [
|
|
178
|
+
thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
|
|
179
|
+
thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
|
|
180
|
+
]
|
|
181
|
+
const here = computePinStacks(stubEditor(SHAPE), threads)
|
|
182
|
+
const moved = computePinStacks(stubEditor(SHAPE_MOVED), threads)
|
|
183
|
+
expect(pinStacksEqual(here, moved)).toBe(true)
|
|
184
|
+
expect(stackKeyOn(SHAPE)).not.toBe(stackKeyOn(SHAPE_MOVED))
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('isOpenStackKeyLive', () => {
|
|
189
|
+
const threads = [
|
|
190
|
+
thread('t1', impreciseAnchor('shape:a'), { createdAt: 1 }),
|
|
191
|
+
thread('t2', impreciseAnchor('shape:a'), { createdAt: 2 }),
|
|
192
|
+
]
|
|
193
|
+
const byId = new Map(threads.map((t) => [t.id, t]))
|
|
194
|
+
|
|
195
|
+
it('is live while a stack still sits at the key', () => {
|
|
196
|
+
const editor = stubEditor(SHAPE)
|
|
197
|
+
expect(
|
|
198
|
+
isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), byId)
|
|
199
|
+
).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('goes stale when the stack moves — the case pinStacksEqual cannot see', () => {
|
|
203
|
+
// The anchor shape moved, so the point key changed while the membership didn't. Staying live
|
|
204
|
+
// here strands the old key in `openStackId`, killing every hover preview on the layer.
|
|
205
|
+
const editor = stubEditor(SHAPE_MOVED)
|
|
206
|
+
expect(
|
|
207
|
+
isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), byId)
|
|
208
|
+
).toBe(false)
|
|
209
|
+
// and the key the stack moved *to* is live, so the caller clears rather than thrashing
|
|
210
|
+
expect(
|
|
211
|
+
isOpenStackKeyLive(editor, stackKeyOn(SHAPE_MOVED), computePinStacks(editor, threads), byId)
|
|
212
|
+
).toBe(true)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('goes stale once nothing stacks there, or the threads are gone', () => {
|
|
216
|
+
const editor = stubEditor(SHAPE)
|
|
217
|
+
// the stack collapsed to a single pin: no entries at all
|
|
218
|
+
expect(isOpenStackKeyLive(editor, stackKeyOn(SHAPE), new Map(), byId)).toBe(false)
|
|
219
|
+
// entries for threads that have left the input
|
|
220
|
+
expect(
|
|
221
|
+
isOpenStackKeyLive(editor, stackKeyOn(SHAPE), computePinStacks(editor, threads), new Map())
|
|
222
|
+
).toBe(false)
|
|
223
|
+
})
|
|
224
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Editor, TLCommentThread } from 'tldraw'
|
|
2
|
+
import { anchorPagePoint } from './thread-state'
|
|
3
|
+
|
|
4
|
+
/** Two anchors within this page-space distance (per axis) share a stack. Identical imprecise
|
|
5
|
+
* anchors on one shape resolve to the same point exactly; the tolerance only absorbs float noise. */
|
|
6
|
+
const PIN_STACK_QUANTUM = 0.1
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The key a page point stacks under. Threads sharing a key are coincident. Stable identity for a
|
|
10
|
+
* stack itself: it survives losing any member (the survivors keep the same key), unlike keying by
|
|
11
|
+
* a particular thread id, so open-stack state stays put when the stack's oldest thread is deleted.
|
|
12
|
+
*/
|
|
13
|
+
export function pinStackKey(point: { x: number; y: number }): string {
|
|
14
|
+
return `${Math.round(point.x / PIN_STACK_QUANTUM)}:${Math.round(point.y / PIN_STACK_QUANTUM)}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Group threads whose pins land on the same page point — coincident pins (typically several
|
|
19
|
+
* imprecise comments on one shape) that zooming can never separate. The overlay renders each
|
|
20
|
+
* group as a single count-badge pin that opens the threads as a list. Every member id maps to
|
|
21
|
+
* its group's ordered member ids (oldest first); threads without an entry pin individually.
|
|
22
|
+
*
|
|
23
|
+
* Keyed by page-space anchor point, not screen position, so the result only changes when threads
|
|
24
|
+
* or their anchors move — never on camera moves.
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function computePinStacks(
|
|
28
|
+
editor: Editor,
|
|
29
|
+
threads: readonly TLCommentThread[]
|
|
30
|
+
): Map<string, readonly string[]> {
|
|
31
|
+
const pageId = editor.getCurrentPageId()
|
|
32
|
+
const groups = new Map<string, TLCommentThread[]>()
|
|
33
|
+
|
|
34
|
+
for (const thread of threads) {
|
|
35
|
+
if (thread.pageId !== pageId) continue
|
|
36
|
+
const point = anchorPagePoint(editor, thread.anchor)
|
|
37
|
+
if (!point) continue
|
|
38
|
+
const key = pinStackKey(point)
|
|
39
|
+
const group = groups.get(key)
|
|
40
|
+
if (group) {
|
|
41
|
+
group.push(thread)
|
|
42
|
+
} else {
|
|
43
|
+
groups.set(key, [thread])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stacks = new Map<string, readonly string[]>()
|
|
48
|
+
for (const group of groups.values()) {
|
|
49
|
+
if (group.length < 2) continue
|
|
50
|
+
group.sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))
|
|
51
|
+
const ids = group.map((thread) => thread.id)
|
|
52
|
+
for (const id of ids) {
|
|
53
|
+
stacks.set(id, ids)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return stacks
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Value equality for pin-stack maps, so the overlay can hold the map's identity across a recompute
|
|
61
|
+
* that changed no grouping. Membership only: two equal maps can describe stacks at different page
|
|
62
|
+
* points, so anything keyed on a stack's *point* must read the anchors itself, as
|
|
63
|
+
* {@link isOpenStackKeyLive} does.
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export function pinStacksEqual(
|
|
67
|
+
a: ReadonlyMap<string, readonly string[]>,
|
|
68
|
+
b: ReadonlyMap<string, readonly string[]>
|
|
69
|
+
): boolean {
|
|
70
|
+
if (a === b) return true
|
|
71
|
+
if (a.size !== b.size) return false
|
|
72
|
+
for (const [id, group] of b) {
|
|
73
|
+
const prevGroup = a.get(id)
|
|
74
|
+
if (!prevGroup) return false
|
|
75
|
+
if (prevGroup === group) continue
|
|
76
|
+
if (prevGroup.length !== group.length) return false
|
|
77
|
+
for (let i = 0; i < group.length; i++) {
|
|
78
|
+
if (prevGroup[i] !== group[i]) return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether an open-stack key still names a stack on the canvas — false once that stack is emptied,
|
|
86
|
+
* moved, or off the page, at which point the caller must clear the key. Reads the anchors, so a
|
|
87
|
+
* reactive caller re-evaluates on a move that {@link pinStacksEqual} can't see.
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export function isOpenStackKeyLive(
|
|
91
|
+
editor: Editor,
|
|
92
|
+
key: string,
|
|
93
|
+
stacks: ReadonlyMap<string, readonly string[]>,
|
|
94
|
+
threadsById: ReadonlyMap<string, TLCommentThread>
|
|
95
|
+
): boolean {
|
|
96
|
+
for (const id of stacks.keys()) {
|
|
97
|
+
const thread = threadsById.get(id)
|
|
98
|
+
if (!thread) continue
|
|
99
|
+
const point = anchorPagePoint(editor, thread.anchor)
|
|
100
|
+
if (point && pinStackKey(point) === key) return true
|
|
101
|
+
}
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, useRef } from 'react'
|
|
2
|
+
import { type BoxModel, Editor, useValue, VecLike } from 'tldraw'
|
|
3
|
+
import { regionDraft } from './state'
|
|
4
|
+
|
|
5
|
+
/** A region's dashed box. Purely visual — a region moves by its pin and resizes from its corner
|
|
6
|
+
* handles, so the box itself takes no pointer events. Positioned in viewport space, as a sibling
|
|
7
|
+
* of the pins in the canvas layer. */
|
|
8
|
+
export function RegionBox({ editor, box }: { editor: Editor; box: BoxModel }) {
|
|
9
|
+
const rect = useValue(
|
|
10
|
+
'region rect',
|
|
11
|
+
() => {
|
|
12
|
+
// Position from the page→viewport top-left; screen size scales with zoom, page size doesn't.
|
|
13
|
+
const topLeft = editor.pageToViewport({ x: box.x, y: box.y })
|
|
14
|
+
const zoom = editor.getZoomLevel()
|
|
15
|
+
return { left: topLeft.x, top: topLeft.y, width: box.w * zoom, height: box.h * zoom }
|
|
16
|
+
},
|
|
17
|
+
[editor, box.x, box.y, box.w, box.h]
|
|
18
|
+
)
|
|
19
|
+
return <div className="tlui-cmt-canvas-region" style={rect} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The live region being dragged out by the comment tool, or nothing when not dragging. */
|
|
23
|
+
export function RegionDraftBox({ editor }: { editor: Editor }) {
|
|
24
|
+
const box = useValue('region draft', () => regionDraft.get(editor), [editor])
|
|
25
|
+
if (!box) return null
|
|
26
|
+
return <RegionBox editor={editor} box={box} />
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A resize handle's normalized 0–1 spot on the box, and its cursor. An axis at 0.5 is *not*
|
|
30
|
+
// controlled by that handle — the resize math reads the spot rather than special-casing corners.
|
|
31
|
+
export interface RegionHandle {
|
|
32
|
+
x: number
|
|
33
|
+
y: number
|
|
34
|
+
cursor: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The four corners, each resizing both axes.
|
|
38
|
+
export const REGION_CORNERS: readonly RegionHandle[] = [
|
|
39
|
+
{ x: 0, y: 0, cursor: 'nwse-resize' },
|
|
40
|
+
{ x: 1, y: 0, cursor: 'nesw-resize' },
|
|
41
|
+
{ x: 0, y: 1, cursor: 'nesw-resize' },
|
|
42
|
+
{ x: 1, y: 1, cursor: 'nwse-resize' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
// Screen-space slack around a region's bounds within which its box and handles stay revealed, so
|
|
46
|
+
// the handles (which sit on the edge) are comfortably reachable.
|
|
47
|
+
export const REGION_HANDLE_MARGIN_PX = 12
|
|
48
|
+
|
|
49
|
+
/** Resize `box` by dragging `handle` to `cursor` (page coords). Each controlled axis spans from the
|
|
50
|
+
* handle's fixed opposite edge to the cursor (normalized, so dragging past it flips); an axis the
|
|
51
|
+
* handle doesn't control (a midpoint, at 0.5) keeps its original position and size. */
|
|
52
|
+
function resizeRegion(box: BoxModel, handle: RegionHandle, cursor: VecLike): BoxModel {
|
|
53
|
+
const controlsX = handle.x !== 0.5
|
|
54
|
+
const controlsY = handle.y !== 0.5
|
|
55
|
+
const fixedX = box.x + (1 - handle.x) * box.w
|
|
56
|
+
const fixedY = box.y + (1 - handle.y) * box.h
|
|
57
|
+
return {
|
|
58
|
+
x: controlsX ? Math.min(fixedX, cursor.x) : box.x,
|
|
59
|
+
y: controlsY ? Math.min(fixedY, cursor.y) : box.y,
|
|
60
|
+
w: controlsX ? Math.abs(cursor.x - fixedX) : box.w,
|
|
61
|
+
h: controlsY ? Math.abs(cursor.y - fixedY) : box.h,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Draggable handles that resize a region — corners (both axes) or edges (one axis), per the resize
|
|
66
|
+
* option. Previews live, commits on release. */
|
|
67
|
+
export function RegionResizeHandles({
|
|
68
|
+
editor,
|
|
69
|
+
box,
|
|
70
|
+
handles,
|
|
71
|
+
onPreview,
|
|
72
|
+
onCommit,
|
|
73
|
+
}: {
|
|
74
|
+
editor: Editor
|
|
75
|
+
box: BoxModel
|
|
76
|
+
handles: readonly RegionHandle[]
|
|
77
|
+
onPreview(bounds: BoxModel | null): void
|
|
78
|
+
onCommit(bounds: BoxModel): void
|
|
79
|
+
}) {
|
|
80
|
+
// The box at pointer-down, captured so the box prop reflowing under the live preview doesn't move
|
|
81
|
+
// the fixed edges mid-drag.
|
|
82
|
+
const boxRef = useRef<BoxModel | null>(null)
|
|
83
|
+
const points = useValue(
|
|
84
|
+
'region handle points',
|
|
85
|
+
() =>
|
|
86
|
+
handles.map((h) => {
|
|
87
|
+
const p = editor.pageToViewport({ x: box.x + h.x * box.w, y: box.y + h.y * box.h })
|
|
88
|
+
return { ...h, key: `${h.x}-${h.y}`, left: p.x, top: p.y }
|
|
89
|
+
}),
|
|
90
|
+
[editor, box.x, box.y, box.w, box.h, handles]
|
|
91
|
+
)
|
|
92
|
+
const startResize = (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
93
|
+
e.stopPropagation()
|
|
94
|
+
boxRef.current = box
|
|
95
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
96
|
+
}
|
|
97
|
+
const resizedTo = (h: RegionHandle, e: ReactPointerEvent<HTMLDivElement>): BoxModel =>
|
|
98
|
+
resizeRegion(boxRef.current!, h, editor.screenToPage({ x: e.clientX, y: e.clientY }))
|
|
99
|
+
const onResize = (h: RegionHandle) => (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
100
|
+
if (boxRef.current) onPreview(resizedTo(h, e))
|
|
101
|
+
}
|
|
102
|
+
const endResize = (h: RegionHandle) => (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
103
|
+
if (!boxRef.current) return
|
|
104
|
+
const bounds = resizedTo(h, e)
|
|
105
|
+
boxRef.current = null
|
|
106
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId))
|
|
107
|
+
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
108
|
+
onCommit(bounds)
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
{points.map((h) => (
|
|
113
|
+
<div
|
|
114
|
+
key={h.key}
|
|
115
|
+
className="tlui-cmt-canvas-region-handle"
|
|
116
|
+
style={{ left: h.left, top: h.top, cursor: h.cursor }}
|
|
117
|
+
onPointerDown={startResize}
|
|
118
|
+
onPointerMove={onResize(h)}
|
|
119
|
+
onPointerUp={endResize(h)}
|
|
120
|
+
/>
|
|
121
|
+
))}
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -16,10 +16,11 @@ export interface SidebarFilters {
|
|
|
16
16
|
onlyCurrentPage: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** The out-of-the-box view: every page's threads, resolved ones hidden until asked for.
|
|
20
|
+
* @public */
|
|
20
21
|
export const DEFAULT_SIDEBAR_FILTERS: SidebarFilters = {
|
|
21
|
-
showResolved:
|
|
22
|
+
showResolved: false,
|
|
22
23
|
onlyMine: false,
|
|
23
24
|
onlyUnread: false,
|
|
24
|
-
onlyCurrentPage:
|
|
25
|
+
onlyCurrentPage: false,
|
|
25
26
|
}
|