@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02

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.
Files changed (238) hide show
  1. package/commenting.css +63 -71
  2. package/dist-cjs/canvas/anchor-lifecycle.js +2 -2
  3. package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
  4. package/dist-cjs/{ui/tooltip-button.js → canvas/canvas-events.js} +16 -13
  5. package/dist-cjs/canvas/canvas-events.js.map +7 -0
  6. package/dist-cjs/canvas/cluster-badge.js +121 -0
  7. package/dist-cjs/canvas/cluster-badge.js.map +7 -0
  8. package/dist-cjs/canvas/cluster-fade.js +85 -0
  9. package/dist-cjs/canvas/cluster-fade.js.map +7 -0
  10. package/dist-cjs/canvas/cluster-input.js +51 -1
  11. package/dist-cjs/canvas/cluster-input.js.map +2 -2
  12. package/dist-cjs/canvas/cluster-model.js +252 -0
  13. package/dist-cjs/canvas/cluster-model.js.map +7 -0
  14. package/dist-cjs/canvas/comment-mutations.js +57 -28
  15. package/dist-cjs/canvas/comment-mutations.js.map +2 -2
  16. package/dist-cjs/canvas/comment-reactions.js +4 -7
  17. package/dist-cjs/canvas/comment-reactions.js.map +2 -2
  18. package/dist-cjs/canvas/comment-render.js +4 -3
  19. package/dist-cjs/canvas/comment-render.js.map +2 -2
  20. package/dist-cjs/canvas/comment-store.js.map +2 -2
  21. package/dist-cjs/canvas/comment-tool.js +0 -1
  22. package/dist-cjs/canvas/comment-tool.js.map +2 -2
  23. package/dist-cjs/canvas/comments-filter-menu.js +11 -13
  24. package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
  25. package/dist-cjs/canvas/comments-overflow-menu.js +10 -2
  26. package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
  27. package/dist-cjs/canvas/comments-overlay.js +77 -870
  28. package/dist-cjs/canvas/comments-overlay.js.map +3 -3
  29. package/dist-cjs/canvas/comments-sidebar.js +16 -11
  30. package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
  31. package/dist-cjs/canvas/comments-visibility-toggle.js +5 -25
  32. package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
  33. package/dist-cjs/canvas/context.js.map +1 -1
  34. package/dist-cjs/canvas/hooks.js +8 -2
  35. package/dist-cjs/canvas/hooks.js.map +2 -2
  36. package/dist-cjs/canvas/license.js.map +1 -1
  37. package/dist-cjs/canvas/mobile-placement.js +91 -0
  38. package/dist-cjs/canvas/mobile-placement.js.map +7 -0
  39. package/dist-cjs/canvas/options.js +39 -1
  40. package/dist-cjs/canvas/options.js.map +2 -2
  41. package/dist-cjs/canvas/pending-composer.js +134 -0
  42. package/dist-cjs/canvas/pending-composer.js.map +7 -0
  43. package/dist-cjs/canvas/pin-stacking.js +26 -1
  44. package/dist-cjs/canvas/pin-stacking.js.map +2 -2
  45. package/dist-cjs/canvas/region-box.js +113 -0
  46. package/dist-cjs/canvas/region-box.js.map +7 -0
  47. package/dist-cjs/canvas/state.js.map +2 -2
  48. package/dist-cjs/canvas/thread-pin.js +304 -0
  49. package/dist-cjs/canvas/thread-pin.js.map +7 -0
  50. package/dist-cjs/canvas/thread-preview.js +41 -49
  51. package/dist-cjs/canvas/thread-preview.js.map +2 -2
  52. package/dist-cjs/canvas/thread-stack.js +7 -7
  53. package/dist-cjs/canvas/thread-stack.js.map +2 -2
  54. package/dist-cjs/canvas/thread-state.js +15 -0
  55. package/dist-cjs/canvas/thread-state.js.map +2 -2
  56. package/dist-cjs/canvas/thread-view.js +120 -67
  57. package/dist-cjs/canvas/thread-view.js.map +3 -3
  58. package/dist-cjs/clustering/computeClusterTable.js +2 -2
  59. package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
  60. package/dist-cjs/clustering/replay.js +89 -22
  61. package/dist-cjs/clustering/replay.js.map +3 -3
  62. package/dist-cjs/clustering/runtime.js +19 -12
  63. package/dist-cjs/clustering/runtime.js.map +2 -2
  64. package/dist-cjs/clustering/types.js.map +1 -1
  65. package/dist-cjs/index.d.ts +323 -202
  66. package/dist-cjs/index.js +7 -1
  67. package/dist-cjs/index.js.map +2 -2
  68. package/dist-cjs/ui/byline.js +5 -3
  69. package/dist-cjs/ui/byline.js.map +2 -2
  70. package/dist-cjs/ui/comment-composer.js +2 -18
  71. package/dist-cjs/ui/comment-composer.js.map +2 -2
  72. package/dist-cjs/ui/comment-pin.js +2 -15
  73. package/dist-cjs/ui/comment-pin.js.map +2 -2
  74. package/dist-cjs/ui/comments-list.js +11 -24
  75. package/dist-cjs/ui/comments-list.js.map +2 -2
  76. package/dist-cjs/ui/format-time.js +12 -0
  77. package/dist-cjs/ui/format-time.js.map +2 -2
  78. package/dist-cjs/ui/icons.js +115 -0
  79. package/dist-cjs/ui/icons.js.map +7 -0
  80. package/dist-cjs/ui/reaction-picker.js +11 -13
  81. package/dist-cjs/ui/reaction-picker.js.map +2 -2
  82. package/dist-cjs/ui/reaction.js +3 -4
  83. package/dist-cjs/ui/reaction.js.map +2 -2
  84. package/dist-cjs/ui/send-button.js +2 -9
  85. package/dist-cjs/ui/send-button.js.map +2 -2
  86. package/dist-cjs/ui/visual-viewport.js +32 -0
  87. package/dist-cjs/ui/visual-viewport.js.map +7 -0
  88. package/dist-esm/canvas/anchor-lifecycle.mjs +3 -3
  89. package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
  90. package/dist-esm/canvas/canvas-events.mjs +15 -0
  91. package/dist-esm/canvas/canvas-events.mjs.map +7 -0
  92. package/dist-esm/canvas/cluster-badge.mjs +106 -0
  93. package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
  94. package/dist-esm/canvas/cluster-fade.mjs +65 -0
  95. package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
  96. package/dist-esm/canvas/cluster-input.mjs +52 -2
  97. package/dist-esm/canvas/cluster-input.mjs.map +2 -2
  98. package/dist-esm/canvas/cluster-model.mjs +236 -0
  99. package/dist-esm/canvas/cluster-model.mjs.map +7 -0
  100. package/dist-esm/canvas/comment-mutations.mjs +57 -28
  101. package/dist-esm/canvas/comment-mutations.mjs.map +2 -2
  102. package/dist-esm/canvas/comment-reactions.mjs +5 -12
  103. package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
  104. package/dist-esm/canvas/comment-render.mjs +5 -4
  105. package/dist-esm/canvas/comment-render.mjs.map +2 -2
  106. package/dist-esm/canvas/comment-store.mjs.map +2 -2
  107. package/dist-esm/canvas/comment-tool.mjs +0 -1
  108. package/dist-esm/canvas/comment-tool.mjs.map +2 -2
  109. package/dist-esm/canvas/comments-filter-menu.mjs +12 -13
  110. package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
  111. package/dist-esm/canvas/comments-overflow-menu.mjs +11 -2
  112. package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
  113. package/dist-esm/canvas/comments-overlay.mjs +54 -878
  114. package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
  115. package/dist-esm/canvas/comments-sidebar.mjs +18 -14
  116. package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
  117. package/dist-esm/canvas/comments-visibility-toggle.mjs +5 -25
  118. package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
  119. package/dist-esm/canvas/hooks.mjs +9 -2
  120. package/dist-esm/canvas/hooks.mjs.map +2 -2
  121. package/dist-esm/canvas/license.mjs.map +1 -1
  122. package/dist-esm/canvas/mobile-placement.mjs +71 -0
  123. package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
  124. package/dist-esm/canvas/options.mjs +39 -1
  125. package/dist-esm/canvas/options.mjs.map +2 -2
  126. package/dist-esm/canvas/pending-composer.mjs +126 -0
  127. package/dist-esm/canvas/pending-composer.mjs.map +7 -0
  128. package/dist-esm/canvas/pin-stacking.mjs +26 -1
  129. package/dist-esm/canvas/pin-stacking.mjs.map +2 -2
  130. package/dist-esm/canvas/region-box.mjs +93 -0
  131. package/dist-esm/canvas/region-box.mjs.map +7 -0
  132. package/dist-esm/canvas/state.mjs.map +2 -2
  133. package/dist-esm/canvas/thread-pin.mjs +310 -0
  134. package/dist-esm/canvas/thread-pin.mjs.map +7 -0
  135. package/dist-esm/canvas/thread-preview.mjs +42 -49
  136. package/dist-esm/canvas/thread-preview.mjs.map +2 -2
  137. package/dist-esm/canvas/thread-stack.mjs +8 -9
  138. package/dist-esm/canvas/thread-stack.mjs.map +2 -2
  139. package/dist-esm/canvas/thread-state.mjs +15 -0
  140. package/dist-esm/canvas/thread-state.mjs.map +2 -2
  141. package/dist-esm/canvas/thread-view.mjs +131 -72
  142. package/dist-esm/canvas/thread-view.mjs.map +3 -3
  143. package/dist-esm/clustering/computeClusterTable.mjs +2 -2
  144. package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
  145. package/dist-esm/clustering/replay.mjs +89 -22
  146. package/dist-esm/clustering/replay.mjs.map +3 -3
  147. package/dist-esm/clustering/runtime.mjs +19 -12
  148. package/dist-esm/clustering/runtime.mjs.map +2 -2
  149. package/dist-esm/index.d.mts +323 -202
  150. package/dist-esm/index.mjs +16 -3
  151. package/dist-esm/index.mjs.map +2 -2
  152. package/dist-esm/ui/byline.mjs +6 -4
  153. package/dist-esm/ui/byline.mjs.map +2 -2
  154. package/dist-esm/ui/comment-composer.mjs +2 -18
  155. package/dist-esm/ui/comment-composer.mjs.map +2 -2
  156. package/dist-esm/ui/comment-pin.mjs +2 -15
  157. package/dist-esm/ui/comment-pin.mjs.map +2 -2
  158. package/dist-esm/ui/comments-list.mjs +10 -23
  159. package/dist-esm/ui/comments-list.mjs.map +2 -2
  160. package/dist-esm/ui/format-time.mjs +12 -0
  161. package/dist-esm/ui/format-time.mjs.map +2 -2
  162. package/dist-esm/ui/icons.mjs +95 -0
  163. package/dist-esm/ui/icons.mjs.map +7 -0
  164. package/dist-esm/ui/reaction-picker.mjs +12 -13
  165. package/dist-esm/ui/reaction-picker.mjs.map +2 -2
  166. package/dist-esm/ui/reaction.mjs +3 -4
  167. package/dist-esm/ui/reaction.mjs.map +2 -2
  168. package/dist-esm/ui/send-button.mjs +2 -9
  169. package/dist-esm/ui/send-button.mjs.map +2 -2
  170. package/dist-esm/ui/visual-viewport.mjs +12 -0
  171. package/dist-esm/ui/visual-viewport.mjs.map +7 -0
  172. package/package.json +5 -5
  173. package/src/canvas/anchor-lifecycle.test.ts +2 -2
  174. package/src/canvas/anchor-lifecycle.ts +25 -39
  175. package/src/canvas/canvas-events.ts +18 -0
  176. package/src/canvas/canvas.css +5 -11
  177. package/src/canvas/cluster-badge.tsx +133 -0
  178. package/src/canvas/cluster-fade.ts +102 -0
  179. package/src/canvas/cluster-input.test.ts +148 -14
  180. package/src/canvas/cluster-input.ts +92 -5
  181. package/src/canvas/cluster-model.ts +378 -0
  182. package/src/canvas/comment-mutations.test.ts +46 -4
  183. package/src/canvas/comment-mutations.ts +115 -104
  184. package/src/canvas/comment-reactions.test.ts +22 -0
  185. package/src/canvas/comment-reactions.tsx +33 -32
  186. package/src/canvas/comment-render.ts +8 -8
  187. package/src/canvas/comment-store.ts +12 -17
  188. package/src/canvas/comment-tool.tsx +8 -12
  189. package/src/canvas/comments-filter-menu.tsx +9 -16
  190. package/src/canvas/comments-overflow-menu.tsx +8 -3
  191. package/src/canvas/comments-overlay.tsx +154 -1344
  192. package/src/canvas/comments-sidebar.tsx +40 -19
  193. package/src/canvas/comments-visibility-toggle.tsx +6 -30
  194. package/src/canvas/context.ts +4 -3
  195. package/src/canvas/hooks.test.ts +94 -0
  196. package/src/canvas/hooks.ts +17 -3
  197. package/src/canvas/license.ts +1 -1
  198. package/src/canvas/mobile-placement.ts +115 -0
  199. package/src/canvas/options.test.ts +124 -2
  200. package/src/canvas/options.ts +192 -58
  201. package/src/canvas/pending-composer.tsx +157 -0
  202. package/src/canvas/pin-stacking.test.ts +111 -1
  203. package/src/canvas/pin-stacking.ts +46 -0
  204. package/src/canvas/region-box.tsx +124 -0
  205. package/src/canvas/state.ts +20 -28
  206. package/src/canvas/thread-pin.tsx +418 -0
  207. package/src/canvas/thread-preview.tsx +77 -87
  208. package/src/canvas/thread-stack.tsx +19 -29
  209. package/src/canvas/thread-state.test.ts +51 -0
  210. package/src/canvas/thread-state.ts +56 -23
  211. package/src/canvas/thread-view.test.ts +72 -0
  212. package/src/canvas/thread-view.tsx +231 -133
  213. package/src/clustering/computeClusterTable.ts +12 -3
  214. package/src/clustering/replay.test.ts +0 -7
  215. package/src/clustering/replay.ts +131 -32
  216. package/src/clustering/runtime.test.ts +50 -6
  217. package/src/clustering/runtime.ts +42 -39
  218. package/src/clustering/schedule.test.ts +0 -6
  219. package/src/clustering/screen-offsets.test.ts +171 -0
  220. package/src/clustering/types.ts +10 -0
  221. package/src/index.ts +15 -2
  222. package/src/ui/byline.tsx +16 -6
  223. package/src/ui/comment-composer.tsx +25 -69
  224. package/src/ui/comment-pin.tsx +3 -16
  225. package/src/ui/comments-list.tsx +40 -29
  226. package/src/ui/comments.css +58 -60
  227. package/src/ui/format-time.test.ts +69 -0
  228. package/src/ui/format-time.ts +20 -0
  229. package/src/ui/icons.tsx +116 -0
  230. package/src/ui/reaction-picker.tsx +9 -16
  231. package/src/ui/reaction.tsx +10 -6
  232. package/src/ui/send-button.tsx +3 -8
  233. package/src/ui/visual-viewport.test.ts +36 -0
  234. package/src/ui/visual-viewport.ts +27 -0
  235. package/dist-cjs/ui/tooltip-button.js.map +0 -7
  236. package/dist-esm/ui/tooltip-button.mjs +0 -12
  237. package/dist-esm/ui/tooltip-button.mjs.map +0 -7
  238. package/src/ui/tooltip-button.tsx +0 -20
@@ -1,116 +1,43 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { Avatar, isMentionPickerOpen } from "@tldraw/mentions";
3
- import {
4
- Fragment as Fragment2,
5
- memo,
6
- useCallback,
7
- useEffect,
8
- useLayoutEffect,
9
- useMemo,
10
- useRef,
11
- useState
12
- } from "react";
13
- import { createPortal } from "react-dom";
14
- import {
15
- createComment,
16
- createCommentThread,
17
- react,
18
- useContainer,
19
- useEditor,
20
- usePassThroughMouseOverEvents,
21
- usePassThroughWheelEvents,
22
- useTranslation,
23
- useValue
24
- } from "tldraw";
25
- import { computeClusterTable } from "../clustering/computeClusterTable.mjs";
26
- import { createClusterRuntime } from "../clustering/runtime.mjs";
27
- import { CommentComposer } from "../ui/comment-composer.mjs";
28
- import { EMPTY_COMMENT, isCommentEmpty } from "../ui/comment-extensions.mjs";
29
- import { CommentPin } from "../ui/comment-pin.mjs";
30
- import { CountBadge } from "../ui/count-badge.mjs";
2
+ import { isMentionPickerOpen } from "@tldraw/mentions";
3
+ import { Fragment as Fragment2, useCallback, useEffect, useMemo, useRef } from "react";
4
+ import { EditorPortal, useEditor, useValue } from "tldraw";
31
5
  import { registerCommentAnchorLifecycle } from "./anchor-lifecycle.mjs";
32
- import { collectClusterLeaves } from "./cluster-input.mjs";
6
+ import { ClusterBadge } from "./cluster-badge.mjs";
7
+ import { clusterFadeClassName } from "./cluster-fade.mjs";
33
8
  import {
34
- clearCommentDraft,
35
- getCommentDraft,
36
- NEW_COMMENT_DRAFT,
37
- saveCommentDraft
38
- } from "./comment-drafts.mjs";
39
- import { commitCommentMutation, putRecordsInCommit } from "./comment-mutations.mjs";
40
- import { UNKNOWN_AUTHOR, UNKNOWN_COMMENT_AUTHOR } from "./comment-render.mjs";
9
+ CLUSTER_EXPAND_ZOOM_MS,
10
+ revealThreadPin,
11
+ useClusterModel,
12
+ zoomToClusterSplit
13
+ } from "./cluster-model.mjs";
41
14
  import { getCommentRecord } from "./comment-store.mjs";
42
- import { useCommentThreads, useThreadComments } from "./hooks.mjs";
15
+ import { useCommentThreads } from "./hooks.mjs";
43
16
  import { useCommentingEnabled } from "./license.mjs";
44
- import {
45
- getCommentingOptions,
46
- useCanComment,
47
- useCommentingOptions
48
- } from "./options.mjs";
49
- import { computePinStacks, pinStackKey } from "./pin-stacking.mjs";
17
+ import { useCanComment, useCommentingOptions } from "./options.mjs";
18
+ import { PendingComposer } from "./pending-composer.mjs";
19
+ import { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from "./pin-stacking.mjs";
20
+ import { RegionBox, RegionDraftBox } from "./region-box.mjs";
50
21
  import {
51
22
  commentsHidden,
52
23
  openStackId,
53
24
  openThreadId,
54
25
  pendingComment,
55
- regionDraft,
56
26
  revealThreadRequest,
57
27
  sidebarFilters,
58
28
  toggleCommentsHidden,
59
29
  usePendingComment
60
30
  } from "./state.mjs";
61
- import { ThreadPreview, sortThreadsForPreview, useMarkerPreview } from "./thread-preview.mjs";
31
+ import { ThreadPin } from "./thread-pin.mjs";
62
32
  import { ThreadStackPin } from "./thread-stack.mjs";
63
- import {
64
- anchorPagePoint,
65
- commentCenterScreenOffset,
66
- commentTargetShapeAt,
67
- impreciseShapePinInset,
68
- REGION_PIN_CORNER,
69
- regionAnchorPinCorner,
70
- regionPinPoint,
71
- shapeAnchorAt
72
- } from "./thread-state.mjs";
73
- import { POPOVER_OFFSET, ThreadPopover, ThreadView } from "./thread-view.mjs";
74
- const stop = (e) => e.stopPropagation();
75
- const isCanvasPanGesture = (editor, e) => e.button !== 0 || editor.inputs.keys.has("Space");
76
- function forwardPointerEventToCanvas(container, e) {
77
- const cvs = container.querySelector(".tl-canvas");
78
- if (!cvs) return;
79
- const newEvent = new PointerEvent(e.type, e.nativeEvent);
80
- newEvent.isSpecialRedispatchedEvent = true;
81
- cvs.dispatchEvent(newEvent);
82
- }
83
- const CLUSTER_FADE_MS = 150;
84
- const CLUSTER_EXPAND_ZOOM_MS = 450;
85
- const CLUSTER_SPLIT_ZOOM_FACTOR = 1.05;
86
- const CLUSTER_CULL_MARGIN_PX = 120;
87
- const THREAD_HEADER_BLOCK = 36;
88
33
  function CanvasComments(props) {
89
34
  const commentingEnabled = useCommentingEnabled();
90
35
  if (!commentingEnabled) return null;
91
36
  return /* @__PURE__ */ jsx(CanvasCommentsLayer, { ...props });
92
37
  }
93
- function useTrailingPortalHost(container) {
94
- const [host, setHost] = useState(null);
95
- useLayoutEffect(() => {
96
- const elm = container.ownerDocument.createElement("div");
97
- elm.style.display = "contents";
98
- container.appendChild(elm);
99
- setHost(elm);
100
- return () => {
101
- elm.remove();
102
- setHost(null);
103
- };
104
- }, [container]);
105
- return host;
106
- }
107
38
  function CanvasCommentsLayer(props) {
108
39
  const editor = useEditor();
109
40
  const options = useCommentingOptions();
110
- const container = useContainer();
111
- const portalHost = useTrailingPortalHost(container);
112
- const layerRef = useRef(null);
113
- usePassThroughMouseOverEvents(layerRef);
114
41
  const allThreads = useCommentThreads(editor);
115
42
  const pending = usePendingComment();
116
43
  const canComment = useCanComment(props.currentUserId);
@@ -128,121 +55,28 @@ function CanvasCommentsLayer(props) {
128
55
  [allThreads, showResolved, openId]
129
56
  );
130
57
  useEffect(() => registerCommentAnchorLifecycle(editor), [editor]);
131
- const [heldThreadIds, setHeldThreadIds] = useState(EMPTY_SET);
132
- const adoptOnRebuild = useRef(false);
133
- const clusterLeaves = useValue(
134
- "comment cluster leaves",
135
- () => collectClusterLeaves(
136
- editor,
137
- threads.filter((thread) => !heldThreadIds.has(thread.id)),
138
- openThreadId.get(editor)
139
- ),
140
- [editor, threads, heldThreadIds]
141
- );
142
- const clusterZoomBounds = useValue(
143
- "comment cluster zoom bounds",
144
- () => getClusterZoomBounds(editor),
145
- [editor]
146
- );
147
- const latestModel = useMemo(() => {
148
- const table = computeClusterTable(clusterLeaves, clusterZoomBounds);
149
- const runtime = createClusterRuntime(table);
150
- runtime.seed(editor.getZoomLevel());
151
- return { runtime, table };
152
- }, [clusterLeaves, clusterZoomBounds, editor]);
153
- const [renderedModel, setRenderedModel] = useState(latestModel);
154
- let clusterModel = renderedModel;
155
- const pageId = useValue("comment cluster page", () => editor.getCurrentPageId(), [editor]);
156
- const pageRef = useRef(pageId);
157
- if (pageRef.current !== pageId) {
158
- pageRef.current = pageId;
159
- adoptOnRebuild.current = false;
160
- latestModel.runtime.seed(editor.getZoomLevel());
161
- if (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET);
162
- setRenderedModel(latestModel);
163
- clusterModel = latestModel;
164
- }
165
- const rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current;
166
- if (renderedModel !== latestModel && rejoinPending) {
167
- adoptOnRebuild.current = false;
168
- latestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible());
169
- setRenderedModel(latestModel);
170
- clusterModel = latestModel;
171
- } else if (heldThreadIds.size === 0 && renderedModel === latestModel) {
172
- adoptOnRebuild.current = false;
173
- }
174
- const newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel);
175
- if (newlyMovedIds.length > 0) {
176
- const next = new Set(heldThreadIds);
177
- for (const id of newlyMovedIds) next.add(id);
178
- setHeldThreadIds(next);
179
- }
180
- {
181
- const latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
182
- for (const leaf of clusterModel.table.leaves) {
183
- if (!latestLeafIds.has(leaf.id)) {
184
- clusterModel.runtime.detachLeaf(leaf.id);
185
- }
186
- }
187
- }
188
- useEffect(() => {
189
- if (heldThreadIds.size === 0) return;
190
- let lastZoom = editor.getZoomLevel();
191
- return react("rejoin moved comment pins on zoom out", () => {
192
- const zoom = editor.getZoomLevel();
193
- const prevZoom = lastZoom;
194
- lastZoom = zoom;
195
- if (zoom >= prevZoom) return;
196
- adoptOnRebuild.current = true;
197
- setHeldThreadIds(EMPTY_SET);
198
- });
199
- }, [heldThreadIds, editor]);
200
- useEffect(() => {
201
- if (clusterModel === latestModel) return;
202
- let lastZoom = editor.getZoomLevel();
203
- return react("adopt pending cluster model on zoom out", () => {
204
- const zoom = editor.getZoomLevel();
205
- const prevZoom = lastZoom;
206
- lastZoom = zoom;
207
- if (zoom >= prevZoom) return;
208
- latestModel.runtime.seedFrom(zoom, clusterModel.runtime.getVisible());
209
- setRenderedModel(latestModel);
210
- });
211
- }, [clusterModel, latestModel, editor]);
212
- const partitionVersion = clusterModel.runtime.version;
213
- const orphanThreads = useMemo(() => {
214
- if (clusterModel === latestModel) return [];
215
- const displayed = /* @__PURE__ */ new Set();
216
- for (const node of clusterModel.runtime.getVisible().values()) {
217
- for (const member of node.members) displayed.add(member);
218
- }
219
- const latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id));
220
- return threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id));
221
- }, [clusterModel, latestModel, threads, partitionVersion]);
222
- const heldThreads = useMemo(
223
- () => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),
224
- [threads, heldThreadIds, openId]
225
- );
226
- useValue(
227
- "comment cluster version",
228
- () => {
229
- clusterModel.runtime.onCamera(editor.getZoomLevel());
230
- return clusterModel.runtime.version;
231
- },
232
- [clusterModel, editor]
233
- );
234
- const visibleNodes = useMemo(() => {
235
- return Array.from(clusterModel.runtime.getVisible().values());
236
- }, [clusterModel, partitionVersion]);
237
- const fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel);
58
+ const {
59
+ model: clusterModel,
60
+ zoomBounds: clusterZoomBounds,
61
+ fadeNodes,
62
+ orphanThreads,
63
+ heldThreads
64
+ } = useClusterModel(editor, threads, openId);
238
65
  const threadsById = useMemo(
239
66
  () => new Map(threads.map((thread) => [thread.id, thread])),
240
67
  [threads]
241
68
  );
242
- const pinStacks = useValue("comment pin stacks", () => computePinStacks(editor, threads), [
243
- editor,
244
- threads
245
- ]);
69
+ const pinStacksRef = useRef(/* @__PURE__ */ new Map());
70
+ const pinStacks = useValue(
71
+ "comment pin stacks",
72
+ () => {
73
+ const stacks = computePinStacks(editor, threads);
74
+ if (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current;
75
+ pinStacksRef.current = stacks;
76
+ return stacks;
77
+ },
78
+ [editor, threads]
79
+ );
246
80
  const openThread = openId ? threadsById.get(openId) : null;
247
81
  const hidden = useValue("comments hidden", () => commentsHidden.get(editor), [editor]);
248
82
  useEffect(() => {
@@ -253,17 +87,18 @@ function CanvasCommentsLayer(props) {
253
87
  revealThreadRequest.set(editor, null);
254
88
  };
255
89
  }, [editor]);
90
+ const openStackKeyIsStale = useValue(
91
+ "open stack key stale",
92
+ () => {
93
+ const key = openStackId.get(editor);
94
+ if (!key) return false;
95
+ return !isOpenStackKeyLive(editor, key, pinStacks, threadsById);
96
+ },
97
+ [editor, pinStacks, threadsById]
98
+ );
256
99
  useEffect(() => {
257
- const key = openStackId.get(editor);
258
- if (!key) return;
259
- for (const id of pinStacks.keys()) {
260
- const thread = threadsById.get(id);
261
- if (!thread) continue;
262
- const point = anchorPagePoint(editor, thread.anchor);
263
- if (point && pinStackKey(point) === key) return;
264
- }
265
- openStackId.set(editor, null);
266
- }, [editor, pinStacks, threadsById]);
100
+ if (openStackKeyIsStale) openStackId.set(editor, null);
101
+ }, [editor, openStackKeyIsStale]);
267
102
  const requestedRevealThread = useValue(
268
103
  "requested reveal thread",
269
104
  () => {
@@ -297,18 +132,11 @@ function CanvasCommentsLayer(props) {
297
132
  },
298
133
  [clusterModel.table, clusterZoomBounds, editor, options]
299
134
  );
300
- const zoomToClusterSplit = useCallback(
135
+ const expandCluster = useCallback(
301
136
  (node) => {
302
- const event = clusterModel.table.events.find((e) => e.result.id === node.id);
303
- if (!event || !Number.isFinite(event.zSplit)) return;
304
- const zoom = clamp(
305
- event.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
306
- clusterZoomBounds.minZoom,
307
- clusterZoomBounds.maxZoom
308
- );
309
- centerOnPointAtZoom(editor, node.centroid, zoom, CLUSTER_EXPAND_ZOOM_MS);
137
+ zoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node);
310
138
  },
311
- [clusterModel, clusterZoomBounds, editor]
139
+ [clusterModel.table, clusterZoomBounds, editor]
312
140
  );
313
141
  useEffect(() => {
314
142
  const onKeyDown = (e) => {
@@ -362,9 +190,8 @@ function CanvasCommentsLayer(props) {
362
190
  }
363
191
  return /* @__PURE__ */ jsx(ThreadPin, { editor, thread, ...props });
364
192
  };
365
- if (!portalHost) return null;
366
- return createPortal(
367
- /* @__PURE__ */ jsxs("div", { ref: layerRef, className: "tlui-cmt-canvas-layer", children: [
193
+ return (
194
+ /* @__PURE__ */ jsx(EditorPortal, { children: /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-canvas-layer", children: [
368
195
  options.enableClustering ? /* @__PURE__ */ jsxs(Fragment, { children: [
369
196
  fadeNodes.map(({ node, phase }) => {
370
197
  let content;
@@ -382,7 +209,7 @@ function CanvasCommentsLayer(props) {
382
209
  {
383
210
  editor,
384
211
  node,
385
- onExpand: zoomToClusterSplit,
212
+ onExpand: expandCluster,
386
213
  onSelectThread: revealClusteredThread,
387
214
  threadsById,
388
215
  currentUserId: props.currentUserId,
@@ -395,10 +222,8 @@ function CanvasCommentsLayer(props) {
395
222
  orphanThreads.map((thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id)),
396
223
  heldThreads.map((thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id))
397
224
  ] }) : (
398
- // Clustering off: every thread renders as its own live pin (each returns null when it's
399
- // not on the current page or its anchor is missing). The open thread is excluded here and
400
- // rendered once below, mirroring how the clustering path keeps it out of the cluster leaves —
401
- // otherwise it would mount a second, stacked pin.
225
+ // Clustering off: every thread renders its own pin. The open thread is excluded and
226
+ // rendered once below, or it would mount a second, stacked pin.
402
227
  (threads.filter((thread) => thread.id !== openId).map(
403
228
  (thread) => /* @__PURE__ */ jsx(Fragment2, { children: renderThreadPin(thread) }, thread.id)
404
229
  ))
@@ -407,656 +232,7 @@ function CanvasCommentsLayer(props) {
407
232
  /* @__PURE__ */ jsx(RegionDraftBox, { editor }),
408
233
  pending?.anchor.type === "region" && showPendingComposer && /* @__PURE__ */ jsx(RegionBox, { editor, box: pending.anchor }),
409
234
  pending && showPendingComposer && /* @__PURE__ */ jsx(PendingComposer, { editor, pending, ...props })
410
- ] }),
411
- portalHost
412
- );
413
- }
414
- const EMPTY_SET = /* @__PURE__ */ new Set();
415
- const MOVED_LEAF_EPSILON = 1e-6;
416
- function useFadeVisibleNodes(nodes, resetKey) {
417
- const resetKeyRef = useRef(resetKey);
418
- const didReset = resetKeyRef.current !== resetKey;
419
- if (didReset) {
420
- resetKeyRef.current = resetKey;
421
- }
422
- const [fadeNodes, setFadeNodes] = useState(() => toPresentFadeNodes(nodes));
423
- const renderedNodes = didReset ? toPresentFadeNodes(nodes) : fadeNodes;
424
- useEffect(() => {
425
- setFadeNodes(toPresentFadeNodes(nodes));
426
- }, [resetKey]);
427
- useEffect(() => {
428
- if (didReset) return;
429
- setFadeNodes((previous) => reconcileFadeNodes(previous, nodes));
430
- }, [didReset, nodes]);
431
- const hasEntering = renderedNodes.some((item) => item.phase === "entering");
432
- useEffect(() => {
433
- if (!hasEntering) return;
434
- const frame = requestClusterFadeFrame(() => {
435
- setFadeNodes(
436
- (previous) => previous.map((item) => item.phase === "entering" ? { ...item, phase: "present" } : item)
437
- );
438
- });
439
- return () => cancelClusterFadeFrame(frame);
440
- }, [hasEntering, renderedNodes]);
441
- const hasExiting = renderedNodes.some((item) => item.phase === "exiting");
442
- useEffect(() => {
443
- if (!hasExiting) return;
444
- const timeout = window.setTimeout(() => {
445
- setFadeNodes((previous) => previous.filter((item) => item.phase !== "exiting"));
446
- }, CLUSTER_FADE_MS);
447
- return () => window.clearTimeout(timeout);
448
- }, [hasExiting, renderedNodes]);
449
- return renderedNodes;
450
- }
451
- function toPresentFadeNodes(nodes) {
452
- return nodes.map((node) => ({ node, phase: "present" }));
453
- }
454
- function reconcileFadeNodes(previous, nextNodes) {
455
- const previousById = new Map(previous.map((item) => [item.node.id, item]));
456
- const nextIds = new Set(nextNodes.map((node) => node.id));
457
- const next = [];
458
- for (const node of nextNodes) {
459
- const previousItem = previousById.get(node.id);
460
- next.push({
461
- node,
462
- phase: previousItem && previousItem.phase !== "exiting" ? previousItem.phase : previousItem ? "present" : "entering"
463
- });
464
- }
465
- for (const item of previous) {
466
- if (nextIds.has(item.node.id)) continue;
467
- next.push(item.phase === "exiting" ? item : { ...item, phase: "exiting" });
468
- }
469
- return next;
470
- }
471
- function requestClusterFadeFrame(callback) {
472
- if (typeof requestAnimationFrame === "function") return requestAnimationFrame(callback);
473
- return window.setTimeout(() => callback(0), 16);
474
- }
475
- function cancelClusterFadeFrame(frame) {
476
- if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame);
477
- else window.clearTimeout(frame);
478
- }
479
- function clusterFadeClassName(phase) {
480
- return `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`;
481
- }
482
- function findMovedClusteredLeafIds(rendered, latest) {
483
- if (rendered.table === latest.table) return [];
484
- const visible = rendered.runtime.getVisible();
485
- const latestById = new Map(latest.table.leaves.map((leaf) => [leaf.id, leaf]));
486
- const moved = [];
487
- for (const leaf of rendered.table.leaves) {
488
- if (visible.has(leaf.id)) continue;
489
- const current = latestById.get(leaf.id);
490
- if (!current) continue;
491
- if (Math.abs(current.centroid.x - leaf.centroid.x) > MOVED_LEAF_EPSILON || Math.abs(current.centroid.y - leaf.centroid.y) > MOVED_LEAF_EPSILON) {
492
- moved.push(leaf.id);
493
- }
494
- }
495
- return moved;
496
- }
497
- function getClusterZoomBounds(editor) {
498
- const cameraOptions = editor.getCameraOptions();
499
- const baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1;
500
- const zoomSteps = cameraOptions.zoomSteps;
501
- return {
502
- minZoom: zoomSteps[0] * baseZoom,
503
- maxZoom: zoomSteps[zoomSteps.length - 1] * baseZoom
504
- };
505
- }
506
- function revealThreadPin(editor, thread, table, zoomBounds, options, duration = 200) {
507
- if (thread.pageId !== editor.getCurrentPageId()) {
508
- editor.setCurrentPage(thread.pageId);
509
- }
510
- const point = anchorPagePoint(editor, thread.anchor);
511
- if (!point) return;
512
- if (options.enableClustering) {
513
- const parentEvent = findDirectParentEvent(table, thread.id);
514
- if (parentEvent && Number.isFinite(parentEvent.zSplit) && parentEvent.zSplit <= zoomBounds.maxZoom) {
515
- const zoom = clamp(
516
- parentEvent.zSplit * CLUSTER_SPLIT_ZOOM_FACTOR,
517
- zoomBounds.minZoom,
518
- zoomBounds.maxZoom
519
- );
520
- centerOnPointAtZoom(editor, point, zoom, duration);
521
- return;
522
- }
523
- }
524
- const offset = commentCenterScreenOffset(editor) / editor.getZoomLevel();
525
- editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration } });
526
- }
527
- function findDirectParentEvent(table, threadId) {
528
- return table.events.find((event) => event.children.some((child) => child.id === threadId));
529
- }
530
- function centerOnPointAtZoom(editor, point, zoom, duration = 200) {
531
- const viewport = editor.getViewportScreenBounds();
532
- const offset = commentCenterScreenOffset(editor);
533
- editor.setCamera(
534
- {
535
- x: (viewport.w / 2 - offset) / zoom - point.x,
536
- y: viewport.h / (2 * zoom) - point.y,
537
- z: zoom
538
- },
539
- { animation: { duration } }
540
- );
541
- }
542
- function clamp(value, min, max) {
543
- return Math.max(min, Math.min(max, value));
544
- }
545
- const ClusterBadge = memo(function ClusterBadge2({
546
- editor,
547
- node,
548
- onExpand,
549
- onSelectThread,
550
- threadsById,
551
- ...props
552
- }) {
553
- const container = useContainer();
554
- const msg = useTranslation();
555
- const badgeRef = useRef(null);
556
- const { previewShown, previewHandlers } = useMarkerPreview(editor, `cluster:${node.id}`);
557
- usePassThroughWheelEvents(badgeRef);
558
- const point = useValue(
559
- "cluster badge point",
560
- () => {
561
- const pagePoint = editor.pageToViewport(node.centroid);
562
- if (!isInInflatedViewport(editor, pagePoint)) return null;
563
- return pagePoint;
564
- },
565
- [editor, node]
566
- );
567
- const previewThreads = useMemo(() => {
568
- if (!previewShown) return [];
569
- const threads = [];
570
- for (const id of node.members) {
571
- const thread = threadsById.get(id);
572
- if (thread) threads.push(thread);
573
- }
574
- return sortThreadsForPreview(threads);
575
- }, [previewShown, node.members, threadsById]);
576
- if (!point) return null;
577
- return /* @__PURE__ */ jsxs(Fragment, { children: [
578
- /* @__PURE__ */ jsx(
579
- "button",
580
- {
581
- ref: badgeRef,
582
- type: "button",
583
- className: "tlui-cmt-button tlui-cmt-canvas-cluster",
584
- style: { left: point.x, top: point.y },
585
- "aria-label": msg("comments.cluster-label").replace("{count}", String(node.count)),
586
- onPointerDown: (e) => {
587
- if (isCanvasPanGesture(editor, e)) {
588
- forwardPointerEventToCanvas(container, e);
589
- return;
590
- }
591
- e.stopPropagation();
592
- },
593
- onClick: (e) => {
594
- e.stopPropagation();
595
- onExpand(node);
596
- },
597
- ...previewHandlers,
598
- onFocus: previewHandlers.onPointerEnter,
599
- onBlur: previewHandlers.onPointerLeave,
600
- children: /* @__PURE__ */ jsx(CountBadge, { count: node.count })
601
- }
602
- ),
603
- previewShown && previewThreads.length > 0 && /* @__PURE__ */ jsx(
604
- ThreadPreview,
605
- {
606
- editor,
607
- threads: previewThreads,
608
- container,
609
- variant: "list",
610
- point,
611
- onSelectThread,
612
- ...previewHandlers,
613
- ...props
614
- }
615
- )
616
- ] });
617
- });
618
- function isInInflatedViewport(editor, point) {
619
- const viewport = editor.getViewportScreenBounds();
620
- const margin = CLUSTER_CULL_MARGIN_PX;
621
- return point.x >= -margin && point.y >= -margin && point.x <= viewport.w + margin && point.y <= viewport.h + margin;
622
- }
623
- function RegionBox({ editor, box }) {
624
- const rect = useValue(
625
- "region rect",
626
- () => {
627
- const topLeft = editor.pageToViewport({ x: box.x, y: box.y });
628
- const zoom = editor.getZoomLevel();
629
- return { left: topLeft.x, top: topLeft.y, width: box.w * zoom, height: box.h * zoom };
630
- },
631
- [editor, box.x, box.y, box.w, box.h]
632
- );
633
- return /* @__PURE__ */ jsx("div", { className: "tlui-cmt-canvas-region", style: rect });
634
- }
635
- function RegionDraftBox({ editor }) {
636
- const box = useValue("region draft", () => regionDraft.get(editor), [editor]);
637
- if (!box) return null;
638
- return /* @__PURE__ */ jsx(RegionBox, { editor, box });
639
- }
640
- const REGION_CORNERS = [
641
- { x: 0, y: 0, cursor: "nwse-resize" },
642
- { x: 1, y: 0, cursor: "nesw-resize" },
643
- { x: 0, y: 1, cursor: "nesw-resize" },
644
- { x: 1, y: 1, cursor: "nwse-resize" }
645
- ];
646
- const REGION_HANDLE_MARGIN_PX = 12;
647
- function resizeRegion(box, handle, cursor) {
648
- const controlsX = handle.x !== 0.5;
649
- const controlsY = handle.y !== 0.5;
650
- const fixedX = box.x + (1 - handle.x) * box.w;
651
- const fixedY = box.y + (1 - handle.y) * box.h;
652
- return {
653
- x: controlsX ? Math.min(fixedX, cursor.x) : box.x,
654
- y: controlsY ? Math.min(fixedY, cursor.y) : box.y,
655
- w: controlsX ? Math.abs(cursor.x - fixedX) : box.w,
656
- h: controlsY ? Math.abs(cursor.y - fixedY) : box.h
657
- };
658
- }
659
- function RegionResizeHandles({
660
- editor,
661
- box,
662
- handles,
663
- onPreview,
664
- onCommit
665
- }) {
666
- const boxRef = useRef(null);
667
- const points = useValue(
668
- "region handle points",
669
- () => handles.map((h) => {
670
- const p = editor.pageToViewport({ x: box.x + h.x * box.w, y: box.y + h.y * box.h });
671
- return { ...h, key: `${h.x}-${h.y}`, left: p.x, top: p.y };
672
- }),
673
- [editor, box.x, box.y, box.w, box.h, handles]
674
- );
675
- const startResize = (e) => {
676
- e.stopPropagation();
677
- boxRef.current = box;
678
- e.currentTarget.setPointerCapture(e.pointerId);
679
- };
680
- const resizedTo = (h, e) => resizeRegion(boxRef.current, h, editor.screenToPage({ x: e.clientX, y: e.clientY }));
681
- const onResize = (h) => (e) => {
682
- if (boxRef.current) onPreview(resizedTo(h, e));
683
- };
684
- const endResize = (h) => (e) => {
685
- if (!boxRef.current) return;
686
- const bounds = resizedTo(h, e);
687
- boxRef.current = null;
688
- if (e.currentTarget.hasPointerCapture(e.pointerId))
689
- e.currentTarget.releasePointerCapture(e.pointerId);
690
- onCommit(bounds);
691
- };
692
- return /* @__PURE__ */ jsx(Fragment, { children: points.map((h) => /* @__PURE__ */ jsx(
693
- "div",
694
- {
695
- className: "tlui-cmt-canvas-region-handle",
696
- style: { left: h.left, top: h.top, cursor: h.cursor },
697
- onPointerDown: startResize,
698
- onPointerMove: onResize(h),
699
- onPointerUp: endResize(h)
700
- },
701
- h.key
702
- )) });
703
- }
704
- const ThreadPin = memo(function ThreadPin2({
705
- editor,
706
- thread,
707
- ...props
708
- }) {
709
- const { resolveAuthor } = props;
710
- const options = useCommentingOptions();
711
- const canComment = useCanComment(props.currentUserId);
712
- const container = useContainer();
713
- const msg = useTranslation();
714
- const comments = useThreadComments(editor, thread.id);
715
- const open = useValue("thread open", () => openThreadId.get(editor) === thread.id, [
716
- editor,
717
- thread.id
718
- ]);
719
- const [dragPagePoint, setDragPagePoint] = useState(null);
720
- const [resizeBounds, setResizeBounds] = useState(null);
721
- const { previewShown, previewHandlers } = useMarkerPreview(editor, `pin:${thread.id}`);
722
- const previewThreads = useMemo(() => [thread], [thread]);
723
- const pointerInRegion = useValue(
724
- "pointer in region",
725
- () => {
726
- if (thread.anchor.type !== "region" || thread.pageId !== editor.getCurrentPageId())
727
- return false;
728
- const m = REGION_HANDLE_MARGIN_PX / editor.getZoomLevel();
729
- const p = editor.inputs.getCurrentPagePoint();
730
- const a = thread.anchor;
731
- return p.x >= a.x - m && p.x <= a.x + a.w + m && p.y >= a.y - m && p.y <= a.y + a.h + m;
732
- },
733
- [editor, thread.anchor, thread.pageId]
734
- );
735
- const revealed = open || resizeBounds != null || pointerInRegion;
736
- const pinCorner = thread.anchor.type === "region" ? regionAnchorPinCorner(thread.anchor) : REGION_PIN_CORNER;
737
- const resizeHandles = useMemo(
738
- () => REGION_CORNERS.filter((c) => c.x !== pinCorner.x || c.y !== pinCorner.y),
739
- [pinCorner]
740
- );
741
- const dragRef = useRef(null);
742
- const markerRef = useRef(null);
743
- usePassThroughWheelEvents(markerRef);
744
- useEffect(() => {
745
- return () => {
746
- if (dragRef.current) editor.setHintingShapes([]);
747
- };
748
- }, [editor]);
749
- useEffect(() => {
750
- if (!open) return;
751
- const onPointerDown = (e) => {
752
- const target = e.target;
753
- if (!target) return;
754
- if (target.closest(".tlui-cmt-canvas-popover")) return;
755
- const marker = markerRef.current;
756
- if (marker && marker.contains(target)) return;
757
- if (target.closest(".tlui-cmt-canvas-region-handle, .tlui-cmt-canvas-region--movable")) return;
758
- if (target.closest(".tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup"))
759
- return;
760
- openThreadId.set(editor, null);
761
- };
762
- document.addEventListener("pointerdown", onPointerDown, true);
763
- return () => document.removeEventListener("pointerdown", onPointerDown, true);
764
- }, [open, editor]);
765
- const point = useValue(
766
- "pin point",
767
- () => {
768
- if (thread.pageId !== editor.getCurrentPageId()) return null;
769
- const pagePoint = anchorPagePoint(editor, thread.anchor);
770
- if (!pagePoint) return null;
771
- const viewportPoint = editor.pageToViewport(pagePoint);
772
- const inset = impreciseShapePinInset(editor, thread.anchor);
773
- return inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint;
774
- },
775
- [editor, thread.anchor, thread.pageId]
776
- );
777
- if (!point) return null;
778
- const PinContent = options.components.PinContent;
779
- const threadAuthor = resolveAuthor(thread.createdBy);
780
- const pinContent = PinContent ? /* @__PURE__ */ jsx(PinContent, { thread, comments }) : /* @__PURE__ */ jsx(Avatar, { author: threadAuthor ?? UNKNOWN_COMMENT_AUTHOR });
781
- const pinLabel = msg(
782
- thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
783
- ).replace("{name}", threadAuthor?.name ?? UNKNOWN_AUTHOR);
784
- const isRegion = thread.anchor.type === "region";
785
- const startDrag = (e) => {
786
- if (isCanvasPanGesture(editor, e)) {
787
- forwardPointerEventToCanvas(container, e);
788
- return;
789
- }
790
- e.stopPropagation();
791
- const grabPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
792
- const anchorPage = anchorPagePoint(editor, thread.anchor);
793
- const inset = impreciseShapePinInset(editor, thread.anchor);
794
- if (anchorPage && inset) {
795
- const zoom = editor.getZoomLevel();
796
- anchorPage.x += inset.x / zoom;
797
- anchorPage.y += inset.y / zoom;
798
- }
799
- dragRef.current = {
800
- startX: e.clientX,
801
- startY: e.clientY,
802
- moved: false,
803
- offsetX: anchorPage ? anchorPage.x - grabPage.x : 0,
804
- offsetY: anchorPage ? anchorPage.y - grabPage.y : 0
805
- };
806
- e.currentTarget.setPointerCapture(e.pointerId);
807
- };
808
- const onDrag = (e) => {
809
- const drag = dragRef.current;
810
- if (!drag) return;
811
- if (!canComment) return;
812
- if (!drag.moved && Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;
813
- drag.moved = true;
814
- const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
815
- const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
816
- setDragPagePoint(pagePoint);
817
- if (!isRegion) {
818
- const hit = commentTargetShapeAt(editor, pagePoint);
819
- editor.setHintingShapes(hit ? [hit.id] : []);
820
- }
821
- };
822
- const cancelDrag = (e) => {
823
- const drag = dragRef.current;
824
- dragRef.current = null;
825
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
826
- e.currentTarget.releasePointerCapture(e.pointerId);
827
- }
828
- if (!drag) return;
829
- setDragPagePoint(null);
830
- editor.setHintingShapes([]);
831
- };
832
- const endDrag = (e) => {
833
- const drag = dragRef.current;
834
- dragRef.current = null;
835
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
836
- e.currentTarget.releasePointerCapture(e.pointerId);
837
- }
838
- if (!drag) return;
839
- editor.setHintingShapes([]);
840
- if (!drag.moved) {
841
- openThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id);
842
- return;
843
- }
844
- const cursorPage = editor.screenToPage({ x: e.clientX, y: e.clientY });
845
- const pagePoint = { x: cursorPage.x + drag.offsetX, y: cursorPage.y + drag.offsetY };
846
- setDragPagePoint(null);
847
- let anchor;
848
- if (thread.anchor.type === "region") {
849
- anchor = {
850
- ...thread.anchor,
851
- x: pagePoint.x - pinCorner.x * thread.anchor.w,
852
- y: pagePoint.y - pinCorner.y * thread.anchor.h
853
- };
854
- } else {
855
- const hit = commentTargetShapeAt(editor, pagePoint);
856
- anchor = hit ? shapeAnchorAt(
857
- editor,
858
- hit.id,
859
- pagePoint,
860
- getCommentingOptions(editor).shouldBePrecise(editor, {
861
- shapeId: hit.id,
862
- point: pagePoint,
863
- altKey: e.altKey
864
- })
865
- ) : { type: "point", x: pagePoint.x, y: pagePoint.y };
866
- }
867
- commitCommentMutation(editor, () => putRecordsInCommit(editor, [{ ...thread, anchor }]), "drag");
868
- };
869
- const livePinPage = resizeBounds ? regionPinPoint(resizeBounds, pinCorner) : dragPagePoint;
870
- const renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point;
871
- const renderPoint = isRegion ? { x: renderPointBase.x - 17, y: renderPointBase.y + 17 } : renderPointBase;
872
- const regionAnchor = thread.anchor.type === "region" ? thread.anchor : void 0;
873
- const movedRegion = regionAnchor && dragPagePoint ? {
874
- ...regionAnchor,
875
- x: dragPagePoint.x - pinCorner.x * regionAnchor.w,
876
- y: dragPagePoint.y - pinCorner.y * regionAnchor.h
877
- } : regionAnchor;
878
- const regionBoxBounds = resizeBounds ?? movedRegion;
879
- const commitResize = (bounds) => {
880
- setResizeBounds(null);
881
- if (!canComment) return;
882
- commitCommentMutation(
883
- editor,
884
- // Spread the existing anchor first so the region's pin corner survives a resize.
885
- () => putRecordsInCommit(editor, [{ ...thread, anchor: { ...regionAnchor, ...bounds } }]),
886
- "drag"
887
- );
888
- };
889
- return /* @__PURE__ */ jsxs(Fragment, { children: [
890
- regionBoxBounds && (dragPagePoint || revealed) && /* @__PURE__ */ jsx(RegionBox, { editor, box: regionBoxBounds }),
891
- regionBoxBounds && revealed && !dragPagePoint && canComment && /* @__PURE__ */ jsx(
892
- RegionResizeHandles,
893
- {
894
- editor,
895
- box: regionBoxBounds,
896
- handles: resizeHandles,
897
- onPreview: setResizeBounds,
898
- onCommit: commitResize
899
- }
900
- ),
901
- /* @__PURE__ */ jsxs(
902
- "div",
903
- {
904
- className: [
905
- "tlui-cmt-canvas-pin",
906
- open && "tlui-cmt-canvas-pin--open",
907
- dragPagePoint && "tlui-cmt-canvas-pin--dragging"
908
- ].filter(Boolean).join(" "),
909
- style: { left: renderPoint.x, top: renderPoint.y },
910
- children: [
911
- /* @__PURE__ */ jsx(
912
- "button",
913
- {
914
- ref: markerRef,
915
- type: "button",
916
- className: "tlui-cmt-button tlui-cmt-canvas-pin__marker",
917
- "aria-label": pinLabel,
918
- "aria-expanded": open,
919
- onPointerDown: startDrag,
920
- onPointerMove: onDrag,
921
- onPointerUp: endDrag,
922
- onPointerCancel: cancelDrag,
923
- onClick: (e) => {
924
- if (e.detail !== 0) return;
925
- openThreadId.set(editor, openThreadId.get(editor) === thread.id ? null : thread.id);
926
- },
927
- onPointerEnter: previewHandlers.onPointerEnter,
928
- onPointerLeave: previewHandlers.onPointerLeave,
929
- onFocus: previewHandlers.onPointerEnter,
930
- onBlur: previewHandlers.onPointerLeave,
931
- children: /* @__PURE__ */ jsx(CommentPin, { resolved: thread.resolved != null, open, children: pinContent })
932
- }
933
- ),
934
- open && /* @__PURE__ */ jsx(
935
- ThreadPopover,
936
- {
937
- container,
938
- style: {
939
- left: renderPoint.x + POPOVER_OFFSET.thread.x,
940
- top: renderPoint.y + POPOVER_OFFSET.thread.y - THREAD_HEADER_BLOCK
941
- },
942
- children: /* @__PURE__ */ jsx(ThreadView, { editor, thread, ...props })
943
- }
944
- ),
945
- previewShown && !dragPagePoint && /* @__PURE__ */ jsx(
946
- ThreadPreview,
947
- {
948
- editor,
949
- threads: previewThreads,
950
- container,
951
- variant: "thread",
952
- point: renderPoint,
953
- onSelectThread: () => openThreadId.set(editor, thread.id),
954
- ...previewHandlers,
955
- currentUserId: props.currentUserId,
956
- resolveAuthor
957
- }
958
- )
959
- ]
960
- }
961
- )
962
- ] });
963
- });
964
- function PendingComposer({
965
- editor,
966
- pending,
967
- currentUserId,
968
- resolveAuthor,
969
- onPostComment,
970
- getMentionSuggestions,
971
- renderMentionSuggestion
972
- }) {
973
- const ComposerFallback = useCommentingOptions().components.ComposerFallback;
974
- const canComment = useCanComment(currentUserId);
975
- const me = currentUserId ? resolveAuthor(currentUserId) : void 0;
976
- const draftAvatar = /* @__PURE__ */ jsx(CommentPin, { children: /* @__PURE__ */ jsx(Avatar, { author: me ?? UNKNOWN_COMMENT_AUTHOR }) });
977
- const [text, setText] = useState(
978
- () => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT
979
- );
980
- const ref = useRef(null);
981
- const msg = useTranslation();
982
- const container = useContainer();
983
- usePassThroughWheelEvents(ref);
984
- usePassThroughMouseOverEvents(ref);
985
- const point = useValue("composer point", () => editor.pageToViewport(pending.point), [
986
- editor,
987
- pending.point
988
- ]);
989
- useEffect(() => {
990
- const onPointerDown = (e) => {
991
- const el = ref.current;
992
- const target = e.target;
993
- if (!el || !target) return;
994
- if (el.contains(target) || target.closest(".tlui-cmt-mention-popup")) return;
995
- pendingComment.set(editor, null);
996
- };
997
- document.addEventListener("pointerdown", onPointerDown, true);
998
- return () => document.removeEventListener("pointerdown", onPointerDown, true);
999
- }, [editor]);
1000
- const submit = () => {
1001
- if (isCommentEmpty(text) || !currentUserId) return;
1002
- commitCommentMutation(editor, () => {
1003
- const pageId = editor.getCurrentPageId();
1004
- const thread = createCommentThread({
1005
- pageId,
1006
- anchor: pending.anchor,
1007
- createdBy: currentUserId
1008
- });
1009
- const comment = createComment({
1010
- threadId: thread.id,
1011
- pageId,
1012
- authorId: currentUserId,
1013
- body: text
1014
- });
1015
- putRecordsInCommit(editor, [thread, comment]);
1016
- if (onPostComment) onPostComment(comment);
1017
- });
1018
- setText(EMPTY_COMMENT);
1019
- clearCommentDraft(NEW_COMMENT_DRAFT);
1020
- pendingComment.set(editor, null);
1021
- };
1022
- return createPortal(
1023
- /* @__PURE__ */ jsx(
1024
- "div",
1025
- {
1026
- ref,
1027
- className: [
1028
- "tlui-cmt-canvas-composer",
1029
- pending.anchor.type === "region" && "tlui-cmt-canvas-composer--region",
1030
- !canComment && "tlui-cmt-canvas-composer--fallback"
1031
- ].filter(Boolean).join(" "),
1032
- style: { left: point.x, top: point.y },
1033
- onPointerDown: stop,
1034
- onContextMenu: stop,
1035
- onKeyDown: (e) => {
1036
- if (e.key === "Escape" && !isMentionPickerOpen()) pendingComment.set(editor, null);
1037
- },
1038
- children: canComment ? /* @__PURE__ */ jsx(
1039
- CommentComposer,
1040
- {
1041
- author: me ?? UNKNOWN_COMMENT_AUTHOR,
1042
- placeholder: msg("comments.add-placeholder"),
1043
- sendLabel: msg("comments.send"),
1044
- value: text,
1045
- onChange: (value) => {
1046
- setText(value);
1047
- saveCommentDraft(NEW_COMMENT_DRAFT, value);
1048
- },
1049
- onSubmit: submit,
1050
- disabled: isCommentEmpty(text) || !currentUserId,
1051
- getMentionSuggestions,
1052
- renderMentionSuggestion,
1053
- autoFocus: true,
1054
- leading: draftAvatar
1055
- }
1056
- ) : ComposerFallback && /* @__PURE__ */ jsx(ComposerFallback, { context: "pending" })
1057
- }
1058
- ),
1059
- container
235
+ ] }) })
1060
236
  );
1061
237
  }
1062
238
  export {