@ecsplico/editor 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -3
- package/dist/adapters/anchors.d.ts +80 -0
- package/dist/adapters/anchors.js +270 -0
- package/dist/adapters/anchors.js.map +1 -0
- package/dist/adapters/comments/client.d.ts +64 -0
- package/dist/adapters/comments/client.js +536 -0
- package/dist/adapters/comments/client.js.map +1 -0
- package/dist/adapters/comments/drizzle.d.ts +430 -0
- package/dist/adapters/comments/drizzle.js +236 -0
- package/dist/adapters/comments/drizzle.js.map +1 -0
- package/dist/adapters/comments/errors.d.ts +9 -0
- package/dist/adapters/comments/errors.js +19 -0
- package/dist/adapters/comments/errors.js.map +1 -0
- package/dist/adapters/comments/json.d.ts +19 -0
- package/dist/adapters/comments/json.js +94 -0
- package/dist/adapters/comments/json.js.map +1 -0
- package/dist/adapters/comments/record.js +78 -0
- package/dist/adapters/comments/record.js.map +1 -0
- package/dist/adapters/comments/server.d.ts +23 -0
- package/dist/adapters/comments/server.js +102 -0
- package/dist/adapters/comments/server.js.map +1 -0
- package/dist/adapters/comments/types.d.ts +109 -0
- package/dist/adapters/documents.js +108 -0
- package/dist/adapters/documents.js.map +1 -0
- package/dist/adapters/drizzle.d.ts +293 -0
- package/dist/adapters/drizzle.js +157 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/files.js +30 -0
- package/dist/adapters/files.js.map +1 -0
- package/dist/adapters/forge.d.ts +58 -0
- package/dist/adapters/forge.js +91 -0
- package/dist/adapters/forge.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +0 -0
- package/dist/adapters/json.d.ts +24 -0
- package/dist/adapters/json.js +128 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/nitro.d.ts +61 -0
- package/dist/adapters/nitro.js +212 -0
- package/dist/adapters/nitro.js.map +1 -0
- package/dist/adapters/routes.js +16 -0
- package/dist/adapters/routes.js.map +1 -0
- package/dist/adapters/shared.d.ts +37 -0
- package/dist/adapters/shared.js +32 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/vue.d.ts +67 -0
- package/dist/adapters/vue.js +187 -0
- package/dist/adapters/vue.js.map +1 -0
- package/dist/code/CodeSurface.vue.d.ts +3 -3
- package/dist/core/doc-anchors.d.ts +5 -0
- package/dist/core/doc-anchors.js +4 -3
- package/dist/core/doc-anchors.js.map +1 -1
- package/dist/core/editor-ui.js +57 -1
- package/dist/core/editor-ui.js.map +1 -1
- package/dist/core/surface-session.js +139 -0
- package/dist/core/surface-session.js.map +1 -0
- package/dist/document/DocumentSurface.d.ts +1 -1
- package/dist/document/DocumentSurface.vue.d.ts +3 -3
- package/dist/features/ai/agent-op-review.d.ts +2 -2
- package/dist/features/ai/agent-op-review.js.map +1 -1
- package/dist/features/ai/agent-ops.d.ts +1 -1
- package/dist/features/ai/agent-ops.js +9 -6
- package/dist/features/ai/agent-ops.js.map +1 -1
- package/dist/features/ai/ai-apply.d.ts +1 -1
- package/dist/features/ai/ai-review.js +10 -1
- package/dist/features/ai/ai-review.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.js +78 -252
- package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
- package/dist/features/ai/components/AiSidebar.js +2 -2
- package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/components/DocumentChatOps.js +1 -0
- package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/components/SettingsAiTab.js +1 -1
- package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/document-chat.d.ts +3 -2
- package/dist/features/ai/document-chat.js +16 -19
- package/dist/features/ai/document-chat.js.map +1 -1
- package/dist/features/ai/index.d.ts +5 -3
- package/dist/features/ai/index.js +4 -2
- package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
- package/dist/features/ai/protocol/agent-ops.js +65 -39
- package/dist/features/ai/protocol/agent-ops.js.map +1 -1
- package/dist/features/ai/protocol/block-lines.d.ts +89 -0
- package/dist/features/ai/protocol/block-lines.js +177 -0
- package/dist/features/ai/protocol/block-lines.js.map +1 -0
- package/dist/features/ai/protocol/chat.js +10 -10
- package/dist/features/ai/protocol/chat.js.map +1 -1
- package/dist/features/ai/protocol/index.d.ts +3 -2
- package/dist/features/ai/protocol/index.js +3 -2
- package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
- package/dist/features/ai/react/AiSelectionMenu.js +64 -255
- package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
- package/dist/features/ai/react/AiSidebar.js +2 -2
- package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
- package/dist/features/ai/react/DocumentChatOps.js +1 -0
- package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
- package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
- package/dist/features/ai/react/SettingsAiTab.js +1 -1
- package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
- package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
- package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
- package/dist/features/ai/selection-flow.d.ts +133 -0
- package/dist/features/ai/selection-flow.js +283 -0
- package/dist/features/ai/selection-flow.js.map +1 -0
- package/dist/features/ai/use-ai-assistant.d.ts +1 -1
- package/dist/features/ai/use-document-chat.d.ts +1 -1
- package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
- package/dist/features/authors/feature.js +1 -1
- package/dist/features/authors/index.js +1 -1
- package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
- package/dist/features/comments/components/CommentBubble.js +1 -1
- package/dist/features/comments/components/CommentComposer.js +1 -1
- package/dist/features/comments/components/CommentThreadView.js +5 -2
- package/dist/features/comments/components/CommentThreadView.js.map +1 -1
- package/dist/features/comments/components/CommentsMargin.js +108 -230
- package/dist/features/comments/components/CommentsMargin.js.map +1 -1
- package/dist/features/comments/components/CommentsOverlay.js +1 -1
- package/dist/features/comments/feature.js +1 -1
- package/dist/features/comments/index.d.ts +4 -2
- package/dist/features/comments/index.js +4 -2
- package/dist/features/comments/margin-controller.d.ts +44 -0
- package/dist/features/comments/margin-controller.js +132 -0
- package/dist/features/comments/margin-controller.js.map +1 -0
- package/dist/features/comments/margin-layout.d.ts +55 -0
- package/dist/features/comments/margin-layout.js +79 -0
- package/dist/features/comments/margin-layout.js.map +1 -0
- package/dist/features/comments/model.d.ts +8 -0
- package/dist/features/comments/model.js +60 -34
- package/dist/features/comments/model.js.map +1 -1
- package/dist/features/comments/react/CommentBubble.js +1 -1
- package/dist/features/comments/react/CommentComposer.js +1 -1
- package/dist/features/comments/react/CommentThreadView.js +2 -2
- package/dist/features/comments/react/CommentThreadView.js.map +1 -1
- package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
- package/dist/features/comments/react/CommentsMargin.js +85 -251
- package/dist/features/comments/react/CommentsMargin.js.map +1 -1
- package/dist/features/comments/react/CommentsOverlay.js +1 -1
- package/dist/features/comments/react/use-comments.js +1 -1
- package/dist/features/comments/react/use-margin-layout.js +45 -0
- package/dist/features/comments/react/use-margin-layout.js.map +1 -0
- package/dist/features/comments/react.d.ts +1 -1
- package/dist/features/comments/schema.d.ts +23 -0
- package/dist/features/comments/schema.js.map +1 -1
- package/dist/features/comments/snapshot-equality.js +40 -0
- package/dist/features/comments/snapshot-equality.js.map +1 -0
- package/dist/features/comments/thread-anchor.js +33 -0
- package/dist/features/comments/thread-anchor.js.map +1 -0
- package/dist/features/comments/use-comments.d.ts +3 -1
- package/dist/features/comments/use-comments.js +1 -0
- package/dist/features/comments/use-comments.js.map +1 -1
- package/dist/features/comments/use-margin-layout.js +29 -0
- package/dist/features/comments/use-margin-layout.js.map +1 -0
- package/dist/features/comments/vue.d.ts +1 -1
- package/dist/features/composites/accepted-children.d.ts +21 -0
- package/dist/features/composites/accepted-children.js +126 -0
- package/dist/features/composites/accepted-children.js.map +1 -0
- package/dist/features/composites/components/CompositeDetails.js +1 -1
- package/dist/features/composites/components/CompositeInspector.js +38 -164
- package/dist/features/composites/components/CompositeInspector.js.map +1 -1
- package/dist/features/composites/feature.d.ts +2 -1
- package/dist/features/composites/feature.js +8 -2
- package/dist/features/composites/feature.js.map +1 -1
- package/dist/features/composites/index.d.ts +5 -3
- package/dist/features/composites/index.js +8 -6
- package/dist/features/composites/inspector-model.d.ts +97 -0
- package/dist/features/composites/inspector-model.js +255 -0
- package/dist/features/composites/inspector-model.js.map +1 -0
- package/dist/features/composites/kinds.d.ts +11 -0
- package/dist/features/composites/kinds.js +17 -2
- package/dist/features/composites/kinds.js.map +1 -1
- package/dist/features/composites/outline.d.ts +18 -5
- package/dist/features/composites/outline.js +44 -12
- package/dist/features/composites/outline.js.map +1 -1
- package/dist/features/composites/react/CompositeDetails.js +1 -1
- package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
- package/dist/features/composites/react/CompositeInspector.js +21 -180
- package/dist/features/composites/react/CompositeInspector.js.map +1 -1
- package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
- package/dist/features/composites/react/CompositeView.js +1 -1
- package/dist/features/composites/react/use-composite-inspector.js +29 -0
- package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/schema.d.ts +2 -2
- package/dist/features/composites/schema.js +2 -2
- package/dist/features/composites/schema.js.map +1 -1
- package/dist/features/composites/use-composite-anchors.d.ts +1 -1
- package/dist/features/composites/use-composite-inspector.js +41 -0
- package/dist/features/composites/use-composite-inspector.js.map +1 -0
- package/dist/features/composites/use-composite-rows.d.ts +1 -1
- package/dist/features/media/index.d.ts +1 -1
- package/dist/features/media/vue.d.ts +1 -1
- package/dist/features/mentions/index.d.ts +3 -3
- package/dist/features/mentions/index.js +3 -3
- package/dist/features/mentions/schema.d.ts +1 -1
- package/dist/features/mentions/schema.js +1 -1
- package/dist/features/themes/index.d.ts +1 -1
- package/dist/features/themes/index.js +1 -1
- package/dist/features/transcript/feature.js +1 -1
- package/dist/features/transcript/index.js +2 -2
- package/dist/i18n/de/ai.js +2 -0
- package/dist/i18n/de/ai.js.map +1 -1
- package/dist/i18n/de/composites.js +8 -1
- package/dist/i18n/de/composites.js.map +1 -1
- package/dist/i18n/de/tree.js +14 -1
- package/dist/i18n/de/tree.js.map +1 -1
- package/dist/i18n/de.d.ts +22 -0
- package/dist/i18n/en/ai.js +2 -0
- package/dist/i18n/en/ai.js.map +1 -1
- package/dist/i18n/en/composites.js +8 -1
- package/dist/i18n/en/composites.js.map +1 -1
- package/dist/i18n/en/tree.js +14 -1
- package/dist/i18n/en/tree.js.map +1 -1
- package/dist/integrations/composite-ai/composite-fill.js +2 -2
- package/dist/integrations/composite-ai/composite-summary.js +2 -2
- package/dist/integrations/composite-ai/index.js +2 -2
- package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
- package/dist/markdown/MarkdownSurface.d.ts +1 -1
- package/dist/markdown/MarkdownSurface.js +2 -2
- package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
- package/dist/markdown/index.d.ts +2 -2
- package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
- package/dist/react/EditorSurface.d.ts +1 -1
- package/dist/react/EditorSurface.js +43 -149
- package/dist/react/EditorSurface.js.map +1 -1
- package/dist/react/EditorToolbar.js +3 -40
- package/dist/react/EditorToolbar.js.map +1 -1
- package/dist/schema/advanced.js +2 -2
- package/dist/server/adapter.d.ts +2 -2
- package/dist/server/collab.js +1 -1
- package/dist/server/handlers/ai.d.ts +4 -2
- package/dist/server/handlers/ai.js +11 -9
- package/dist/server/handlers/ai.js.map +1 -1
- package/dist/server/handlers/context.d.ts +1 -1
- package/dist/server/handlers/editor.d.ts +1 -1
- package/dist/server/handlers/editor.js +2 -2
- package/dist/server/host.d.ts +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/settings/index.d.ts +1 -1
- package/dist/styles/tokens.css +1 -1
- package/dist/tree/DocumentTree.js +159 -387
- package/dist/tree/DocumentTree.js.map +1 -1
- package/dist/tree/drop-rules.js +37 -0
- package/dist/tree/drop-rules.js.map +1 -0
- package/dist/tree/react/DocumentTree.d.ts +2 -2
- package/dist/tree/react/DocumentTree.js +135 -397
- package/dist/tree/react/DocumentTree.js.map +1 -1
- package/dist/tree/react/use-tree-model.js +129 -0
- package/dist/tree/react/use-tree-model.js.map +1 -0
- package/dist/tree/react.js +1 -1
- package/dist/tree/tree-host.js +100 -0
- package/dist/tree/tree-host.js.map +1 -0
- package/dist/tree/tree-items.js +22 -43
- package/dist/tree/tree-items.js.map +1 -1
- package/dist/tree/tree-model.js +580 -0
- package/dist/tree/tree-model.js.map +1 -0
- package/dist/tree/tree-rows.js +77 -0
- package/dist/tree/tree-rows.js.map +1 -0
- package/dist/tree/use-tree-model.js +94 -0
- package/dist/tree/use-tree-model.js.map +1 -0
- package/dist/tree/vue.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +2 -2
- package/dist/vue/components/EditorSurface.js +27 -87
- package/dist/vue/components/EditorSurface.js.map +1 -1
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/dist/vue/components/EditorToolbar.js +7 -44
- package/dist/vue/components/EditorToolbar.js.map +1 -1
- package/dist/vue/context.d.ts +1 -1
- package/dist/vue/context.js +1 -1
- package/dist/vue/context.js.map +1 -1
- package/dist/vue/use-editor-ui.d.ts +1 -1
- package/dist/vue/use-notifier.d.ts +1 -1
- package/docs/integration.md +97 -4
- package/docs/llms.md +9 -1
- package/docs/usage.md +10 -5
- package/package.json +67 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DRAFT_CARD, marginCards } from "../margin-layout.js";
|
|
1
2
|
import { useEditorI18n } from "../../../react/use-editor-i18n.js";
|
|
2
3
|
import { cn } from "../../../react/ui/utils.js";
|
|
3
4
|
import { Icon } from "../../../react/Icon.js";
|
|
@@ -7,285 +8,118 @@ import { CommentComposer } from "./CommentComposer.js";
|
|
|
7
8
|
import { CommentAvatar } from "./CommentAvatar.js";
|
|
8
9
|
import { CommentThreadView } from "./CommentThreadView.js";
|
|
9
10
|
import { useComments } from "./use-comments.js";
|
|
10
|
-
import {
|
|
11
|
+
import { useMarginLayout } from "./use-margin-layout.js";
|
|
12
|
+
import { useMemo, useState } from "react";
|
|
11
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
12
14
|
//#region src/features/comments/react/CommentsMargin.tsx
|
|
13
|
-
const GAP = 8;
|
|
14
|
-
const DRAFT = "__draft";
|
|
15
15
|
const excerpt = (text) => text.length > 60 ? `${text.slice(0, 60)}…` : text;
|
|
16
|
-
function sameTops(a, b) {
|
|
17
|
-
const keys = Object.keys(a);
|
|
18
|
-
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Comments margin („Randspalte“): a column the host places inside its scroll container right of the editor, so it
|
|
22
|
-
* scrolls with the text. Cards sit at the height of their text, the active thread is expanded; shown only while `open`
|
|
23
|
-
* and the user's comments layout is `margin`.
|
|
24
|
-
*/
|
|
25
16
|
function CommentsMargin({ editor, container, open = true }) {
|
|
26
17
|
const { settings } = useEditorSettings();
|
|
27
18
|
const { t } = useEditorI18n();
|
|
28
19
|
const enabled = settings.editing.commentsLayout === "margin";
|
|
29
20
|
const comments = useComments(enabled ? editor : void 0);
|
|
30
|
-
/** Draft and open threads
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
for (const thread of comments.open) {
|
|
39
|
-
const range = comments.ranges.get(thread.id);
|
|
40
|
-
if (range) items.push({
|
|
41
|
-
key: thread.id,
|
|
42
|
-
from: range.from,
|
|
43
|
-
thread
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
return items.sort((a, b) => a.from - b.from);
|
|
47
|
-
}, [
|
|
21
|
+
/** Draft and open threads in document order, each at its text (an orphan where its text was). */
|
|
22
|
+
const cards = useMemo(() => marginCards({
|
|
23
|
+
draft: comments.draft,
|
|
24
|
+
open: comments.open,
|
|
25
|
+
ranges: comments.ranges,
|
|
26
|
+
orphanAt: comments.orphanAt
|
|
27
|
+
}), [
|
|
48
28
|
comments.draft,
|
|
49
29
|
comments.open,
|
|
50
|
-
comments.ranges
|
|
30
|
+
comments.ranges,
|
|
31
|
+
comments.orphanAt
|
|
51
32
|
]);
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const [resolvedTop, setResolvedTop] = useState(0);
|
|
56
|
-
const [minHeight, setMinHeight] = useState(0);
|
|
57
|
-
const [layout] = useState(() => ({
|
|
58
|
-
observer: null,
|
|
59
|
-
observed: /* @__PURE__ */ new WeakSet(),
|
|
60
|
-
cards: /* @__PURE__ */ new Map(),
|
|
61
|
-
frame: 0,
|
|
62
|
-
column: null,
|
|
63
|
-
resolvedSection: null
|
|
64
|
-
}));
|
|
65
|
-
const latest = useRef({
|
|
33
|
+
/** The card that owns its line: the draft composer, else the active thread. */
|
|
34
|
+
const activeId = comments.draft ? DRAFT_CARD : comments.active;
|
|
35
|
+
const { state: layout, setColumn, setResolvedSection, setCard } = useMarginLayout({
|
|
66
36
|
editor,
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
container,
|
|
38
|
+
cards,
|
|
39
|
+
activeId
|
|
69
40
|
});
|
|
70
|
-
|
|
71
|
-
editor,
|
|
72
|
-
positioned,
|
|
73
|
-
orphans
|
|
74
|
-
};
|
|
75
|
-
const observe = useCallback((el) => {
|
|
76
|
-
if (!layout.observer || layout.observed.has(el)) return;
|
|
77
|
-
layout.observed.add(el);
|
|
78
|
-
layout.observer.observe(el);
|
|
79
|
-
}, [layout]);
|
|
80
|
-
const unobserve = useCallback((el) => {
|
|
81
|
-
layout.observer?.unobserve(el);
|
|
82
|
-
layout.observed.delete(el);
|
|
83
|
-
}, [layout]);
|
|
84
|
-
const relayout = useCallback(() => {
|
|
85
|
-
layout.frame = 0;
|
|
86
|
-
const { editor: instance, positioned: items, orphans: lost } = latest.current;
|
|
87
|
-
const col = layout.column;
|
|
88
|
-
if (!instance || instance.isDestroyed || !col || !col.getClientRects().length) return;
|
|
89
|
-
const live = /* @__PURE__ */ new Set([...items.map((i) => i.key), ...lost.map((t) => t.id)]);
|
|
90
|
-
for (const [key, el] of layout.cards) {
|
|
91
|
-
if (live.has(key)) continue;
|
|
92
|
-
unobserve(el);
|
|
93
|
-
layout.cards.delete(key);
|
|
94
|
-
}
|
|
95
|
-
const base = col.getBoundingClientRect().top;
|
|
96
|
-
const size = instance.state.doc.content.size;
|
|
97
|
-
const next = {};
|
|
98
|
-
let bottom = -8;
|
|
99
|
-
for (const item of items) {
|
|
100
|
-
let desired = bottom + GAP;
|
|
101
|
-
try {
|
|
102
|
-
desired = instance.view.coordsAtPos(Math.min(item.from, size)).top - base;
|
|
103
|
-
} catch {}
|
|
104
|
-
const top = Math.max(desired, bottom + GAP, 0);
|
|
105
|
-
next[item.key] = top;
|
|
106
|
-
bottom = top + (layout.cards.get(item.key)?.offsetHeight ?? 0);
|
|
107
|
-
}
|
|
108
|
-
for (const thread of lost) {
|
|
109
|
-
const top = bottom + GAP;
|
|
110
|
-
next[thread.id] = top;
|
|
111
|
-
bottom = top + (layout.cards.get(thread.id)?.offsetHeight ?? 0);
|
|
112
|
-
}
|
|
113
|
-
const nextResolvedTop = bottom < 0 ? 0 : bottom + 16;
|
|
114
|
-
setTops((previous) => sameTops(previous, next) ? previous : next);
|
|
115
|
-
setResolvedTop(nextResolvedTop);
|
|
116
|
-
setMinHeight(nextResolvedTop + (layout.resolvedSection?.offsetHeight ?? 0));
|
|
117
|
-
}, [layout, unobserve]);
|
|
118
|
-
const schedule = useCallback(() => {
|
|
119
|
-
if (layout.frame || typeof requestAnimationFrame === "undefined") return;
|
|
120
|
-
layout.frame = requestAnimationFrame(relayout);
|
|
121
|
-
}, [layout, relayout]);
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
124
|
-
layout.observer = new ResizeObserver(() => schedule());
|
|
125
|
-
layout.observed = /* @__PURE__ */ new WeakSet();
|
|
126
|
-
for (const el of layout.cards.values()) observe(el);
|
|
127
|
-
if (layout.column) observe(layout.column);
|
|
128
|
-
if (layout.resolvedSection) observe(layout.resolvedSection);
|
|
129
|
-
}
|
|
130
|
-
window.addEventListener("resize", schedule);
|
|
131
|
-
schedule();
|
|
132
|
-
return () => {
|
|
133
|
-
window.removeEventListener("resize", schedule);
|
|
134
|
-
layout.observer?.disconnect();
|
|
135
|
-
layout.observer = null;
|
|
136
|
-
if (layout.frame) cancelAnimationFrame(layout.frame);
|
|
137
|
-
layout.frame = 0;
|
|
138
|
-
};
|
|
139
|
-
}, [
|
|
140
|
-
layout,
|
|
141
|
-
observe,
|
|
142
|
-
schedule
|
|
143
|
-
]);
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
if (!editor) return;
|
|
146
|
-
editor.on("transaction", schedule);
|
|
147
|
-
schedule();
|
|
148
|
-
return () => {
|
|
149
|
-
editor.off("transaction", schedule);
|
|
150
|
-
};
|
|
151
|
-
}, [editor, schedule]);
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
if (!container || !layout.observer) return;
|
|
154
|
-
layout.observer.observe(container);
|
|
155
|
-
return () => layout.observer?.unobserve(container);
|
|
156
|
-
}, [container, layout]);
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
schedule();
|
|
159
|
-
}, [
|
|
160
|
-
positioned,
|
|
161
|
-
orphans,
|
|
162
|
-
comments.active,
|
|
163
|
-
comments.resolved.length,
|
|
164
|
-
showResolved,
|
|
165
|
-
enabled,
|
|
166
|
-
schedule
|
|
167
|
-
]);
|
|
168
|
-
const setColumn = useCallback((el) => {
|
|
169
|
-
if (layout.column) unobserve(layout.column);
|
|
170
|
-
layout.column = el;
|
|
171
|
-
if (el) observe(el);
|
|
172
|
-
schedule();
|
|
173
|
-
}, [
|
|
174
|
-
layout,
|
|
175
|
-
observe,
|
|
176
|
-
unobserve,
|
|
177
|
-
schedule
|
|
178
|
-
]);
|
|
179
|
-
const setResolvedSection = useCallback((el) => {
|
|
180
|
-
if (layout.resolvedSection) unobserve(layout.resolvedSection);
|
|
181
|
-
layout.resolvedSection = el;
|
|
182
|
-
if (el) observe(el);
|
|
183
|
-
schedule();
|
|
184
|
-
}, [
|
|
185
|
-
layout,
|
|
186
|
-
observe,
|
|
187
|
-
unobserve,
|
|
188
|
-
schedule
|
|
189
|
-
]);
|
|
190
|
-
const setCard = (key, el) => {
|
|
191
|
-
if (!el) return;
|
|
192
|
-
layout.cards.set(key, el);
|
|
193
|
-
observe(el);
|
|
194
|
-
};
|
|
41
|
+
const [showResolved, setShowResolved] = useState(false);
|
|
195
42
|
/** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */
|
|
196
43
|
function select(id) {
|
|
197
44
|
if (comments.active !== id) comments.activate(id, { reveal: true });
|
|
198
45
|
}
|
|
199
|
-
const cardStyle = (key) => ({ top: `${tops[key] ?? 0}px` });
|
|
200
46
|
return /* @__PURE__ */ jsx("div", {
|
|
201
47
|
ref: setColumn,
|
|
202
48
|
className: "relative w-[300px] shrink-0",
|
|
203
49
|
style: {
|
|
204
|
-
minHeight: `${minHeight}px`,
|
|
50
|
+
minHeight: `${layout.minHeight}px`,
|
|
205
51
|
...open && enabled ? {} : { display: "none" }
|
|
206
52
|
},
|
|
207
53
|
"aria-label": t("comments.title"),
|
|
208
54
|
children: !comments.available ? /* @__PURE__ */ jsx("p", {
|
|
209
55
|
className: "p-3 text-sm text-muted-foreground",
|
|
210
56
|
children: t("comments.unavailable")
|
|
211
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
ref: (el) => setCard(thread.id, el),
|
|
258
|
-
className: cn("absolute inset-x-0", tops[thread.id] === void 0 && "invisible"),
|
|
259
|
-
style: cardStyle(thread.id),
|
|
260
|
-
children: /* @__PURE__ */ jsx(CommentThreadView, {
|
|
261
|
-
thread,
|
|
262
|
-
user: comments.user,
|
|
263
|
-
active: comments.active === thread.id,
|
|
264
|
-
orphan: true,
|
|
265
|
-
comments,
|
|
266
|
-
onClick: () => select(thread.id)
|
|
267
|
-
})
|
|
268
|
-
}, thread.id)),
|
|
269
|
-
comments.resolved.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
270
|
-
ref: setResolvedSection,
|
|
271
|
-
className: "absolute inset-x-0 flex flex-col gap-2",
|
|
272
|
-
style: { top: `${resolvedTop}px` },
|
|
273
|
-
children: [/* @__PURE__ */ jsxs(Button, {
|
|
274
|
-
variant: "ghost",
|
|
275
|
-
size: "sm",
|
|
276
|
-
className: "self-start",
|
|
277
|
-
onClick: () => setShowResolved((value) => !value),
|
|
278
|
-
children: [t("comments.resolvedCount", { n: comments.resolved.length }), /* @__PURE__ */ jsx(Icon, { name: showResolved ? "i-lucide-chevron-up" : "i-lucide-chevron-down" })]
|
|
279
|
-
}), showResolved && comments.resolved.map((thread) => /* @__PURE__ */ jsx(CommentThreadView, {
|
|
280
|
-
thread,
|
|
281
|
-
user: comments.user,
|
|
282
|
-
active: comments.active === thread.id,
|
|
283
|
-
orphan: comments.isOrphan(thread.id),
|
|
284
|
-
comments,
|
|
285
|
-
onClick: () => select(thread.id)
|
|
286
|
-
}, thread.id))]
|
|
57
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [cards.map((card) => /* @__PURE__ */ jsx("div", {
|
|
58
|
+
ref: (el) => setCard(card.id, el),
|
|
59
|
+
className: cn("absolute inset-x-0 transition-[top] duration-100", !layout.tops.has(card.id) && "invisible"),
|
|
60
|
+
style: { top: `${layout.tops.get(card.id) ?? 0}px` },
|
|
61
|
+
children: !card.thread ? /* @__PURE__ */ jsx(CommentComposer, {
|
|
62
|
+
quote: comments.draftQuote,
|
|
63
|
+
className: "shadow-md",
|
|
64
|
+
onSubmit: (text) => {
|
|
65
|
+
comments.add(text);
|
|
66
|
+
},
|
|
67
|
+
onCancel: () => comments.cancelDraft()
|
|
68
|
+
}) : activeId === card.id ? /* @__PURE__ */ jsx(CommentThreadView, {
|
|
69
|
+
thread: card.thread,
|
|
70
|
+
user: comments.user,
|
|
71
|
+
active: true,
|
|
72
|
+
orphan: card.orphan,
|
|
73
|
+
comments,
|
|
74
|
+
className: "shadow-md",
|
|
75
|
+
onClick: () => select(card.id)
|
|
76
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
77
|
+
type: "button",
|
|
78
|
+
className: cn("flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted", card.orphan && "opacity-70"),
|
|
79
|
+
onClick: () => select(card.id),
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsx(CommentAvatar, {
|
|
82
|
+
name: card.thread.author.name,
|
|
83
|
+
color: card.thread.author.color,
|
|
84
|
+
size: "2xs"
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ jsx("span", {
|
|
87
|
+
className: "shrink-0 font-medium",
|
|
88
|
+
children: card.thread.author.name
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ jsx("span", {
|
|
91
|
+
className: "min-w-0 flex-1 truncate text-muted-foreground",
|
|
92
|
+
children: excerpt(card.thread.text)
|
|
93
|
+
}),
|
|
94
|
+
card.orphan && /* @__PURE__ */ jsx("span", {
|
|
95
|
+
className: "shrink-0 text-xs text-muted-foreground/70 italic",
|
|
96
|
+
children: t("comments.orphan")
|
|
97
|
+
}),
|
|
98
|
+
card.thread.replies.length > 0 && /* @__PURE__ */ jsx("span", {
|
|
99
|
+
className: "shrink-0 text-xs text-muted-foreground/70",
|
|
100
|
+
children: card.thread.replies.length
|
|
101
|
+
})
|
|
102
|
+
]
|
|
287
103
|
})
|
|
288
|
-
|
|
104
|
+
}, card.id)), comments.resolved.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
105
|
+
ref: setResolvedSection,
|
|
106
|
+
className: "absolute inset-x-0 flex flex-col gap-2",
|
|
107
|
+
style: { top: `${layout.resolvedTop}px` },
|
|
108
|
+
children: [/* @__PURE__ */ jsxs(Button, {
|
|
109
|
+
variant: "ghost",
|
|
110
|
+
size: "sm",
|
|
111
|
+
className: "self-start",
|
|
112
|
+
onClick: () => setShowResolved((value) => !value),
|
|
113
|
+
children: [t("comments.resolvedCount", { n: comments.resolved.length }), /* @__PURE__ */ jsx(Icon, { name: showResolved ? "i-lucide-chevron-up" : "i-lucide-chevron-down" })]
|
|
114
|
+
}), showResolved && comments.resolved.map((thread) => /* @__PURE__ */ jsx(CommentThreadView, {
|
|
115
|
+
thread,
|
|
116
|
+
user: comments.user,
|
|
117
|
+
active: comments.active === thread.id,
|
|
118
|
+
orphan: comments.isOrphan(thread.id),
|
|
119
|
+
comments,
|
|
120
|
+
onClick: () => select(thread.id)
|
|
121
|
+
}, thread.id))]
|
|
122
|
+
})] })
|
|
289
123
|
});
|
|
290
124
|
}
|
|
291
125
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CommentsMargin.js","names":[],"sources":["../../../../src/features/comments/react/CommentsMargin.tsx"],"sourcesContent":["/**\n * Comments margin („Randspalte“): a 300px column the host places inside its scroll container right of the editor,\n * so it scrolls with the text. Cards sit absolutely at the height of their text (top-down in document order, pushed\n * down so they never overlap); the active thread is expanded, the others collapse to one line. The draft composer\n * takes part in the layout, orphaned threads stack after the positioned cards and resolved threads („Erledigt“)\n * sit at the bottom behind a toggle. Shown only while `open` (the host binds the surface's `commentsOpen` model) and the\n * user's comments layout is `margin`; the layout is recomputed whenever the column becomes visible again.\n */\nimport type { Editor } from '@tiptap/core'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport type { CommentThread as Thread } from '../schema'\nimport { CommentAvatar } from './CommentAvatar'\nimport { CommentComposer } from './CommentComposer'\nimport { CommentThreadView } from './CommentThreadView'\nimport { useComments } from './use-comments'\n\nconst GAP = 8\nconst DRAFT = '__draft'\n\ninterface Item { key: string, from: number, thread: Thread | null }\n\nconst excerpt = (text: string) => text.length > 60 ? `${text.slice(0, 60)}…` : text\n\nfunction sameTops(a: Record<string, number>, b: Record<string, number>): boolean {\n const keys = Object.keys(a)\n return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key])\n}\n\n/**\n * Comments margin („Randspalte“): a column the host places inside its scroll container right of the editor, so it\n * scrolls with the text. Cards sit at the height of their text, the active thread is expanded; shown only while `open`\n * and the user's comments layout is `margin`.\n */\nexport function CommentsMargin({ editor, container, open = true }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** Whether the column is shown (the surface's `commentsOpen` model). */\n open?: boolean\n}) {\n const { settings } = useEditorSettings()\n const { t } = useEditorI18n()\n const enabled = settings.editing.commentsLayout === 'margin'\n // Idle (no Y.Doc observer) while another layout shows the comments.\n const comments = useComments(enabled ? editor : undefined)\n\n /** Draft and open threads with a text range, in document order. */\n const positioned = useMemo<Item[]>(() => {\n const items: Item[] = []\n if (comments.draft) items.push({ key: DRAFT, from: comments.draft.from, thread: null })\n for (const thread of comments.open) {\n const range = comments.ranges.get(thread.id)\n if (range) items.push({ key: thread.id, from: range.from, thread })\n }\n return items.sort((a, b) => a.from - b.from)\n }, [comments.draft, comments.open, comments.ranges])\n const orphans = useMemo(() => comments.open.filter(thread => !comments.ranges.has(thread.id)), [comments.open, comments.ranges])\n\n const [showResolved, setShowResolved] = useState(false)\n const [tops, setTops] = useState<Record<string, number>>({})\n const [resolvedTop, setResolvedTop] = useState(0)\n const [minHeight, setMinHeight] = useState(0)\n\n // Layout machinery that lives outside rendering: the observer, the pending frame, the cards by key.\n const [layout] = useState(() => ({\n observer: null as ResizeObserver | null,\n observed: new WeakSet<Element>(),\n cards: new Map<string, HTMLElement>(),\n frame: 0,\n column: null as HTMLElement | null,\n resolvedSection: null as HTMLElement | null,\n }))\n // What `relayout` reads: the latest render's values.\n const latest = useRef({ editor, positioned, orphans })\n latest.current = { editor, positioned, orphans }\n\n const observe = useCallback((el: Element) => {\n if (!layout.observer || layout.observed.has(el)) return\n layout.observed.add(el)\n layout.observer.observe(el)\n }, [layout])\n\n const unobserve = useCallback((el: Element) => {\n layout.observer?.unobserve(el)\n layout.observed.delete(el)\n }, [layout])\n\n const relayout = useCallback(() => {\n layout.frame = 0\n const { editor: instance, positioned: items, orphans: lost } = latest.current\n const col = layout.column\n // Hidden columns have no layout box; the column's own ResizeObserver re-runs this once shown.\n if (!instance || instance.isDestroyed || !col || !col.getClientRects().length) return\n const live = new Set([...items.map(i => i.key), ...lost.map(t => t.id)])\n for (const [key, el] of layout.cards) {\n if (live.has(key)) continue\n unobserve(el)\n layout.cards.delete(key)\n }\n\n const base = col.getBoundingClientRect().top\n const size = instance.state.doc.content.size\n const next: Record<string, number> = {}\n let bottom = -GAP\n for (const item of items) {\n let desired = bottom + GAP\n try {\n desired = instance.view.coordsAtPos(Math.min(item.from, size)).top - base\n }\n catch {\n // Position not rendered (yet): stack it below the previous card.\n }\n const top = Math.max(desired, bottom + GAP, 0)\n next[item.key] = top\n bottom = top + (layout.cards.get(item.key)?.offsetHeight ?? 0)\n }\n for (const thread of lost) {\n const top = bottom + GAP\n next[thread.id] = top\n bottom = top + (layout.cards.get(thread.id)?.offsetHeight ?? 0)\n }\n const nextResolvedTop = bottom < 0 ? 0 : bottom + 2 * GAP\n setTops(previous => sameTops(previous, next) ? previous : next)\n setResolvedTop(nextResolvedTop)\n setMinHeight(nextResolvedTop + (layout.resolvedSection?.offsetHeight ?? 0))\n }, [layout, unobserve])\n\n const schedule = useCallback(() => {\n if (layout.frame || typeof requestAnimationFrame === 'undefined') return\n layout.frame = requestAnimationFrame(relayout)\n }, [layout, relayout])\n\n // The observer and the window listener live while mounted; the elements already rendered are observed on creation.\n useEffect(() => {\n if (typeof ResizeObserver !== 'undefined') {\n layout.observer = new ResizeObserver(() => schedule())\n layout.observed = new WeakSet()\n for (const el of layout.cards.values()) observe(el)\n if (layout.column) observe(layout.column)\n if (layout.resolvedSection) observe(layout.resolvedSection)\n }\n window.addEventListener('resize', schedule)\n schedule()\n return () => {\n window.removeEventListener('resize', schedule)\n layout.observer?.disconnect()\n layout.observer = null\n if (layout.frame) cancelAnimationFrame(layout.frame)\n layout.frame = 0\n }\n }, [layout, observe, schedule])\n\n useEffect(() => {\n if (!editor) return\n editor.on('transaction', schedule)\n schedule()\n return () => { editor.off('transaction', schedule) }\n }, [editor, schedule])\n\n useEffect(() => {\n if (!container || !layout.observer) return\n layout.observer.observe(container)\n return () => layout.observer?.unobserve(container)\n }, [container, layout])\n\n // Cards appearing/disappearing or expanding: lay out once the DOM has them.\n useEffect(() => { schedule() }, [positioned, orphans, comments.active, comments.resolved.length, showResolved, enabled, schedule])\n\n // The column itself: its size flips between zero and real when the host toggles it.\n const setColumn = useCallback((el: HTMLElement | null) => {\n if (layout.column) unobserve(layout.column)\n layout.column = el\n if (el) observe(el)\n schedule()\n }, [layout, observe, unobserve, schedule])\n\n const setResolvedSection = useCallback((el: HTMLElement | null) => {\n if (layout.resolvedSection) unobserve(layout.resolvedSection)\n layout.resolvedSection = el\n if (el) observe(el)\n schedule()\n }, [layout, observe, unobserve, schedule])\n\n const setCard = (key: string, el: HTMLElement | null) => {\n if (!el) return\n layout.cards.set(key, el)\n observe(el)\n }\n\n /** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */\n function select(id: string) {\n if (comments.active !== id) comments.activate(id, { reveal: true })\n }\n\n const cardStyle = (key: string) => ({ top: `${tops[key] ?? 0}px` })\n\n return (\n <div\n ref={setColumn}\n className=\"relative w-[300px] shrink-0\"\n style={{ minHeight: `${minHeight}px`, ...(open && enabled ? {} : { display: 'none' }) }}\n aria-label={t('comments.title')}\n >\n {!comments.available\n ? <p className=\"p-3 text-sm text-muted-foreground\">{t('comments.unavailable')}</p>\n : (\n <>\n {positioned.map(item => (\n <div\n key={item.key}\n ref={el => setCard(item.key, el)}\n className={cn('absolute inset-x-0 transition-[top] duration-100', tops[item.key] === undefined && 'invisible')}\n style={cardStyle(item.key)}\n >\n {!item.thread\n ? (\n <CommentComposer\n quote={comments.draftQuote}\n className=\"shadow-md\"\n onSubmit={(text) => { comments.add(text) }}\n onCancel={() => comments.cancelDraft()}\n />\n )\n : comments.active === item.thread.id\n ? (\n <CommentThreadView\n thread={item.thread}\n user={comments.user}\n active\n orphan={false}\n comments={comments}\n className=\"shadow-md\"\n onClick={() => select(item.thread!.id)}\n />\n )\n : (\n <button\n type=\"button\"\n className=\"flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted\"\n onClick={() => select(item.thread!.id)}\n >\n <CommentAvatar name={item.thread.author.name} color={item.thread.author.color} size=\"2xs\" />\n <span className=\"shrink-0 font-medium\">{item.thread.author.name}</span>\n <span className=\"min-w-0 flex-1 truncate text-muted-foreground\">{excerpt(item.thread.text)}</span>\n {item.thread.replies.length > 0 && <span className=\"shrink-0 text-xs text-muted-foreground/70\">{item.thread.replies.length}</span>}\n </button>\n )}\n </div>\n ))}\n\n {orphans.map(thread => (\n <div\n key={thread.id}\n ref={el => setCard(thread.id, el)}\n className={cn('absolute inset-x-0', tops[thread.id] === undefined && 'invisible')}\n style={cardStyle(thread.id)}\n >\n <CommentThreadView\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan\n comments={comments}\n onClick={() => select(thread.id)}\n />\n </div>\n ))}\n\n {comments.resolved.length > 0 && (\n <div ref={setResolvedSection} className=\"absolute inset-x-0 flex flex-col gap-2\" style={{ top: `${resolvedTop}px` }}>\n <Button variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={() => setShowResolved(value => !value)}>\n {t('comments.resolvedCount', { n: comments.resolved.length })}\n <Icon name={showResolved ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'} />\n </Button>\n {showResolved && comments.resolved.map(thread => (\n <CommentThreadView\n key={thread.id}\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan={comments.isOrphan(thread.id)}\n comments={comments}\n onClick={() => select(thread.id)}\n />\n ))}\n </div>\n )}\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CommentsMargin.js","names":[],"sources":["../../../../src/features/comments/react/CommentsMargin.tsx"],"sourcesContent":["/**\n * Comments margin („Randspalte“): a 300px column the host places inside its scroll container right of the editor,\n * so it scrolls with the text. Layout „Aktiver Faden“ (`layoutMargin`): the active thread's card sits exactly at the\n * top of its text's line and expands, the others collapse to one line and give way up and down; without an active\n * thread the cards stack top-down at their lines. The draft composer takes part like an active card. An orphaned thread\n * (its text was deleted) is greyed out and sits where the text was; resolved threads („Erledigt“) sit at the bottom\n * behind a toggle. Shown only while `open` (the host binds the surface's `commentsOpen` model) and the user's comments\n * layout is `margin`; the layout is recomputed whenever the column becomes visible again.\n */\nimport type { Editor } from '@tiptap/core'\nimport { useMemo, useState } from 'react'\nimport { Icon } from '../../../react/Icon'\nimport { useEditorSettings } from '../../../react/settings'\nimport { Button } from '../../../react/ui/button'\nimport { cn } from '../../../react/ui/utils'\nimport { useEditorI18n } from '../../../react/use-editor-i18n'\nimport { DRAFT_CARD, marginCards } from '../margin-layout'\nimport { CommentAvatar } from './CommentAvatar'\nimport { CommentComposer } from './CommentComposer'\nimport { CommentThreadView } from './CommentThreadView'\nimport { useComments } from './use-comments'\nimport { useMarginLayout } from './use-margin-layout'\n\nconst excerpt = (text: string) => text.length > 60 ? `${text.slice(0, 60)}…` : text\n\nexport function CommentsMargin({ editor, container, open = true }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** Whether the column is shown (the surface's `commentsOpen` model). */\n open?: boolean\n}) {\n const { settings } = useEditorSettings()\n const { t } = useEditorI18n()\n const enabled = settings.editing.commentsLayout === 'margin'\n // Idle (no Y.Doc observer) while another layout shows the comments.\n const comments = useComments(enabled ? editor : undefined)\n\n /** Draft and open threads in document order, each at its text (an orphan where its text was). */\n const cards = useMemo(\n () => marginCards({ draft: comments.draft, open: comments.open, ranges: comments.ranges, orphanAt: comments.orphanAt }),\n [comments.draft, comments.open, comments.ranges, comments.orphanAt],\n )\n /** The card that owns its line: the draft composer, else the active thread. */\n const activeId = comments.draft ? DRAFT_CARD : comments.active\n\n const { state: layout, setColumn, setResolvedSection, setCard } = useMarginLayout({ editor, container, cards, activeId })\n const [showResolved, setShowResolved] = useState(false)\n\n /** Clicking a card activates (expands) it and reveals its text; clicks inside the already active card do nothing. */\n function select(id: string) {\n if (comments.active !== id) comments.activate(id, { reveal: true })\n }\n\n return (\n <div\n ref={setColumn}\n className=\"relative w-[300px] shrink-0\"\n style={{ minHeight: `${layout.minHeight}px`, ...(open && enabled ? {} : { display: 'none' }) }}\n aria-label={t('comments.title')}\n >\n {!comments.available\n ? <p className=\"p-3 text-sm text-muted-foreground\">{t('comments.unavailable')}</p>\n : (\n <>\n {cards.map(card => (\n <div\n key={card.id}\n ref={el => setCard(card.id, el)}\n className={cn('absolute inset-x-0 transition-[top] duration-100', !layout.tops.has(card.id) && 'invisible')}\n style={{ top: `${layout.tops.get(card.id) ?? 0}px` }}\n >\n {!card.thread\n ? (\n <CommentComposer\n quote={comments.draftQuote}\n className=\"shadow-md\"\n onSubmit={(text) => { comments.add(text) }}\n onCancel={() => comments.cancelDraft()}\n />\n )\n : activeId === card.id\n ? (\n <CommentThreadView\n thread={card.thread}\n user={comments.user}\n active\n orphan={card.orphan}\n comments={comments}\n className=\"shadow-md\"\n onClick={() => select(card.id)}\n />\n )\n : (\n <button\n type=\"button\"\n className={cn('flex w-full items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 text-left text-sm hover:bg-muted', card.orphan && 'opacity-70')}\n onClick={() => select(card.id)}\n >\n <CommentAvatar name={card.thread.author.name} color={card.thread.author.color} size=\"2xs\" />\n <span className=\"shrink-0 font-medium\">{card.thread.author.name}</span>\n <span className=\"min-w-0 flex-1 truncate text-muted-foreground\">{excerpt(card.thread.text)}</span>\n {card.orphan && <span className=\"shrink-0 text-xs text-muted-foreground/70 italic\">{t('comments.orphan')}</span>}\n {card.thread.replies.length > 0 && <span className=\"shrink-0 text-xs text-muted-foreground/70\">{card.thread.replies.length}</span>}\n </button>\n )}\n </div>\n ))}\n\n {comments.resolved.length > 0 && (\n <div ref={setResolvedSection} className=\"absolute inset-x-0 flex flex-col gap-2\" style={{ top: `${layout.resolvedTop}px` }}>\n <Button variant=\"ghost\" size=\"sm\" className=\"self-start\" onClick={() => setShowResolved(value => !value)}>\n {t('comments.resolvedCount', { n: comments.resolved.length })}\n <Icon name={showResolved ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'} />\n </Button>\n {showResolved && comments.resolved.map(thread => (\n <CommentThreadView\n key={thread.id}\n thread={thread}\n user={comments.user}\n active={comments.active === thread.id}\n orphan={comments.isOrphan(thread.id)}\n comments={comments}\n onClick={() => select(thread.id)}\n />\n ))}\n </div>\n )}\n </>\n )}\n </div>\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { cancelCommentDraft, commentsKey, setActiveComment } from "../comments-plugin.js";
|
|
1
2
|
import { useEditorSettings } from "../../../react/settings.js";
|
|
2
3
|
import { useEditorUi } from "../../../react/editor-ui.js";
|
|
3
|
-
import { cancelCommentDraft, commentsKey, setActiveComment } from "../comments-plugin.js";
|
|
4
4
|
import { CommentBubble } from "./CommentBubble.js";
|
|
5
5
|
import { CommentsPopover } from "./CommentsPopover.js";
|
|
6
6
|
import { useEffect, useRef } from "react";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useStore } from "../../../react/use-store.js";
|
|
2
1
|
import { createEditorCommentsModel } from "../model.js";
|
|
2
|
+
import { useStore } from "../../../react/use-store.js";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
//#region src/features/comments/react/use-comments.ts
|
|
5
5
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createMarginController } from "../margin-controller.js";
|
|
2
|
+
import { useStore } from "../../../react/use-store.js";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
//#region src/features/comments/react/use-margin-layout.ts
|
|
5
|
+
/**
|
|
6
|
+
* The layout of the comments margin for React: a thin hook over `createMarginController` (see there for the
|
|
7
|
+
* measuring). Hand the column, the card and the resolved section elements in through the returned `set*` functions
|
|
8
|
+
* (as ref callbacks); `state` holds the measured tops. Re-measures when `cards` or `activeId` change. It starts on
|
|
9
|
+
* mount and stops on unmount.
|
|
10
|
+
*/
|
|
11
|
+
function useMarginLayout({ editor, container, cards, activeId }) {
|
|
12
|
+
const latest = useRef({
|
|
13
|
+
cards,
|
|
14
|
+
activeId
|
|
15
|
+
});
|
|
16
|
+
latest.current = {
|
|
17
|
+
cards,
|
|
18
|
+
activeId
|
|
19
|
+
};
|
|
20
|
+
const [controller] = useState(() => createMarginController({
|
|
21
|
+
cards: () => latest.current.cards,
|
|
22
|
+
activeId: () => latest.current.activeId
|
|
23
|
+
}));
|
|
24
|
+
useEffect(() => controller.setEditor(editor), [controller, editor]);
|
|
25
|
+
useEffect(() => controller.setContainer(container), [controller, container]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
controller.connect();
|
|
28
|
+
return controller.disconnect;
|
|
29
|
+
}, [controller]);
|
|
30
|
+
useEffect(() => controller.schedule(), [
|
|
31
|
+
controller,
|
|
32
|
+
cards,
|
|
33
|
+
activeId
|
|
34
|
+
]);
|
|
35
|
+
return {
|
|
36
|
+
state: useStore(controller.state),
|
|
37
|
+
setColumn: controller.setColumn,
|
|
38
|
+
setResolvedSection: controller.setResolvedSection,
|
|
39
|
+
setCard: controller.setCard
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { useMarginLayout };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=use-margin-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-margin-layout.js","names":[],"sources":["../../../../src/features/comments/react/use-margin-layout.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { useEffect, useRef, useState } from 'react'\nimport { useStore } from '../../../react/use-store'\nimport { createMarginController, type MarginState } from '../margin-controller'\nimport type { MarginCard } from '../margin-layout'\n\n/**\n * The layout of the comments margin for React: a thin hook over `createMarginController` (see there for the\n * measuring). Hand the column, the card and the resolved section elements in through the returned `set*` functions\n * (as ref callbacks); `state` holds the measured tops. Re-measures when `cards` or `activeId` change. It starts on\n * mount and stops on unmount.\n */\nexport function useMarginLayout({ editor, container, cards, activeId }: {\n editor: Editor | undefined\n /** The editor's scroll container, which the column sits in: its size changes re-run the layout. */\n container: HTMLElement | null\n /** The cards in document order, and the one that owns its line. */\n cards: readonly MarginCard[]\n activeId: string | null\n}) {\n // What the controller reads at each measurement: the latest render's values.\n const latest = useRef({ cards, activeId })\n latest.current = { cards, activeId }\n const [controller] = useState(() => createMarginController({ cards: () => latest.current.cards, activeId: () => latest.current.activeId }))\n\n useEffect(() => controller.setEditor(editor), [controller, editor])\n useEffect(() => controller.setContainer(container), [controller, container])\n // Paired start and stop, so StrictMode's mount, cleanup, mount leaves one observer.\n useEffect(() => {\n controller.connect()\n return controller.disconnect\n }, [controller])\n // Cards appearing, disappearing or expanding: measure once the DOM has them.\n useEffect(() => controller.schedule(), [controller, cards, activeId])\n\n const state: MarginState = useStore(controller.state)\n return { state, setColumn: controller.setColumn, setResolvedSection: controller.setResolvedSection, setCard: controller.setCard }\n}\n"],"mappings":";;;;;;;;;;AAYA,SAAgB,gBAAgB,EAAE,QAAQ,WAAW,OAAO,YAOzD;CAED,MAAM,SAAS,OAAO;EAAE;EAAO;CAAS,CAAC;CACzC,OAAO,UAAU;EAAE;EAAO;CAAS;CACnC,MAAM,CAAC,cAAc,eAAe,uBAAuB;EAAE,aAAa,OAAO,QAAQ;EAAO,gBAAgB,OAAO,QAAQ;CAAS,CAAC,CAAC;CAE1I,gBAAgB,WAAW,UAAU,MAAM,GAAG,CAAC,YAAY,MAAM,CAAC;CAClE,gBAAgB,WAAW,aAAa,SAAS,GAAG,CAAC,YAAY,SAAS,CAAC;CAE3E,gBAAgB;EACd,WAAW,QAAQ;EACnB,OAAO,WAAW;CACpB,GAAG,CAAC,UAAU,CAAC;CAEf,gBAAgB,WAAW,SAAS,GAAG;EAAC;EAAY;EAAO;CAAQ,CAAC;CAGpE,OAAO;EAAE,OADkB,SAAS,WAAW,KAClC;EAAG,WAAW,WAAW;EAAW,oBAAoB,WAAW;EAAoB,SAAS,WAAW;CAAQ;AAClI"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { CommentsFeatureOptions } from "./feature.js";
|
|
1
2
|
import { ReactFeature } from "../../react/EditorSurface.js";
|
|
2
3
|
import "../../react/index.js";
|
|
3
|
-
import { CommentsFeatureOptions } from "./feature.js";
|
|
4
4
|
import { CommentBubble } from "./react/CommentBubble.js";
|
|
5
5
|
import { CommentComposer } from "./react/CommentComposer.js";
|
|
6
6
|
import { CommentsListMenu } from "./react/CommentsListMenu.js";
|
|
@@ -20,6 +20,23 @@ export interface CommentReply {
|
|
|
20
20
|
createdAt: string;
|
|
21
21
|
editedAt?: string;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the
|
|
25
|
+
* first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text
|
|
26
|
+
* around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.
|
|
27
|
+
*/
|
|
28
|
+
export interface CommentAnchor {
|
|
29
|
+
type?: {
|
|
30
|
+
client: number;
|
|
31
|
+
clock: number;
|
|
32
|
+
};
|
|
33
|
+
tname?: string;
|
|
34
|
+
item?: {
|
|
35
|
+
client: number;
|
|
36
|
+
clock: number;
|
|
37
|
+
};
|
|
38
|
+
assoc?: number;
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
41
|
* A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`
|
|
25
42
|
* (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the
|
|
@@ -35,6 +52,12 @@ export interface CommentThread {
|
|
|
35
52
|
quote: string;
|
|
36
53
|
/** Resolved threads are kept, without highlight, until reopened. */
|
|
37
54
|
resolved: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it
|
|
57
|
+
* once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads
|
|
58
|
+
* not yet seen with a range, and when the position could not be captured.
|
|
59
|
+
*/
|
|
60
|
+
anchor?: CommentAnchor;
|
|
38
61
|
replies: CommentReply[];
|
|
39
62
|
}
|
|
40
63
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/comments/schema.ts"],"sourcesContent":["import { type AnyExtension, Mark, mergeAttributes } from '@tiptap/core'\nimport type { AuthorIdentity } from '../../core/feature'\nimport '../../core/schema/mark-flags'\n\n// Vue-free and server-importable (`@ecsplico/editor/comments/schema`): the comment mark that anchors a thread to\n// text, the thread data model and the limits. The Y.Map store, the highlights and the UI live in the feature.\n\nexport { COMMENTS_LAYOUTS, type CommentsLayout } from '../../settings/values'\n\n/** Name of the `Y.Map` holding the comment threads in the Y.Doc, beside the document's 'default' XmlFragment. */\nexport const COMMENTS_FIELD = 'comments'\n\n/** Maximum lengths of a comment's or reply's text and of the quoted text a thread keeps of its range. */\nexport const COMMENT_LIMITS = { text: 5000, quote: 200 } as const\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n replies: CommentReply[]\n}\n\n/**\n * Anchors a comment thread to text (attribute `id` = the thread's id). Storage only: the visible highlight is a\n * client-side decoration driven by the thread's state (`Comments` extension). `excludes: ''` lets the marks of\n * overlapping threads coexist on the same text; `inclusive: false` keeps text typed at the end of a range outside it.\n * No Markdown form (`markdownInvisible`).\n */\nexport const CommentMark = Mark.create({\n name: 'comment',\n excludes: '',\n inclusive: false,\n markdownInvisible: true,\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: el => el.getAttribute('data-comment-id'),\n renderHTML: attrs => ({ 'data-comment-id': attrs.id }),\n },\n }\n },\n parseHTML: () => [{ tag: 'span[data-comment-id]' }],\n renderHTML: ({ HTMLAttributes }) => ['span', mergeAttributes(HTMLAttributes, { class: 'comment' }), 0],\n})\n\n/** The comment mark of the document schema. */\nexport const commentsSchemaExtensions = (): AnyExtension[] => [CommentMark]\n"],"mappings":";;;;AAUA,MAAa,iBAAiB;;AAG9B,MAAa,iBAAiB;CAAE,MAAM;CAAM,OAAO;AAAI;;;;;;;
|
|
1
|
+
{"version":3,"file":"schema.js","names":[],"sources":["../../../src/features/comments/schema.ts"],"sourcesContent":["import { type AnyExtension, Mark, mergeAttributes } from '@tiptap/core'\nimport type { AuthorIdentity } from '../../core/feature'\nimport '../../core/schema/mark-flags'\n\n// Vue-free and server-importable (`@ecsplico/editor/comments/schema`): the comment mark that anchors a thread to\n// text, the thread data model and the limits. The Y.Map store, the highlights and the UI live in the feature.\n\nexport { COMMENTS_LAYOUTS, type CommentsLayout } from '../../settings/values'\n\n/** Name of the `Y.Map` holding the comment threads in the Y.Doc, beside the document's 'default' XmlFragment. */\nexport const COMMENTS_FIELD = 'comments'\n\n/** Maximum lengths of a comment's or reply's text and of the quoted text a thread keeps of its range. */\nexport const COMMENT_LIMITS = { text: 5000, quote: 200 } as const\n\n/** Who wrote a comment or reply: the writing user's identity, copied in when writing. */\nexport type CommentAuthor = AuthorIdentity\n\nexport interface CommentReply {\n id: string\n author: CommentAuthor\n text: string\n /** ISO timestamps; `editedAt` once the author changed the text. */\n createdAt: string\n editedAt?: string\n}\n\n/**\n * Where a thread's text starts, as the JSON form of a Yjs relative position (`Y.relativePositionToJSON`): bound to the\n * first commented character, so it keeps pointing at the place after that text was deleted (and moves with the text\n * around it). Stored in the thread's `Y.Map` under `anchor`, so every collaborator resolves the same place.\n */\nexport interface CommentAnchor {\n type?: { client: number, clock: number }\n tname?: string\n item?: { client: number, clock: number }\n assoc?: number\n}\n\n/**\n * A comment thread: the root comment plus its replies. Stored as a nested `Y.Map` in `ydoc.getMap(COMMENTS_FIELD)`\n * (replies as a nested `Y.Array`), so concurrent replies and edits of different users merge. Its text range is the\n * `comment` mark with the thread's `id`.\n */\nexport interface CommentThread {\n id: string\n author: CommentAuthor\n text: string\n createdAt: string\n editedAt?: string\n /** The commented text when the thread was created, cut to `COMMENT_LIMITS.quote`. */\n quote: string\n /** Resolved threads are kept, without highlight, until reopened. */\n resolved: boolean\n /**\n * Start of the commented text as a Yjs relative position, written when the thread is created (older threads get it\n * once they are seen with their text still in the document). Tells where an orphaned thread's text was. Absent on threads\n * not yet seen with a range, and when the position could not be captured.\n */\n anchor?: CommentAnchor\n replies: CommentReply[]\n}\n\n/**\n * Anchors a comment thread to text (attribute `id` = the thread's id). Storage only: the visible highlight is a\n * client-side decoration driven by the thread's state (`Comments` extension). `excludes: ''` lets the marks of\n * overlapping threads coexist on the same text; `inclusive: false` keeps text typed at the end of a range outside it.\n * No Markdown form (`markdownInvisible`).\n */\nexport const CommentMark = Mark.create({\n name: 'comment',\n excludes: '',\n inclusive: false,\n markdownInvisible: true,\n addAttributes() {\n return {\n id: {\n default: null,\n parseHTML: el => el.getAttribute('data-comment-id'),\n renderHTML: attrs => ({ 'data-comment-id': attrs.id }),\n },\n }\n },\n parseHTML: () => [{ tag: 'span[data-comment-id]' }],\n renderHTML: ({ HTMLAttributes }) => ['span', mergeAttributes(HTMLAttributes, { class: 'comment' }), 0],\n})\n\n/** The comment mark of the document schema. */\nexport const commentsSchemaExtensions = (): AnyExtension[] => [CommentMark]\n"],"mappings":";;;;AAUA,MAAa,iBAAiB;;AAG9B,MAAa,iBAAiB;CAAE,MAAM;CAAM,OAAO;AAAI;;;;;;;AAwDvD,MAAa,cAAc,KAAK,OAAO;CACrC,MAAM;CACN,UAAU;CACV,WAAW;CACX,mBAAmB;CACnB,gBAAgB;EACd,OAAO,EACL,IAAI;GACF,SAAS;GACT,YAAW,OAAM,GAAG,aAAa,iBAAiB;GAClD,aAAY,WAAU,EAAE,mBAAmB,MAAM,GAAG;EACtD,EACF;CACF;CACA,iBAAiB,CAAC,EAAE,KAAK,wBAAwB,CAAC;CAClD,aAAa,EAAE,qBAAqB;EAAC;EAAQ,gBAAgB,gBAAgB,EAAE,OAAO,UAAU,CAAC;EAAG;CAAC;AACvG,CAAC;;AAGD,MAAa,iCAAiD,CAAC,WAAW"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/features/comments/snapshot-equality.ts
|
|
2
|
+
const NO_RANGES = /* @__PURE__ */ new Map();
|
|
3
|
+
const NO_THREADS = [];
|
|
4
|
+
const NO_POSITIONS = /* @__PURE__ */ new Map();
|
|
5
|
+
/** The snapshot of an editor that has no comments (yet). */
|
|
6
|
+
const EMPTY_SNAPSHOT = {
|
|
7
|
+
available: false,
|
|
8
|
+
canComment: false,
|
|
9
|
+
user: null,
|
|
10
|
+
threads: NO_THREADS,
|
|
11
|
+
ranges: NO_RANGES,
|
|
12
|
+
orphanAt: NO_POSITIONS,
|
|
13
|
+
active: null,
|
|
14
|
+
draft: null,
|
|
15
|
+
draftQuote: "",
|
|
16
|
+
open: NO_THREADS,
|
|
17
|
+
resolved: NO_THREADS
|
|
18
|
+
};
|
|
19
|
+
function sameRange(a, b) {
|
|
20
|
+
return a === b || !!a && !!b && a.from === b.from && a.to === b.to;
|
|
21
|
+
}
|
|
22
|
+
function sameRanges(a, b) {
|
|
23
|
+
if (a === b) return true;
|
|
24
|
+
if (a.size !== b.size) return false;
|
|
25
|
+
for (const [id, range] of a) if (!sameRange(range, b.get(id) ?? null)) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function sameOrphanAt(a, b) {
|
|
29
|
+
if (a === b) return true;
|
|
30
|
+
if (a.size !== b.size) return false;
|
|
31
|
+
for (const [id, position] of a) if (b.get(id) !== position) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function sameUser(a, b) {
|
|
35
|
+
return a === b || !!a && !!b && a.id === b.id && a.name === b.name && a.color === b.color;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { EMPTY_SNAPSHOT, NO_POSITIONS, NO_RANGES, NO_THREADS, sameOrphanAt, sameRange, sameRanges, sameUser };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=snapshot-equality.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-equality.js","names":[],"sources":["../../../src/features/comments/snapshot-equality.ts"],"sourcesContent":["import type { AuthorIdentity } from '../../core'\nimport type { CommentsSnapshot, DocRange } from './model'\nimport type { CommentThread } from './schema'\n\n// What both comments models (the Y.Doc one in `./model` and the REST one in `adapters/comments/client`) need to hand\n// out a snapshot that keeps its identity while nothing in it changed. Type-only imports: this module stays free of `yjs`.\n\nexport const NO_RANGES: ReadonlyMap<string, DocRange> = new Map()\nexport const NO_THREADS: readonly CommentThread[] = []\nexport const NO_POSITIONS: ReadonlyMap<string, number> = new Map()\n\n/** The snapshot of an editor that has no comments (yet). */\nexport const EMPTY_SNAPSHOT: CommentsSnapshot = {\n available: false,\n canComment: false,\n user: null,\n threads: NO_THREADS,\n ranges: NO_RANGES,\n orphanAt: NO_POSITIONS,\n active: null,\n draft: null,\n draftQuote: '',\n open: NO_THREADS,\n resolved: NO_THREADS,\n}\n\nexport function sameRange(a: DocRange | null, b: DocRange | null): boolean {\n return a === b || (!!a && !!b && a.from === b.from && a.to === b.to)\n}\n\nexport function sameRanges(a: ReadonlyMap<string, DocRange>, b: ReadonlyMap<string, DocRange>): boolean {\n if (a === b) return true\n if (a.size !== b.size) return false\n for (const [id, range] of a) {\n if (!sameRange(range, b.get(id) ?? null)) return false\n }\n return true\n}\n\nexport function sameOrphanAt(a: ReadonlyMap<string, number>, b: ReadonlyMap<string, number>): boolean {\n if (a === b) return true\n if (a.size !== b.size) return false\n for (const [id, position] of a) {\n if (b.get(id) !== position) return false\n }\n return true\n}\n\nexport function sameUser(a: AuthorIdentity | null, b: AuthorIdentity | null): boolean {\n return a === b || (!!a && !!b && a.id === b.id && a.name === b.name && a.color === b.color)\n}\n"],"mappings":";AAOA,MAAa,4BAA2C,IAAI,IAAI;AAChE,MAAa,aAAuC,CAAC;AACrD,MAAa,+BAA4C,IAAI,IAAI;;AAGjE,MAAa,iBAAmC;CAC9C,WAAW;CACX,YAAY;CACZ,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,OAAO;CACP,YAAY;CACZ,MAAM;CACN,UAAU;AACZ;AAEA,SAAgB,UAAU,GAAoB,GAA6B;CACzE,OAAO,MAAM,KAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE;AACnE;AAEA,SAAgB,WAAW,GAAkC,GAA2C;CACtG,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,SAAS,EAAE,MAAM,OAAO;CAC9B,KAAK,MAAM,CAAC,IAAI,UAAU,GACxB,IAAI,CAAC,UAAU,OAAO,EAAE,IAAI,EAAE,KAAK,IAAI,GAAG,OAAO;CAEnD,OAAO;AACT;AAEA,SAAgB,aAAa,GAAgC,GAAyC;CACpG,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,EAAE,SAAS,EAAE,MAAM,OAAO;CAC9B,KAAK,MAAM,CAAC,IAAI,aAAa,GAC3B,IAAI,EAAE,IAAI,EAAE,MAAM,UAAU,OAAO;CAErC,OAAO;AACT;AAEA,SAAgB,SAAS,GAA0B,GAAmC;CACpF,OAAO,MAAM,KAAM,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE;AACvF"}
|