collabmd 0.1.45 → 0.1.46
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 +39 -19
- package/bin/collabmd.js +27 -1
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
- package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
- package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
- package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
- package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
- package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
- package/dist/client/assets/dist-B5HTH2BO.js +1 -0
- package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
- package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
- package/dist/client/assets/dist-BEwK4csK.js +1 -0
- package/dist/client/assets/dist-BKW4PALF.js +1 -0
- package/dist/client/assets/dist-BWcHHOx-.js +1 -0
- package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
- package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
- package/dist/client/assets/dist-BjHJDtyU.js +1 -0
- package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
- package/dist/client/assets/dist-BoVBdSjk.js +1 -0
- package/dist/client/assets/dist-CC5IrQMG.js +1 -0
- package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
- package/dist/client/assets/dist-CZgOmxK6.js +1 -0
- package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
- package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
- package/dist/client/assets/dist-DLMiIsNC.js +11 -0
- package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
- package/dist/client/assets/dist-DaYdhcVs.js +1 -0
- package/dist/client/assets/dist-FWJsgGYI.js +1 -0
- package/dist/client/assets/dist-Kh2COra_.js +1 -0
- package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
- package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
- package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
- package/dist/client/assets/dist-jv4WdPnK.js +9 -0
- package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
- package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
- package/dist/client/assets/dist-tOaegDYS2.js +1 -0
- package/dist/client/assets/dist-ule2n6ak.js +1 -0
- package/dist/client/assets/dist-xElaxbia.js +1 -0
- package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-FzY1cVw8.js} +1 -1
- package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
- package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
- package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
- package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
- package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
- package/dist/client/assets/{excalidraw-editor-CyGwOhLz.css → excalidraw-editor-Bt7gMSoh.css} +1 -1
- package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
- package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
- package/dist/client/assets/{export-page-CUfws2T-.js → export-page-A7NHawJY.js} +3 -3
- package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
- package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
- package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
- package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
- package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
- package/dist/client/assets/index-ClbjMBVt.js +2 -0
- package/dist/client/assets/index-Pum8ET-5.css +1 -0
- package/dist/client/assets/main-B3uPyqic.js +893 -0
- package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
- package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
- package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
- package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
- package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
- package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
- package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
- package/dist/client/assets/prod-Dy20X7eT.js +1 -0
- package/dist/client/assets/prod-hnwh7wRM.js +177 -0
- package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
- package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
- package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +15 -3
- package/docker-compose.yml +42 -0
- package/package.json +17 -4
- package/scripts/local-structurizr-compose.mjs +101 -0
- package/src/client/app/index.html +13 -1
- package/src/client/application/app-shell/comments-feature.js +19 -10
- package/src/client/application/app-shell/git-feature.js +12 -0
- package/src/client/application/app-shell/presence-feature.js +2 -1
- package/src/client/application/app-shell/ui-feature-shell.js +45 -2
- package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
- package/src/client/application/app-shell/workspace-feature.js +5 -1
- package/src/client/application/app-shell-elements.js +2 -0
- package/src/client/application/pdf-preview-controller.js +992 -0
- package/src/client/application/structurizr-preview-controller.js +395 -0
- package/src/client/application/workspace-coordinator.js +25 -3
- package/src/client/application/workspace-preview-controller.js +91 -2
- package/src/client/bootstrap/collabmd-app-shell.js +31 -5
- package/src/client/browser-utils.js +44 -0
- package/src/client/domain/runtime-paths.js +26 -5
- package/src/client/domain/structurizr-language.js +99 -0
- package/src/client/domain/vault-paths.js +48 -0
- package/src/client/excalidraw-editor.js +833 -51
- package/src/client/infrastructure/browser-preferences-port.js +10 -0
- package/src/client/infrastructure/comment-thread-store.js +34 -13
- package/src/client/infrastructure/editor-session.js +10 -0
- package/src/client/infrastructure/editor-view-adapter.js +46 -2
- package/src/client/infrastructure/excalidraw-room-client.js +103 -14
- package/src/client/infrastructure/git-api-client.js +5 -0
- package/src/client/infrastructure/vault-api-client.js +26 -0
- package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
- package/src/client/presentation/drawio-embed-controller.js +9 -102
- package/src/client/presentation/drawio-viewer.js +118 -0
- package/src/client/presentation/excalidraw-embed-controller.js +74 -8
- package/src/client/presentation/file-action-controller.js +102 -0
- package/src/client/presentation/file-explorer-view.js +10 -29
- package/src/client/presentation/file-icon-svg.js +40 -0
- package/src/client/presentation/file-tree-state.js +2 -0
- package/src/client/presentation/git-diff-view-controller.js +731 -3
- package/src/client/presentation/outline-controller.js +86 -0
- package/src/client/presentation/quick-switcher-controller.js +25 -13
- package/src/client/presentation/quick-switcher-text-search.js +1 -1
- package/src/client/styles/components/navigation.css +11 -1
- package/src/client/styles/features/diagram-preview.css +76 -0
- package/src/client/styles/features/diff-special.css +309 -0
- package/src/client/styles/foundation/themes.css +6 -0
- package/src/client/styles/layout/shell.css +358 -1
- package/src/client/styles/layout/view-modes.css +1 -0
- package/src/client/styles/style.css +1 -0
- package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
- package/src/domain/comment-threads.js +63 -5
- package/src/domain/excalidraw-room-codec.js +6 -4
- package/src/domain/file-kind.js +35 -3
- package/src/server/config/env.js +79 -0
- package/src/server/create-app-server.js +12 -0
- package/src/server/domain/bases/base-definition.js +1 -1
- package/src/server/domain/bases/base-expression-runtime.js +1 -4
- package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
- package/src/server/domain/bases/base-transform.js +1 -4
- package/src/server/domain/collaboration/collaboration-room.js +55 -21
- package/src/server/domain/comment-overview.js +15 -5
- package/src/server/domain/ripgrep-search-service.js +4 -0
- package/src/server/infrastructure/git/diff-service.js +5 -0
- package/src/server/infrastructure/git/git-service.js +21 -0
- package/src/server/infrastructure/git/history-service.js +54 -0
- package/src/server/infrastructure/git/image-mime.js +14 -0
- package/src/server/infrastructure/git/untracked-files.js +28 -1
- package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
- package/src/server/infrastructure/http/create-request-handler.js +16 -23
- package/src/server/infrastructure/http/create-static-handler.js +5 -2
- package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
- package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
- package/src/server/infrastructure/http/http-request-helpers.js +22 -0
- package/src/server/infrastructure/persistence/path-utils.js +1 -1
- package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
- package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
- package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
- package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
- package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
- package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
- package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
- package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
- package/dist/client/assets/dist-BH87JoHx.js +0 -11
- package/dist/client/assets/dist-BIkPRZO7.js +0 -6
- package/dist/client/assets/dist-BqCsFpx4.js +0 -1
- package/dist/client/assets/dist-C2JHCt8U.js +0 -1
- package/dist/client/assets/dist-CZjTtmTu.js +0 -1
- package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
- package/dist/client/assets/dist-CqQEC8gR.js +0 -1
- package/dist/client/assets/dist-D4Rzqod1.js +0 -1
- package/dist/client/assets/dist-D5sn-mwk.js +0 -9
- package/dist/client/assets/dist-D7G-n87v.js +0 -1
- package/dist/client/assets/dist-DRqOrNgN.js +0 -1
- package/dist/client/assets/dist-DetkMS_4.js +0 -1
- package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
- package/dist/client/assets/dist-DtbjNB28.js +0 -1
- package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
- package/dist/client/assets/dist-GrygD2zE.js +0 -1
- package/dist/client/assets/dist-HmEpWUow.js +0 -1
- package/dist/client/assets/dist-RTa81Km7.js +0 -1
- package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
- package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
- package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
- package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
- package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
- package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
- package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
- package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
- package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
- package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
- package/dist/client/assets/index-C3YnUgMD.css +0 -1
- package/dist/client/assets/index-D2wXraJ1.js +0 -2
- package/dist/client/assets/main-DtiGz7uI.js +0 -795
- package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
- package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
- package/dist/client/assets/room-CQLoqtMo.js +0 -1
- package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
- package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
- package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
reconcileElements,
|
|
7
7
|
restoreAppState,
|
|
8
8
|
restoreElements,
|
|
9
|
+
sceneCoordsToViewportCoords,
|
|
9
10
|
} from '@excalidraw/excalidraw';
|
|
11
|
+
import { getElementBounds } from '@excalidraw/element';
|
|
10
12
|
import '@excalidraw/excalidraw/index.css';
|
|
11
13
|
|
|
12
14
|
import {
|
|
@@ -17,6 +19,9 @@ import {
|
|
|
17
19
|
resolveLocalAwarenessUser,
|
|
18
20
|
} from './domain/excalidraw-collaboration.js';
|
|
19
21
|
import './styles/surfaces/embedded-editor-base.css';
|
|
22
|
+
import './styles/features/comment-markdown.css';
|
|
23
|
+
import './styles/features/comment-overview.css';
|
|
24
|
+
import './styles/features/comments-drawer.css';
|
|
20
25
|
import './styles/surfaces/excalidraw-editor.css';
|
|
21
26
|
import {
|
|
22
27
|
applySceneUpdateWithFiles,
|
|
@@ -27,6 +32,11 @@ import {
|
|
|
27
32
|
parseSceneJson,
|
|
28
33
|
sceneToInitialData,
|
|
29
34
|
} from './domain/excalidraw-scene.js';
|
|
35
|
+
import {
|
|
36
|
+
createCommentOverviewThread,
|
|
37
|
+
formatAnchorLabel,
|
|
38
|
+
getLatestMessage,
|
|
39
|
+
} from './presentation/comment-ui/comment-ui-shared.js';
|
|
30
40
|
import {
|
|
31
41
|
buildReconciledExcalidrawSceneUpdate,
|
|
32
42
|
} from './domain/excalidraw-scene-reconcile.js';
|
|
@@ -76,17 +86,39 @@ let apiStateCleanupCallbacks = [];
|
|
|
76
86
|
let collaboratorRenderFrame = 0;
|
|
77
87
|
let queuedCollaborators = null;
|
|
78
88
|
let initialViewportFitPending = true;
|
|
89
|
+
let viewportFitGeneration = 0;
|
|
79
90
|
let previewViewportFitTimerId = 0;
|
|
91
|
+
let previewViewportFitRetryTimerId = 0;
|
|
80
92
|
let roomClient = null;
|
|
81
93
|
let roomClientGeneration = 0;
|
|
82
94
|
let reactRoot = null;
|
|
83
95
|
let editorRenderKey = 0;
|
|
84
96
|
let skipRoomDisconnectOnUnmount = false;
|
|
85
97
|
let roomConnectionState = EXCALIDRAW_ROOM_CONNECTION_STATE.CONNECTING;
|
|
98
|
+
let diagramCommentThreads = [];
|
|
99
|
+
let pendingDiagramCommentFocus = null;
|
|
100
|
+
let diagramCommentFocusRequestId = 0;
|
|
86
101
|
let parkRequestedWhileBlocked = false;
|
|
87
102
|
const pendingDisconnectRequestIds = new Set();
|
|
88
103
|
const reportedFileConflictSignatures = new Set();
|
|
89
104
|
|
|
105
|
+
function getMountedExcalidrawAPI() {
|
|
106
|
+
return excalidrawAPI && !excalidrawAPI.isDestroyed ? excalidrawAPI : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function requestDiagramCommentFocus(threadId) {
|
|
110
|
+
const normalizedThreadId = String(threadId ?? '').trim();
|
|
111
|
+
if (!normalizedThreadId) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pendingDiagramCommentFocus = {
|
|
116
|
+
requestId: ++diagramCommentFocusRequestId,
|
|
117
|
+
threadId: normalizedThreadId,
|
|
118
|
+
};
|
|
119
|
+
renderExcalidrawApp();
|
|
120
|
+
}
|
|
121
|
+
|
|
90
122
|
if (diagnostics.enabled) {
|
|
91
123
|
window.__COLLABMD_EXCALIDRAW_DEBUG__ = diagnostics;
|
|
92
124
|
}
|
|
@@ -120,6 +152,213 @@ function getAuthorityBannerText() {
|
|
|
120
152
|
return '';
|
|
121
153
|
}
|
|
122
154
|
|
|
155
|
+
function getSelectedDiagramElement() {
|
|
156
|
+
const api = getMountedExcalidrawAPI();
|
|
157
|
+
const selectedElementIds = api?.getAppState?.()?.selectedElementIds || {};
|
|
158
|
+
const selectedElements = api?.getSceneElementsIncludingDeleted?.()
|
|
159
|
+
?.filter((element) => selectedElementIds[element.id] && !element.isDeleted) ?? [];
|
|
160
|
+
return selectedElements.length === 1 ? selectedElements[0] : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeDiagramLabelText(value, maxLength = 72) {
|
|
164
|
+
const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
165
|
+
return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getDiagramElementContent(element, api = getMountedExcalidrawAPI()) {
|
|
169
|
+
const directText = normalizeDiagramLabelText(element?.text);
|
|
170
|
+
if (directText) {
|
|
171
|
+
return directText;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const sceneElements = api?.getSceneElementsIncludingDeleted?.() || [];
|
|
175
|
+
const boundElementIds = new Set(
|
|
176
|
+
Array.isArray(element?.boundElements)
|
|
177
|
+
? element.boundElements.map((boundElement) => boundElement?.id).filter(Boolean)
|
|
178
|
+
: [],
|
|
179
|
+
);
|
|
180
|
+
if (boundElementIds.size === 0) {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return normalizeDiagramLabelText(
|
|
185
|
+
sceneElements
|
|
186
|
+
.filter((sceneElement) => boundElementIds.has(sceneElement.id) && !sceneElement.isDeleted)
|
|
187
|
+
.map((sceneElement) => sceneElement.text)
|
|
188
|
+
.filter((text) => typeof text === 'string' && text.trim())
|
|
189
|
+
.join(' '),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function formatDiagramAnchorLabel(type, content = '') {
|
|
194
|
+
const normalizedType = typeof type === 'string' && type.trim() ? type.trim() : 'element';
|
|
195
|
+
const typeLabel = normalizedType === 'element'
|
|
196
|
+
? 'Diagram element'
|
|
197
|
+
: `${normalizedType.charAt(0).toUpperCase()}${normalizedType.slice(1)} element`;
|
|
198
|
+
const normalizedContent = normalizeDiagramLabelText(content);
|
|
199
|
+
return normalizedContent ? `${typeLabel} · ${normalizedContent}` : typeLabel;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getDiagramAnchorLabel(thread) {
|
|
203
|
+
const snapshot = thread?.anchorSnapshot;
|
|
204
|
+
const element = getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
|
|
205
|
+
?.find((candidate) => candidate.id === thread?.elementId && !candidate.isDeleted);
|
|
206
|
+
const elementContent = normalizeDiagramLabelText(snapshot?.text) || getDiagramElementContent(element);
|
|
207
|
+
const firstMessageContent = thread?.messages?.[0]?.body;
|
|
208
|
+
return formatDiagramAnchorLabel(snapshot?.type || element?.type, elementContent || firstMessageContent);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const diagramCommentTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
212
|
+
day: 'numeric',
|
|
213
|
+
hour: 'numeric',
|
|
214
|
+
minute: '2-digit',
|
|
215
|
+
month: 'short',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
function formatDiagramCommentTimestamp(value) {
|
|
219
|
+
if (!Number.isFinite(value)) {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
return diagramCommentTimeFormatter.format(new Date(value));
|
|
225
|
+
} catch {
|
|
226
|
+
return '';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function renderDiagramCommentOverviewThread(thread, activeThreadId, onOpen) {
|
|
231
|
+
const latestMessage = getLatestMessage(thread?.messages ?? []);
|
|
232
|
+
const messageCount = Array.isArray(thread?.messages) ? thread.messages.length : 0;
|
|
233
|
+
const button = createCommentOverviewThread({
|
|
234
|
+
authorName: latestMessage?.userName || thread?.createdByName || 'Anonymous',
|
|
235
|
+
buttonClassName: 'comment-overview-thread comments-drawer-item',
|
|
236
|
+
footerClassName: 'comment-overview-thread-footer comments-drawer-item-footer',
|
|
237
|
+
headerClassName: 'comment-overview-thread-header comments-drawer-item-header',
|
|
238
|
+
lineClassName: 'comment-overview-thread-line comments-drawer-item-title',
|
|
239
|
+
lineLabel: formatAnchorLabel(thread),
|
|
240
|
+
messageCount,
|
|
241
|
+
previewBody: latestMessage?.body || '',
|
|
242
|
+
previewClassName: 'comment-markdown comment-overview-thread-preview comments-drawer-item-preview',
|
|
243
|
+
quote: thread?.anchorQuote || thread?.anchorSnapshot?.text || 'Diagram element',
|
|
244
|
+
quoteClassName: 'comment-overview-thread-quote comments-drawer-item-quote',
|
|
245
|
+
timestamp: formatDiagramCommentTimestamp(latestMessage?.createdAt),
|
|
246
|
+
});
|
|
247
|
+
button.classList.toggle('is-active', thread.id === activeThreadId);
|
|
248
|
+
|
|
249
|
+
return React.createElement('button', {
|
|
250
|
+
className: button.className,
|
|
251
|
+
dangerouslySetInnerHTML: { __html: button.innerHTML },
|
|
252
|
+
key: thread.id,
|
|
253
|
+
onClick: () => onOpen(thread.id),
|
|
254
|
+
type: 'button',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function DiagramCommentIcon({ add = false }) {
|
|
259
|
+
const paths = [React.createElement('path', {
|
|
260
|
+
d: 'M3 4.75A1.75 1.75 0 0 1 4.75 3h6.5A1.75 1.75 0 0 1 13 4.75v4.5A1.75 1.75 0 0 1 11.25 11H8.9L6.5 13v-2H4.75A1.75 1.75 0 0 1 3 9.25v-4.5Z',
|
|
261
|
+
fill: 'none',
|
|
262
|
+
key: 'bubble',
|
|
263
|
+
stroke: 'currentColor',
|
|
264
|
+
strokeLinejoin: 'round',
|
|
265
|
+
strokeWidth: '1.35',
|
|
266
|
+
})];
|
|
267
|
+
if (add) {
|
|
268
|
+
paths.push(React.createElement('path', {
|
|
269
|
+
d: 'M8 5.5v3.5M6.25 7.25h3.5',
|
|
270
|
+
fill: 'none',
|
|
271
|
+
key: 'plus',
|
|
272
|
+
stroke: 'currentColor',
|
|
273
|
+
strokeLinecap: 'round',
|
|
274
|
+
strokeWidth: '1.35',
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return React.createElement('svg', {
|
|
279
|
+
'aria-hidden': 'true',
|
|
280
|
+
className: 'diagram-comment-icon',
|
|
281
|
+
fill: 'none',
|
|
282
|
+
focusable: 'false',
|
|
283
|
+
viewBox: '0 0 16 16',
|
|
284
|
+
}, paths);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getDiagramCommentMarkerPosition(thread, api = getMountedExcalidrawAPI()) {
|
|
288
|
+
if (!api || thread?.anchorKind !== 'diagram-element') {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let scenePoint = thread.anchorPoint;
|
|
293
|
+
const element = api.getSceneElementsIncludingDeleted?.()
|
|
294
|
+
?.find((candidate) => candidate.id === thread.elementId && !candidate.isDeleted);
|
|
295
|
+
if (element) {
|
|
296
|
+
try {
|
|
297
|
+
const bounds = getElementBounds(element, api.getSceneElementsMapIncludingDeleted?.());
|
|
298
|
+
scenePoint = {
|
|
299
|
+
x: bounds[2],
|
|
300
|
+
y: bounds[1],
|
|
301
|
+
};
|
|
302
|
+
} catch {
|
|
303
|
+
scenePoint = thread.anchorPoint;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const x = Number(scenePoint?.x);
|
|
308
|
+
const y = Number(scenePoint?.y);
|
|
309
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const appState = api.getAppState?.();
|
|
314
|
+
if (!appState?.zoom || !Number.isFinite(appState.scrollX) || !Number.isFinite(appState.scrollY)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return sceneCoordsToViewportCoords(
|
|
319
|
+
{ sceneX: x, sceneY: y },
|
|
320
|
+
{
|
|
321
|
+
offsetLeft: Number.isFinite(appState.offsetLeft) ? appState.offsetLeft : 0,
|
|
322
|
+
offsetTop: Number.isFinite(appState.offsetTop) ? appState.offsetTop : 0,
|
|
323
|
+
scrollX: appState.scrollX,
|
|
324
|
+
scrollY: appState.scrollY,
|
|
325
|
+
zoom: appState.zoom,
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function focusDiagramCommentThread(thread) {
|
|
331
|
+
const api = getMountedExcalidrawAPI();
|
|
332
|
+
const element = api?.getSceneElementsIncludingDeleted?.()
|
|
333
|
+
?.find((candidate) => candidate.id === thread?.elementId && !candidate.isDeleted);
|
|
334
|
+
if (!api || !element) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
api.updateScene({
|
|
339
|
+
appState: {
|
|
340
|
+
selectedElementIds: { [element.id]: true },
|
|
341
|
+
},
|
|
342
|
+
captureUpdate: CaptureUpdateAction.NEVER,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
if (typeof api.setViewport !== 'function') {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
suppressViewportBroadcast = true;
|
|
350
|
+
try {
|
|
351
|
+
api.setViewport({
|
|
352
|
+
animation: false,
|
|
353
|
+
fit: 'contain',
|
|
354
|
+
offsets: { ui: true },
|
|
355
|
+
target: element,
|
|
356
|
+
});
|
|
357
|
+
} finally {
|
|
358
|
+
releaseViewportBroadcastSuppressionAfterPaint();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
123
362
|
function applyDocumentMode(mode = currentDocument.mode) {
|
|
124
363
|
document.body.dataset.documentMode = normalizeDocumentMode(mode);
|
|
125
364
|
}
|
|
@@ -140,6 +379,16 @@ function createRoomClient(filePath) {
|
|
|
140
379
|
|
|
141
380
|
queueCollaboratorsRender(collaborators);
|
|
142
381
|
},
|
|
382
|
+
onCommentThreadsChange: (threads) => {
|
|
383
|
+
if (generation !== roomClientGeneration) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
diagramCommentThreads = Array.isArray(threads) ? threads : [];
|
|
388
|
+
if (reactRoot) {
|
|
389
|
+
renderExcalidrawApp();
|
|
390
|
+
}
|
|
391
|
+
},
|
|
143
392
|
onConnectionStateChange: (event) => {
|
|
144
393
|
if (generation !== roomClientGeneration) {
|
|
145
394
|
return;
|
|
@@ -164,7 +413,7 @@ function createRoomClient(filePath) {
|
|
|
164
413
|
};
|
|
165
414
|
}
|
|
166
415
|
|
|
167
|
-
function buildExcalidrawProps({ initialData } = {}) {
|
|
416
|
+
function buildExcalidrawProps({ initialData, renderTopRightUI, viewModeEnabled } = {}) {
|
|
168
417
|
const props = {
|
|
169
418
|
onMount: handleEditorMount,
|
|
170
419
|
onInitialize: (api) => {
|
|
@@ -172,6 +421,7 @@ function buildExcalidrawProps({ initialData } = {}) {
|
|
|
172
421
|
},
|
|
173
422
|
onUnmount: () => {
|
|
174
423
|
clearEditorApiStateBindings();
|
|
424
|
+
clearPreviewViewportFitTimers();
|
|
175
425
|
if (skipRoomDisconnectOnUnmount) {
|
|
176
426
|
skipRoomDisconnectOnUnmount = false;
|
|
177
427
|
} else {
|
|
@@ -189,12 +439,17 @@ function buildExcalidrawProps({ initialData } = {}) {
|
|
|
189
439
|
onPointerUpdate: (payload) => {
|
|
190
440
|
roomClient?.scheduleLocalPointerAwareness(payload);
|
|
191
441
|
},
|
|
442
|
+
onPointerDown: () => {
|
|
443
|
+
initialViewportFitPending = false;
|
|
444
|
+
},
|
|
445
|
+
renderTopRightUI,
|
|
192
446
|
historyOptions: {
|
|
193
447
|
traversal: 'single-entry',
|
|
194
448
|
},
|
|
195
449
|
onHistoryAction: handleHistoryAction,
|
|
196
450
|
theme: currentTheme,
|
|
197
451
|
...getDocumentViewState(),
|
|
452
|
+
...(typeof viewModeEnabled === 'boolean' ? { viewModeEnabled } : {}),
|
|
198
453
|
UIOptions: {
|
|
199
454
|
canvasActions: {
|
|
200
455
|
export: false,
|
|
@@ -212,6 +467,425 @@ function buildExcalidrawProps({ initialData } = {}) {
|
|
|
212
467
|
return props;
|
|
213
468
|
}
|
|
214
469
|
|
|
470
|
+
const diagramCommentsContext = React.createContext(null);
|
|
471
|
+
|
|
472
|
+
function DiagramCommentsToolbar() {
|
|
473
|
+
const context = React.useContext(diagramCommentsContext);
|
|
474
|
+
if (!context?.visible || !context.room) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const addCommentButton = context.selectedElement
|
|
479
|
+
? React.createElement('button', {
|
|
480
|
+
'aria-label': 'Add comment',
|
|
481
|
+
className: 'diagram-comment-add sidebar-trigger',
|
|
482
|
+
'data-testid': 'diagram-add-comment',
|
|
483
|
+
disabled: !context.canWrite,
|
|
484
|
+
key: 'add',
|
|
485
|
+
onClick: context.openComposer,
|
|
486
|
+
title: context.canWrite ? 'Add comment to selected element' : 'Reconnect to add a comment',
|
|
487
|
+
type: 'button',
|
|
488
|
+
}, React.createElement(DiagramCommentIcon, { add: true }))
|
|
489
|
+
: null;
|
|
490
|
+
|
|
491
|
+
return React.createElement('div', {
|
|
492
|
+
'aria-label': 'Diagram comments',
|
|
493
|
+
className: 'diagram-comments-toolbar',
|
|
494
|
+
role: 'toolbar',
|
|
495
|
+
}, [
|
|
496
|
+
addCommentButton,
|
|
497
|
+
React.createElement('button', {
|
|
498
|
+
'aria-expanded': context.drawerOpen,
|
|
499
|
+
'aria-label': 'Comments',
|
|
500
|
+
className: `diagram-comment-button sidebar-trigger${context.drawerOpen ? ' active' : ''}`,
|
|
501
|
+
'data-testid': 'diagram-comments-toggle',
|
|
502
|
+
key: 'toggle',
|
|
503
|
+
onClick: context.toggleDrawer,
|
|
504
|
+
title: context.drawerOpen ? 'Close comments' : 'Open comments',
|
|
505
|
+
type: 'button',
|
|
506
|
+
}, [
|
|
507
|
+
React.createElement(DiagramCommentIcon, { key: 'icon' }),
|
|
508
|
+
context.threads.length > 0
|
|
509
|
+
? React.createElement('span', {
|
|
510
|
+
className: 'diagram-comment-count',
|
|
511
|
+
key: 'count',
|
|
512
|
+
}, String(context.threads.length))
|
|
513
|
+
: null,
|
|
514
|
+
]),
|
|
515
|
+
]);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const DiagramCommentsExcalidraw = React.memo(function DiagramCommentsExcalidraw({ initialData = null, renderKey = 0, viewModeEnabled }) {
|
|
519
|
+
const renderTopRightUI = React.useCallback(
|
|
520
|
+
() => React.createElement(DiagramCommentsToolbar),
|
|
521
|
+
[],
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return React.createElement(Excalidraw, {
|
|
525
|
+
key: `editor-${renderKey}`,
|
|
526
|
+
...buildExcalidrawProps({ initialData, renderTopRightUI, viewModeEnabled }),
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
function useCloseDiagramCommentsOnCanvas(drawerOpen, setDrawerOpen) {
|
|
531
|
+
React.useEffect(() => {
|
|
532
|
+
if (!drawerOpen) {
|
|
533
|
+
return undefined;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const closeOnCanvasPointerDown = (event) => {
|
|
537
|
+
if (!(event.target instanceof Element) || !event.target.closest('canvas.excalidraw__canvas')) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
setDrawerOpen(false);
|
|
542
|
+
};
|
|
543
|
+
const closeOnEscape = (event) => {
|
|
544
|
+
if (event.key !== 'Escape') {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
event.stopPropagation();
|
|
550
|
+
setDrawerOpen(false);
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
document.addEventListener('pointerdown', closeOnCanvasPointerDown, true);
|
|
554
|
+
document.addEventListener('keydown', closeOnEscape, true);
|
|
555
|
+
return () => {
|
|
556
|
+
document.removeEventListener('pointerdown', closeOnCanvasPointerDown, true);
|
|
557
|
+
document.removeEventListener('keydown', closeOnEscape, true);
|
|
558
|
+
};
|
|
559
|
+
}, [drawerOpen, setDrawerOpen]);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function DiagramCommentsEditor({ apiId = '', canWrite = false, focusRequest = null, initialData = null, room = null, threads = [], visible = false }) {
|
|
563
|
+
const [activeThreadId, setActiveThreadId] = React.useState(null);
|
|
564
|
+
const [composerOpen, setComposerOpen] = React.useState(false);
|
|
565
|
+
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
|
566
|
+
const [draft, setDraft] = React.useState('');
|
|
567
|
+
const [replyDraft, setReplyDraft] = React.useState('');
|
|
568
|
+
const [, setViewportRevision] = React.useState(0);
|
|
569
|
+
|
|
570
|
+
React.useEffect(() => {
|
|
571
|
+
const api = getMountedExcalidrawAPI();
|
|
572
|
+
if (!api) {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const refresh = () => setViewportRevision((revision) => revision + 1);
|
|
577
|
+
const cleanups = [
|
|
578
|
+
api.onChange?.(refresh),
|
|
579
|
+
api.onScrollChange?.(refresh),
|
|
580
|
+
].filter((cleanup) => typeof cleanup === 'function');
|
|
581
|
+
window.addEventListener('resize', refresh);
|
|
582
|
+
return () => {
|
|
583
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
584
|
+
window.removeEventListener('resize', refresh);
|
|
585
|
+
};
|
|
586
|
+
}, [apiId]);
|
|
587
|
+
|
|
588
|
+
useCloseDiagramCommentsOnCanvas(drawerOpen, setDrawerOpen);
|
|
589
|
+
|
|
590
|
+
React.useEffect(() => {
|
|
591
|
+
if (activeThreadId && !threads.some((thread) => thread.id === activeThreadId)) {
|
|
592
|
+
setActiveThreadId(null);
|
|
593
|
+
}
|
|
594
|
+
}, [activeThreadId, threads]);
|
|
595
|
+
|
|
596
|
+
React.useEffect(() => {
|
|
597
|
+
const threadId = focusRequest?.threadId;
|
|
598
|
+
const thread = threads.find((candidate) => candidate.id === threadId);
|
|
599
|
+
if (!thread) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
setDrawerOpen(true);
|
|
604
|
+
setComposerOpen(false);
|
|
605
|
+
setActiveThreadId(threadId);
|
|
606
|
+
setReplyDraft('');
|
|
607
|
+
focusDiagramCommentThread(thread);
|
|
608
|
+
}, [focusRequest?.requestId, threads]);
|
|
609
|
+
|
|
610
|
+
const selectedElement = getSelectedDiagramElement();
|
|
611
|
+
const activeThread = threads.find((thread) => thread.id === activeThreadId) ?? null;
|
|
612
|
+
const openComposer = () => {
|
|
613
|
+
if (!canWrite || !selectedElement) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
setDrawerOpen(true);
|
|
618
|
+
setComposerOpen(true);
|
|
619
|
+
setActiveThreadId(null);
|
|
620
|
+
};
|
|
621
|
+
const openThread = (threadId) => {
|
|
622
|
+
setDrawerOpen(true);
|
|
623
|
+
setComposerOpen(false);
|
|
624
|
+
setActiveThreadId(threadId);
|
|
625
|
+
setReplyDraft('');
|
|
626
|
+
};
|
|
627
|
+
const submitComposer = (event) => {
|
|
628
|
+
event.preventDefault();
|
|
629
|
+
const selectedElementForAnchor = getSelectedDiagramElement();
|
|
630
|
+
const elementContent = getDiagramElementContent(selectedElementForAnchor);
|
|
631
|
+
const threadId = room.createCommentThread({
|
|
632
|
+
body: draft,
|
|
633
|
+
element: elementContent
|
|
634
|
+
? { ...selectedElementForAnchor, text: elementContent }
|
|
635
|
+
: selectedElementForAnchor,
|
|
636
|
+
});
|
|
637
|
+
if (!threadId) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
setDraft('');
|
|
642
|
+
setComposerOpen(false);
|
|
643
|
+
setActiveThreadId(threadId);
|
|
644
|
+
};
|
|
645
|
+
const submitReply = (event) => {
|
|
646
|
+
event.preventDefault();
|
|
647
|
+
if (!activeThread) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const messageId = room.replyToCommentThread(activeThread.id, replyDraft);
|
|
652
|
+
if (!messageId) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
setReplyDraft('');
|
|
657
|
+
};
|
|
658
|
+
const resolveThread = () => {
|
|
659
|
+
if (!activeThread || !room.deleteCommentThread(activeThread.id)) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setActiveThreadId(null);
|
|
664
|
+
};
|
|
665
|
+
const renderComposer = () => React.createElement('form', {
|
|
666
|
+
className: 'diagram-comment-form',
|
|
667
|
+
onSubmit: submitComposer,
|
|
668
|
+
}, [
|
|
669
|
+
React.createElement('p', {
|
|
670
|
+
className: 'diagram-comment-anchor-label',
|
|
671
|
+
key: 'anchor',
|
|
672
|
+
}, selectedElement
|
|
673
|
+
? `On ${formatDiagramAnchorLabel(selectedElement.type, getDiagramElementContent(selectedElement))}`
|
|
674
|
+
: 'On selected element'),
|
|
675
|
+
React.createElement('textarea', {
|
|
676
|
+
'aria-label': 'Comment',
|
|
677
|
+
autoFocus: true,
|
|
678
|
+
className: 'diagram-comment-input',
|
|
679
|
+
key: 'input',
|
|
680
|
+
maxLength: 2000,
|
|
681
|
+
onChange: (event) => setDraft(event.target.value),
|
|
682
|
+
placeholder: 'Add context, feedback, or a question…',
|
|
683
|
+
rows: 4,
|
|
684
|
+
value: draft,
|
|
685
|
+
}),
|
|
686
|
+
React.createElement('div', {
|
|
687
|
+
className: 'diagram-comment-form-actions',
|
|
688
|
+
key: 'actions',
|
|
689
|
+
}, [
|
|
690
|
+
React.createElement('button', {
|
|
691
|
+
className: 'diagram-comment-button is-secondary',
|
|
692
|
+
key: 'cancel',
|
|
693
|
+
onClick: () => setComposerOpen(false),
|
|
694
|
+
type: 'button',
|
|
695
|
+
}, 'Cancel'),
|
|
696
|
+
React.createElement('button', {
|
|
697
|
+
className: 'diagram-comment-button is-primary',
|
|
698
|
+
disabled: !draft.trim() || !canWrite,
|
|
699
|
+
key: 'submit',
|
|
700
|
+
type: 'submit',
|
|
701
|
+
}, 'Post comment'),
|
|
702
|
+
]),
|
|
703
|
+
]);
|
|
704
|
+
const renderThread = () => React.createElement(React.Fragment, null, [
|
|
705
|
+
React.createElement('button', {
|
|
706
|
+
className: 'diagram-comment-back',
|
|
707
|
+
key: 'back',
|
|
708
|
+
onClick: () => setActiveThreadId(null),
|
|
709
|
+
type: 'button',
|
|
710
|
+
}, '← All comments'),
|
|
711
|
+
React.createElement('p', {
|
|
712
|
+
className: 'diagram-comment-anchor-label',
|
|
713
|
+
key: 'label',
|
|
714
|
+
}, getDiagramAnchorLabel(activeThread)),
|
|
715
|
+
React.createElement('div', {
|
|
716
|
+
className: 'diagram-comment-messages',
|
|
717
|
+
key: 'messages',
|
|
718
|
+
}, activeThread.messages?.map((message) => React.createElement('article', {
|
|
719
|
+
className: 'diagram-comment-message',
|
|
720
|
+
key: message.id,
|
|
721
|
+
}, [
|
|
722
|
+
React.createElement('div', {
|
|
723
|
+
className: 'diagram-comment-message-meta',
|
|
724
|
+
key: 'meta',
|
|
725
|
+
}, message.userName || 'Anonymous'),
|
|
726
|
+
React.createElement('p', {
|
|
727
|
+
className: 'diagram-comment-message-body',
|
|
728
|
+
key: 'body',
|
|
729
|
+
}, message.body),
|
|
730
|
+
]))),
|
|
731
|
+
React.createElement('form', {
|
|
732
|
+
className: 'diagram-comment-form',
|
|
733
|
+
key: 'reply-form',
|
|
734
|
+
onSubmit: submitReply,
|
|
735
|
+
}, [
|
|
736
|
+
React.createElement('textarea', {
|
|
737
|
+
'aria-label': 'Reply',
|
|
738
|
+
className: 'diagram-comment-input',
|
|
739
|
+
disabled: !canWrite,
|
|
740
|
+
key: 'reply-input',
|
|
741
|
+
maxLength: 2000,
|
|
742
|
+
onChange: (event) => setReplyDraft(event.target.value),
|
|
743
|
+
placeholder: canWrite ? 'Reply…' : 'Reconnect to reply',
|
|
744
|
+
rows: 3,
|
|
745
|
+
value: replyDraft,
|
|
746
|
+
}),
|
|
747
|
+
React.createElement('div', {
|
|
748
|
+
className: 'diagram-comment-form-actions',
|
|
749
|
+
key: 'reply-actions',
|
|
750
|
+
}, [
|
|
751
|
+
React.createElement('button', {
|
|
752
|
+
className: 'diagram-comment-button is-primary',
|
|
753
|
+
disabled: !replyDraft.trim() || !canWrite,
|
|
754
|
+
type: 'submit',
|
|
755
|
+
}, 'Reply'),
|
|
756
|
+
React.createElement('button', {
|
|
757
|
+
className: 'diagram-comment-button is-danger',
|
|
758
|
+
disabled: !canWrite,
|
|
759
|
+
onClick: resolveThread,
|
|
760
|
+
type: 'button',
|
|
761
|
+
}, 'Resolve'),
|
|
762
|
+
]),
|
|
763
|
+
]),
|
|
764
|
+
]);
|
|
765
|
+
const renderDrawer = () => {
|
|
766
|
+
if (!drawerOpen) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const isListView = !composerOpen && !activeThread;
|
|
771
|
+
const content = composerOpen
|
|
772
|
+
? renderComposer()
|
|
773
|
+
: activeThread
|
|
774
|
+
? renderThread()
|
|
775
|
+
: threads.length > 0
|
|
776
|
+
? threads.map((thread) => renderDiagramCommentOverviewThread(thread, activeThreadId, openThread))
|
|
777
|
+
: React.createElement('div', {
|
|
778
|
+
className: 'comments-drawer-empty ui-empty-state ui-empty-state--compact',
|
|
779
|
+
}, [
|
|
780
|
+
React.createElement('p', {
|
|
781
|
+
className: 'ui-empty-state-title',
|
|
782
|
+
key: 'title',
|
|
783
|
+
}, 'No comments yet'),
|
|
784
|
+
React.createElement('p', {
|
|
785
|
+
className: 'ui-empty-state-copy',
|
|
786
|
+
key: 'copy',
|
|
787
|
+
}, 'Select an element and add the first comment.'),
|
|
788
|
+
]);
|
|
789
|
+
const drawerAddButton = selectedElement && !composerOpen
|
|
790
|
+
? React.createElement('button', {
|
|
791
|
+
'aria-label': 'Add comment',
|
|
792
|
+
className: 'diagram-comment-add',
|
|
793
|
+
disabled: !canWrite,
|
|
794
|
+
key: 'add',
|
|
795
|
+
onClick: openComposer,
|
|
796
|
+
title: canWrite ? 'Add comment to selected element' : 'Reconnect to add a comment',
|
|
797
|
+
type: 'button',
|
|
798
|
+
}, React.createElement(DiagramCommentIcon, { add: true }))
|
|
799
|
+
: null;
|
|
800
|
+
|
|
801
|
+
return React.createElement('aside', {
|
|
802
|
+
'aria-label': 'Diagram comments',
|
|
803
|
+
className: 'diagram-comments-drawer',
|
|
804
|
+
'data-testid': 'diagram-comments-drawer',
|
|
805
|
+
}, [
|
|
806
|
+
React.createElement('div', {
|
|
807
|
+
className: 'comments-drawer-header diagram-comments-drawer-header',
|
|
808
|
+
key: 'header',
|
|
809
|
+
}, [
|
|
810
|
+
React.createElement('span', {
|
|
811
|
+
className: 'comments-drawer-title',
|
|
812
|
+
key: 'title',
|
|
813
|
+
}, composerOpen ? 'New comment' : activeThread ? 'Comment thread' : 'Comments'),
|
|
814
|
+
React.createElement('div', {
|
|
815
|
+
className: 'diagram-comments-drawer-header-actions',
|
|
816
|
+
key: 'actions',
|
|
817
|
+
}, [
|
|
818
|
+
drawerAddButton,
|
|
819
|
+
React.createElement('button', {
|
|
820
|
+
'aria-label': 'Close comments',
|
|
821
|
+
className: 'diagram-comment-close',
|
|
822
|
+
key: 'close',
|
|
823
|
+
onClick: () => setDrawerOpen(false),
|
|
824
|
+
type: 'button',
|
|
825
|
+
}, '×'),
|
|
826
|
+
]),
|
|
827
|
+
]),
|
|
828
|
+
React.createElement('div', {
|
|
829
|
+
className: `diagram-comments-drawer-content${isListView ? ' comments-drawer-list is-list' : ''}`,
|
|
830
|
+
key: 'content',
|
|
831
|
+
}, content),
|
|
832
|
+
]);
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
const renderCommentsOverlay = visible && room
|
|
836
|
+
? React.createElement('div', {
|
|
837
|
+
'aria-label': 'Diagram comments overlay',
|
|
838
|
+
className: 'diagram-comments-overlay',
|
|
839
|
+
key: 'overlay',
|
|
840
|
+
}, [
|
|
841
|
+
React.createElement('div', {
|
|
842
|
+
className: 'diagram-comment-markers',
|
|
843
|
+
key: 'markers',
|
|
844
|
+
}, threads.map((thread) => {
|
|
845
|
+
const position = getDiagramCommentMarkerPosition(thread);
|
|
846
|
+
if (!position) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return React.createElement('button', {
|
|
851
|
+
'aria-label': `Open comment on ${getDiagramAnchorLabel(thread)}`,
|
|
852
|
+
className: `diagram-comment-marker${thread.id === activeThreadId ? ' is-active' : ''}`,
|
|
853
|
+
'data-comment-thread-id': thread.id,
|
|
854
|
+
key: thread.id,
|
|
855
|
+
onClick: () => openThread(thread.id),
|
|
856
|
+
style: {
|
|
857
|
+
left: `${position.x}px`,
|
|
858
|
+
top: `${position.y}px`,
|
|
859
|
+
},
|
|
860
|
+
type: 'button',
|
|
861
|
+
}, React.createElement(DiagramCommentIcon));
|
|
862
|
+
})),
|
|
863
|
+
renderDrawer(),
|
|
864
|
+
])
|
|
865
|
+
: null;
|
|
866
|
+
|
|
867
|
+
return React.createElement(diagramCommentsContext.Provider, {
|
|
868
|
+
value: {
|
|
869
|
+
canWrite,
|
|
870
|
+
drawerOpen,
|
|
871
|
+
openComposer,
|
|
872
|
+
room,
|
|
873
|
+
selectedElement,
|
|
874
|
+
threads,
|
|
875
|
+
toggleDrawer: () => setDrawerOpen((open) => !open),
|
|
876
|
+
visible,
|
|
877
|
+
},
|
|
878
|
+
}, [
|
|
879
|
+
React.createElement(DiagramCommentsExcalidraw, {
|
|
880
|
+
initialData,
|
|
881
|
+
key: 'excalidraw',
|
|
882
|
+
renderKey: editorRenderKey,
|
|
883
|
+
viewModeEnabled: getDocumentViewState().viewModeEnabled,
|
|
884
|
+
}),
|
|
885
|
+
renderCommentsOverlay,
|
|
886
|
+
]);
|
|
887
|
+
}
|
|
888
|
+
|
|
215
889
|
function renderExcalidrawApp({ initialData } = {}) {
|
|
216
890
|
if (!reactRoot) {
|
|
217
891
|
return;
|
|
@@ -221,9 +895,14 @@ function renderExcalidrawApp({ initialData } = {}) {
|
|
|
221
895
|
React.createElement(
|
|
222
896
|
'div',
|
|
223
897
|
{ className: 'excalidraw-editor-shell' },
|
|
224
|
-
React.createElement(
|
|
225
|
-
|
|
226
|
-
|
|
898
|
+
React.createElement(DiagramCommentsEditor, {
|
|
899
|
+
apiId: getMountedExcalidrawAPI()?.id || '',
|
|
900
|
+
canWrite: roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE,
|
|
901
|
+
focusRequest: pendingDiagramCommentFocus,
|
|
902
|
+
initialData,
|
|
903
|
+
room: roomClient,
|
|
904
|
+
threads: diagramCommentThreads,
|
|
905
|
+
visible: normalizeDocumentMode(currentDocument.mode) !== 'preview',
|
|
227
906
|
}),
|
|
228
907
|
getAuthorityBannerText()
|
|
229
908
|
? React.createElement('div', {
|
|
@@ -260,12 +939,13 @@ function recordSceneDiagnostic(event, details = {}, sceneJson = '') {
|
|
|
260
939
|
|
|
261
940
|
function handleHistoryAction({ action = '', outcome = '' } = {}) {
|
|
262
941
|
recordSceneDiagnostic('history-action', { action, outcome });
|
|
263
|
-
|
|
942
|
+
const api = getMountedExcalidrawAPI();
|
|
943
|
+
if (outcome !== 'no-visible-change' || !api) {
|
|
264
944
|
return;
|
|
265
945
|
}
|
|
266
946
|
|
|
267
947
|
const label = action === 'redo' ? 'Redo' : 'Undo';
|
|
268
|
-
|
|
948
|
+
api.setToast?.({
|
|
269
949
|
message: `${label} skipped: a collaborator changed that item`,
|
|
270
950
|
});
|
|
271
951
|
}
|
|
@@ -298,7 +978,7 @@ function handleRoomConnectionStateChange({
|
|
|
298
978
|
roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE
|
|
299
979
|
&& previousState === EXCALIDRAW_ROOM_CONNECTION_STATE.RECONNECTING_READONLY
|
|
300
980
|
) {
|
|
301
|
-
|
|
981
|
+
getMountedExcalidrawAPI()?.setToast?.({ message: 'Live diagram reconnected' });
|
|
302
982
|
}
|
|
303
983
|
|
|
304
984
|
if (roomConnectionState === EXCALIDRAW_ROOM_CONNECTION_STATE.AUTHORITATIVE) {
|
|
@@ -349,7 +1029,7 @@ if (isTestMode) {
|
|
|
349
1029
|
window.__COLLABMD_EXCALIDRAW_TEST__ = {
|
|
350
1030
|
disconnectTransport: () => roomClient?.provider?.disconnect?.(),
|
|
351
1031
|
getElementBounds: (elementId) => {
|
|
352
|
-
const element =
|
|
1032
|
+
const element = getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()?.find((entry) => entry.id === elementId && !entry.isDeleted);
|
|
353
1033
|
if (!element) {
|
|
354
1034
|
return null;
|
|
355
1035
|
}
|
|
@@ -364,49 +1044,66 @@ if (isTestMode) {
|
|
|
364
1044
|
};
|
|
365
1045
|
},
|
|
366
1046
|
getElementCount: () => (
|
|
367
|
-
|
|
1047
|
+
getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()?.filter((element) => !element.isDeleted).length ?? 0
|
|
368
1048
|
),
|
|
369
1049
|
getElementIds: () => (
|
|
370
|
-
|
|
1050
|
+
getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
|
|
371
1051
|
?.filter((element) => !element.isDeleted)
|
|
372
1052
|
.map((element) => element.id)
|
|
373
1053
|
.sort() ?? []
|
|
374
1054
|
),
|
|
375
1055
|
getElementStatus: (elementId) => (
|
|
376
|
-
|
|
1056
|
+
getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.()
|
|
377
1057
|
?.find((entry) => entry.id === elementId && !entry.isDeleted)?.status ?? null
|
|
378
1058
|
),
|
|
379
1059
|
getFileIds: () => (
|
|
380
|
-
Object.keys(
|
|
1060
|
+
Object.keys(getMountedExcalidrawAPI()?.getFiles?.() || {}).sort()
|
|
381
1061
|
),
|
|
382
|
-
getFileVersion: (fileId) =>
|
|
383
|
-
getEditorId: () =>
|
|
1062
|
+
getFileVersion: (fileId) => getMountedExcalidrawAPI()?.getFiles?.()?.[fileId]?.version ?? null,
|
|
1063
|
+
getEditorId: () => getMountedExcalidrawAPI()?.id || null,
|
|
384
1064
|
getForkCapabilities: () => ({
|
|
385
|
-
replaceFiles: typeof
|
|
1065
|
+
replaceFiles: typeof getMountedExcalidrawAPI()?.replaceFiles === 'function',
|
|
386
1066
|
}),
|
|
387
1067
|
getAuthorityState: () => roomConnectionState,
|
|
1068
|
+
getCommentThreads: () => roomClient?.getCommentThreads?.() || [],
|
|
388
1069
|
getDiagnosticTrace: () => diagnostics.exportTrace(),
|
|
389
1070
|
getHistoryState: () => getNativeHistoryState(),
|
|
390
1071
|
getLocalUserName: () => localAwarenessUser?.name || '',
|
|
391
1072
|
getLocalPeerId: () => localAwarenessUser?.peerId || '',
|
|
392
1073
|
getViewport: () => {
|
|
393
|
-
const appState =
|
|
1074
|
+
const appState = getMountedExcalidrawAPI()?.getAppState?.();
|
|
394
1075
|
return appState ? {
|
|
1076
|
+
offsetLeft: appState.offsetLeft,
|
|
1077
|
+
offsetTop: appState.offsetTop,
|
|
395
1078
|
scrollX: appState.scrollX,
|
|
396
1079
|
scrollY: appState.scrollY,
|
|
397
1080
|
zoom: appState.zoom?.value ?? null,
|
|
398
1081
|
} : null;
|
|
399
1082
|
},
|
|
400
|
-
isViewMode: () => Boolean(
|
|
1083
|
+
isViewMode: () => Boolean(getMountedExcalidrawAPI()?.getAppState?.().viewModeEnabled),
|
|
1084
|
+
selectElement: (elementId) => {
|
|
1085
|
+
const api = getMountedExcalidrawAPI();
|
|
1086
|
+
if (!api || !elementId) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
api.updateScene({
|
|
1091
|
+
appState: {
|
|
1092
|
+
selectedElementIds: { [elementId]: true },
|
|
1093
|
+
},
|
|
1094
|
+
captureUpdate: CaptureUpdateAction.NEVER,
|
|
1095
|
+
});
|
|
1096
|
+
return true;
|
|
1097
|
+
},
|
|
401
1098
|
getSceneJson: () => roomClient?.getLastSceneJson?.() || '',
|
|
402
1099
|
isAuthoritativeReady: () => (
|
|
403
|
-
Boolean(
|
|
1100
|
+
Boolean(getMountedExcalidrawAPI())
|
|
404
1101
|
&& collabReady
|
|
405
1102
|
&& roomClient?.canWriteToRoom === true
|
|
406
1103
|
&& roomClient?.waitingForAuthoritativeSync === false
|
|
407
1104
|
&& roomClient?.isApplyingSharedSnapshot?.() === false
|
|
408
1105
|
),
|
|
409
|
-
isReady: () => collabReady && Boolean(
|
|
1106
|
+
isReady: () => collabReady && Boolean(getMountedExcalidrawAPI()),
|
|
410
1107
|
redoShared: () => triggerNativeHistory('redo'),
|
|
411
1108
|
reconnectTransport: () => roomClient?.provider?.connect?.(),
|
|
412
1109
|
setScene: (scene) => {
|
|
@@ -415,12 +1112,13 @@ if (isTestMode) {
|
|
|
415
1112
|
});
|
|
416
1113
|
},
|
|
417
1114
|
setViewport: (viewport) => {
|
|
418
|
-
|
|
1115
|
+
const api = getMountedExcalidrawAPI();
|
|
1116
|
+
if (!api) {
|
|
419
1117
|
return;
|
|
420
1118
|
}
|
|
421
1119
|
|
|
422
|
-
const currentAppState =
|
|
423
|
-
|
|
1120
|
+
const currentAppState = api.getAppState();
|
|
1121
|
+
api.updateScene({
|
|
424
1122
|
appState: {
|
|
425
1123
|
scrollX: Number.isFinite(viewport?.scrollX) ? viewport.scrollX : currentAppState.scrollX,
|
|
426
1124
|
scrollY: Number.isFinite(viewport?.scrollY) ? viewport.scrollY : currentAppState.scrollY,
|
|
@@ -440,14 +1138,15 @@ function applyCollaborators(collaborators) {
|
|
|
440
1138
|
const renderableCollaborators = buildRenderableCollaboratorsMap(activeCollaborators);
|
|
441
1139
|
const renderSignature = getCollaboratorsRenderSignature(renderableCollaborators);
|
|
442
1140
|
|
|
443
|
-
|
|
1141
|
+
const api = getMountedExcalidrawAPI();
|
|
1142
|
+
if (!api) {
|
|
444
1143
|
pendingCollaborators = activeCollaborators;
|
|
445
1144
|
return;
|
|
446
1145
|
}
|
|
447
1146
|
|
|
448
1147
|
if (renderSignature !== lastRenderedCollaboratorsSignature) {
|
|
449
1148
|
lastRenderedCollaboratorsSignature = renderSignature;
|
|
450
|
-
|
|
1149
|
+
api.updateScene({
|
|
451
1150
|
collaborators: renderableCollaborators,
|
|
452
1151
|
captureUpdate: CaptureUpdateAction.NEVER,
|
|
453
1152
|
});
|
|
@@ -476,11 +1175,11 @@ function queueCollaboratorsRender(collaborators) {
|
|
|
476
1175
|
}
|
|
477
1176
|
|
|
478
1177
|
function isEditingTextElement() {
|
|
479
|
-
return Boolean(
|
|
1178
|
+
return Boolean(getMountedExcalidrawAPI()?.getAppState?.()?.editingTextElement);
|
|
480
1179
|
}
|
|
481
1180
|
|
|
482
1181
|
function flushPendingRemoteScene() {
|
|
483
|
-
if (!pendingRemoteSceneJson || !
|
|
1182
|
+
if (!pendingRemoteSceneJson || !getMountedExcalidrawAPI() || !collabReady || isEditingTextElement()) {
|
|
484
1183
|
return false;
|
|
485
1184
|
}
|
|
486
1185
|
|
|
@@ -504,7 +1203,7 @@ function applySceneFromJson(rawJson, {
|
|
|
504
1203
|
appliedSceneJson = normalizedJson;
|
|
505
1204
|
recordSceneDiagnostic('remote-scene-received', {}, normalizedJson);
|
|
506
1205
|
|
|
507
|
-
if (!
|
|
1206
|
+
if (!getMountedExcalidrawAPI() || !collabReady) {
|
|
508
1207
|
pendingRemoteSceneJson = normalizedJson;
|
|
509
1208
|
return;
|
|
510
1209
|
}
|
|
@@ -531,9 +1230,14 @@ function releaseViewportBroadcastSuppressionAfterPaint() {
|
|
|
531
1230
|
|
|
532
1231
|
function buildApiSceneUpdate(scene, {
|
|
533
1232
|
appStateOverrides = {},
|
|
1233
|
+
api = getMountedExcalidrawAPI(),
|
|
534
1234
|
} = {}) {
|
|
535
|
-
|
|
536
|
-
|
|
1235
|
+
if (!api) {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const currentAppState = api.getAppState();
|
|
1240
|
+
const currentElements = api.getSceneElementsIncludingDeleted?.() || api.getSceneElements();
|
|
537
1241
|
|
|
538
1242
|
return buildReconciledExcalidrawSceneUpdate({
|
|
539
1243
|
appStateOverrides,
|
|
@@ -575,6 +1279,7 @@ function requestEditorRemount(scene) {
|
|
|
575
1279
|
appliedSceneJson = normalizedJson;
|
|
576
1280
|
pendingCollaborators = activeCollaborators;
|
|
577
1281
|
initialViewportFitPending = true;
|
|
1282
|
+
clearPreviewViewportFitTimers();
|
|
578
1283
|
clearEditorApiStateBindings();
|
|
579
1284
|
skipRoomDisconnectOnUnmount = true;
|
|
580
1285
|
editorRenderKey += 1;
|
|
@@ -589,8 +1294,14 @@ function applySceneToMountedApi(scene, {
|
|
|
589
1294
|
captureUpdate = CaptureUpdateAction.NEVER,
|
|
590
1295
|
trackedSharedSnapshot = false,
|
|
591
1296
|
} = {}) {
|
|
1297
|
+
const api = getMountedExcalidrawAPI();
|
|
1298
|
+
if (!api) {
|
|
1299
|
+
return { skipped: true };
|
|
1300
|
+
}
|
|
1301
|
+
|
|
592
1302
|
const nextSceneUpdate = buildApiSceneUpdate(scene, {
|
|
593
1303
|
appStateOverrides,
|
|
1304
|
+
api,
|
|
594
1305
|
});
|
|
595
1306
|
let applyResult;
|
|
596
1307
|
|
|
@@ -599,7 +1310,7 @@ function applySceneToMountedApi(scene, {
|
|
|
599
1310
|
}
|
|
600
1311
|
|
|
601
1312
|
try {
|
|
602
|
-
applyResult = applySceneUpdateWithFiles(
|
|
1313
|
+
applyResult = applySceneUpdateWithFiles(api, {
|
|
603
1314
|
captureUpdate,
|
|
604
1315
|
files: scene?.files || {},
|
|
605
1316
|
sceneUpdate: nextSceneUpdate,
|
|
@@ -643,7 +1354,7 @@ function applyLocalScene(scene, {
|
|
|
643
1354
|
|
|
644
1355
|
appliedSceneJson = normalizedJson;
|
|
645
1356
|
|
|
646
|
-
if (!
|
|
1357
|
+
if (!getMountedExcalidrawAPI() || !collabReady) {
|
|
647
1358
|
pendingRemoteSceneJson = normalizedJson;
|
|
648
1359
|
return;
|
|
649
1360
|
}
|
|
@@ -661,7 +1372,7 @@ function onRoomTextUpdate() {
|
|
|
661
1372
|
}
|
|
662
1373
|
|
|
663
1374
|
function getLiveSceneElementsForSync(fallbackElements = []) {
|
|
664
|
-
return
|
|
1375
|
+
return getMountedExcalidrawAPI()?.getSceneElementsIncludingDeleted?.() || fallbackElements;
|
|
665
1376
|
}
|
|
666
1377
|
|
|
667
1378
|
function postToParent(type, payload = {}) {
|
|
@@ -679,8 +1390,9 @@ function handleQuickSwitcherKeyDown(event) {
|
|
|
679
1390
|
}
|
|
680
1391
|
|
|
681
1392
|
function getSceneElementsForPreviewFit() {
|
|
1393
|
+
const api = getMountedExcalidrawAPI();
|
|
682
1394
|
return (
|
|
683
|
-
|
|
1395
|
+
api?.getSceneElementsIncludingDeleted?.()
|
|
684
1396
|
?.filter((element) => !element.isDeleted) ?? []
|
|
685
1397
|
);
|
|
686
1398
|
}
|
|
@@ -691,7 +1403,8 @@ function scheduleViewportFit({
|
|
|
691
1403
|
consumeInitialFit = false,
|
|
692
1404
|
} = {}) {
|
|
693
1405
|
const normalizedMode = normalizeDocumentMode(currentDocument.mode);
|
|
694
|
-
|
|
1406
|
+
const api = getMountedExcalidrawAPI();
|
|
1407
|
+
if (!api || (forcePreview && normalizedMode !== 'preview')) {
|
|
695
1408
|
return;
|
|
696
1409
|
}
|
|
697
1410
|
|
|
@@ -699,6 +1412,11 @@ function scheduleViewportFit({
|
|
|
699
1412
|
return;
|
|
700
1413
|
}
|
|
701
1414
|
|
|
1415
|
+
if (!forcePreview && api.getAppState?.().cursorButton === 'down') {
|
|
1416
|
+
initialViewportFitPending = false;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
702
1420
|
const elements = getSceneElementsForPreviewFit();
|
|
703
1421
|
if (elements.length === 0) {
|
|
704
1422
|
return;
|
|
@@ -707,15 +1425,42 @@ function scheduleViewportFit({
|
|
|
707
1425
|
initialViewportFitPending = false;
|
|
708
1426
|
}
|
|
709
1427
|
|
|
1428
|
+
if (!forcePreview && delayMs === 0) {
|
|
1429
|
+
api.setViewport({
|
|
1430
|
+
target: elements,
|
|
1431
|
+
animation: false,
|
|
1432
|
+
fit: 'contain',
|
|
1433
|
+
});
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const generation = ++viewportFitGeneration;
|
|
1438
|
+
|
|
710
1439
|
if (previewViewportFitTimerId) {
|
|
711
1440
|
window.clearTimeout(previewViewportFitTimerId);
|
|
712
1441
|
}
|
|
713
1442
|
|
|
714
1443
|
previewViewportFitTimerId = window.setTimeout(() => {
|
|
715
1444
|
previewViewportFitTimerId = 0;
|
|
1445
|
+
if (generation !== viewportFitGeneration) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
716
1449
|
requestAnimationFrame(() => {
|
|
1450
|
+
if (generation !== viewportFitGeneration) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
717
1454
|
requestAnimationFrame(() => {
|
|
718
|
-
if (
|
|
1455
|
+
if (generation !== viewportFitGeneration) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const api = getMountedExcalidrawAPI();
|
|
1460
|
+
if (!api) {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (!forcePreview && api.getAppState?.().cursorButton === 'down') {
|
|
719
1464
|
return;
|
|
720
1465
|
}
|
|
721
1466
|
|
|
@@ -728,11 +1473,10 @@ function scheduleViewportFit({
|
|
|
728
1473
|
suppressViewportBroadcast = true;
|
|
729
1474
|
}
|
|
730
1475
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
viewportZoomFactor: normalizedMode === 'preview' ? 0.92 : 0.88,
|
|
1476
|
+
api.setViewport({
|
|
1477
|
+
target: latestElements,
|
|
1478
|
+
animation: false,
|
|
1479
|
+
fit: 'contain',
|
|
736
1480
|
});
|
|
737
1481
|
|
|
738
1482
|
if (forcePreview) {
|
|
@@ -748,18 +1492,38 @@ function scheduleInitialViewportFit() {
|
|
|
748
1492
|
}
|
|
749
1493
|
|
|
750
1494
|
function schedulePreviewViewportFit() {
|
|
1495
|
+
if (previewViewportFitRetryTimerId) {
|
|
1496
|
+
window.clearTimeout(previewViewportFitRetryTimerId);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
751
1499
|
scheduleViewportFit({ forcePreview: true, delayMs: 80 });
|
|
752
|
-
window.setTimeout(() => {
|
|
1500
|
+
previewViewportFitRetryTimerId = window.setTimeout(() => {
|
|
1501
|
+
previewViewportFitRetryTimerId = 0;
|
|
753
1502
|
scheduleViewportFit({ forcePreview: true });
|
|
754
1503
|
}, 240);
|
|
755
1504
|
}
|
|
756
1505
|
|
|
1506
|
+
function clearPreviewViewportFitTimers() {
|
|
1507
|
+
viewportFitGeneration += 1;
|
|
1508
|
+
|
|
1509
|
+
if (previewViewportFitTimerId) {
|
|
1510
|
+
window.clearTimeout(previewViewportFitTimerId);
|
|
1511
|
+
previewViewportFitTimerId = 0;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
if (previewViewportFitRetryTimerId) {
|
|
1515
|
+
window.clearTimeout(previewViewportFitRetryTimerId);
|
|
1516
|
+
previewViewportFitRetryTimerId = 0;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
757
1520
|
function syncLocalViewportToRoom() {
|
|
758
|
-
|
|
1521
|
+
const api = getMountedExcalidrawAPI();
|
|
1522
|
+
if (!collabReady || !api || !roomClient || suppressViewportBroadcast) {
|
|
759
1523
|
return;
|
|
760
1524
|
}
|
|
761
1525
|
|
|
762
|
-
const appState =
|
|
1526
|
+
const appState = api.getAppState();
|
|
763
1527
|
roomClient.scheduleLocalViewportAwareness({
|
|
764
1528
|
scrollX: appState.scrollX,
|
|
765
1529
|
scrollY: appState.scrollY,
|
|
@@ -781,7 +1545,8 @@ function setFollowedSocket(nextSocketId, { force = false } = {}) {
|
|
|
781
1545
|
}
|
|
782
1546
|
|
|
783
1547
|
function applyFollowedViewport(collaborators = activeCollaborators, { force = false } = {}) {
|
|
784
|
-
|
|
1548
|
+
const api = getMountedExcalidrawAPI();
|
|
1549
|
+
if (!api || !followedSocketId) {
|
|
785
1550
|
return;
|
|
786
1551
|
}
|
|
787
1552
|
|
|
@@ -798,7 +1563,7 @@ function applyFollowedViewport(collaborators = activeCollaborators, { force = fa
|
|
|
798
1563
|
|
|
799
1564
|
lastAppliedFollowViewportSignature = nextSignature;
|
|
800
1565
|
suppressViewportBroadcast = true;
|
|
801
|
-
|
|
1566
|
+
api.updateScene({
|
|
802
1567
|
appState: {
|
|
803
1568
|
scrollX: viewport.scrollX,
|
|
804
1569
|
scrollY: viewport.scrollY,
|
|
@@ -811,12 +1576,13 @@ function applyFollowedViewport(collaborators = activeCollaborators, { force = fa
|
|
|
811
1576
|
|
|
812
1577
|
function applyHostFollowRequest(peerId) {
|
|
813
1578
|
pendingHostFollowPeerId = peerId || null;
|
|
814
|
-
|
|
1579
|
+
const api = getMountedExcalidrawAPI();
|
|
1580
|
+
if (!api) {
|
|
815
1581
|
return;
|
|
816
1582
|
}
|
|
817
1583
|
|
|
818
1584
|
if (!peerId) {
|
|
819
|
-
|
|
1585
|
+
api.updateScene({
|
|
820
1586
|
appState: { userToFollow: null },
|
|
821
1587
|
captureUpdate: CaptureUpdateAction.NEVER,
|
|
822
1588
|
});
|
|
@@ -830,7 +1596,7 @@ function applyHostFollowRequest(peerId) {
|
|
|
830
1596
|
return;
|
|
831
1597
|
}
|
|
832
1598
|
|
|
833
|
-
|
|
1599
|
+
api.updateScene({
|
|
834
1600
|
appState: {
|
|
835
1601
|
userToFollow: {
|
|
836
1602
|
socketId: collaborator.socketId,
|
|
@@ -853,6 +1619,8 @@ function resetRealtimeRoomState() {
|
|
|
853
1619
|
pendingRemoteSceneJson = '';
|
|
854
1620
|
pendingCollaborators = null;
|
|
855
1621
|
activeCollaborators = new Map();
|
|
1622
|
+
diagramCommentThreads = [];
|
|
1623
|
+
pendingDiagramCommentFocus = null;
|
|
856
1624
|
followedSocketId = null;
|
|
857
1625
|
pendingHostFollowPeerId = null;
|
|
858
1626
|
suppressViewportBroadcast = false;
|
|
@@ -984,6 +1752,10 @@ window.addEventListener('message', (event) => {
|
|
|
984
1752
|
return;
|
|
985
1753
|
}
|
|
986
1754
|
|
|
1755
|
+
if (event.source !== window.parent) {
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
987
1759
|
const message = event.data;
|
|
988
1760
|
if (!message || message.source !== 'collabmd-host') {
|
|
989
1761
|
return;
|
|
@@ -992,8 +1764,9 @@ window.addEventListener('message', (event) => {
|
|
|
992
1764
|
if (message.type === 'set-theme') {
|
|
993
1765
|
currentTheme = message.theme || 'dark';
|
|
994
1766
|
applySurfaceTheme(currentTheme);
|
|
995
|
-
|
|
996
|
-
|
|
1767
|
+
const api = getMountedExcalidrawAPI();
|
|
1768
|
+
if (api) {
|
|
1769
|
+
api.updateScene({
|
|
997
1770
|
appState: { theme: currentTheme },
|
|
998
1771
|
captureUpdate: CaptureUpdateAction.NEVER,
|
|
999
1772
|
});
|
|
@@ -1007,6 +1780,11 @@ window.addEventListener('message', (event) => {
|
|
|
1007
1780
|
return;
|
|
1008
1781
|
}
|
|
1009
1782
|
|
|
1783
|
+
if (message.type === 'open-comment-thread') {
|
|
1784
|
+
requestDiagramCommentFocus(message.threadId);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1010
1788
|
if (message.type === 'follow-user') {
|
|
1011
1789
|
applyHostFollowRequest(message.peerId || null);
|
|
1012
1790
|
return;
|
|
@@ -1102,6 +1880,9 @@ function initializeEditor(api) {
|
|
|
1102
1880
|
|
|
1103
1881
|
setFollowedSocket(null, { force: true });
|
|
1104
1882
|
}));
|
|
1883
|
+
apiStateCleanupCallbacks.push(api.onStateChange('selectedElementIds', () => {
|
|
1884
|
+
renderExcalidrawApp();
|
|
1885
|
+
}));
|
|
1105
1886
|
apiStateCleanupCallbacks.push(api.onStateChange('editingTextElement', (editingTextElement) => {
|
|
1106
1887
|
if (!editingTextElement) {
|
|
1107
1888
|
requestAnimationFrame(() => {
|
|
@@ -1121,7 +1902,7 @@ function initializeEditor(api) {
|
|
|
1121
1902
|
if (pendingCollaborators) {
|
|
1122
1903
|
const renderableCollaborators = buildRenderableCollaboratorsMap(pendingCollaborators);
|
|
1123
1904
|
lastRenderedCollaboratorsSignature = getCollaboratorsRenderSignature(renderableCollaborators);
|
|
1124
|
-
|
|
1905
|
+
api.updateScene({
|
|
1125
1906
|
collaborators: renderableCollaborators,
|
|
1126
1907
|
captureUpdate: CaptureUpdateAction.NEVER,
|
|
1127
1908
|
});
|
|
@@ -1137,6 +1918,7 @@ function initializeEditor(api) {
|
|
|
1137
1918
|
|
|
1138
1919
|
scheduleInitialViewportFit();
|
|
1139
1920
|
postToParent('ready');
|
|
1921
|
+
renderExcalidrawApp();
|
|
1140
1922
|
}
|
|
1141
1923
|
|
|
1142
1924
|
function handleEditorMount({ excalidrawAPI: api }) {
|