@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,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
commentSchemaRecords,
|
|
4
|
+
createShapeId,
|
|
5
|
+
createTLSchema,
|
|
6
|
+
createTLStore,
|
|
7
|
+
defaultBindingUtils,
|
|
8
|
+
defaultShapeUtils,
|
|
9
|
+
defaultTools,
|
|
10
|
+
Editor,
|
|
11
|
+
TLShapeId,
|
|
12
|
+
} from 'tldraw'
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
14
|
+
import { commentsSidebarOpen } from './state'
|
|
15
|
+
import {
|
|
16
|
+
anchorPagePoint,
|
|
17
|
+
commentCenterScreenOffset,
|
|
18
|
+
commentTargetShapeAt,
|
|
19
|
+
impreciseShapePinInset,
|
|
20
|
+
IMPRECISE_PIN_INSET_PX,
|
|
21
|
+
isBoxInInflatedViewport,
|
|
22
|
+
isInInflatedViewport,
|
|
23
|
+
shapeAnchorAt,
|
|
24
|
+
} from './thread-state'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A real editor: the behavior under test is hit-testing against real shape geometry, which is
|
|
28
|
+
* exactly what a stub can't model.
|
|
29
|
+
*/
|
|
30
|
+
let editor: Editor
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
editor = new Editor({
|
|
34
|
+
store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
35
|
+
shapeUtils: defaultShapeUtils,
|
|
36
|
+
bindingUtils: defaultBindingUtils,
|
|
37
|
+
tools: defaultTools,
|
|
38
|
+
getContainer: () => document.body,
|
|
39
|
+
})
|
|
40
|
+
editor.updateViewportScreenBounds(new Box(0, 0, 1000, 1000))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('commentTargetShapeAt', () => {
|
|
44
|
+
it('hits an arrow pointed at near its line', () => {
|
|
45
|
+
const id = createShapeId()
|
|
46
|
+
editor.createShape({
|
|
47
|
+
id,
|
|
48
|
+
type: 'arrow',
|
|
49
|
+
x: 100,
|
|
50
|
+
y: 100,
|
|
51
|
+
props: { start: { x: 0, y: 0 }, end: { x: 200, y: 0 } },
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Dead on the line, and a couple of pixels off it — both are a hit. Without a hit-test
|
|
55
|
+
// margin only the pixel-perfect click would land, which made arrows uncommentable.
|
|
56
|
+
expect(commentTargetShapeAt(editor, { x: 200, y: 100 })?.id).toBe(id)
|
|
57
|
+
expect(commentTargetShapeAt(editor, { x: 200, y: 102 })?.id).toBe(id)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('hits a line shape near its segment', () => {
|
|
61
|
+
const id = createShapeId()
|
|
62
|
+
editor.createShape({ id, type: 'line', x: 100, y: 100 })
|
|
63
|
+
|
|
64
|
+
const bounds = editor.getShapePageBounds(id)!
|
|
65
|
+
expect(commentTargetShapeAt(editor, { x: bounds.center.x, y: bounds.center.y })?.id).toBe(id)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('hits inside a hollow shape', () => {
|
|
69
|
+
const id = createShapeId()
|
|
70
|
+
editor.createShape({
|
|
71
|
+
id,
|
|
72
|
+
type: 'geo',
|
|
73
|
+
x: 100,
|
|
74
|
+
y: 100,
|
|
75
|
+
props: { w: 100, h: 100, fill: 'none' },
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(commentTargetShapeAt(editor, { x: 150, y: 150 })?.id).toBe(id)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('returns undefined on empty canvas', () => {
|
|
82
|
+
editor.createShape({
|
|
83
|
+
id: createShapeId(),
|
|
84
|
+
type: 'arrow',
|
|
85
|
+
x: 100,
|
|
86
|
+
y: 100,
|
|
87
|
+
props: { start: { x: 0, y: 0 }, end: { x: 200, y: 0 } },
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(commentTargetShapeAt(editor, { x: 600, y: 600 })).toBeUndefined()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
/** A 100×100 square whose page bounds are (100,100)–(200,200), unrotated. */
|
|
95
|
+
function createSquare(): TLShapeId {
|
|
96
|
+
const id = createShapeId()
|
|
97
|
+
editor.createShape({ id, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } })
|
|
98
|
+
return id
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('shape anchors under a shape transform', () => {
|
|
102
|
+
it('carries a precise pin around when the shape rotates', () => {
|
|
103
|
+
const id = createSquare()
|
|
104
|
+
// Top-right corner of the square.
|
|
105
|
+
const anchor = shapeAnchorAt(editor, id, { x: 200, y: 100 }, true)
|
|
106
|
+
expect(anchor).toMatchObject({ type: 'shape', shapeId: id, x: 1, y: 0, isPrecise: true })
|
|
107
|
+
expect(anchorPagePoint(editor, anchor)).toMatchObject({ x: 200, y: 100 })
|
|
108
|
+
|
|
109
|
+
// A quarter turn about the square's centre takes that corner to the bottom-right. The
|
|
110
|
+
// square's page bounds don't change, so a pin normalized against them would sit still.
|
|
111
|
+
editor.rotateShapesBy([id], Math.PI / 2)
|
|
112
|
+
const rotated = anchorPagePoint(editor, anchor)!
|
|
113
|
+
expect(rotated.x).toBeCloseTo(200)
|
|
114
|
+
expect(rotated.y).toBeCloseTo(200)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('records the spot a pin was dropped on a rotated shape', () => {
|
|
118
|
+
const id = createSquare()
|
|
119
|
+
editor.rotateShapesBy([id], Math.PI / 2)
|
|
120
|
+
|
|
121
|
+
// After the quarter turn the square's local top-right corner sits at the page's bottom-right.
|
|
122
|
+
const anchor = shapeAnchorAt(editor, id, { x: 200, y: 200 }, true)
|
|
123
|
+
expect(anchor.type).toBe('shape')
|
|
124
|
+
if (anchor.type !== 'shape') return
|
|
125
|
+
expect(anchor.x).toBeCloseTo(1)
|
|
126
|
+
expect(anchor.y).toBeCloseTo(0)
|
|
127
|
+
|
|
128
|
+
const point = anchorPagePoint(editor, anchor)!
|
|
129
|
+
expect(point.x).toBeCloseTo(200)
|
|
130
|
+
expect(point.y).toBeCloseTo(200)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('still tracks moves and resizes', () => {
|
|
134
|
+
const id = createSquare()
|
|
135
|
+
const anchor = shapeAnchorAt(editor, id, { x: 200, y: 100 }, true)
|
|
136
|
+
|
|
137
|
+
editor.updateShape({ id, type: 'geo', x: 300, props: { w: 200 } })
|
|
138
|
+
expect(anchorPagePoint(editor, anchor)).toMatchObject({ x: 500, y: 100 })
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('turns an imprecise pin inset with the shape so it keeps stepping inward', () => {
|
|
142
|
+
const id = createSquare()
|
|
143
|
+
const anchor = shapeAnchorAt(editor, id, { x: 200, y: 100 }, false)
|
|
144
|
+
// The default top-right spot: step left and down, into the square.
|
|
145
|
+
expect(impreciseShapePinInset(editor, anchor)).toMatchObject({
|
|
146
|
+
x: -IMPRECISE_PIN_INSET_PX,
|
|
147
|
+
y: IMPRECISE_PIN_INSET_PX,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
editor.rotateShapesBy([id], Math.PI / 2)
|
|
151
|
+
// The corner is now bottom-right, so stepping inward means left and up.
|
|
152
|
+
const inset = impreciseShapePinInset(editor, anchor)!
|
|
153
|
+
expect(inset.x).toBeCloseTo(-IMPRECISE_PIN_INSET_PX)
|
|
154
|
+
expect(inset.y).toBeCloseTo(-IMPRECISE_PIN_INSET_PX)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('hides the pin for a shape that no longer exists', () => {
|
|
158
|
+
const id = createSquare()
|
|
159
|
+
const anchor = shapeAnchorAt(editor, id, { x: 200, y: 100 }, true)
|
|
160
|
+
editor.deleteShape(id)
|
|
161
|
+
expect(anchorPagePoint(editor, anchor)).toBe(null)
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('commentCenterScreenOffset', () => {
|
|
166
|
+
/** A sidebar element whose left edge sits at `left` client px, in the editor's container. */
|
|
167
|
+
function mountSidebar(left: number) {
|
|
168
|
+
const el = document.createElement('div')
|
|
169
|
+
el.className = 'tlui-cmt-canvas-sidebar'
|
|
170
|
+
el.getBoundingClientRect = () => ({ left }) as DOMRect
|
|
171
|
+
document.body.appendChild(el)
|
|
172
|
+
return el
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
document.querySelector('.tlui-cmt-canvas-sidebar')?.remove()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('is zero while the sidebar is closed', () => {
|
|
180
|
+
mountSidebar(712)
|
|
181
|
+
expect(commentCenterScreenOffset(editor)).toBe(0)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('is half the covered width, so the pin centers in the uncovered area', () => {
|
|
185
|
+
commentsSidebarOpen.set(editor, true)
|
|
186
|
+
// Viewport is 1000px wide; the sidebar covers the rightmost 288px. The centered pin (at
|
|
187
|
+
// 356px) leaves the thread UI clear of the sidebar, so no nudge is needed.
|
|
188
|
+
mountSidebar(712)
|
|
189
|
+
expect(commentCenterScreenOffset(editor)).toBe(144)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('nudges the pin further left when the thread UI would reach the sidebar', () => {
|
|
193
|
+
commentsSidebarOpen.set(editor, true)
|
|
194
|
+
// Centered in the uncovered area the pin sits at 300px, and the thread UI (334px wide,
|
|
195
|
+
// plus the 8px gap) would cross the sidebar's edge — so the pin backs off to 258px.
|
|
196
|
+
mountSidebar(600)
|
|
197
|
+
expect(commentCenterScreenOffset(editor)).toBe(500 - 258)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('never nudges the pin past the left edge', () => {
|
|
201
|
+
commentsSidebarOpen.set(editor, true)
|
|
202
|
+
// Clearing the sidebar would need a negative pin position; the left inset wins.
|
|
203
|
+
mountSidebar(300)
|
|
204
|
+
expect(commentCenterScreenOffset(editor)).toBe(500 - 8)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('is zero while the sidebar element is not mounted', () => {
|
|
208
|
+
commentsSidebarOpen.set(editor, true)
|
|
209
|
+
expect(commentCenterScreenOffset(editor)).toBe(0)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('isInInflatedViewport', () => {
|
|
214
|
+
// The viewport is 1000x1000 (see beforeEach) and the margin is 120 screen px.
|
|
215
|
+
it('accepts a point on screen', () => {
|
|
216
|
+
expect(isInInflatedViewport(editor, { x: 500, y: 500 })).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('accepts a point just off screen, so a pan finds it already mounted', () => {
|
|
220
|
+
expect(isInInflatedViewport(editor, { x: -119, y: 500 })).toBe(true)
|
|
221
|
+
expect(isInInflatedViewport(editor, { x: 1119, y: 500 })).toBe(true)
|
|
222
|
+
expect(isInInflatedViewport(editor, { x: 500, y: -119 })).toBe(true)
|
|
223
|
+
expect(isInInflatedViewport(editor, { x: 500, y: 1119 })).toBe(true)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('rejects a point past the margin on any side', () => {
|
|
227
|
+
expect(isInInflatedViewport(editor, { x: -121, y: 500 })).toBe(false)
|
|
228
|
+
expect(isInInflatedViewport(editor, { x: 1121, y: 500 })).toBe(false)
|
|
229
|
+
expect(isInInflatedViewport(editor, { x: 500, y: -121 })).toBe(false)
|
|
230
|
+
expect(isInInflatedViewport(editor, { x: 500, y: 1121 })).toBe(false)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('isBoxInInflatedViewport', () => {
|
|
235
|
+
it('accepts a box wholly on screen', () => {
|
|
236
|
+
expect(isBoxInInflatedViewport(editor, { x: 100, y: 100, w: 200, h: 200 })).toBe(true)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('accepts a box straddling the edge, whose pin corner alone would be culled', () => {
|
|
240
|
+
// This is the case the point test gets wrong: the box's visible edge is on screen while
|
|
241
|
+
// its far corner — where a region thread's pin can sit — is thousands of px away.
|
|
242
|
+
expect(isBoxInInflatedViewport(editor, { x: -5000, y: -5000, w: 5100, h: 5100 })).toBe(true)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('accepts a box larger than the viewport on every side', () => {
|
|
246
|
+
expect(isBoxInInflatedViewport(editor, { x: -5000, y: -5000, w: 10000, h: 10000 })).toBe(true)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('rejects a box wholly past the margin', () => {
|
|
250
|
+
expect(isBoxInInflatedViewport(editor, { x: 1200, y: 100, w: 100, h: 100 })).toBe(false)
|
|
251
|
+
expect(isBoxInInflatedViewport(editor, { x: 100, y: -400, w: 100, h: 100 })).toBe(false)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('follows the camera rather than page coordinates', () => {
|
|
255
|
+
const box = { x: 2000, y: 0, w: 100, h: 100 }
|
|
256
|
+
expect(isBoxInInflatedViewport(editor, box)).toBe(false)
|
|
257
|
+
editor.setCamera({ x: -2000, y: 0, z: 1 })
|
|
258
|
+
expect(isBoxInInflatedViewport(editor, box)).toBe(true)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
@@ -1,15 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
BoxModel,
|
|
3
|
+
Editor,
|
|
4
|
+
Mat,
|
|
5
|
+
TLCommentAnchor,
|
|
6
|
+
TLCommentThread,
|
|
7
|
+
TLShape,
|
|
8
|
+
TLShapeId,
|
|
9
|
+
Vec,
|
|
10
|
+
VecLike,
|
|
11
|
+
} from 'tldraw'
|
|
12
|
+
import { getCommentingOptions } from './options'
|
|
13
|
+
import { commentsSidebarOpen, openThreadId } from './state'
|
|
14
|
+
import { POPOVER_OFFSET } from './thread-view'
|
|
4
15
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
16
|
+
/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px —
|
|
17
|
+
* most of the marker sits within the shape, with a small overhang past the corner. */
|
|
18
|
+
export const IMPRECISE_PIN_INSET_PX = 20
|
|
7
19
|
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
|
|
20
|
+
/** Screen-pixel margin by which the viewport is inflated when culling canvas markers (thread pins
|
|
21
|
+
* and cluster badges), so a marker just off-screen is already mounted when a pan brings it in. */
|
|
22
|
+
const MARKER_CULL_MARGIN_PX = 120
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether a viewport-space point sits within the viewport inflated by
|
|
26
|
+
* {@link MARKER_CULL_MARGIN_PX}. The cull test for screen-fixed markers: off-screen markers
|
|
27
|
+
* return null from their position signal and unmount instead of tracking every camera frame.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export function isInInflatedViewport(editor: Editor, point: VecLike): boolean {
|
|
31
|
+
const viewport = editor.getViewportScreenBounds()
|
|
32
|
+
const margin = MARKER_CULL_MARGIN_PX
|
|
33
|
+
return (
|
|
34
|
+
point.x >= -margin &&
|
|
35
|
+
point.y >= -margin &&
|
|
36
|
+
point.x <= viewport.w + margin &&
|
|
37
|
+
point.y <= viewport.h + margin
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether any part of a page-space box overlaps the viewport inflated by
|
|
43
|
+
* {@link MARKER_CULL_MARGIN_PX}. The cull test for region-anchored threads, whose dashed box can
|
|
44
|
+
* be on screen while the pin corner itself is not.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function isBoxInInflatedViewport(editor: Editor, box: BoxModel): boolean {
|
|
48
|
+
const viewport = editor.getViewportScreenBounds()
|
|
49
|
+
const margin = MARKER_CULL_MARGIN_PX
|
|
50
|
+
// Zoom is positive, so the page box's corners keep their order through the transform.
|
|
51
|
+
const min = editor.pageToViewport({ x: box.x, y: box.y })
|
|
52
|
+
const max = editor.pageToViewport({ x: box.x + box.w, y: box.y + box.h })
|
|
53
|
+
return (
|
|
54
|
+
max.x >= -margin &&
|
|
55
|
+
max.y >= -margin &&
|
|
56
|
+
min.x <= viewport.w + margin &&
|
|
57
|
+
min.y <= viewport.h + margin
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker
|
|
62
|
+
* extends up-right of its anchor point, so step it toward the shape's centre. Screen px — the
|
|
63
|
+
* pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */
|
|
64
|
+
export function impreciseShapePinInset(
|
|
65
|
+
editor: Editor,
|
|
66
|
+
anchor: TLCommentThread['anchor']
|
|
67
|
+
): { x: number; y: number } | null {
|
|
68
|
+
if (anchor.type !== 'shape' || anchor.isPrecise) return null
|
|
69
|
+
const spot = getCommentingOptions(editor).impreciseShapeAnchor
|
|
70
|
+
const inset = {
|
|
71
|
+
x: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,
|
|
72
|
+
y: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,
|
|
73
|
+
}
|
|
74
|
+
// The spot is a corner of the shape's own bounds, so which way "toward the centre" points turns
|
|
75
|
+
// with the shape: without this a rotated shape's pin would step out of the shape, not into it.
|
|
76
|
+
const rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0
|
|
77
|
+
return rotation === 0 ? inset : Vec.Rot(inset, rotation)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The corner a region's pin and composer sit on, as a normalized 0–1 offset (bottom-right). Pin
|
|
81
|
+
* position, composer placement, region move, and which corner has no resize handle all derive
|
|
82
|
+
* from it. */
|
|
11
83
|
export const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }
|
|
12
84
|
|
|
85
|
+
/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else
|
|
86
|
+
* {@link REGION_PIN_CORNER}. @internal */
|
|
87
|
+
export function regionAnchorPinCorner(
|
|
88
|
+
anchor: Extract<TLCommentAnchor, { type: 'region' }>
|
|
89
|
+
): VecLike {
|
|
90
|
+
if (anchor.pinX !== undefined && anchor.pinY !== undefined) {
|
|
91
|
+
return { x: anchor.pinX, y: anchor.pinY }
|
|
92
|
+
}
|
|
93
|
+
return REGION_PIN_CORNER
|
|
94
|
+
}
|
|
95
|
+
|
|
13
96
|
/** The page point of a region's pin corner. */
|
|
14
97
|
export function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {
|
|
15
98
|
return {
|
|
@@ -19,42 +102,62 @@ export function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CO
|
|
|
19
102
|
}
|
|
20
103
|
|
|
21
104
|
/**
|
|
22
|
-
* Where a thread's pin sits on the page, for each anchor kind. Null hides the pin.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
105
|
+
* Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. Imprecise shape
|
|
106
|
+
* anchors use {@link CommentingOptions.impreciseShapeAnchor} rather than the stored `x`/`y`.
|
|
107
|
+
*
|
|
108
|
+
* A shape anchor's `x`/`y` are normalized within the shape's bounds and resolved through its page
|
|
109
|
+
* transform, so the pin rides rotation instead of being left behind in the bounding box.
|
|
25
110
|
* @public
|
|
26
111
|
*/
|
|
27
112
|
export function anchorPagePoint(
|
|
28
113
|
editor: Editor,
|
|
29
|
-
anchor: TLCommentAnchor
|
|
30
|
-
impreciseShapeAnchor: { x: number; y: number } = DEFAULT_IMPRECISE_SHAPE_ANCHOR
|
|
114
|
+
anchor: TLCommentAnchor
|
|
31
115
|
): { x: number; y: number } | null {
|
|
32
116
|
switch (anchor.type) {
|
|
33
117
|
case 'shape': {
|
|
34
|
-
const
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!bounds) return null
|
|
43
|
-
return { x: bounds.maxX, y: bounds.minY }
|
|
118
|
+
const shape = editor.getShape(anchor.shapeId as TLShapeId)
|
|
119
|
+
if (!shape) return null
|
|
120
|
+
const transform = editor.getShapePageTransform(shape)
|
|
121
|
+
if (!transform) return null
|
|
122
|
+
const { point, size } = editor.getShapeGeometry(shape).bounds
|
|
123
|
+
// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.
|
|
124
|
+
const spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor
|
|
125
|
+
return Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))
|
|
44
126
|
}
|
|
45
127
|
case 'point':
|
|
46
128
|
return { x: anchor.x, y: anchor.y }
|
|
47
129
|
case 'region':
|
|
48
|
-
return regionPinPoint(anchor,
|
|
130
|
+
return regionPinPoint(anchor, regionAnchorPinCorner(anchor))
|
|
49
131
|
case 'page':
|
|
50
132
|
return null
|
|
51
133
|
}
|
|
52
134
|
}
|
|
53
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The shape a comment placed at a page point should anchor to, or undefined for empty canvas.
|
|
138
|
+
*
|
|
139
|
+
* Uses the editor's hit-test margin, the same slack select and hover use — without it, open-path
|
|
140
|
+
* shapes (arrows, lines, draw strokes) are unhittable in practice.
|
|
141
|
+
*
|
|
142
|
+
* `hitFrameInside` lets a click inside a frame's body anchor to the frame, which it otherwise
|
|
143
|
+
* wouldn't (the select-tool convention hits only the edge/label). A child shape under the pointer
|
|
144
|
+
* still wins, so only a frame's empty interior anchors the frame.
|
|
145
|
+
*
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
export function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {
|
|
149
|
+
return editor.getShapeAtPoint(page, {
|
|
150
|
+
hitInside: true,
|
|
151
|
+
hitFrameInside: true,
|
|
152
|
+
margin: editor.getHitTestMargin(),
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
54
156
|
/**
|
|
55
157
|
* A shape anchor for a page point. `x`/`y` are the point's normalized (0–1) offset within the
|
|
56
|
-
* shape's
|
|
57
|
-
*
|
|
158
|
+
* shape's own bounds, taken in the shape's own space, so a pin on a rotated shape records the spot
|
|
159
|
+
* it was dropped on. Remembered either way: when `precise` the pin sits at exactly `x`/`y`,
|
|
160
|
+
* otherwise at the consumer's imprecise default.
|
|
58
161
|
* @public
|
|
59
162
|
*/
|
|
60
163
|
export function shapeAnchorAt(
|
|
@@ -63,29 +166,60 @@ export function shapeAnchorAt(
|
|
|
63
166
|
page: { x: number; y: number },
|
|
64
167
|
precise: boolean
|
|
65
168
|
): TLCommentAnchor {
|
|
66
|
-
const
|
|
67
|
-
|
|
169
|
+
const shape = editor.getShape(shapeId)
|
|
170
|
+
const bounds = shape && editor.getShapeGeometry(shape).bounds
|
|
171
|
+
if (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {
|
|
68
172
|
return { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }
|
|
69
173
|
}
|
|
174
|
+
const local = editor.getPointInShapeSpace(shape, page)
|
|
70
175
|
return {
|
|
71
176
|
type: 'shape',
|
|
72
177
|
shapeId,
|
|
73
|
-
x: (
|
|
74
|
-
y: (
|
|
178
|
+
x: (local.x - bounds.minX) / bounds.w,
|
|
179
|
+
y: (local.y - bounds.minY) / bounds.h,
|
|
75
180
|
isPrecise: precise,
|
|
76
181
|
}
|
|
77
182
|
}
|
|
78
183
|
|
|
79
184
|
/** Open a thread and bring it into view — switch to its page if needed, then center its pin. @public */
|
|
80
|
-
export function focusThread(
|
|
81
|
-
editor: Editor,
|
|
82
|
-
thread: TLCommentThread,
|
|
83
|
-
impreciseShapeAnchor?: { x: number; y: number }
|
|
84
|
-
): void {
|
|
185
|
+
export function focusThread(editor: Editor, thread: TLCommentThread): void {
|
|
85
186
|
if (thread.pageId !== editor.getCurrentPageId()) {
|
|
86
187
|
editor.setCurrentPage(thread.pageId as any)
|
|
87
188
|
}
|
|
88
189
|
openThreadId.set(editor, thread.id)
|
|
89
|
-
const point = anchorPagePoint(editor, thread.anchor
|
|
90
|
-
if (point)
|
|
190
|
+
const point = anchorPagePoint(editor, thread.anchor)
|
|
191
|
+
if (!point) return
|
|
192
|
+
const offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()
|
|
193
|
+
editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */
|
|
197
|
+
const CENTER_MARGIN_PX = 8
|
|
198
|
+
|
|
199
|
+
/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus
|
|
200
|
+
* the thread panel's fixed width (300px, `.tlui-cmt-thread`). */
|
|
201
|
+
const THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* How far right of a centered-on pin the true viewport center should sit, in screen px. While the
|
|
205
|
+
* comments sidebar covers the viewport's right edge, dead-centering a pin would put its thread
|
|
206
|
+
* popover under the sidebar — so centering aims at the middle of the uncovered area instead, never
|
|
207
|
+
* past the viewport's left edge. Zero when the sidebar is closed or not on screen.
|
|
208
|
+
* @internal
|
|
209
|
+
*/
|
|
210
|
+
export function commentCenterScreenOffset(editor: Editor): number {
|
|
211
|
+
if (!commentsSidebarOpen.get(editor)) return 0
|
|
212
|
+
const sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')
|
|
213
|
+
if (!sidebar) return 0
|
|
214
|
+
const viewport = editor.getViewportScreenBounds()
|
|
215
|
+
// Viewport screen bounds are the container's client rect, so this is container-local.
|
|
216
|
+
const sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x
|
|
217
|
+
if (sidebarLeft >= viewport.w) return 0
|
|
218
|
+
// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the
|
|
219
|
+
// sidebar — but never past the left edge (the edge wins when there's no room for both).
|
|
220
|
+
const pinX = Math.max(
|
|
221
|
+
Math.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),
|
|
222
|
+
CENTER_MARGIN_PX
|
|
223
|
+
)
|
|
224
|
+
return viewport.w / 2 - pinX
|
|
91
225
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CommentAuthor } from '@tldraw/mentions'
|
|
2
|
+
import type { TLComment } from 'tldraw'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { absoluteThreadLink, toCardProps } from './thread-view'
|
|
5
|
+
|
|
6
|
+
const AUTHOR: CommentAuthor = { name: 'Ada' }
|
|
7
|
+
|
|
8
|
+
function comment(overrides: Partial<TLComment> = {}): TLComment {
|
|
9
|
+
return {
|
|
10
|
+
id: 'comment:1',
|
|
11
|
+
typeName: 'comment',
|
|
12
|
+
threadId: 'comment-thread:1',
|
|
13
|
+
pageId: 'page:one',
|
|
14
|
+
authorId: 'user:1',
|
|
15
|
+
body: { type: 'doc', content: [] },
|
|
16
|
+
createdAt: Date.parse('2026-07-30T12:00:00.000Z'),
|
|
17
|
+
editedAt: null,
|
|
18
|
+
meta: {},
|
|
19
|
+
...overrides,
|
|
20
|
+
} as unknown as TLComment
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const context = {
|
|
24
|
+
currentUserId: 'user:1',
|
|
25
|
+
resolveAuthor: (id: string) => (id === 'user:1' ? AUTHOR : undefined),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolveName = (id: string) => context.resolveAuthor(id)?.name
|
|
29
|
+
|
|
30
|
+
describe('toCardProps', () => {
|
|
31
|
+
it('marks your own comments and resolves the author', () => {
|
|
32
|
+
const card = toCardProps(comment(), context, {}, resolveName)
|
|
33
|
+
expect(card.you).toBe(true)
|
|
34
|
+
expect(card.author).toBe(AUTHOR)
|
|
35
|
+
expect(card.date).toBe('2026-07-30T12:00:00.000Z')
|
|
36
|
+
expect(card.edited).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('falls back to the unknown author when the id does not resolve', () => {
|
|
40
|
+
const card = toCardProps(comment({ authorId: 'user:gone' }), context, {}, resolveName)
|
|
41
|
+
expect(card.you).toBe(false)
|
|
42
|
+
expect(card.author).not.toBe(AUTHOR)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('absoluteThreadLink', () => {
|
|
47
|
+
const BASE = 'https://example.com/f/abc123?page=2'
|
|
48
|
+
|
|
49
|
+
// A host's `getThreadHref` is an href — the sidebar rows use it as one — so it's allowed to be
|
|
50
|
+
// relative. Copying that verbatim would put "/f/abc123?comment=…" on someone's clipboard.
|
|
51
|
+
it('resolves a root-relative href against the current document', () => {
|
|
52
|
+
expect(absoluteThreadLink('/f/abc123?comment=comment-thread%3A1', BASE)).toBe(
|
|
53
|
+
'https://example.com/f/abc123?comment=comment-thread%3A1'
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('leaves an already absolute href alone', () => {
|
|
58
|
+
const href = 'https://other.example/f/xyz?comment=comment-thread%3A1'
|
|
59
|
+
expect(absoluteThreadLink(href, BASE)).toBe(href)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('resolves a path-relative href', () => {
|
|
63
|
+
expect(absoluteThreadLink('?comment=comment-thread%3A1', BASE)).toBe(
|
|
64
|
+
'https://example.com/f/abc123?comment=comment-thread%3A1'
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Better a link that's odd than a copy button that silently does nothing.
|
|
69
|
+
it('copies an unparseable href as-is', () => {
|
|
70
|
+
expect(absoluteThreadLink('::not a url::', '::also not::')).toBe('::not a url::')
|
|
71
|
+
})
|
|
72
|
+
})
|