@tldraw/commenting 5.3.0-internal.c5c7f1d817d0 → 5.3.0-next.2fa9c61a8de6
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 +1040 -230
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +2 -2
- package/dist-cjs/canvas/cluster-input.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 +123 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +163 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +5 -3
- 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 +64 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +20 -38
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +377 -437
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +34 -31
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +65 -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 +22 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/options.js +19 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pin-stacking.js +56 -0
- package/dist-cjs/canvas/pin-stacking.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-preview.js +185 -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 +67 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +397 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +4 -4
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +9 -9
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +763 -353
- package/dist-cjs/index.js +46 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +3 -1
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +12 -5
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +112 -17
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +20 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +4 -2
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +22 -10
- 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/format-time.js +4 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/reaction-picker.js +73 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +50 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -15
- 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 +20 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
- package/dist-cjs/ui/tooltip-button.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/cluster-input.mjs +2 -2
- package/dist-esm/canvas/cluster-input.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 +103 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +152 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +5 -3
- 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 +67 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +398 -442
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +37 -34
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -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 +28 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs +21 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pin-stacking.mjs +36 -0
- package/dist-esm/canvas/pin-stacking.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-preview.mjs +170 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +190 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +71 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +402 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +4 -4
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +9 -9
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +763 -353
- package/dist-esm/index.mjs +87 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +3 -1
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +12 -5
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +122 -16
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +20 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +4 -2
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +39 -25
- 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/format-time.mjs +4 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/reaction-picker.mjs +60 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +51 -5
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -16
- 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 +20 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/tooltip-button.mjs +12 -0
- package/dist-esm/ui/tooltip-button.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +205 -0
- package/src/canvas/canvas.css +439 -24
- package/src/canvas/cluster-input.test.ts +31 -23
- package/src/canvas/cluster-input.ts +3 -4
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +410 -0
- package/src/canvas/comment-mutations.ts +288 -0
- package/src/canvas/comment-reactions.test.ts +132 -0
- package/src/canvas/comment-reactions.tsx +245 -0
- package/src/canvas/comment-render.ts +9 -5
- package/src/canvas/comment-store.ts +67 -27
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +105 -23
- package/src/canvas/comments-filter-menu.tsx +23 -32
- package/src/canvas/comments-overflow-menu.tsx +12 -19
- package/src/canvas/comments-overlay.tsx +602 -562
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +75 -57
- package/src/canvas/comments-visibility-toggle.tsx +49 -0
- package/src/canvas/context.ts +47 -0
- package/src/canvas/hooks.ts +52 -11
- package/src/canvas/options.test.ts +52 -2
- package/src/canvas/options.ts +133 -9
- package/src/canvas/pin-stacking.test.ts +114 -0
- package/src/canvas/pin-stacking.ts +57 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +92 -35
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +304 -0
- package/src/canvas/thread-stack.tsx +252 -0
- package/src/canvas/thread-state.test.ts +209 -0
- package/src/canvas/thread-state.ts +137 -36
- package/src/canvas/thread-view.tsx +516 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +5 -5
- package/src/clustering/runtime.test.ts +34 -0
- package/src/clustering/runtime.ts +20 -15
- package/src/clustering/types.ts +9 -9
- package/src/index.ts +76 -40
- package/src/ui/byline.tsx +4 -2
- package/src/ui/comment-card.tsx +17 -7
- package/src/ui/comment-composer.tsx +160 -13
- package/src/ui/comment-pin.tsx +22 -2
- package/src/ui/comment-thread.tsx +6 -1
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +32 -12
- package/src/ui/comments.css +489 -204
- 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/format-time.ts +7 -2
- package/src/ui/reaction-picker.tsx +109 -0
- package/src/ui/reaction.tsx +116 -4
- package/src/ui/reactions.tsx +68 -13
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +18 -3
- package/src/ui/tooltip-button.tsx +20 -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 +0 -51
- 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 -31
- 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 -38
- 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
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
function SendButton({ label, disabled, onClick }) {
|
|
3
|
-
return /* @__PURE__ */ jsx(
|
|
3
|
+
return /* @__PURE__ */ jsx(
|
|
4
|
+
"button",
|
|
5
|
+
{
|
|
6
|
+
className: "tlui-cmt-send",
|
|
7
|
+
type: "button",
|
|
8
|
+
disabled,
|
|
9
|
+
onClick,
|
|
10
|
+
"aria-label": label,
|
|
11
|
+
title: label,
|
|
12
|
+
children: /* @__PURE__ */ jsx("svg", { width: "15", height: "15", viewBox: "0 0 15 15", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
13
|
+
"path",
|
|
14
|
+
{
|
|
15
|
+
d: "M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z",
|
|
16
|
+
fill: "currentColor",
|
|
17
|
+
fillRule: "evenodd",
|
|
18
|
+
clipRule: "evenodd"
|
|
19
|
+
}
|
|
20
|
+
) })
|
|
21
|
+
}
|
|
22
|
+
);
|
|
4
23
|
}
|
|
5
24
|
export {
|
|
6
25
|
SendButton
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/send-button.tsx"],
|
|
4
|
-
"sourcesContent": ["/** @public */\nexport interface SendButtonProps {\n\tlabel: string\n\tdisabled?: boolean\n\tonClick?(): void\n}\n\n/** The button that posts a comment. @public @react */\nexport function SendButton({ label, disabled, onClick }: SendButtonProps) {\n\treturn (\n\t\t<button
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/** @public */\nexport interface SendButtonProps {\n\t/** The button's accessible name (e.g. \"Send\"). Shown as an up-arrow icon, so this is its label. */\n\tlabel: string\n\tdisabled?: boolean\n\tonClick?(): void\n}\n\n/** The button that posts a comment \u2014 an up arrow. @public @react */\nexport function SendButton({ label, disabled, onClick }: SendButtonProps) {\n\treturn (\n\t\t<button\n\t\t\tclassName=\"tlui-cmt-send\"\n\t\t\ttype=\"button\"\n\t\t\tdisabled={disabled}\n\t\t\tonClick={onClick}\n\t\t\taria-label={label}\n\t\t\ttitle={label}\n\t\t>\n\t\t\t<svg width=\"15\" height=\"15\" viewBox=\"0 0 15 15\" fill=\"none\" aria-hidden=\"true\">\n\t\t\t\t<path\n\t\t\t\t\td=\"M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z\"\n\t\t\t\t\tfill=\"currentColor\"\n\t\t\t\t\tfillRule=\"evenodd\"\n\t\t\t\t\tclipRule=\"evenodd\"\n\t\t\t\t/>\n\t\t\t</svg>\n\t\t</button>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAoBI;AAXG,SAAS,WAAW,EAAE,OAAO,UAAU,QAAQ,GAAoB;AACzE,SACC;AAAA,IAAC;AAAA;AAAA,MACA,WAAU;AAAA,MACV,MAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,cAAY;AAAA,MACZ,OAAO;AAAA,MAEP,8BAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACvE;AAAA,QAAC;AAAA;AAAA,UACA,GAAE;AAAA,UACF,MAAK;AAAA,UACL,UAAS;AAAA,UACT,UAAS;AAAA;AAAA,MACV,GACD;AAAA;AAAA,EACD;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { TldrawUiTooltip } from "tldraw";
|
|
4
|
+
const TooltipButton = forwardRef(
|
|
5
|
+
function TooltipButton2({ tooltip, children, ...props }, ref) {
|
|
6
|
+
return /* @__PURE__ */ jsx(TldrawUiTooltip, { content: tooltip, children: /* @__PURE__ */ jsx("button", { ref, type: "button", "aria-label": tooltip, ...props, children }) });
|
|
7
|
+
}
|
|
8
|
+
);
|
|
9
|
+
export {
|
|
10
|
+
TooltipButton
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=tooltip-button.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/tooltip-button.tsx"],
|
|
4
|
+
"sourcesContent": ["import { type ButtonHTMLAttributes, forwardRef } from 'react'\nimport { TldrawUiTooltip } from 'tldraw'\n\ninterface TooltipButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n\ttooltip: string\n}\n\n/** An icon button labelled by a styled tooltip. Forwards ref and props to the underlying button\n * so `asChild` triggers (dropdown, tooltip) both compose onto the real element. */\nexport const TooltipButton = forwardRef<HTMLButtonElement, TooltipButtonProps>(\n\tfunction TooltipButton({ tooltip, children, ...props }, ref) {\n\t\treturn (\n\t\t\t<TldrawUiTooltip content={tooltip}>\n\t\t\t\t<button ref={ref} type=\"button\" aria-label={tooltip} {...props}>\n\t\t\t\t\t{children}\n\t\t\t\t</button>\n\t\t\t</TldrawUiTooltip>\n\t\t)\n\t}\n)\n"],
|
|
5
|
+
"mappings": "AAaI;AAbJ,SAAoC,kBAAkB;AACtD,SAAS,uBAAuB;AAQzB,MAAM,gBAAgB;AAAA,EAC5B,SAASA,eAAc,EAAE,SAAS,UAAU,GAAG,MAAM,GAAG,KAAK;AAC5D,WACC,oBAAC,mBAAgB,SAAS,SACzB,8BAAC,YAAO,KAAU,MAAK,UAAS,cAAY,SAAU,GAAG,OACvD,UACF,GACD;AAAA,EAEF;AACD;",
|
|
6
|
+
"names": ["TooltipButton"]
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/commenting",
|
|
3
3
|
"description": "tldraw commenting UI components.",
|
|
4
|
-
"version": "5.3.0-
|
|
4
|
+
"version": "5.3.0-next.2fa9c61a8de6",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test": "yarn run -T vitest --passWithNoTests",
|
|
29
29
|
"test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
|
|
30
30
|
"predev": "node ./scripts/copy-css-files.mjs",
|
|
31
|
-
"dev": "chokidar 'src/ui/comments.css' 'src/canvas/canvas.css' -c 'node ./scripts/copy-css-files.mjs'",
|
|
31
|
+
"dev": "chokidar '../mentions/mentions.css' 'src/ui/comments.css' 'src/canvas/canvas.css' -c 'node ./scripts/copy-css-files.mjs'",
|
|
32
32
|
"prebuild": "node ./scripts/copy-css-files.mjs",
|
|
33
33
|
"build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
|
|
34
34
|
"build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"@tldraw/driver": "5.3.0-next.2fa9c61a8de6",
|
|
41
42
|
"@types/react": "^19.2.7",
|
|
42
43
|
"@types/react-dom": "^19.2.3",
|
|
43
44
|
"react": "^19.2.1",
|
|
@@ -52,12 +53,11 @@
|
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"@tiptap/core": "^3.12.1",
|
|
55
|
-
"@tiptap/extension-mention": "3.13.0",
|
|
56
56
|
"@tiptap/pm": "^3.12.1",
|
|
57
57
|
"@tiptap/react": "^3.12.1",
|
|
58
|
-
"@
|
|
59
|
-
"@tldraw/utils": "5.3.0-
|
|
60
|
-
"tldraw": "5.3.0-
|
|
58
|
+
"@tldraw/mentions": "5.3.0-next.2fa9c61a8de6",
|
|
59
|
+
"@tldraw/utils": "5.3.0-next.2fa9c61a8de6",
|
|
60
|
+
"tldraw": "5.3.0-next.2fa9c61a8de6"
|
|
61
61
|
},
|
|
62
62
|
"module": "dist-esm/index.mjs",
|
|
63
63
|
"source": "src/index.ts",
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
commentSchemaRecords,
|
|
3
|
+
createCommentThread,
|
|
4
|
+
createShapeId,
|
|
5
|
+
createTLSchema,
|
|
6
|
+
createTLStore,
|
|
7
|
+
defaultBindingUtils,
|
|
8
|
+
defaultShapeUtils,
|
|
9
|
+
defaultTools,
|
|
10
|
+
Editor,
|
|
11
|
+
toRichText,
|
|
12
|
+
createComment as tlCreateComment,
|
|
13
|
+
TLPageId,
|
|
14
|
+
TLShapeId,
|
|
15
|
+
} from 'tldraw'
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
17
|
+
import { registerCommentAnchorLifecycle } from './anchor-lifecycle'
|
|
18
|
+
import { commitCommentMutation, putCommentRecords } from './comment-mutations'
|
|
19
|
+
import { getCommentRecord, getComments } from './comment-store'
|
|
20
|
+
import { CommentTool } from './comment-tool'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* These tests need a real editor: the behavior under test is the interplay between store
|
|
24
|
+
* side effects (the beforeDelete snapshot, operation-complete settlement) and real editor
|
|
25
|
+
* operations like `deleteShapes` and `moveShapesToPage` — exactly what a stub can't model.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
let editor: Editor
|
|
29
|
+
let dispose: () => void
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
editor = new Editor({
|
|
33
|
+
store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
34
|
+
shapeUtils: defaultShapeUtils,
|
|
35
|
+
bindingUtils: defaultBindingUtils,
|
|
36
|
+
tools: defaultTools,
|
|
37
|
+
getContainer: () => document.body,
|
|
38
|
+
})
|
|
39
|
+
dispose = registerCommentAnchorLifecycle(editor)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
dispose()
|
|
44
|
+
editor.dispose()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
function makeShape(x = 100, y = 100, w = 100, h = 50): TLShapeId {
|
|
48
|
+
const id = createShapeId()
|
|
49
|
+
editor.createShape({ id, type: 'geo', x, y, props: { w, h } })
|
|
50
|
+
return id
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeThread(shapeId: TLShapeId, anchorX = 0.5, anchorY = 0.5, isPrecise = true) {
|
|
54
|
+
const thread = createCommentThread({
|
|
55
|
+
pageId: editor.getCurrentPageId(),
|
|
56
|
+
anchor: { type: 'shape', shapeId, x: anchorX, y: anchorY, isPrecise },
|
|
57
|
+
createdBy: 'me',
|
|
58
|
+
})
|
|
59
|
+
const comment = tlCreateComment({
|
|
60
|
+
threadId: thread.id,
|
|
61
|
+
pageId: thread.pageId,
|
|
62
|
+
authorId: 'me',
|
|
63
|
+
body: toRichText('hello'),
|
|
64
|
+
})
|
|
65
|
+
putCommentRecords(editor, [thread, comment])
|
|
66
|
+
return { thread, comment }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeSecondPage(): TLPageId {
|
|
70
|
+
editor.createPage({ name: 'page 2' })
|
|
71
|
+
return editor.getPages()[1].id
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('shape deletion', () => {
|
|
75
|
+
it('converts a precise shape anchor to a point anchor where the pin sat', () => {
|
|
76
|
+
const shapeId = makeShape(100, 100, 100, 50)
|
|
77
|
+
const { thread } = makeThread(shapeId, 0.5, 0.5, true)
|
|
78
|
+
|
|
79
|
+
editor.deleteShape(shapeId)
|
|
80
|
+
|
|
81
|
+
const updated = getCommentRecord(editor, thread.id)!
|
|
82
|
+
expect(updated.typeName).toBe('comment-thread')
|
|
83
|
+
expect((updated as typeof thread).anchor).toEqual({ type: 'point', x: 150, y: 125 })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('converts an imprecise shape anchor where the inset pin is drawn, not at the raw corner', () => {
|
|
87
|
+
const shapeId = makeShape(100, 100, 100, 50)
|
|
88
|
+
const { thread } = makeThread(shapeId, 0.2, 0.9, false)
|
|
89
|
+
|
|
90
|
+
editor.deleteShape(shapeId)
|
|
91
|
+
|
|
92
|
+
// Top-right corner (200, 100) plus the 20px screen inset toward the shape's centre.
|
|
93
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
94
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 180, y: 120 })
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('resolves the imprecise spot from the commenting options when no override is passed', () => {
|
|
98
|
+
const configured = new Editor({
|
|
99
|
+
store: createTLStore({ schema: createTLSchema({ records: commentSchemaRecords }) }),
|
|
100
|
+
shapeUtils: defaultShapeUtils,
|
|
101
|
+
bindingUtils: defaultBindingUtils,
|
|
102
|
+
tools: [...defaultTools, CommentTool.configure({ impreciseShapeAnchor: { x: 0, y: 1 } })],
|
|
103
|
+
getContainer: () => document.body,
|
|
104
|
+
})
|
|
105
|
+
const disposeConfigured = registerCommentAnchorLifecycle(configured)
|
|
106
|
+
try {
|
|
107
|
+
const shapeId = createShapeId()
|
|
108
|
+
configured.createShape({ id: shapeId, type: 'geo', x: 100, y: 100, props: { w: 100, h: 50 } })
|
|
109
|
+
const thread = createCommentThread({
|
|
110
|
+
pageId: configured.getCurrentPageId(),
|
|
111
|
+
anchor: { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: false },
|
|
112
|
+
createdBy: 'me',
|
|
113
|
+
})
|
|
114
|
+
putCommentRecords(configured, [thread])
|
|
115
|
+
|
|
116
|
+
configured.deleteShape(shapeId)
|
|
117
|
+
|
|
118
|
+
// Bottom-left corner (100, 150) plus the inset toward the shape's centre.
|
|
119
|
+
const updated = getCommentRecord(configured, thread.id) as typeof thread
|
|
120
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 120, y: 130 })
|
|
121
|
+
} finally {
|
|
122
|
+
disposeConfigured()
|
|
123
|
+
configured.dispose()
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('converts anchors on children when their frame is deleted with them', () => {
|
|
128
|
+
const frameId = createShapeId()
|
|
129
|
+
editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
|
|
130
|
+
const childId = createShapeId()
|
|
131
|
+
editor.createShape({
|
|
132
|
+
id: childId,
|
|
133
|
+
type: 'geo',
|
|
134
|
+
parentId: frameId,
|
|
135
|
+
x: 50,
|
|
136
|
+
y: 50,
|
|
137
|
+
props: { w: 100, h: 100 },
|
|
138
|
+
})
|
|
139
|
+
const { thread } = makeThread(childId, 0.5, 0.5, true)
|
|
140
|
+
|
|
141
|
+
editor.deleteShape(frameId)
|
|
142
|
+
|
|
143
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
144
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 100, y: 100 })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('re-attaches the thread when the delete is undone', () => {
|
|
148
|
+
const shapeId = makeShape(100, 100, 100, 50)
|
|
149
|
+
const { thread } = makeThread(shapeId, 0.5, 0.5, true)
|
|
150
|
+
editor.markHistoryStoppingPoint()
|
|
151
|
+
|
|
152
|
+
editor.deleteShape(shapeId)
|
|
153
|
+
expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
|
|
154
|
+
|
|
155
|
+
editor.undo()
|
|
156
|
+
|
|
157
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
158
|
+
expect(updated.anchor).toEqual(thread.anchor)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('keeps the restore when a returning shape cannot resolve its page yet', () => {
|
|
162
|
+
const frameId = createShapeId()
|
|
163
|
+
editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
|
|
164
|
+
const childId = createShapeId()
|
|
165
|
+
editor.createShape({
|
|
166
|
+
id: childId,
|
|
167
|
+
type: 'geo',
|
|
168
|
+
parentId: frameId,
|
|
169
|
+
x: 50,
|
|
170
|
+
y: 50,
|
|
171
|
+
props: { w: 100, h: 100 },
|
|
172
|
+
})
|
|
173
|
+
const { thread } = makeThread(childId, 0.5, 0.5, true)
|
|
174
|
+
const childRecord = editor.getShape(childId)!
|
|
175
|
+
const frameRecord = editor.getShape(frameId)!
|
|
176
|
+
|
|
177
|
+
editor.deleteShape(frameId)
|
|
178
|
+
expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
|
|
179
|
+
|
|
180
|
+
// The child returns in an earlier operation than its parent, so its ancestor page can't
|
|
181
|
+
// resolve when it first reappears.
|
|
182
|
+
editor.store.put([childRecord])
|
|
183
|
+
editor.store.put([frameRecord])
|
|
184
|
+
|
|
185
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
186
|
+
expect(updated.anchor).toEqual(thread.anchor)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('re-attaches on undo even when the lifecycle was re-registered in between', () => {
|
|
190
|
+
const shapeId = makeShape(100, 100, 100, 50)
|
|
191
|
+
const { thread } = makeThread(shapeId, 0.5, 0.5, true)
|
|
192
|
+
editor.markHistoryStoppingPoint()
|
|
193
|
+
|
|
194
|
+
editor.deleteShape(shapeId)
|
|
195
|
+
expect((getCommentRecord(editor, thread.id) as typeof thread).anchor.type).toBe('point')
|
|
196
|
+
|
|
197
|
+
// The registering effect re-runs whenever its inputs change identity; conversion memory
|
|
198
|
+
// must not live and die with any one registration.
|
|
199
|
+
dispose()
|
|
200
|
+
dispose = registerCommentAnchorLifecycle(editor)
|
|
201
|
+
|
|
202
|
+
editor.undo()
|
|
203
|
+
|
|
204
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
205
|
+
expect(updated.anchor).toEqual(thread.anchor)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('does not re-attach on undo when the pin was moved after the conversion', () => {
|
|
209
|
+
const shapeId = makeShape(100, 100, 100, 50)
|
|
210
|
+
const { thread } = makeThread(shapeId, 0.5, 0.5, true)
|
|
211
|
+
editor.markHistoryStoppingPoint()
|
|
212
|
+
|
|
213
|
+
editor.deleteShape(shapeId)
|
|
214
|
+
const converted = getCommentRecord(editor, thread.id) as typeof thread
|
|
215
|
+
commitCommentMutation(editor, () =>
|
|
216
|
+
putCommentRecords(editor, [{ ...converted, anchor: { type: 'point', x: 900, y: 900 } }])
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
editor.undo()
|
|
220
|
+
|
|
221
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
222
|
+
expect(updated.anchor).toEqual({ type: 'point', x: 900, y: 900 })
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('leaves threads on other shapes untouched', () => {
|
|
226
|
+
const shapeId = makeShape()
|
|
227
|
+
const otherId = makeShape(300, 300)
|
|
228
|
+
const { thread } = makeThread(otherId)
|
|
229
|
+
|
|
230
|
+
editor.deleteShape(shapeId)
|
|
231
|
+
|
|
232
|
+
const updated = getCommentRecord(editor, thread.id) as typeof thread
|
|
233
|
+
expect(updated.anchor.type).toBe('shape')
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('moving a shape to another page', () => {
|
|
238
|
+
it('re-homes the thread and its comments to the new page, keeping the shape anchor', () => {
|
|
239
|
+
const shapeId = makeShape()
|
|
240
|
+
const { thread, comment } = makeThread(shapeId)
|
|
241
|
+
const page2 = makeSecondPage()
|
|
242
|
+
|
|
243
|
+
editor.moveShapesToPage([shapeId], page2)
|
|
244
|
+
|
|
245
|
+
const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
|
|
246
|
+
expect(updatedThread.pageId).toBe(page2)
|
|
247
|
+
expect(updatedThread.anchor).toEqual(thread.anchor)
|
|
248
|
+
const updatedComment = getComments(editor).find((c) => c.id === comment.id)!
|
|
249
|
+
expect(updatedComment.pageId).toBe(page2)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('re-homes threads anchored to children of a moved frame', () => {
|
|
253
|
+
const frameId = createShapeId()
|
|
254
|
+
editor.createShape({ id: frameId, type: 'frame', x: 0, y: 0, props: { w: 400, h: 400 } })
|
|
255
|
+
const childId = createShapeId()
|
|
256
|
+
editor.createShape({
|
|
257
|
+
id: childId,
|
|
258
|
+
type: 'geo',
|
|
259
|
+
parentId: frameId,
|
|
260
|
+
x: 50,
|
|
261
|
+
y: 50,
|
|
262
|
+
props: { w: 100, h: 100 },
|
|
263
|
+
})
|
|
264
|
+
const { thread } = makeThread(childId)
|
|
265
|
+
const page2 = makeSecondPage()
|
|
266
|
+
|
|
267
|
+
editor.moveShapesToPage([frameId], page2)
|
|
268
|
+
|
|
269
|
+
const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
|
|
270
|
+
expect(updatedThread.pageId).toBe(page2)
|
|
271
|
+
expect(updatedThread.anchor.type).toBe('shape')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('re-homes the thread back when the move is undone', () => {
|
|
275
|
+
const shapeId = makeShape()
|
|
276
|
+
const { thread } = makeThread(shapeId)
|
|
277
|
+
const page1 = editor.getCurrentPageId()
|
|
278
|
+
const page2 = makeSecondPage()
|
|
279
|
+
editor.markHistoryStoppingPoint()
|
|
280
|
+
|
|
281
|
+
editor.moveShapesToPage([shapeId], page2)
|
|
282
|
+
expect((getCommentRecord(editor, thread.id) as typeof thread).pageId).toBe(page2)
|
|
283
|
+
|
|
284
|
+
editor.undo()
|
|
285
|
+
|
|
286
|
+
const updatedThread = getCommentRecord(editor, thread.id) as typeof thread
|
|
287
|
+
expect(updatedThread.pageId).toBe(page1)
|
|
288
|
+
expect(updatedThread.anchor.type).toBe('shape')
|
|
289
|
+
})
|
|
290
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { WeakCache } from '@tldraw/utils'
|
|
2
|
+
import { Editor, TLCommentAnchor, TLCommentThread, TLPageId, TLShapeId, VecLike } from 'tldraw'
|
|
3
|
+
import { commitCommentMutation, putRecordsInCommit } from './comment-mutations'
|
|
4
|
+
import { getCommentRecord, getComments, getCommentThreads, TLCommentRecord } from './comment-store'
|
|
5
|
+
import { anchorPagePoint, impreciseShapePinInset } from './thread-state'
|
|
6
|
+
|
|
7
|
+
type ShapeAnchor = Extract<TLCommentAnchor, { type: 'shape' }>
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Threads converted to point anchors because their shape was deleted, kept so the anchor can be
|
|
11
|
+
* restored if the shape comes back (page move re-create, undo of the delete). Owned by the
|
|
12
|
+
* editor, not the registration closure: the registering effect can re-run (a prop identity
|
|
13
|
+
* change, a remount), and an undo can arrive arbitrarily long after the delete — the memory has
|
|
14
|
+
* to outlive any one registration.
|
|
15
|
+
*/
|
|
16
|
+
const convertedByShapeCache = new WeakCache<
|
|
17
|
+
Editor,
|
|
18
|
+
Map<TLShapeId, { threadId: string; anchor: ShapeAnchor; point: VecLike }[]>
|
|
19
|
+
>()
|
|
20
|
+
|
|
21
|
+
function isAnchoredToShape(
|
|
22
|
+
thread: TLCommentThread,
|
|
23
|
+
shapeId: TLShapeId
|
|
24
|
+
): thread is TLCommentThread & { anchor: ShapeAnchor } {
|
|
25
|
+
const anchor = thread.anchor
|
|
26
|
+
return anchor.type === 'shape' && anchor.shapeId === shapeId
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keep shape-anchored threads alive across their shape's lifecycle:
|
|
31
|
+
*
|
|
32
|
+
* - When the shape is deleted, the thread converts to a `point` anchor at the spot its pin last
|
|
33
|
+
* occupied, so the conversation outlives the shape instead of becoming invisible (a missing
|
|
34
|
+
* shape has no bounds, which hides the pin).
|
|
35
|
+
* - When the shape moves to another page, the thread follows: its `pageId` and each comment's
|
|
36
|
+
* denormalized `pageId` update to the shape's new page, and the anchor keeps riding the shape.
|
|
37
|
+
* - When a deleted shape comes back (a page move re-creates it with its id preserved; undoing a
|
|
38
|
+
* delete restores it), the thread re-attaches — unless its pin was manually moved in the
|
|
39
|
+
* meantime, in which case the manual placement wins.
|
|
40
|
+
*
|
|
41
|
+
* A page move is `deleteShapes` + re-create with preserved ids inside one `editor.run`, but each
|
|
42
|
+
* store write is its own operation — so at no single moment does "this shape is being moved"
|
|
43
|
+
* exist as an observable event. The handlers therefore cooperate across operations:
|
|
44
|
+
* `beforeDelete` snapshots where each affected pin sits (every record is still in the store at
|
|
45
|
+
* that point, so page bounds resolve even for children of a deleted frame); the
|
|
46
|
+
* operation-complete pass converts threads whose shape is really gone, remembering the original
|
|
47
|
+
* anchor; `afterCreate` notes a reappeared shape id, and the operation-complete pass restores
|
|
48
|
+
* the anchor once the store has settled (creation order within an operation is arbitrary, so a
|
|
49
|
+
* fresh shape may not resolve an ancestor page mid-operation). Undo/redo of a move
|
|
50
|
+
* replays as a `parentId` update (remove + add of one id squash to an update in the history
|
|
51
|
+
* diff), so an `afterChange` handler re-homes threads on cross-page reparents — including
|
|
52
|
+
* threads anchored to descendants, which move with their parent without change events of their
|
|
53
|
+
* own.
|
|
54
|
+
*
|
|
55
|
+
* Remote changes are ignored: the client that performed the operation runs this same
|
|
56
|
+
* maintenance and syncs the resulting thread updates. Writes honour the
|
|
57
|
+
* {@link CommentingOptions.history} option, so the consumer decides whether they're undoable.
|
|
58
|
+
*
|
|
59
|
+
* Registered by `CanvasComments` on mount; parts-built consumers can call this directly.
|
|
60
|
+
* Returns a cleanup function that unregisters all handlers.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export function registerCommentAnchorLifecycle(editor: Editor): () => void {
|
|
65
|
+
// Pin positions snapshotted during the current operation: shape id -> (thread id -> pin page
|
|
66
|
+
// point).
|
|
67
|
+
const pendingByShape = new Map<TLShapeId, Map<string, VecLike | null>>()
|
|
68
|
+
const convertedByShape = convertedByShapeCache.get(editor, () => new Map())
|
|
69
|
+
|
|
70
|
+
function rehomeThread(thread: TLCommentThread, pageId: TLPageId, updates: TLCommentRecord[]) {
|
|
71
|
+
updates.push({ ...thread, pageId })
|
|
72
|
+
for (const comment of getComments(editor)) {
|
|
73
|
+
if (comment.threadId === thread.id) updates.push({ ...comment, pageId })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const disposeBeforeDelete = editor.sideEffects.registerBeforeDeleteHandler(
|
|
78
|
+
'shape',
|
|
79
|
+
(shape, source) => {
|
|
80
|
+
if (source === 'remote') return
|
|
81
|
+
for (const thread of getCommentThreads(editor)) {
|
|
82
|
+
if (!isAnchoredToShape(thread, shape.id)) continue
|
|
83
|
+
let snapshot = pendingByShape.get(shape.id)
|
|
84
|
+
if (!snapshot) {
|
|
85
|
+
snapshot = new Map()
|
|
86
|
+
pendingByShape.set(shape.id, snapshot)
|
|
87
|
+
}
|
|
88
|
+
// Snapshot where the pin is drawn, not just the anchor point: imprecise shape pins
|
|
89
|
+
// render inset toward the shape's centre, and the converted point pin renders at
|
|
90
|
+
// its point exactly — without the inset the pin would jump on deletion. The inset
|
|
91
|
+
// is screen-fixed, so bake it in at the current zoom.
|
|
92
|
+
let point = anchorPagePoint(editor, thread.anchor)
|
|
93
|
+
const inset = impreciseShapePinInset(editor, thread.anchor)
|
|
94
|
+
if (point && inset) {
|
|
95
|
+
const zoom = editor.getZoomLevel()
|
|
96
|
+
point = { x: point.x + inset.x / zoom, y: point.y + inset.y / zoom }
|
|
97
|
+
}
|
|
98
|
+
snapshot.set(thread.id, point)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Shape ids from `convertedByShape` re-created during the current operation. Restores are
|
|
104
|
+
// settled at operation complete rather than in `afterCreate` itself: creation order within an
|
|
105
|
+
// operation is arbitrary, so a shape can appear before its parent and not resolve an ancestor
|
|
106
|
+
// page yet — and the conversion memory must outlive any such partial state.
|
|
107
|
+
const returnedShapeIds = new Set<TLShapeId>()
|
|
108
|
+
|
|
109
|
+
const disposeOperationComplete = editor.sideEffects.registerOperationCompleteHandler((source) => {
|
|
110
|
+
// Only user-sourced handlers populate the maps and only local operations should settle
|
|
111
|
+
// them; a remote operation completing leaves any (impossible-in-practice) leftovers for
|
|
112
|
+
// the next local settle rather than judging them against remote state.
|
|
113
|
+
if (source === 'remote') return
|
|
114
|
+
if (pendingByShape.size === 0 && returnedShapeIds.size === 0) return
|
|
115
|
+
const settled = [...pendingByShape.entries()]
|
|
116
|
+
pendingByShape.clear()
|
|
117
|
+
const returned = [...returnedShapeIds]
|
|
118
|
+
returnedShapeIds.clear()
|
|
119
|
+
|
|
120
|
+
const updates: TLCommentRecord[] = []
|
|
121
|
+
for (const [shapeId, threadPoints] of settled) {
|
|
122
|
+
if (editor.getShape(shapeId)) continue
|
|
123
|
+
for (const [threadId, point] of threadPoints) {
|
|
124
|
+
if (!point) continue
|
|
125
|
+
const thread = getCommentRecord(editor, threadId)
|
|
126
|
+
if (!thread || thread.typeName !== 'comment-thread') continue
|
|
127
|
+
if (!isAnchoredToShape(thread, shapeId)) continue
|
|
128
|
+
let converted = convertedByShape.get(shapeId)
|
|
129
|
+
if (!converted) {
|
|
130
|
+
converted = []
|
|
131
|
+
convertedByShape.set(shapeId, converted)
|
|
132
|
+
}
|
|
133
|
+
converted.push({ threadId, anchor: thread.anchor, point })
|
|
134
|
+
updates.push({ ...thread, anchor: { type: 'point', x: point.x, y: point.y } })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const shapeId of returned) {
|
|
139
|
+
const shape = editor.getShape(shapeId)
|
|
140
|
+
if (!shape) continue
|
|
141
|
+
const converted = convertedByShape.get(shapeId)
|
|
142
|
+
if (!converted) continue
|
|
143
|
+
convertedByShape.delete(shapeId)
|
|
144
|
+
|
|
145
|
+
const pageId = editor.getAncestorPageId(shape)
|
|
146
|
+
for (const { threadId, anchor, point } of converted) {
|
|
147
|
+
const thread = getCommentRecord(editor, threadId)
|
|
148
|
+
if (!thread || thread.typeName !== 'comment-thread') continue
|
|
149
|
+
// Re-attach only threads still sitting exactly where the conversion left them — a
|
|
150
|
+
// pin moved since then was placed deliberately, and that placement wins.
|
|
151
|
+
const current = thread.anchor
|
|
152
|
+
if (current.type !== 'point' || current.x !== point.x || current.y !== point.y) {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (!pageId || thread.pageId === pageId) {
|
|
156
|
+
updates.push({ ...thread, anchor })
|
|
157
|
+
} else {
|
|
158
|
+
rehomeThread({ ...thread, anchor }, pageId, updates)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (updates.length > 0) {
|
|
164
|
+
commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const disposeAfterCreate = editor.sideEffects.registerAfterCreateHandler(
|
|
169
|
+
'shape',
|
|
170
|
+
(shape, source) => {
|
|
171
|
+
if (source === 'remote') return
|
|
172
|
+
if (convertedByShape.has(shape.id)) returnedShapeIds.add(shape.id)
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const disposeAfterChange = editor.sideEffects.registerAfterChangeHandler(
|
|
177
|
+
'shape',
|
|
178
|
+
(prev, next, source) => {
|
|
179
|
+
if (source === 'remote') return
|
|
180
|
+
if (prev.parentId === next.parentId) return
|
|
181
|
+
const pageId = editor.getAncestorPageId(next)
|
|
182
|
+
if (!pageId) return
|
|
183
|
+
// Descendants move with their parent without change events of their own.
|
|
184
|
+
const movedIds = editor.getShapeAndDescendantIds([next.id])
|
|
185
|
+
const updates: TLCommentRecord[] = []
|
|
186
|
+
for (const thread of getCommentThreads(editor)) {
|
|
187
|
+
const anchor = thread.anchor
|
|
188
|
+
if (anchor.type !== 'shape') continue
|
|
189
|
+
if (!movedIds.has(anchor.shapeId)) continue
|
|
190
|
+
if (thread.pageId === pageId) continue
|
|
191
|
+
rehomeThread(thread, pageId, updates)
|
|
192
|
+
}
|
|
193
|
+
if (updates.length > 0) {
|
|
194
|
+
commitCommentMutation(editor, () => putRecordsInCommit(editor, updates))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
disposeBeforeDelete()
|
|
201
|
+
disposeOperationComplete()
|
|
202
|
+
disposeAfterCreate()
|
|
203
|
+
disposeAfterChange()
|
|
204
|
+
}
|
|
205
|
+
}
|