@warkypublic/svelix 0.1.46 → 0.2.1

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.
Files changed (129) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +492 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +489 -614
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
  15. package/dist/components/CardGrid/DefaultCard.svelte +86 -97
  16. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
  17. package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
  18. package/dist/components/CardGrid/cardGrid.d.ts +24 -0
  19. package/dist/components/CardGrid/cardGrid.js +20 -0
  20. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  21. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  22. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  23. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  24. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  25. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  26. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  27. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  28. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  29. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  30. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  31. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  32. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  33. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  34. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  35. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  36. package/dist/components/Former/Former.svelte +306 -424
  37. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  38. package/dist/components/Former/FormerDrawer.svelte +16 -80
  39. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  40. package/dist/components/Former/FormerModal.svelte +16 -74
  41. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  42. package/dist/components/Former/FormerPreview.svelte +76 -78
  43. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  44. package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
  45. package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
  46. package/dist/components/Former/index.d.ts +1 -0
  47. package/dist/components/Former/index.js +1 -0
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -49
  49. package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
  50. package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +285 -457
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  53. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  54. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  55. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -47
  56. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  57. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  58. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -39
  59. package/dist/components/FormerControllers/NumberInputCtrl.svelte +21 -43
  60. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +8 -33
  61. package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -20
  62. package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -34
  63. package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -34
  64. package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
  65. package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
  66. package/dist/components/FormerControllers/index.d.ts +2 -0
  67. package/dist/components/FormerControllers/index.js +2 -0
  68. package/dist/components/FormerControllers/utils.d.ts +1 -0
  69. package/dist/components/FormerControllers/utils.js +3 -0
  70. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  71. package/dist/components/Gridler/components/Gridler.svelte +344 -442
  72. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  73. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  74. package/dist/components/Gridler/components/GridlerFull.svelte +521 -841
  75. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  76. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  77. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  78. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  79. package/dist/components/Gridler/types.d.ts +1 -1
  80. package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
  81. package/dist/components/Gridler/utils/serverPaging.js +7 -0
  82. package/dist/components/Icons/IconAdd.svelte +1 -3
  83. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  84. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  85. package/dist/components/Icons/IconCamera.svelte +1 -3
  86. package/dist/components/Icons/IconClose.svelte +1 -3
  87. package/dist/components/Icons/IconEdit.svelte +1 -3
  88. package/dist/components/Icons/IconFilter.svelte +1 -3
  89. package/dist/components/Icons/IconSearch.svelte +1 -3
  90. package/dist/components/Icons/IconSort.svelte +1 -3
  91. package/dist/components/Icons/IconTrash.svelte +1 -3
  92. package/dist/components/Portal/Portal.svelte +14 -38
  93. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  94. package/dist/components/Svark/Svark.svelte +680 -852
  95. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  96. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  97. package/dist/components/Svark/SvarkPager.svelte +2 -28
  98. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  99. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  100. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  101. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  102. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  103. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  104. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  105. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  106. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  107. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  108. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  109. package/dist/components/VTree/VTree.svelte +454 -431
  110. package/dist/components/VTree/VTree.utils.d.ts +23 -0
  111. package/dist/components/VTree/VTree.utils.js +73 -0
  112. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  113. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
  114. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
  115. package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
  116. package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
  117. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  118. package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
  119. package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
  120. package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
  121. package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
  122. package/dist/components/VTree/VTreeRow.svelte +77 -115
  123. package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
  124. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  125. package/dist/components/VTree/VTreeVirtualViewport.svelte +54 -143
  126. package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
  127. package/dist/components/VTree/types.d.ts +39 -1
  128. package/llm/COMPONENT_GUIDE.md +25 -0
  129. package/package.json +51 -50
@@ -1,312 +1,247 @@
1
- <script lang="ts">
2
- import { onMount, untrack } from "svelte";
3
- import type { CollaboraEditorProps } from "../types.js";
4
- import { getMimeFromExt, getExtFromFilename } from "../utils.js";
5
- import { SvelteURLSearchParams } from "svelte/reactivity";
6
-
7
- let {
8
- value,
9
- filename,
10
- readonly = false,
11
- wopiUrl,
12
- discoverUrl,
13
- adminKey = "",
14
- userid = 0,
15
- username = "user",
16
- viewtype: _viewtype = "tabbed",
17
- headers,
18
- onChange,
19
- onSave,
20
- insertText = $bindable(),
21
- insertHtml = $bindable(),
22
- }: CollaboraEditorProps = $props();
23
-
24
- let frameEl = $state<HTMLIFrameElement | undefined>(undefined);
25
- let ready = $state(false);
26
- let loading = $state(false);
27
- let errmsg = $state("");
28
-
29
- // Stable IDs for this editor instance — untrack prevents Svelte from treating
30
- // these as reactive dependencies (they are intentional one-time reads).
31
- const editorId = `collabora_${crypto.randomUUID().slice(0, 8)}`;
32
- // const ext = untrack(() => (filename ? getExtFromFilename(filename) : ""));
33
- const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
34
-
35
- // Mutable buffer — not reactive; mirrors React's useRef<Blob>
36
- let buffer: Blob | undefined = untrack(() => value);
37
- let saveRequestComplete = true;
38
- let signedToken = "";
39
- let signedWopiSrc = "";
40
-
41
- const contentType = $derived(
42
- getMimeFromExt(filename ? getExtFromFilename(filename) : ""),
43
- );
44
-
45
- // ── WOPI helpers ────────────────────────────────────────────────────────────
46
-
47
- async function fetchToken(
48
- canWrite: boolean,
49
- ): Promise<{ token: string; wopiSrc: string }> {
50
- const base = wopiUrl.replace(/\/$/, "");
51
- const reqHeaders: Record<string, string> = {
52
- "Content-Type": "application/json",
53
- ...headers,
54
- };
55
- if (adminKey) reqHeaders["X-API-Key"] = adminKey;
56
- const res = await fetch(`${base}/api/tokens`, {
57
- method: "POST",
58
- headers: reqHeaders,
59
- body: JSON.stringify({
60
- resource_type: "file",
61
- file_id: fileId,
62
- user_id: String(userid),
63
- user_name: username,
64
- can_write: canWrite,
65
- expires_in_seconds: 3600,
66
- }),
67
- });
68
- if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
69
- const data = await res.json();
70
- return { token: data.access_token, wopiSrc: data.wopi_src };
71
- }
72
-
73
- async function fileExistsInWopi(token: string): Promise<boolean> {
74
- const params = new URLSearchParams({ access_token: token });
75
- const base = wopiUrl.replace(/\/$/, "");
76
- const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
77
- headers,
78
- method: "GET",
79
- });
80
- return res.ok;
81
- }
82
-
83
- async function uploadToWopi(blob: Blob, token: string): Promise<void> {
84
- const params = new SvelteURLSearchParams();
85
- params.append("access_token", token);
86
- if (filename !== undefined) {
87
- params.append("filename", filename);
88
- params.append(
89
- "contenttype",
90
- getMimeFromExt(getExtFromFilename(filename)),
91
- );
92
- }
93
- if (userid !== undefined) params.append("userid", String(userid));
94
- if (username !== undefined) params.append("username", username);
95
-
96
- const base = wopiUrl.replace(/\/$/, "");
97
- const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
98
- const res = await fetch(fullurl, {
99
- headers: { ...headers, "X-WOPI-Override": "PUT" },
100
- method: "POST",
101
- body: blob,
102
- });
103
- if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
104
- }
105
-
106
- async function downloadFromWopi(): Promise<Blob | undefined> {
107
- const params = new SvelteURLSearchParams();
108
- params.append("access_token", signedToken);
109
- if (userid !== undefined) params.append("userid", String(userid));
110
-
111
- const base = wopiUrl.replace(/\/$/, "");
112
- const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
113
- const res = await fetch(fullurl, { headers, method: "GET" });
114
- if (res.ok) return res.blob();
115
- return undefined;
116
- }
117
-
118
- // ── Discovery ────────────────────────────────────────────────────────────────
119
-
120
- async function findEditorUrl(mimeType: string): Promise<string> {
121
- const res = await fetch(discoverUrl);
122
- if (!res.ok) throw new Error(`Discovery request failed: ${res.status}`);
123
- const xml = await res.text();
124
- const doc = new DOMParser().parseFromString(xml, "text/xml");
125
- const action = doc.querySelector(`app[name="${mimeType}"] action`);
126
- const urlsrc = action?.getAttribute("urlsrc");
127
- if (!urlsrc)
128
- throw new Error(`No Collabora editor found for MIME type: ${mimeType}`);
129
- const params = new URLSearchParams({
130
- WOPISrc: signedWopiSrc,
131
- access_token: signedToken,
132
- });
133
- return `${urlsrc}${params}`;
134
- }
135
-
136
- // ── Load ─────────────────────────────────────────────────────────────────────
137
-
138
- async function loadEditor(): Promise<void> {
139
- if (!filename || !buffer) return;
140
- errmsg = "";
141
- loading = true;
142
- ready = false;
143
- insertText = undefined;
144
- insertHtml = undefined;
145
- try {
146
- try {
147
- localStorage.setItem("WSDWelcomeDisabled", "true");
148
- } catch {
149
- /* unavailable */
150
- }
151
- // Always use a write token for the initial upload regardless of readonly
152
- const { token: writeToken } = await fetchToken(true);
153
- const exists = await fileExistsInWopi(writeToken);
154
- if (!exists) await uploadToWopi(buffer, writeToken);
155
-
156
- // Editor token respects readonly
157
- const { token, wopiSrc } = await fetchToken(!readonly);
158
- signedToken = token;
159
- signedWopiSrc = wopiSrc;
160
-
161
- const url = await findEditorUrl(contentType);
162
- // Navigate iframe to Collabora — Collabora will fetch the file from WOPI
163
- if (frameEl) frameEl.src = url;
164
- } catch (e) {
165
- errmsg = String(e);
166
- loading = false;
167
- }
168
- }
169
-
170
- // ── Save flow ─────────────────────────────────────────────────────────────────
171
-
172
- function sendSaveAction(): Promise<void> {
173
- return new Promise((resolve, reject) => {
174
- if (!frameEl?.contentWindow) {
175
- reject(new Error("No iframe"));
176
- return;
177
- }
178
- saveRequestComplete = false;
179
- frameEl.contentWindow.postMessage(
180
- JSON.stringify({
181
- MessageId: "Action_Save",
182
- SendTime: Date.now(),
183
- Values: {
184
- Notify: true,
185
- DontSaveIfUnmodified: false,
186
- DontTerminateEdit: false,
187
- },
188
- }),
189
- "*",
190
- );
191
- let cnt = 0;
192
- const itv = setInterval(() => {
193
- cnt++;
194
- if (saveRequestComplete) {
195
- clearInterval(itv);
196
- resolve();
197
- }
198
- if (cnt > 60) {
199
- clearInterval(itv);
200
- reject(new Error("Save timeout after 30s"));
201
- }
202
- }, 500);
203
- });
204
- }
205
-
206
- function handleSaveResponse(): void {
207
- setTimeout(async () => {
208
- try {
209
- const blob = await downloadFromWopi();
210
- if (blob) {
211
- buffer = blob;
212
- onChange?.(blob);
213
- }
214
- } catch (e) {
215
- errmsg = `Could not retrieve saved file: ${e}`;
216
- } finally {
217
- saveRequestComplete = true;
218
- onSave?.(buffer ?? new Blob([]));
219
- }
220
- }, 100);
221
- }
222
-
223
- // ── PostMessage handler ───────────────────────────────────────────────────────
224
-
225
- function handlePostMessage(e: MessageEvent): void {
226
- try {
227
- const msg =
228
- typeof e.data === "string"
229
- ? JSON.parse(e.data)
230
- : typeof e.data === "object"
231
- ? e.data
232
- : null;
233
- if (!msg?.MessageId || !frameEl?.contentWindow) return;
234
-
235
- const msgId: string = msg.MessageId;
236
- const msgData = msg.Values;
237
-
238
- if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
239
- frameEl.contentWindow.postMessage(
240
- JSON.stringify({
241
- MessageId: "Host_PostmessageReady",
242
- SendTime: Date.now(),
243
- }),
244
- "*",
245
- );
246
- frameEl.contentWindow.postMessage(
247
- JSON.stringify({
248
- MessageId: "Action_ChangeEditorMode",
249
- SendTime: Date.now(),
250
- Values: { Mode: readonly ? "view" : "edit" },
251
- }),
252
- "*",
253
- );
254
- ready = true;
255
- loading = false;
256
- insertText = (text: string) => postPaste("text/plain", text);
257
- insertHtml = (html: string) => postPaste("text/html", html);
258
- return;
259
- }
260
- if (msgId === "UI_Save") {
261
- sendSaveAction();
262
- return;
263
- }
264
- if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
265
- handleSaveResponse();
266
- return;
267
- }
268
- if (msgId === "Action_Save_Resp" && msgData?.success) {
269
- handleSaveResponse();
270
- }
271
- } catch {
272
- // ignore malformed messages from other origins
273
- }
274
- }
275
-
276
- onMount(() => {
277
- window.addEventListener("message", handlePostMessage);
278
- return () => window.removeEventListener("message", handlePostMessage);
279
- });
280
-
281
- // ── Insert actions (bound by parent via bind:insertText / bind:insertHtml) ────
282
-
283
- function postPaste(mimetype: string, data: string): void {
284
- if (!frameEl?.contentWindow) return;
285
- // Restore focus to the iframe so Collabora has an active cursor position
286
- frameEl.focus();
287
- frameEl.contentWindow.postMessage(
288
- JSON.stringify({
289
- MessageId: "Action_Paste",
290
- SendTime: Date.now(),
291
- Values: { Mimetype: mimetype, Data: data },
292
- }),
293
- "*",
294
- );
295
- }
296
-
297
- // ── Reactive reload ───────────────────────────────────────────────────────────
298
-
299
- $effect(() => {
300
- // Track value, filename and WOPI config as dependencies.
301
- // Update buffer when value prop changes, then reload.
302
- const v = value;
303
- const f = filename;
304
- void wopiUrl;
305
- void discoverUrl;
306
-
307
- if (v !== undefined) buffer = v;
308
- if (f && buffer) void loadEditor();
309
- });
1
+ <script lang="ts">import { onMount, untrack } from "svelte";
2
+ import { getMimeFromExt, getExtFromFilename } from "../utils.js";
3
+ import { SvelteURLSearchParams } from "svelte/reactivity";
4
+ let { value, filename, readonly = false, wopiUrl, discoverUrl, adminKey = "", userid = 0, username = "user", viewtype: _viewtype = "tabbed", headers, onChange, onSave, insertText = $bindable(), insertHtml = $bindable() } = $props();
5
+ let frameEl = $state(undefined);
6
+ let ready = $state(false);
7
+ let loading = $state(false);
8
+ let errmsg = $state("");
9
+ // Stable IDs for this editor instance — untrack prevents Svelte from treating
10
+ // these as reactive dependencies (they are intentional one-time reads).
11
+ const editorId = `collabora_${crypto.randomUUID().slice(0, 8)}`;
12
+ // const ext = untrack(() => (filename ? getExtFromFilename(filename) : ""));
13
+ const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
14
+ // Mutable buffer — not reactive; mirrors React's useRef<Blob>
15
+ let buffer = untrack(() => value);
16
+ let saveRequestComplete = true;
17
+ let signedToken = "";
18
+ let signedWopiSrc = "";
19
+ const contentType = $derived(getMimeFromExt(filename ? getExtFromFilename(filename) : ""));
20
+ // ── WOPI helpers ────────────────────────────────────────────────────────────
21
+ async function fetchToken(canWrite) {
22
+ const base = wopiUrl.replace(/\/$/, "");
23
+ const reqHeaders = {
24
+ "Content-Type": "application/json",
25
+ ...headers
26
+ };
27
+ if (adminKey) reqHeaders["X-API-Key"] = adminKey;
28
+ const res = await fetch(`${base}/api/tokens`, {
29
+ method: "POST",
30
+ headers: reqHeaders,
31
+ body: JSON.stringify({
32
+ resource_type: "file",
33
+ file_id: fileId,
34
+ user_id: String(userid),
35
+ user_name: username,
36
+ can_write: canWrite,
37
+ expires_in_seconds: 3600
38
+ })
39
+ });
40
+ if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
41
+ const data = await res.json();
42
+ return {
43
+ token: data.access_token,
44
+ wopiSrc: data.wopi_src
45
+ };
46
+ }
47
+ async function fileExistsInWopi(token) {
48
+ const params = new URLSearchParams({ access_token: token });
49
+ const base = wopiUrl.replace(/\/$/, "");
50
+ const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
51
+ headers,
52
+ method: "GET"
53
+ });
54
+ return res.ok;
55
+ }
56
+ async function uploadToWopi(blob, token) {
57
+ const params = new SvelteURLSearchParams();
58
+ params.append("access_token", token);
59
+ if (filename !== undefined) {
60
+ params.append("filename", filename);
61
+ params.append("contenttype", getMimeFromExt(getExtFromFilename(filename)));
62
+ }
63
+ if (userid !== undefined) params.append("userid", String(userid));
64
+ if (username !== undefined) params.append("username", username);
65
+ const base = wopiUrl.replace(/\/$/, "");
66
+ const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
67
+ const res = await fetch(fullurl, {
68
+ headers: {
69
+ ...headers,
70
+ "X-WOPI-Override": "PUT"
71
+ },
72
+ method: "POST",
73
+ body: blob
74
+ });
75
+ if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
76
+ }
77
+ async function downloadFromWopi() {
78
+ const params = new SvelteURLSearchParams();
79
+ params.append("access_token", signedToken);
80
+ if (userid !== undefined) params.append("userid", String(userid));
81
+ const base = wopiUrl.replace(/\/$/, "");
82
+ const fullurl = `${base}/wopi/files/${fileId}/contents?${params.toString()}`;
83
+ const res = await fetch(fullurl, {
84
+ headers,
85
+ method: "GET"
86
+ });
87
+ if (res.ok) return res.blob();
88
+ return undefined;
89
+ }
90
+ // ── Discovery ────────────────────────────────────────────────────────────────
91
+ async function findEditorUrl(mimeType) {
92
+ const res = await fetch(discoverUrl);
93
+ if (!res.ok) throw new Error(`Discovery request failed: ${res.status}`);
94
+ const xml = await res.text();
95
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
96
+ const action = doc.querySelector(`app[name="${mimeType}"] action`);
97
+ const urlsrc = action?.getAttribute("urlsrc");
98
+ if (!urlsrc) throw new Error(`No Collabora editor found for MIME type: ${mimeType}`);
99
+ const params = new URLSearchParams({
100
+ WOPISrc: signedWopiSrc,
101
+ access_token: signedToken
102
+ });
103
+ return `${urlsrc}${params}`;
104
+ }
105
+ // ── Load ─────────────────────────────────────────────────────────────────────
106
+ async function loadEditor() {
107
+ if (!filename || !buffer) return;
108
+ errmsg = "";
109
+ loading = true;
110
+ ready = false;
111
+ insertText = undefined;
112
+ insertHtml = undefined;
113
+ try {
114
+ try {
115
+ localStorage.setItem("WSDWelcomeDisabled", "true");
116
+ } catch {}
117
+ // Always use a write token for the initial upload regardless of readonly
118
+ const { token: writeToken } = await fetchToken(true);
119
+ const exists = await fileExistsInWopi(writeToken);
120
+ if (!exists) await uploadToWopi(buffer, writeToken);
121
+ // Editor token respects readonly
122
+ const { token, wopiSrc } = await fetchToken(!readonly);
123
+ signedToken = token;
124
+ signedWopiSrc = wopiSrc;
125
+ const url = await findEditorUrl(contentType);
126
+ // Navigate iframe to Collabora — Collabora will fetch the file from WOPI
127
+ if (frameEl) frameEl.src = url;
128
+ } catch (e) {
129
+ errmsg = String(e);
130
+ loading = false;
131
+ }
132
+ }
133
+ // ── Save flow ─────────────────────────────────────────────────────────────────
134
+ function sendSaveAction() {
135
+ return new Promise((resolve, reject) => {
136
+ if (!frameEl?.contentWindow) {
137
+ reject(new Error("No iframe"));
138
+ return;
139
+ }
140
+ saveRequestComplete = false;
141
+ frameEl.contentWindow.postMessage(JSON.stringify({
142
+ MessageId: "Action_Save",
143
+ SendTime: Date.now(),
144
+ Values: {
145
+ Notify: true,
146
+ DontSaveIfUnmodified: false,
147
+ DontTerminateEdit: false
148
+ }
149
+ }), "*");
150
+ let cnt = 0;
151
+ const itv = setInterval(() => {
152
+ cnt++;
153
+ if (saveRequestComplete) {
154
+ clearInterval(itv);
155
+ resolve();
156
+ }
157
+ if (cnt > 60) {
158
+ clearInterval(itv);
159
+ reject(new Error("Save timeout after 30s"));
160
+ }
161
+ }, 500);
162
+ });
163
+ }
164
+ function handleSaveResponse() {
165
+ setTimeout(async () => {
166
+ try {
167
+ const blob = await downloadFromWopi();
168
+ if (blob) {
169
+ buffer = blob;
170
+ onChange?.(blob);
171
+ }
172
+ } catch (e) {
173
+ errmsg = `Could not retrieve saved file: ${e}`;
174
+ } finally {
175
+ saveRequestComplete = true;
176
+ onSave?.(buffer ?? new Blob([]));
177
+ }
178
+ }, 100);
179
+ }
180
+ // ── PostMessage handler ───────────────────────────────────────────────────────
181
+ function handlePostMessage(e) {
182
+ try {
183
+ const msg = typeof e.data === "string" ? JSON.parse(e.data) : typeof e.data === "object" ? e.data : null;
184
+ if (!msg?.MessageId || !frameEl?.contentWindow) return;
185
+ const msgId = msg.MessageId;
186
+ const msgData = msg.Values;
187
+ if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
188
+ frameEl.contentWindow.postMessage(JSON.stringify({
189
+ MessageId: "Host_PostmessageReady",
190
+ SendTime: Date.now()
191
+ }), "*");
192
+ frameEl.contentWindow.postMessage(JSON.stringify({
193
+ MessageId: "Action_ChangeEditorMode",
194
+ SendTime: Date.now(),
195
+ Values: { Mode: readonly ? "view" : "edit" }
196
+ }), "*");
197
+ ready = true;
198
+ loading = false;
199
+ insertText = (text) => postPaste("text/plain", text);
200
+ insertHtml = (html) => postPaste("text/html", html);
201
+ return;
202
+ }
203
+ if (msgId === "UI_Save") {
204
+ sendSaveAction();
205
+ return;
206
+ }
207
+ if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
208
+ handleSaveResponse();
209
+ return;
210
+ }
211
+ if (msgId === "Action_Save_Resp" && msgData?.success) {
212
+ handleSaveResponse();
213
+ }
214
+ } catch {}
215
+ }
216
+ onMount(() => {
217
+ window.addEventListener("message", handlePostMessage);
218
+ return () => window.removeEventListener("message", handlePostMessage);
219
+ });
220
+ // ── Insert actions (bound by parent via bind:insertText / bind:insertHtml) ────
221
+ function postPaste(mimetype, data) {
222
+ if (!frameEl?.contentWindow) return;
223
+ // Restore focus to the iframe so Collabora has an active cursor position
224
+ frameEl.focus();
225
+ frameEl.contentWindow.postMessage(JSON.stringify({
226
+ MessageId: "Action_Paste",
227
+ SendTime: Date.now(),
228
+ Values: {
229
+ Mimetype: mimetype,
230
+ Data: data
231
+ }
232
+ }), "*");
233
+ }
234
+ // ── Reactive reload ───────────────────────────────────────────────────────────
235
+ $effect(() => {
236
+ // Track value, filename and WOPI config as dependencies.
237
+ // Update buffer when value prop changes, then reload.
238
+ const v = value;
239
+ const f = filename;
240
+ void wopiUrl;
241
+ void discoverUrl;
242
+ if (v !== undefined) buffer = v;
243
+ if (f && buffer) void loadEditor();
244
+ });
310
245
  </script>
311
246
 
312
247
  <div class="relative flex h-full w-full flex-1 flex-col overflow-hidden">
@@ -1,6 +1,5 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
- let { value: _value, filename }: ImageViewerProps = $props();
1
+ <script lang="ts">let { value: _value, filename } = $props();
2
+ export {};
4
3
  </script>
5
4
 
6
5
  <div class="card p-4 text-surface-700-300">
@@ -1,18 +1,17 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
- import IconAlertTriangle from '../../Icons/IconAlertTriangle.svelte';
4
- let { value, filename }: ImageViewerProps = $props();
5
-
6
- let src = $state('');
7
- let err = $state(false);
8
-
9
- $effect(() => {
10
- if (!value) { src = ''; return; }
11
- err = false;
12
- const url = URL.createObjectURL(value);
13
- src = url;
14
- return () => URL.revokeObjectURL(url);
15
- });
1
+ <script lang="ts">import IconAlertTriangle from "../../Icons/IconAlertTriangle.svelte";
2
+ let { value, filename } = $props();
3
+ let src = $state("");
4
+ let err = $state(false);
5
+ $effect(() => {
6
+ if (!value) {
7
+ src = "";
8
+ return;
9
+ }
10
+ err = false;
11
+ const url = URL.createObjectURL(value);
12
+ src = url;
13
+ return () => URL.revokeObjectURL(url);
14
+ });
16
15
  </script>
17
16
 
18
17
  <div class="relative flex h-full w-full items-center justify-center overflow-hidden bg-surface-50-950">