collabmd 0.1.31 → 0.1.32
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/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-Br4qyh3r.js} +1 -1
- package/dist/client/assets/drawioEditor-ClmnTx5J.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Cf-iV5-3.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CXAxVhlw.js} +2 -2
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DoH_kMdu.js} +2 -2
- package/dist/client/assets/exportDocument-9wTrBZNS.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-C8QZYHvV.js} +2 -2
- package/dist/client/assets/index-CxSbUTs2.css +1 -0
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-BjHHMlv0.js +1249 -0
- package/dist/client/assets/vault-api-client-Bf9tB_GW.js +1 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -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 +2 -2
- package/package.json +4 -1
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +176 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +2 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +67 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +26 -7
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +103 -28
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/bases/base-definition.js +255 -0
- package/src/server/domain/bases/base-expression-runtime.js +1159 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +418 -0
- package/src/server/domain/bases/base-query-results.js +205 -0
- package/src/server/domain/bases/base-query-service.js +68 -1965
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +109 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Bia2hI25.js} +0 -0
|
@@ -4,6 +4,39 @@ function encodeHeaderMetadata(value) {
|
|
|
4
4
|
return encodeURIComponent(String(value ?? ''));
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function getPathLeaf(pathValue) {
|
|
8
|
+
return String(pathValue ?? '')
|
|
9
|
+
.replace(/\/+$/u, '')
|
|
10
|
+
.split('/')
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.pop() || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function downloadBlob(blob, fileName) {
|
|
16
|
+
const url = URL.createObjectURL(blob);
|
|
17
|
+
const anchor = document.createElement('a');
|
|
18
|
+
anchor.href = url;
|
|
19
|
+
anchor.download = fileName;
|
|
20
|
+
document.body.appendChild(anchor);
|
|
21
|
+
anchor.click();
|
|
22
|
+
anchor.remove();
|
|
23
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseDownloadFileName(contentDisposition = '', fallbackName = 'download') {
|
|
27
|
+
const utfMatch = String(contentDisposition).match(/filename\*=UTF-8''([^;]+)/iu);
|
|
28
|
+
if (utfMatch) {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(utfMatch[1]);
|
|
31
|
+
} catch {
|
|
32
|
+
return fallbackName;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const asciiMatch = String(contentDisposition).match(/filename="([^"]+)"/iu);
|
|
37
|
+
return asciiMatch?.[1] || fallbackName;
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
function createRequestHeaders(requestId, headers = {}) {
|
|
8
41
|
const nextHeaders = { ...headers };
|
|
9
42
|
if (requestId) {
|
|
@@ -25,6 +58,24 @@ async function parseApiResponse(response, fallbackError) {
|
|
|
25
58
|
return data;
|
|
26
59
|
}
|
|
27
60
|
|
|
61
|
+
async function triggerDownload(url, {
|
|
62
|
+
fallbackError,
|
|
63
|
+
fallbackFileName,
|
|
64
|
+
} = {}) {
|
|
65
|
+
const response = await fetch(url);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const payload = await response.json().catch(() => ({}));
|
|
68
|
+
throw new Error(payload.error || fallbackError);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const blob = await response.blob();
|
|
72
|
+
downloadBlob(
|
|
73
|
+
blob,
|
|
74
|
+
parseDownloadFileName(response.headers.get('content-disposition') || '', fallbackFileName),
|
|
75
|
+
);
|
|
76
|
+
return response;
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
export class VaultApiClient {
|
|
29
80
|
async readTree() {
|
|
30
81
|
const response = await fetch(resolveApiUrl('/files'));
|
|
@@ -170,6 +221,22 @@ export class VaultApiClient {
|
|
|
170
221
|
});
|
|
171
222
|
return parseApiResponse(response, 'Failed to upload image');
|
|
172
223
|
}
|
|
224
|
+
|
|
225
|
+
async downloadFile(path) {
|
|
226
|
+
const fallbackFileName = getPathLeaf(path) || 'download';
|
|
227
|
+
return triggerDownload(resolveApiUrl(`/download/file?path=${encodeURIComponent(path)}`), {
|
|
228
|
+
fallbackError: 'Failed to download file',
|
|
229
|
+
fallbackFileName,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async downloadDirectory(path) {
|
|
234
|
+
const directoryName = getPathLeaf(path) || 'vault';
|
|
235
|
+
return triggerDownload(resolveApiUrl(`/download/directory?path=${encodeURIComponent(path)}`), {
|
|
236
|
+
fallbackError: 'Failed to download folder',
|
|
237
|
+
fallbackFileName: `${directoryName}.zip`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
173
240
|
}
|
|
174
241
|
|
|
175
242
|
export const vaultApiClient = new VaultApiClient();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { stripVaultFileExtension } from '../../domain/file-kind.js';
|
|
2
2
|
import { escapeHtml } from '../domain/vault-utils.js';
|
|
3
|
-
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* BacklinksPanel — renders "Linked Mentions" for the current file.
|
|
@@ -13,6 +12,7 @@ export class BacklinksPanel {
|
|
|
13
12
|
documentRef = document,
|
|
14
13
|
headerPanelElement = null,
|
|
15
14
|
inlinePanelElement = null,
|
|
15
|
+
loadBacklinks = null,
|
|
16
16
|
onFileSelect,
|
|
17
17
|
panelElement,
|
|
18
18
|
}) {
|
|
@@ -20,6 +20,7 @@ export class BacklinksPanel {
|
|
|
20
20
|
this.headerPanel = headerPanelElement;
|
|
21
21
|
this.panelRoot = panelElement;
|
|
22
22
|
this.inlinePanel = inlinePanelElement;
|
|
23
|
+
this.loadBacklinks = loadBacklinks;
|
|
23
24
|
this.onFileSelect = onFileSelect;
|
|
24
25
|
|
|
25
26
|
this._expanded = false;
|
|
@@ -132,15 +133,11 @@ export class BacklinksPanel {
|
|
|
132
133
|
this._fetchController = new AbortController();
|
|
133
134
|
|
|
134
135
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
);
|
|
139
|
-
const data = await response.json();
|
|
140
|
-
|
|
136
|
+
const backlinks = await this.loadBacklinks(filePath, {
|
|
137
|
+
signal: this._fetchController.signal,
|
|
138
|
+
});
|
|
141
139
|
if (this._currentFile !== filePath) return;
|
|
142
|
-
|
|
143
|
-
this._backlinks = data.backlinks ?? [];
|
|
140
|
+
this._backlinks = backlinks;
|
|
144
141
|
} catch (error) {
|
|
145
142
|
if (error.name === 'AbortError') return;
|
|
146
143
|
console.warn('[backlinks] Failed to load:', error.message);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { isImageAttachmentFilePath } from '../../domain/file-kind.js';
|
|
2
2
|
import { resolveApiUrl } from '../domain/runtime-paths.js';
|
|
3
3
|
import { escapeHtml } from '../domain/vault-utils.js';
|
|
4
|
+
import { buttonClassNames } from './components/ui/button.js';
|
|
5
|
+
import { inputClassNames } from './components/ui/input.js';
|
|
6
|
+
import { segmentedButtonClassNames, segmentedControlClassNames } from './components/ui/segmented-control.js';
|
|
4
7
|
|
|
5
8
|
function createShellKey() {
|
|
6
9
|
return `base-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -73,11 +76,11 @@ function renderValue(cell = {}) {
|
|
|
73
76
|
return `<a class="bases-link" href="${escapeHtml(cell.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(formatCellText(cell))}</a>`;
|
|
74
77
|
}
|
|
75
78
|
if (cell.path) {
|
|
76
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
79
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
if (type === 'file' && cell.path) {
|
|
80
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
83
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(cell.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
const text = formatCellText(cell);
|
|
@@ -92,7 +95,7 @@ function renderRowValue(column, row) {
|
|
|
92
95
|
&& cell.type !== 'empty'
|
|
93
96
|
&& (column.id === 'file.name' || column.id === 'file.basename')
|
|
94
97
|
) {
|
|
95
|
-
return `<button type="button" class="bases-link bases-link-button" data-base-open-file="${escapeHtml(row.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
98
|
+
return `<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', size: 'compact', pill: true, extra: ['bases-link', 'bases-link-button'] }))}" data-base-open-file="${escapeHtml(row.path)}">${escapeHtml(formatCellText(cell))}</button>`;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
return renderValue(cell);
|
|
@@ -171,7 +174,7 @@ function renderViewBody(result) {
|
|
|
171
174
|
|
|
172
175
|
function renderViewTabs(result) {
|
|
173
176
|
return (result.views ?? []).map((view) => (
|
|
174
|
-
`<button type="button" class="
|
|
177
|
+
`<button type="button" class="${escapeHtml(segmentedButtonClassNames({ active: view.id === result.view.id, extra: 'bases-view-tab' }))}" data-base-view="${escapeHtml(view.id)}">${escapeHtml(view.name)}</button>`
|
|
175
178
|
)).join('');
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -180,14 +183,20 @@ function renderShellHtml(result, state) {
|
|
|
180
183
|
<section class="bases-shell" data-base-shell-key="${escapeHtml(state.key)}">
|
|
181
184
|
<header class="bases-toolbar">
|
|
182
185
|
<div class="bases-toolbar-main">
|
|
183
|
-
<div class="bases-tabs" data-base-tabs>
|
|
186
|
+
<div class="${escapeHtml(segmentedControlClassNames({ pill: true, extra: 'bases-tabs' }))}" data-base-tabs>
|
|
184
187
|
${renderViewTabs(result)}
|
|
185
188
|
</div>
|
|
186
189
|
<div class="bases-meta" data-base-meta>${escapeHtml(String(result.totalRows ?? 0))} results</div>
|
|
187
190
|
</div>
|
|
188
191
|
<div class="bases-toolbar-actions">
|
|
189
|
-
<
|
|
190
|
-
|
|
192
|
+
<label class="bases-search-shell" aria-label="Search this base">
|
|
193
|
+
<svg class="bases-search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
194
|
+
<circle cx="11" cy="11" r="7"></circle>
|
|
195
|
+
<path d="m20 20-3.5-3.5"></path>
|
|
196
|
+
</svg>
|
|
197
|
+
<input class="${escapeHtml(inputClassNames({ extra: 'bases-search-input' }))}" type="search" value="${escapeHtml(state.search)}" placeholder="Search this base" aria-label="Search this base">
|
|
198
|
+
</label>
|
|
199
|
+
<button type="button" class="${escapeHtml(buttonClassNames({ variant: 'secondary', pill: true, extra: 'bases-export-btn' }))}">Export CSV</button>
|
|
191
200
|
</div>
|
|
192
201
|
</header>
|
|
193
202
|
<div data-base-summary-slot>${renderSummaryBar(result.summaries)}</div>
|
|
@@ -205,10 +214,20 @@ function findShellElement(entry) {
|
|
|
205
214
|
?? entry.placeholder.querySelector('.bases-shell');
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
function markPlaceholderHydrated(placeholder) {
|
|
218
|
+
if (!placeholder?.classList) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
placeholder.classList.add('is-hydrated');
|
|
223
|
+
placeholder.classList.remove('diagram-preview-shell');
|
|
224
|
+
}
|
|
225
|
+
|
|
208
226
|
function updateShellContent(entry, result) {
|
|
209
227
|
const shell = findShellElement(entry);
|
|
210
228
|
if (!shell?.querySelector) {
|
|
211
229
|
entry.placeholder.innerHTML = renderShellHtml(result, entry);
|
|
230
|
+
markPlaceholderHydrated(entry.placeholder);
|
|
212
231
|
return;
|
|
213
232
|
}
|
|
214
233
|
|
|
@@ -75,6 +75,8 @@ export class ExcalidrawEmbedController {
|
|
|
75
75
|
this.maximizedEmbed = null;
|
|
76
76
|
this.overlayRoot = null;
|
|
77
77
|
this.maximizedRoot = null;
|
|
78
|
+
this.warmCacheRoot = null;
|
|
79
|
+
this.warmEntry = null;
|
|
78
80
|
this.placeholderObserver = null;
|
|
79
81
|
this.followedPeerIdsByFilePath = new Map();
|
|
80
82
|
this.pendingDisconnectRequests = new Map();
|
|
@@ -106,6 +108,11 @@ export class ExcalidrawEmbedController {
|
|
|
106
108
|
entry.placeholder = null;
|
|
107
109
|
});
|
|
108
110
|
this.embedEntries.clear();
|
|
111
|
+
if (this.warmEntry) {
|
|
112
|
+
this._clearEntryBootTimeout(this.warmEntry);
|
|
113
|
+
this.warmEntry.wrapper?.remove();
|
|
114
|
+
this.warmEntry = null;
|
|
115
|
+
}
|
|
109
116
|
this.pendingDisconnectRequests.forEach((request) => {
|
|
110
117
|
request.resolve(false);
|
|
111
118
|
});
|
|
@@ -115,6 +122,8 @@ export class ExcalidrawEmbedController {
|
|
|
115
122
|
this.overlayRoot = null;
|
|
116
123
|
this.maximizedRoot?.remove();
|
|
117
124
|
this.maximizedRoot = null;
|
|
125
|
+
this.warmCacheRoot?.remove();
|
|
126
|
+
this.warmCacheRoot = null;
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
async setFollowedUser(filePath, peerId = null) {
|
|
@@ -186,6 +195,7 @@ export class ExcalidrawEmbedController {
|
|
|
186
195
|
}));
|
|
187
196
|
|
|
188
197
|
const { nextEntries, removedEntries } = reconcileEmbedEntries(this.embedEntries, descriptors);
|
|
198
|
+
this._captureWarmEntry(removedEntries);
|
|
189
199
|
this._adoptReusableEntries(nextEntries, removedEntries);
|
|
190
200
|
removedEntries.forEach((entry) => this._destroyEntry(entry));
|
|
191
201
|
this.embedEntries = nextEntries;
|
|
@@ -196,8 +206,7 @@ export class ExcalidrawEmbedController {
|
|
|
196
206
|
entry.queued = false;
|
|
197
207
|
entry.followedPeerId = this.followedPeerIdsByFilePath.get(entry.filePath);
|
|
198
208
|
if (entry.wrapper) {
|
|
199
|
-
this.
|
|
200
|
-
this._attachWrapper(entry);
|
|
209
|
+
void this._hydrateEntry(entry);
|
|
201
210
|
return;
|
|
202
211
|
}
|
|
203
212
|
|
|
@@ -233,7 +242,7 @@ export class ExcalidrawEmbedController {
|
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
updateTheme(theme) {
|
|
236
|
-
this.
|
|
245
|
+
this._forEachManagedEntry((entry) => {
|
|
237
246
|
this._postMessageToEntry(entry, {
|
|
238
247
|
source: 'collabmd-host',
|
|
239
248
|
type: 'set-theme',
|
|
@@ -243,7 +252,7 @@ export class ExcalidrawEmbedController {
|
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
updateLocalUser(user) {
|
|
246
|
-
this.
|
|
255
|
+
this._forEachManagedEntry((entry) => {
|
|
247
256
|
this._syncEntryUser(entry, user);
|
|
248
257
|
});
|
|
249
258
|
}
|
|
@@ -390,19 +399,31 @@ export class ExcalidrawEmbedController {
|
|
|
390
399
|
return;
|
|
391
400
|
}
|
|
392
401
|
|
|
402
|
+
entry.isParked = false;
|
|
403
|
+
|
|
393
404
|
if (!entry.wrapper) {
|
|
394
405
|
const mount = this._createEmbedContainer(entry);
|
|
406
|
+
entry.header = mount.header;
|
|
395
407
|
entry.wrapper = mount.wrapper;
|
|
396
408
|
entry.iframe = mount.iframe;
|
|
397
409
|
entry.labelElement = mount.labelElement;
|
|
410
|
+
entry.maxButton = mount.maxButton;
|
|
411
|
+
entry.openButton = mount.openButton;
|
|
398
412
|
entry.instanceId = mount.instanceId;
|
|
399
413
|
entry.isReady = false;
|
|
414
|
+
entry.bootFilePath = entry.filePath;
|
|
415
|
+
entry.bootMode = this._getEntryMode(entry);
|
|
416
|
+
entry.loadedFilePath = entry.bootFilePath;
|
|
417
|
+
entry.loadedMode = entry.bootMode;
|
|
400
418
|
entry.bootAttempts = (entry.bootAttempts ?? 0) + 1;
|
|
401
419
|
this._armEntryBootTimeout(entry);
|
|
402
420
|
}
|
|
403
421
|
|
|
404
422
|
entry.followedPeerId = this.followedPeerIdsByFilePath.get(entry.filePath);
|
|
405
423
|
this._updateEmbedLabel(entry);
|
|
424
|
+
if (this._entryNeedsHardReload(entry)) {
|
|
425
|
+
this._reloadEntry(entry);
|
|
426
|
+
}
|
|
406
427
|
this._attachWrapper(entry);
|
|
407
428
|
}
|
|
408
429
|
|
|
@@ -444,22 +465,16 @@ export class ExcalidrawEmbedController {
|
|
|
444
465
|
return;
|
|
445
466
|
}
|
|
446
467
|
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
&& this._getEntryMode(entry) === this._getEntryMode(nextEntry)
|
|
450
|
-
&& (entry.wrapper || entry.iframe)
|
|
451
|
-
));
|
|
452
|
-
if (removedIndex < 0) {
|
|
468
|
+
const reusedEntry = this._claimReusableEntry(nextEntry, removedEntries);
|
|
469
|
+
if (!reusedEntry) {
|
|
453
470
|
return;
|
|
454
471
|
}
|
|
455
472
|
|
|
456
|
-
const [reusedEntry] = removedEntries.splice(removedIndex, 1);
|
|
457
473
|
reusedEntry.filePath = nextEntry.filePath;
|
|
458
474
|
reusedEntry.key = nextEntry.key;
|
|
459
475
|
reusedEntry.label = nextEntry.label;
|
|
460
476
|
reusedEntry.placeholder = nextEntry.placeholder;
|
|
461
477
|
reusedEntry.queued = false;
|
|
462
|
-
reusedEntry.reusedFromEmbed = this._isFilePreviewEntry(nextEntry);
|
|
463
478
|
nextEntries.set(key, reusedEntry);
|
|
464
479
|
});
|
|
465
480
|
}
|
|
@@ -477,16 +492,20 @@ export class ExcalidrawEmbedController {
|
|
|
477
492
|
}
|
|
478
493
|
|
|
479
494
|
this._clearEntryBootTimeout(entry);
|
|
495
|
+
if (this.warmEntry === entry) {
|
|
496
|
+
this.warmEntry = null;
|
|
497
|
+
}
|
|
480
498
|
this._resetPlaceholderLayout(entry);
|
|
499
|
+
this._setEntryLoadingState(entry, false);
|
|
481
500
|
entry.wrapper?.remove();
|
|
482
501
|
entry.placeholder = null;
|
|
483
502
|
}
|
|
484
503
|
|
|
485
|
-
_armEntryBootTimeout(entry) {
|
|
504
|
+
_armEntryBootTimeout(entry, delayMs = IFRAME_BOOT_TIMEOUT_MS) {
|
|
486
505
|
this._clearEntryBootTimeout(entry);
|
|
487
506
|
entry.bootTimerId = window.setTimeout(() => {
|
|
488
507
|
void this._handleEntryBootTimeout(entry);
|
|
489
|
-
},
|
|
508
|
+
}, delayMs);
|
|
490
509
|
}
|
|
491
510
|
|
|
492
511
|
_clearEntryBootTimeout(entry) {
|
|
@@ -519,6 +538,7 @@ export class ExcalidrawEmbedController {
|
|
|
519
538
|
|
|
520
539
|
this._clearEntryBootTimeout(entry);
|
|
521
540
|
this._resetPlaceholderLayout(entry);
|
|
541
|
+
this._setEntryLoadingState(entry, false);
|
|
522
542
|
|
|
523
543
|
if (this.maximizedEmbed?.wrapper === entry.wrapper) {
|
|
524
544
|
this._exitMaximizedEmbed();
|
|
@@ -527,9 +547,17 @@ export class ExcalidrawEmbedController {
|
|
|
527
547
|
entry.wrapper?.remove();
|
|
528
548
|
entry.wrapper = null;
|
|
529
549
|
entry.iframe = null;
|
|
550
|
+
entry.header = null;
|
|
530
551
|
entry.labelElement = null;
|
|
552
|
+
entry.maxButton = null;
|
|
553
|
+
entry.openButton = null;
|
|
531
554
|
entry.instanceId = null;
|
|
532
555
|
entry.isReady = false;
|
|
556
|
+
entry.isParked = false;
|
|
557
|
+
entry.bootFilePath = null;
|
|
558
|
+
entry.bootMode = null;
|
|
559
|
+
entry.loadedFilePath = null;
|
|
560
|
+
entry.loadedMode = null;
|
|
533
561
|
|
|
534
562
|
await this._hydrateEntry(entry);
|
|
535
563
|
}
|
|
@@ -545,6 +573,10 @@ export class ExcalidrawEmbedController {
|
|
|
545
573
|
}
|
|
546
574
|
}
|
|
547
575
|
|
|
576
|
+
if (this.warmEntry?.iframe?.contentWindow === contentWindow) {
|
|
577
|
+
return this.warmEntry;
|
|
578
|
+
}
|
|
579
|
+
|
|
548
580
|
return null;
|
|
549
581
|
}
|
|
550
582
|
|
|
@@ -559,6 +591,10 @@ export class ExcalidrawEmbedController {
|
|
|
559
591
|
}
|
|
560
592
|
}
|
|
561
593
|
|
|
594
|
+
if (this.warmEntry?.filePath === filePath) {
|
|
595
|
+
return this.warmEntry;
|
|
596
|
+
}
|
|
597
|
+
|
|
562
598
|
return null;
|
|
563
599
|
}
|
|
564
600
|
|
|
@@ -572,11 +608,20 @@ export class ExcalidrawEmbedController {
|
|
|
572
608
|
if (entry.wrapper) {
|
|
573
609
|
entry.wrapper.dataset.embedKey = entry.key;
|
|
574
610
|
entry.wrapper.dataset.file = entry.filePath;
|
|
611
|
+
entry.wrapper.classList.toggle('is-direct-file', this._isFilePreviewEntry(entry));
|
|
575
612
|
}
|
|
576
613
|
|
|
577
614
|
if (entry.iframe) {
|
|
578
615
|
entry.iframe.title = `Excalidraw: ${entry.filePath}`;
|
|
579
616
|
}
|
|
617
|
+
|
|
618
|
+
if (entry.openButton) {
|
|
619
|
+
if (this._isFilePreviewEntry(entry)) {
|
|
620
|
+
entry.openButton.remove();
|
|
621
|
+
} else if (!entry.openButton.isConnected && entry.header) {
|
|
622
|
+
entry.header.insertBefore(entry.openButton, entry.maxButton || null);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
580
625
|
}
|
|
581
626
|
|
|
582
627
|
_buildIframeUrl(entry) {
|
|
@@ -724,13 +769,33 @@ export class ExcalidrawEmbedController {
|
|
|
724
769
|
syncMaximizeButtonState();
|
|
725
770
|
|
|
726
771
|
return {
|
|
772
|
+
header,
|
|
727
773
|
iframe,
|
|
728
774
|
instanceId: iframe.dataset.instanceId,
|
|
729
775
|
labelElement: label,
|
|
776
|
+
maxButton: maxBtn,
|
|
777
|
+
openButton: openBtn,
|
|
730
778
|
wrapper,
|
|
731
779
|
};
|
|
732
780
|
}
|
|
733
781
|
|
|
782
|
+
_ensureWarmCacheRoot() {
|
|
783
|
+
if (this.warmCacheRoot?.isConnected && this.warmCacheRoot.parentElement === document.body) {
|
|
784
|
+
return this.warmCacheRoot;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
let warmCacheRoot = document.body.querySelector('[data-excalidraw-warm-cache-root="true"]');
|
|
788
|
+
if (!warmCacheRoot) {
|
|
789
|
+
warmCacheRoot = document.createElement('div');
|
|
790
|
+
warmCacheRoot.dataset.excalidrawWarmCacheRoot = 'true';
|
|
791
|
+
warmCacheRoot.className = 'excalidraw-warm-cache-root';
|
|
792
|
+
document.body.appendChild(warmCacheRoot);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
this.warmCacheRoot = warmCacheRoot;
|
|
796
|
+
return warmCacheRoot;
|
|
797
|
+
}
|
|
798
|
+
|
|
734
799
|
_setupResizer(resizer, iframe, onResizeEnd = null) {
|
|
735
800
|
let startY = 0;
|
|
736
801
|
let startHeight = 0;
|
|
@@ -801,7 +866,164 @@ export class ExcalidrawEmbedController {
|
|
|
801
866
|
}
|
|
802
867
|
|
|
803
868
|
_shouldInlineFilePreview(entry) {
|
|
804
|
-
return this._isFilePreviewEntry(entry)
|
|
869
|
+
return this._isFilePreviewEntry(entry);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
_entryNeedsHardReload(entry) {
|
|
873
|
+
const desiredMode = this._getEntryMode(entry);
|
|
874
|
+
return Boolean(
|
|
875
|
+
entry?.wrapper
|
|
876
|
+
&& entry.iframe
|
|
877
|
+
&& (
|
|
878
|
+
entry.isReady
|
|
879
|
+
? (
|
|
880
|
+
entry.loadedFilePath !== entry.filePath
|
|
881
|
+
|| entry.loadedMode !== desiredMode
|
|
882
|
+
)
|
|
883
|
+
: (
|
|
884
|
+
entry.bootFilePath !== entry.filePath
|
|
885
|
+
|| entry.bootMode !== desiredMode
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
_setEntryLoadingState(entry, isLoading) {
|
|
892
|
+
if (!entry?.wrapper) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
entry.wrapper.classList.toggle('is-loading', Boolean(isLoading));
|
|
897
|
+
entry.wrapper.setAttribute('aria-busy', isLoading ? 'true' : 'false');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
_forEachManagedEntry(callback) {
|
|
901
|
+
const seen = new Set();
|
|
902
|
+
this.embedEntries.forEach((entry) => {
|
|
903
|
+
if (!entry || seen.has(entry)) {
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
seen.add(entry);
|
|
908
|
+
callback(entry);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
if (this.warmEntry && !seen.has(this.warmEntry)) {
|
|
912
|
+
callback(this.warmEntry);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
_claimEntryFromRemovedEntries(removedEntries, predicate) {
|
|
917
|
+
const removedIndex = removedEntries.findIndex((entry) => (
|
|
918
|
+
(entry.wrapper || entry.iframe) && predicate(entry)
|
|
919
|
+
));
|
|
920
|
+
if (removedIndex < 0) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const [reusedEntry] = removedEntries.splice(removedIndex, 1);
|
|
925
|
+
return reusedEntry;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
_claimReusableEntry(nextEntry, removedEntries) {
|
|
929
|
+
const desiredMode = this._getEntryMode(nextEntry);
|
|
930
|
+
const exactMatch = this._claimEntryFromRemovedEntries(removedEntries, (entry) => (
|
|
931
|
+
entry.filePath === nextEntry.filePath
|
|
932
|
+
&& this._getEntryMode(entry) === desiredMode
|
|
933
|
+
));
|
|
934
|
+
if (exactMatch) {
|
|
935
|
+
return exactMatch;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (this._isFilePreviewEntry(nextEntry)) {
|
|
939
|
+
const promotedEntry = this._claimEntryFromRemovedEntries(removedEntries, (entry) => (
|
|
940
|
+
entry.filePath === nextEntry.filePath
|
|
941
|
+
));
|
|
942
|
+
if (promotedEntry) {
|
|
943
|
+
return promotedEntry;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const warmEntry = this._claimWarmEntry();
|
|
947
|
+
if (warmEntry) {
|
|
948
|
+
return warmEntry;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return this._claimEntryFromRemovedEntries(removedEntries, (entry) => (
|
|
952
|
+
this._isFilePreviewEntry(entry)
|
|
953
|
+
));
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
_claimWarmEntry() {
|
|
960
|
+
const warmEntry = this.warmEntry;
|
|
961
|
+
if (!warmEntry) {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
this.warmEntry = null;
|
|
966
|
+
return warmEntry;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
_captureWarmEntry(removedEntries) {
|
|
970
|
+
const warmCandidate = this._claimEntryFromRemovedEntries(removedEntries, (entry) => (
|
|
971
|
+
this._isFilePreviewEntry(entry)
|
|
972
|
+
));
|
|
973
|
+
if (!warmCandidate) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (this.warmEntry && this.warmEntry !== warmCandidate) {
|
|
978
|
+
this._destroyEntry(this.warmEntry);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.warmEntry = warmCandidate;
|
|
982
|
+
this._parkWarmEntry(warmCandidate);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
_parkWarmEntry(entry) {
|
|
986
|
+
if (!entry?.wrapper) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (this.maximizedEmbed?.wrapper === entry.wrapper) {
|
|
991
|
+
this._exitMaximizedEmbed();
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const warmCacheRoot = this._ensureWarmCacheRoot();
|
|
995
|
+
this._setEntryLoadingState(entry, false);
|
|
996
|
+
entry.isParked = true;
|
|
997
|
+
entry.isReady = false;
|
|
998
|
+
entry.bootFilePath = null;
|
|
999
|
+
entry.bootMode = null;
|
|
1000
|
+
entry.loadedFilePath = null;
|
|
1001
|
+
entry.loadedMode = null;
|
|
1002
|
+
entry.placeholder = null;
|
|
1003
|
+
if (entry.wrapper.parentElement !== warmCacheRoot) {
|
|
1004
|
+
warmCacheRoot.appendChild(entry.wrapper);
|
|
1005
|
+
}
|
|
1006
|
+
this._postMessageToEntry(entry, {
|
|
1007
|
+
source: 'collabmd-host',
|
|
1008
|
+
type: 'park-room',
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
_reloadEntry(entry) {
|
|
1013
|
+
if (!entry?.iframe) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this._clearEntryBootTimeout(entry);
|
|
1018
|
+
this._setEntryLoadingState(entry, true);
|
|
1019
|
+
entry.isReady = false;
|
|
1020
|
+
entry.bootAttempts = (entry.bootAttempts ?? 0) + 1;
|
|
1021
|
+
entry.bootFilePath = entry.filePath;
|
|
1022
|
+
entry.bootMode = this._getEntryMode(entry);
|
|
1023
|
+
entry.loadedFilePath = null;
|
|
1024
|
+
entry.loadedMode = null;
|
|
1025
|
+
entry.iframe.src = this._buildIframeUrl(entry).toString();
|
|
1026
|
+
this._armEntryBootTimeout(entry);
|
|
805
1027
|
}
|
|
806
1028
|
|
|
807
1029
|
_resetPlaceholderLayout(entry) {
|
|
@@ -950,8 +1172,15 @@ export class ExcalidrawEmbedController {
|
|
|
950
1172
|
}
|
|
951
1173
|
|
|
952
1174
|
if (msg.type === 'ready') {
|
|
1175
|
+
if (entry.isParked) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
953
1179
|
entry.isReady = true;
|
|
954
1180
|
this._clearEntryBootTimeout(entry);
|
|
1181
|
+
entry.loadedFilePath = entry.bootFilePath || entry.filePath;
|
|
1182
|
+
entry.loadedMode = entry.bootMode || this._getEntryMode(entry);
|
|
1183
|
+
this._setEntryLoadingState(entry, false);
|
|
955
1184
|
this._syncEntryUser(entry);
|
|
956
1185
|
this._postMessageToEntry(entry, {
|
|
957
1186
|
source: 'collabmd-host',
|
|
@@ -959,10 +1188,17 @@ export class ExcalidrawEmbedController {
|
|
|
959
1188
|
theme: this.getTheme?.() || 'dark',
|
|
960
1189
|
});
|
|
961
1190
|
this._syncEntryFollowState(entry);
|
|
1191
|
+
if (this._entryNeedsHardReload(entry)) {
|
|
1192
|
+
void this._hydrateEntry(entry);
|
|
1193
|
+
}
|
|
962
1194
|
return;
|
|
963
1195
|
}
|
|
964
1196
|
|
|
965
1197
|
if (msg.type === 'error') {
|
|
1198
|
+
if (entry.isParked) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
966
1202
|
entry.isReady = false;
|
|
967
1203
|
void this._handleEntryBootTimeout(entry);
|
|
968
1204
|
return;
|