@warkypublic/svelix 0.1.44 → 0.1.47

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 (109) 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 +488 -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 +1141 -0
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
  15. package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
  16. package/dist/components/CardGrid/DefaultCard.svelte +287 -0
  17. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
  18. package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
  19. package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
  20. package/dist/components/CardGrid/cardGrid.d.ts +33 -0
  21. package/dist/components/CardGrid/cardGrid.js +21 -0
  22. package/dist/components/CardGrid/index.d.ts +4 -0
  23. package/dist/components/CardGrid/index.js +4 -0
  24. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  25. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  26. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  27. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  28. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  29. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  30. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  31. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  32. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  33. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  34. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  35. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  36. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  37. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  38. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  39. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  40. package/dist/components/Former/Former.svelte +306 -424
  41. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  42. package/dist/components/Former/FormerDrawer.svelte +26 -78
  43. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  44. package/dist/components/Former/FormerModal.svelte +24 -70
  45. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  46. package/dist/components/Former/FormerPreview.svelte +76 -78
  47. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
  49. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
  50. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  53. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
  54. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  55. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  56. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
  57. package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
  58. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
  59. package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
  60. package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
  61. package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
  62. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  63. package/dist/components/Gridler/components/Gridler.svelte +338 -442
  64. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  65. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  66. package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
  67. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  68. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  69. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  70. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  71. package/dist/components/Gridler/types.d.ts +1 -1
  72. package/dist/components/Icons/IconAdd.svelte +1 -3
  73. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  74. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  75. package/dist/components/Icons/IconCamera.svelte +1 -3
  76. package/dist/components/Icons/IconClose.svelte +1 -3
  77. package/dist/components/Icons/IconEdit.svelte +1 -3
  78. package/dist/components/Icons/IconFilter.svelte +1 -3
  79. package/dist/components/Icons/IconSearch.svelte +1 -3
  80. package/dist/components/Icons/IconSort.svelte +1 -3
  81. package/dist/components/Icons/IconTrash.svelte +1 -3
  82. package/dist/components/Portal/Portal.svelte +14 -38
  83. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  84. package/dist/components/Svark/Svark.svelte +680 -852
  85. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  86. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  87. package/dist/components/Svark/SvarkPager.svelte +2 -28
  88. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  89. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  90. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  91. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  92. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  93. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  94. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  95. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  96. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  97. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  98. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  99. package/dist/components/VTree/VTree.svelte +349 -431
  100. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  101. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  102. package/dist/components/VTree/VTreeRow.svelte +13 -65
  103. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  104. package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
  105. package/dist/components/index.d.ts +1 -0
  106. package/dist/components/index.js +1 -0
  107. package/llm/COMPONENT_GUIDE.md +240 -0
  108. package/llm/plans/card_grid.plan.md +305 -0
  109. package/package.json +48 -46
@@ -1,300 +1,237 @@
1
- <script lang="ts">
2
- import { onMount, untrack, tick } from "svelte";
3
- import type { Office365EditorProps } from "../types.js";
4
- import { getExtFromFilename } from "../utils.js";
5
- import { SvelteURLSearchParams } from "svelte/reactivity";
6
-
7
- let {
8
- value,
9
- filename,
10
- readonly = false,
11
- wopiUrl,
12
- officeOnlineUrl = "https://onenote.officeapps.live.com",
13
- adminKey = "",
14
- userid = 0,
15
- username = "user",
16
- headers,
17
- onChange,
18
- onSave,
19
- insertText = $bindable(),
20
- insertHtml = $bindable(),
21
- }: Office365EditorProps = $props();
22
-
23
- let frameEl = $state<HTMLIFrameElement | undefined>(undefined);
24
- let formEl = $state<HTMLFormElement | undefined>(undefined);
25
- let ready = $state(false);
26
- let loading = $state(false);
27
- let errmsg = $state("");
28
- let editorActionUrl = $state("");
29
-
30
- const editorId = `office365_${crypto.randomUUID().slice(0, 8)}`;
31
- const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
32
-
33
- // Mutable buffer — not reactive
34
- let buffer: Blob | undefined = untrack(() => value);
35
- let saveRequestComplete = true;
36
- let signedToken = "";
37
- let signedTokenTtl = 0;
38
- let signedWopiSrc = "";
39
-
40
- // ── Token ─────────────────────────────────────────────────────────────────────
41
-
42
- async function fetchToken(
43
- canWrite: boolean,
44
- ): Promise<{ token: string; wopiSrc: string; ttl: number }> {
45
- const base = wopiUrl.replace(/\/$/, "");
46
- const reqHeaders: Record<string, string> = {
47
- "Content-Type": "application/json",
48
- ...headers,
49
- };
50
- if (adminKey) reqHeaders["X-API-Key"] = adminKey;
51
- const res = await fetch(`${base}/api/tokens`, {
52
- method: "POST",
53
- headers: reqHeaders,
54
- body: JSON.stringify({
55
- resource_type: "file",
56
- file_id: fileId,
57
- user_id: String(userid),
58
- user_name: username,
59
- can_write: canWrite,
60
- expires_in_seconds: 3600,
61
- }),
62
- });
63
- if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
64
- const data = await res.json();
65
- return {
66
- token: data.access_token,
67
- wopiSrc: data.wopi_src,
68
- ttl: Date.now() + (data.expires_in_seconds ?? 3600) * 1000,
69
- };
70
- }
71
-
72
- // ── WOPI helpers ─────────────────────────────────────────────────────────────
73
-
74
- async function fileExistsInWopi(token: string): Promise<boolean> {
75
- const params = new SvelteURLSearchParams();
76
- params.append("access_token", token);
77
- const base = wopiUrl.replace(/\/$/, "");
78
- const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
79
- headers,
80
- method: "GET",
81
- });
82
- return res.ok;
83
- }
84
-
85
- async function uploadToWopi(blob: Blob, token: string): Promise<void> {
86
- const params = new SvelteURLSearchParams();
87
- params.append("access_token", token);
88
- if (filename !== undefined) params.append("filename", filename);
89
- const base = wopiUrl.replace(/\/$/, "");
90
- const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
91
- headers: { ...headers, "X-WOPI-Override": "PUT" },
92
- method: "POST",
93
- body: blob,
94
- });
95
- if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
96
- }
97
-
98
- async function downloadFromWopi(): Promise<Blob | undefined> {
99
- const params = new SvelteURLSearchParams();
100
- params.append("access_token", signedToken);
101
- const base = wopiUrl.replace(/\/$/, "");
102
- const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
103
- headers,
104
- method: "GET",
105
- });
106
- if (res.ok) return res.blob();
107
- return undefined;
108
- }
109
-
110
- // ── Discovery ────────────────────────────────────────────────────────────────
111
- // Office Online uses extension-based action lookup (not MIME-type app names).
112
- // urlsrc may contain optional placeholder blocks like <rs=xx> strip them.
113
-
114
- async function findEditorUrl(ext: string): Promise<string> {
115
- const discoveryUrl = `${officeOnlineUrl.replace(/\/$/, "")}/hosting/discovery`;
116
- const res = await fetch(discoveryUrl);
117
- if (!res.ok) throw new Error(`Discovery failed: ${res.status}`);
118
- const xml = await res.text();
119
- const doc = new DOMParser().parseFromString(xml, "text/xml");
120
- const actionName = readonly ? "view" : "edit";
121
- const actionEl = doc.querySelector(
122
- `action[name="${actionName}"][ext="${ext.toLowerCase()}"]`,
123
- );
124
- const urlsrc = actionEl?.getAttribute("urlsrc");
125
- if (!urlsrc)
126
- throw new Error(
127
- `No Office Online editor for extension: ${ext} (action: ${actionName})`,
128
- );
129
- // Strip optional placeholder blocks, e.g. <rs=xx&dc=xx>
130
- return urlsrc.replace(/<[^>]+>/g, "");
131
- }
132
-
133
- // ── Load ─────────────────────────────────────────────────────────────────────
134
- // Office Online requires a form POST into the iframe (not iframe src assignment).
135
-
136
- async function loadEditor(): Promise<void> {
137
- if (!filename || !buffer) return;
138
- errmsg = "";
139
- loading = true;
140
- ready = false;
141
- insertText = undefined;
142
- insertHtml = undefined;
143
- try {
144
- // Always use a write token for the initial upload regardless of readonly
145
- const { token: writeToken } = await fetchToken(true);
146
- const exists = await fileExistsInWopi(writeToken);
147
- if (!exists) await uploadToWopi(buffer, writeToken);
148
-
149
- // Editor token respects readonly
150
- const { token, wopiSrc, ttl } = await fetchToken(!readonly);
151
- signedToken = token;
152
- signedWopiSrc = wopiSrc;
153
- signedTokenTtl = ttl;
154
-
155
- const ext = getExtFromFilename(filename);
156
- const actionUrl = await findEditorUrl(ext);
157
- const sep = actionUrl.includes("?") ? "&" : "?";
158
- editorActionUrl = `${actionUrl}${sep}WOPISrc=${encodeURIComponent(signedWopiSrc)}`;
159
-
160
- // Flush reactive state so the form action attribute is updated before submit
161
- await tick();
162
- formEl?.submit();
163
- } catch (e) {
164
- errmsg = String(e);
165
- loading = false;
166
- }
167
- }
168
-
169
- // ── Save flow ─────────────────────────────────────────────────────────────────
170
-
171
- function sendSaveAction(): Promise<void> {
172
- return new Promise((resolve, reject) => {
173
- if (!frameEl?.contentWindow) {
174
- reject(new Error("No iframe"));
175
- return;
176
- }
177
- saveRequestComplete = false;
178
- frameEl.contentWindow.postMessage(
179
- JSON.stringify({
180
- MessageId: "Action_Save",
181
- SendTime: Date.now(),
182
- Values: {
183
- Notify: true,
184
- DontSaveIfUnmodified: false,
185
- DontTerminateEdit: false,
186
- },
187
- }),
188
- "*",
189
- );
190
- let cnt = 0;
191
- const itv = setInterval(() => {
192
- cnt++;
193
- if (saveRequestComplete) {
194
- clearInterval(itv);
195
- resolve();
196
- }
197
- if (cnt > 60) {
198
- clearInterval(itv);
199
- reject(new Error("Save timeout after 30s"));
200
- }
201
- }, 500);
202
- });
203
- }
204
-
205
- function handleSaveResponse(): void {
206
- setTimeout(async () => {
207
- try {
208
- const blob = await downloadFromWopi();
209
- if (blob) {
210
- buffer = blob;
211
- onChange?.(blob);
212
- }
213
- } catch (e) {
214
- errmsg = `Could not retrieve saved file: ${e}`;
215
- } finally {
216
- saveRequestComplete = true;
217
- onSave?.(buffer ?? new Blob([]));
218
- }
219
- }, 100);
220
- }
221
-
222
- // ── PostMessage handler ───────────────────────────────────────────────────────
223
-
224
- function handlePostMessage(e: MessageEvent): void {
225
- try {
226
- const msg =
227
- typeof e.data === "string"
228
- ? JSON.parse(e.data)
229
- : typeof e.data === "object"
230
- ? e.data
231
- : null;
232
- if (!msg?.MessageId || !frameEl?.contentWindow) return;
233
-
234
- const msgId: string = msg.MessageId;
235
- const msgData = msg.Values;
236
-
237
- if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
238
- frameEl.contentWindow.postMessage(
239
- JSON.stringify({
240
- MessageId: "Host_PostmessageReady",
241
- SendTime: Date.now(),
242
- }),
243
- "*",
244
- );
245
- ready = true;
246
- loading = false;
247
- insertText = (text: string) => postPaste("text/plain", text);
248
- insertHtml = (html: string) => postPaste("text/html", html);
249
- return;
250
- }
251
- if (msgId === "UI_Save") {
252
- sendSaveAction();
253
- return;
254
- }
255
- if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
256
- handleSaveResponse();
257
- return;
258
- }
259
- if (msgId === "Action_Save_Resp" && msgData?.success) {
260
- handleSaveResponse();
261
- }
262
- } catch {
263
- // ignore malformed messages from other origins
264
- }
265
- }
266
-
267
- onMount(() => {
268
- window.addEventListener("message", handlePostMessage);
269
- return () => window.removeEventListener("message", handlePostMessage);
270
- });
271
-
272
- // ── Insert actions ────────────────────────────────────────────────────────────
273
-
274
- function postPaste(mimetype: string, data: string): void {
275
- if (!frameEl?.contentWindow) return;
276
- frameEl.focus();
277
- frameEl.contentWindow.postMessage(
278
- JSON.stringify({
279
- MessageId: "Action_Paste",
280
- SendTime: Date.now(),
281
- Values: { Mimetype: mimetype, Data: data },
282
- }),
283
- "*",
284
- );
285
- }
286
-
287
- // ── Reactive reload ───────────────────────────────────────────────────────────
288
-
289
- $effect(() => {
290
- const v = value;
291
- const f = filename;
292
- void wopiUrl;
293
- void officeOnlineUrl;
294
-
295
- if (v !== undefined) buffer = v;
296
- if (f && buffer) void loadEditor();
297
- });
1
+ <script lang="ts">import { onMount, untrack, tick } from "svelte";
2
+ import { getExtFromFilename } from "../utils.js";
3
+ import { SvelteURLSearchParams } from "svelte/reactivity";
4
+ let { value, filename, readonly = false, wopiUrl, officeOnlineUrl = "https://onenote.officeapps.live.com", adminKey = "", userid = 0, username = "user", headers, onChange, onSave, insertText = $bindable(), insertHtml = $bindable() } = $props();
5
+ let frameEl = $state(undefined);
6
+ let formEl = $state(undefined);
7
+ let ready = $state(false);
8
+ let loading = $state(false);
9
+ let errmsg = $state("");
10
+ let editorActionUrl = $state("");
11
+ const editorId = `office365_${crypto.randomUUID().slice(0, 8)}`;
12
+ const fileId = `${untrack(() => userid)}_${crypto.randomUUID()}`;
13
+ // Mutable buffer — not reactive
14
+ let buffer = untrack(() => value);
15
+ let saveRequestComplete = true;
16
+ let signedToken = $state("");
17
+ let signedTokenTtl = $state(0);
18
+ let signedWopiSrc = "";
19
+ // ── Token ─────────────────────────────────────────────────────────────────────
20
+ async function fetchToken(canWrite) {
21
+ const base = wopiUrl.replace(/\/$/, "");
22
+ const reqHeaders = {
23
+ "Content-Type": "application/json",
24
+ ...headers
25
+ };
26
+ if (adminKey) reqHeaders["X-API-Key"] = adminKey;
27
+ const res = await fetch(`${base}/api/tokens`, {
28
+ method: "POST",
29
+ headers: reqHeaders,
30
+ body: JSON.stringify({
31
+ resource_type: "file",
32
+ file_id: fileId,
33
+ user_id: String(userid),
34
+ user_name: username,
35
+ can_write: canWrite,
36
+ expires_in_seconds: 3600
37
+ })
38
+ });
39
+ if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
40
+ const data = await res.json();
41
+ return {
42
+ token: data.access_token,
43
+ wopiSrc: data.wopi_src,
44
+ ttl: Date.now() + (data.expires_in_seconds ?? 3600) * 1e3
45
+ };
46
+ }
47
+ // ── WOPI helpers ─────────────────────────────────────────────────────────────
48
+ async function fileExistsInWopi(token) {
49
+ const params = new SvelteURLSearchParams();
50
+ params.append("access_token", token);
51
+ const base = wopiUrl.replace(/\/$/, "");
52
+ const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
53
+ headers,
54
+ method: "GET"
55
+ });
56
+ return res.ok;
57
+ }
58
+ async function uploadToWopi(blob, token) {
59
+ const params = new SvelteURLSearchParams();
60
+ params.append("access_token", token);
61
+ if (filename !== undefined) params.append("filename", filename);
62
+ const base = wopiUrl.replace(/\/$/, "");
63
+ const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
64
+ headers: {
65
+ ...headers,
66
+ "X-WOPI-Override": "PUT"
67
+ },
68
+ method: "POST",
69
+ body: blob
70
+ });
71
+ if (!res.ok) throw new Error(`WOPI upload failed: ${res.status}`);
72
+ }
73
+ async function downloadFromWopi() {
74
+ const params = new SvelteURLSearchParams();
75
+ params.append("access_token", signedToken);
76
+ const base = wopiUrl.replace(/\/$/, "");
77
+ const res = await fetch(`${base}/wopi/files/${fileId}/contents?${params}`, {
78
+ headers,
79
+ method: "GET"
80
+ });
81
+ if (res.ok) return res.blob();
82
+ return undefined;
83
+ }
84
+ // ── Discovery ────────────────────────────────────────────────────────────────
85
+ // Office Online uses extension-based action lookup (not MIME-type app names).
86
+ // urlsrc may contain optional placeholder blocks like <rs=xx> strip them.
87
+ async function findEditorUrl(ext) {
88
+ const discoveryUrl = `${officeOnlineUrl.replace(/\/$/, "")}/hosting/discovery`;
89
+ const res = await fetch(discoveryUrl);
90
+ if (!res.ok) throw new Error(`Discovery failed: ${res.status}`);
91
+ const xml = await res.text();
92
+ const doc = new DOMParser().parseFromString(xml, "text/xml");
93
+ const actionName = readonly ? "view" : "edit";
94
+ const actionEl = doc.querySelector(`action[name="${actionName}"][ext="${ext.toLowerCase()}"]`);
95
+ const urlsrc = actionEl?.getAttribute("urlsrc");
96
+ if (!urlsrc) throw new Error(`No Office Online editor for extension: ${ext} (action: ${actionName})`);
97
+ // Strip optional placeholder blocks, e.g. <rs=xx&dc=xx>
98
+ return urlsrc.replace(/<[^>]+>/g, "");
99
+ }
100
+ // ── Load ─────────────────────────────────────────────────────────────────────
101
+ // Office Online requires a form POST into the iframe (not iframe src assignment).
102
+ async function loadEditor() {
103
+ if (!filename || !buffer) return;
104
+ errmsg = "";
105
+ loading = true;
106
+ ready = false;
107
+ insertText = undefined;
108
+ insertHtml = undefined;
109
+ try {
110
+ // Always use a write token for the initial upload regardless of readonly
111
+ const { token: writeToken } = await fetchToken(true);
112
+ const exists = await fileExistsInWopi(writeToken);
113
+ if (!exists) await uploadToWopi(buffer, writeToken);
114
+ // Editor token respects readonly
115
+ const { token, wopiSrc, ttl } = await fetchToken(!readonly);
116
+ signedToken = token;
117
+ signedWopiSrc = wopiSrc;
118
+ signedTokenTtl = ttl;
119
+ const ext = getExtFromFilename(filename);
120
+ const actionUrl = await findEditorUrl(ext);
121
+ const sep = actionUrl.includes("?") ? "&" : "?";
122
+ editorActionUrl = `${actionUrl}${sep}WOPISrc=${encodeURIComponent(signedWopiSrc)}`;
123
+ // Flush reactive state so the form action attribute is updated before submit
124
+ await tick();
125
+ formEl?.submit();
126
+ } catch (e) {
127
+ errmsg = String(e);
128
+ loading = false;
129
+ }
130
+ }
131
+ // ── Save flow ─────────────────────────────────────────────────────────────────
132
+ function sendSaveAction() {
133
+ return new Promise((resolve, reject) => {
134
+ if (!frameEl?.contentWindow) {
135
+ reject(new Error("No iframe"));
136
+ return;
137
+ }
138
+ saveRequestComplete = false;
139
+ frameEl.contentWindow.postMessage(JSON.stringify({
140
+ MessageId: "Action_Save",
141
+ SendTime: Date.now(),
142
+ Values: {
143
+ Notify: true,
144
+ DontSaveIfUnmodified: false,
145
+ DontTerminateEdit: false
146
+ }
147
+ }), "*");
148
+ let cnt = 0;
149
+ const itv = setInterval(() => {
150
+ cnt++;
151
+ if (saveRequestComplete) {
152
+ clearInterval(itv);
153
+ resolve();
154
+ }
155
+ if (cnt > 60) {
156
+ clearInterval(itv);
157
+ reject(new Error("Save timeout after 30s"));
158
+ }
159
+ }, 500);
160
+ });
161
+ }
162
+ function handleSaveResponse() {
163
+ setTimeout(async () => {
164
+ try {
165
+ const blob = await downloadFromWopi();
166
+ if (blob) {
167
+ buffer = blob;
168
+ onChange?.(blob);
169
+ }
170
+ } catch (e) {
171
+ errmsg = `Could not retrieve saved file: ${e}`;
172
+ } finally {
173
+ saveRequestComplete = true;
174
+ onSave?.(buffer ?? new Blob([]));
175
+ }
176
+ }, 100);
177
+ }
178
+ // ── PostMessage handler ───────────────────────────────────────────────────────
179
+ function handlePostMessage(e) {
180
+ try {
181
+ const msg = typeof e.data === "string" ? JSON.parse(e.data) : typeof e.data === "object" ? e.data : null;
182
+ if (!msg?.MessageId || !frameEl?.contentWindow) return;
183
+ const msgId = msg.MessageId;
184
+ const msgData = msg.Values;
185
+ if (msgId === "App_LoadingStatus" && msgData?.Status === "Frame_Ready") {
186
+ frameEl.contentWindow.postMessage(JSON.stringify({
187
+ MessageId: "Host_PostmessageReady",
188
+ SendTime: Date.now()
189
+ }), "*");
190
+ ready = true;
191
+ loading = false;
192
+ insertText = (text) => postPaste("text/plain", text);
193
+ insertHtml = (html) => postPaste("text/html", html);
194
+ return;
195
+ }
196
+ if (msgId === "UI_Save") {
197
+ sendSaveAction();
198
+ return;
199
+ }
200
+ if (msgId === "Doc_ModifiedStatus" && msgData?.Modified) {
201
+ handleSaveResponse();
202
+ return;
203
+ }
204
+ if (msgId === "Action_Save_Resp" && msgData?.success) {
205
+ handleSaveResponse();
206
+ }
207
+ } catch {}
208
+ }
209
+ onMount(() => {
210
+ window.addEventListener("message", handlePostMessage);
211
+ return () => window.removeEventListener("message", handlePostMessage);
212
+ });
213
+ // ── Insert actions ────────────────────────────────────────────────────────────
214
+ function postPaste(mimetype, data) {
215
+ if (!frameEl?.contentWindow) return;
216
+ frameEl.focus();
217
+ frameEl.contentWindow.postMessage(JSON.stringify({
218
+ MessageId: "Action_Paste",
219
+ SendTime: Date.now(),
220
+ Values: {
221
+ Mimetype: mimetype,
222
+ Data: data
223
+ }
224
+ }), "*");
225
+ }
226
+ // ── Reactive reload ───────────────────────────────────────────────────────────
227
+ $effect(() => {
228
+ const v = value;
229
+ const f = filename;
230
+ void wopiUrl;
231
+ void officeOnlineUrl;
232
+ if (v !== undefined) buffer = v;
233
+ if (f && buffer) void loadEditor();
234
+ });
298
235
  </script>
299
236
 
300
237
  <div class="relative flex h-full w-full flex-1 flex-col overflow-hidden">
@@ -1,23 +1,17 @@
1
- <script lang="ts">
2
- import type { ImageViewerProps } from '../types.js';
3
-
4
- let { value, filename }: ImageViewerProps = $props();
5
-
6
- let src = $state('');
7
-
8
- $effect(() => {
9
- if (!value) {
10
- src = '';
11
- return;
12
- }
13
-
14
- const url = URL.createObjectURL(value);
15
- src = `${url}#toolbar=1&navpanes=0&scrollbar=1`;
16
-
17
- return () => {
18
- URL.revokeObjectURL(url);
19
- };
20
- });
1
+ <script lang="ts">let { value, filename } = $props();
2
+ let src = $state("");
3
+ $effect(() => {
4
+ if (!value) {
5
+ src = "";
6
+ return;
7
+ }
8
+ const url = URL.createObjectURL(value);
9
+ src = `${url}#toolbar=1&navpanes=0&scrollbar=1`;
10
+ return () => {
11
+ URL.revokeObjectURL(url);
12
+ };
13
+ });
14
+ export {};
21
15
  </script>
22
16
 
23
17
  <div class="flex h-full w-full flex-col overflow-hidden bg-surface-50-950">