@widgetic/editor 4.0.10 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +242 -0
  2. package/dist/components/WidgetPreview.svelte +123 -86
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/color-select/ColorSelect.svelte +4 -4
  5. package/dist/components/editors/CompositionEditorApp.svelte +562 -0
  6. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
  7. package/dist/components/editors/EmbedEditor.svelte +33 -22
  8. package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
  9. package/dist/components/editors/PropsEditor.svelte +267 -850
  10. package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
  11. package/dist/components/ic/AngleInputController.svelte +1 -1
  12. package/dist/components/ic/BrowserInputController.svelte +58 -31
  13. package/dist/components/ic/ColorPickerInputController.svelte +55 -21
  14. package/dist/components/ic/DateFieldInputController.svelte +34 -14
  15. package/dist/components/ic/DatePickerInputController.svelte +37 -21
  16. package/dist/components/ic/DropdownInputController.svelte +21 -9
  17. package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
  18. package/dist/components/ic/FixedPositionInputController.svelte +12 -6
  19. package/dist/components/ic/FontInputController.svelte +100 -76
  20. package/dist/components/ic/InputControllerHeader.svelte +16 -9
  21. package/dist/components/ic/NumberControls.svelte +4 -4
  22. package/dist/components/ic/NumberInputController.svelte +6 -4
  23. package/dist/components/ic/PositionInputController.svelte +23 -15
  24. package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
  25. package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
  26. package/dist/components/ic/SliderInputController.svelte +7 -2
  27. package/dist/components/ic/SwitchInputController.svelte +12 -3
  28. package/dist/components/ic/TagsInputController.svelte +19 -4
  29. package/dist/components/ic/TextAreaInputController.svelte +79 -51
  30. package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
  31. package/dist/components/ic/TextInputController.svelte +17 -1
  32. package/dist/components/ic/TimeInputController.svelte +11 -4
  33. package/dist/components/ic/VectorInputController.svelte +101 -70
  34. package/dist/components/ic/shared/ItemGrid.svelte +12 -4
  35. package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
  36. package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
  37. package/dist/composition/compositionApi.d.ts +95 -0
  38. package/dist/composition/compositionApi.js +289 -0
  39. package/dist/composition/compositionEditorController.d.ts +263 -0
  40. package/dist/composition/compositionEditorController.js +746 -0
  41. package/dist/composition/compositionHelpers.d.ts +74 -0
  42. package/dist/composition/compositionHelpers.js +405 -0
  43. package/dist/i18n/index.d.ts +4 -0
  44. package/dist/i18n/index.js +4 -0
  45. package/dist/i18n/labels.d.ts +15 -0
  46. package/dist/i18n/labels.js +57 -0
  47. package/dist/i18n/language.d.ts +228 -0
  48. package/dist/i18n/language.js +34 -0
  49. package/dist/i18n/languageKit.d.ts +54 -0
  50. package/dist/i18n/languageKit.js +102 -0
  51. package/dist/i18n/messages.d.ts +241 -0
  52. package/dist/i18n/messages.js +253 -0
  53. package/dist/i18n/translate.d.ts +6 -0
  54. package/dist/i18n/translate.js +8 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +8 -0
  57. package/dist/mediaLibraryContext.d.ts +10 -0
  58. package/dist/mediaLibraryContext.js +30 -0
  59. package/dist/utils/FontManager.d.ts +21 -5
  60. package/dist/utils/FontManager.js +62 -67
  61. package/dist/utils/VectorManager.d.ts +0 -1
  62. package/dist/utils/VectorManager.js +10 -12
  63. package/dist/utils/dockPlacement.d.ts +12 -0
  64. package/dist/utils/dockPlacement.js +13 -0
  65. package/dist/utils/editorStorage.d.ts +23 -0
  66. package/dist/utils/editorStorage.js +95 -0
  67. package/dist/utils/fieldDirection.d.ts +13 -0
  68. package/dist/utils/fieldDirection.js +25 -0
  69. package/dist/utils/fontUtils.js +5 -3
  70. package/dist/utils/htmlSafety.d.ts +21 -0
  71. package/dist/utils/htmlSafety.js +38 -0
  72. package/dist/utils/itemReorder.d.ts +90 -0
  73. package/dist/utils/itemReorder.js +354 -0
  74. package/dist/utils/recentColors.js +4 -2
  75. package/dist/utils/richTextPreview.d.ts +38 -0
  76. package/dist/utils/richTextPreview.js +207 -0
  77. package/dist/utils/scrollActivity.d.ts +14 -0
  78. package/dist/utils/scrollActivity.js +63 -0
  79. package/dist/utils/shadowDom.d.ts +67 -0
  80. package/dist/utils/shadowDom.js +211 -0
  81. package/dist/utils/vectorUtils.js +5 -3
  82. package/dist/utils/visibilityCondition.d.ts +40 -0
  83. package/dist/utils/visibilityCondition.js +535 -0
  84. package/package.json +7 -6
@@ -0,0 +1,562 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The composition editor: the settings of one Widgetic composition, beside a live preview of its widget.
4
+ *
5
+ * It loads the composition, the address of its widget's preview and both schemas through the Widgetic API
6
+ * with the token `getAccessToken` gives (an editor session token is enough: that is what a partner's page
7
+ * has for its managed users), draws the settings editor and the preview, and saves what is changed: by itself
8
+ * a moment after the last change, and when "Save" is pressed.
9
+ *
10
+ * The page that holds it hears about it through events:
11
+ * ready { name, widgetId } the editor is on the screen
12
+ * dirty { dirty } there is, or is no longer, work that is not saved
13
+ * saved { composition, savedAt }
14
+ * error { during, failure, fatal } during is 'load', 'schemas' or 'save'
15
+ * deviceView { deviceView } the preview is looked at as 'desktop' or 'mobile' now (a button of the
16
+ * preview, a size the user gave it, or `setDeviceView`)
17
+ *
18
+ * A page with controls of its own for what the editor shows can use `setDeviceView` (the same toggle as the
19
+ * preview's buttons) and, when it saved the composition's embed settings itself, `adoptEmbedSettings`.
20
+ *
21
+ * Closing it: `await flush()` first, then remove the component. Removing it cancels whatever is on its way,
22
+ * and edits that were not saved are lost.
23
+ *
24
+ * The component is made for ONE composition: to edit another one, make another component.
25
+ */
26
+ import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte';
27
+ import { get, writable } from 'svelte/store';
28
+ import { createWidgeticMediaLibrary, type MediaLibraryProvider } from '@widgetic/file-browser';
29
+ import type { MessageOverrides } from '@widgetic/design-system/i18n';
30
+ import { shareTextDirection, type TextDirection } from '@widgetic/design-system/components/textDirection';
31
+ import { leftOnThisScreen } from '../../utils/dockPlacement';
32
+ import { provideEditorLanguage } from '../../i18n/language.js';
33
+ import PropsEditor from './PropsEditor.svelte';
34
+ import WidgetPreview from '../WidgetPreview.svelte';
35
+ import { setGoogleFontsApiKey } from '../../config';
36
+ import FontManager from '../../utils/FontManager';
37
+ import { createCompositionApi, type TokenReason } from '../../composition/compositionApi.js';
38
+ import {
39
+ CompositionEditorController,
40
+ failureMessage,
41
+ type CompositionEditorEvent,
42
+ type CompositionEditorState,
43
+ type EditorFailure,
44
+ type PreviewMessage,
45
+ type PropertyChange
46
+ } from '../../composition/compositionEditorController.js';
47
+
48
+ const dispatch = createEventDispatcher<{
49
+ ready: { name: string; widgetId: string };
50
+ dirty: { dirty: boolean };
51
+ saved: { composition: Record<string, unknown>; savedAt: number };
52
+ error: { during: 'load' | 'schemas' | 'save'; failure: EditorFailure; fatal: boolean };
53
+ deviceView: { deviceView: 'desktop' | 'mobile' };
54
+ }>();
55
+
56
+ let className = '';
57
+ export { className as class };
58
+ export let style = '';
59
+
60
+ /** The composition to edit. */
61
+ export let compositionId: string;
62
+ /** The Widgetic API's address, with its version: `https://api.widgetic.com/v1`. */
63
+ export let apiBasePath: string;
64
+ /**
65
+ * Gives the token to call the API with. Asked before every request, so a host that keeps its token fresh is
66
+ * always used up to date; asked again with `'expired'` when the API refuses a token, after which the request
67
+ * is sent once more if the token that comes back is a different one. That second call also gets the token that
68
+ * was refused (`refusedToken`), so a page can tell the refusal of its current token from the late refusal of
69
+ * one it has already replaced, when several requests are refused together.
70
+ */
71
+ export let getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
72
+ /**
73
+ * Where the file picker gets its files, when they are not the Widgetic library: the partner's own media
74
+ * library. Without it the files are the ones of the Widgetic library the token can see.
75
+ */
76
+ export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
77
+ /** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */
78
+ export let googleFontsApiKey = '';
79
+ /** Wait this long after the last change before saving by itself; `0` turns saving by itself off. Read once, when the editor starts. */
80
+ export let autosaveDelayMs: number | undefined = undefined;
81
+ /** Writes what the layout does to the console. */
82
+ export let debug = false;
83
+ /**
84
+ * The language of the editor: a BCP 47 tag ("de", "pt-BR") that sets how plurals, numbers and dates are written.
85
+ * Without it, English. The words themselves come from `messages`.
86
+ */
87
+ export let locale: string | null | undefined = undefined;
88
+ /**
89
+ * The words of the editor in the language of the page: any of the keys of `EDITOR_MESSAGES` (and of the File
90
+ * Browser's and the rich text editor's catalogs, which are inside the editor). One flat object; a key left out keeps
91
+ * its English text. See `checkEditorMessages` to test it.
92
+ */
93
+ export let messages: MessageOverrides | null | undefined = undefined;
94
+ /**
95
+ * Which way the editor is read: "ltr" or "rtl". The page that puts it on screen sets `dir` on an element around it and the
96
+ * browser turns the layout around; this prop says the same to the parts that wrap a library which writes a `dir` of its own
97
+ * (the menus, the scroll areas, the File Browser and the rich text editor inside it), and to the code that places the settings
98
+ * editor and the preview: with "rtl" the settings editor is on the right of the middle and the preview on its left.
99
+ * It is NOT worked out from `locale`: an editor on a page that is laid out left to right can speak Arabic and still be left
100
+ * to right. Left out, the editor is read the way the screen around it is, and left to right when nothing around it says.
101
+ * Can change while the editor is open.
102
+ */
103
+ export let dir: TextDirection | undefined = undefined;
104
+
105
+ // --- the direction ----------------------------------------------------------------------------------
106
+
107
+ // Every part below reads the direction from Svelte's context (the File Browser and the rich text editor too), and follows
108
+ // when the page changes `dir`. `textDirection` is the answer, for the editor's own decisions.
109
+ const givenDirection = writable<TextDirection | undefined>(dir);
110
+ const textDirection = shareTextDirection(givenDirection);
111
+ $: givenDirection.set(dir);
112
+
113
+ // --- the language -----------------------------------------------------------------------------------
114
+
115
+ // Every part of the editor reads its words from here (and the File Browser and the rich text editor inside it from
116
+ // what this hands them). Setting `locale` or `messages` again changes what is on the screen.
117
+ const language = provideEditorLanguage({ locale, messages });
118
+ const { t } = language;
119
+ $: language.update({ locale, messages });
120
+
121
+ // --- the controller -------------------------------------------------------------------------------
122
+
123
+ // A wrong address or token function throws here, when the editor is made: it is a mistake in the page, not a failure to handle.
124
+ // The token function is read when it is called, so a page that gives a new one later is heard.
125
+ const api = createCompositionApi({
126
+ apiBasePath,
127
+ getAccessToken: (reason, refusedToken) => getAccessToken(reason, refusedToken)
128
+ });
129
+ const controller = new CompositionEditorController({
130
+ compositionId,
131
+ api,
132
+ autosaveDelayMs,
133
+ // The sentences the controller makes (why there is no preview) are made in the language of the moment.
134
+ translate: (key, values) => get(language.t)(key, values),
135
+ // The preview page hears about every change as it happens (and everything, once its page has loaded).
136
+ onPreviewMessage: (message: PreviewMessage) =>
137
+ previewRef?.sendMessageToPreview?.({ ...message })
138
+ });
139
+
140
+ let state: CompositionEditorState = controller.getState();
141
+ // What the props editor is given is only replaced when it really is another object: the props editor takes a
142
+ // replaced prop for a change, whether the value differs or not.
143
+ let propsEditorContentSchema: any = controller.getPropsEditorContentSchema();
144
+ let propsEditorContentValues: Record<string, unknown> = controller.getPropsEditorContentValues();
145
+
146
+ // The view the page was last told about, so it hears of a change once, whoever made it.
147
+ let announcedDeviceView = state.deviceView;
148
+
149
+ const stopWatchingState = controller.subscribe((next) => {
150
+ state = next;
151
+ const contentSchema = controller.getPropsEditorContentSchema();
152
+ if (contentSchema !== propsEditorContentSchema) propsEditorContentSchema = contentSchema;
153
+ const contentValues = controller.getPropsEditorContentValues();
154
+ if (contentValues !== propsEditorContentValues) propsEditorContentValues = contentValues;
155
+ if (next.deviceView !== announcedDeviceView) {
156
+ announcedDeviceView = next.deviceView;
157
+ dispatch('deviceView', { deviceView: next.deviceView });
158
+ }
159
+ });
160
+
161
+ const stopWatchingEvents = controller.onEvent((event: CompositionEditorEvent) => {
162
+ switch (event.type) {
163
+ case 'ready':
164
+ dispatch('ready', {
165
+ name: controller.getState().name,
166
+ widgetId: controller.getState().widgetId
167
+ });
168
+ break;
169
+ case 'dirty':
170
+ dispatch('dirty', { dirty: event.dirty });
171
+ break;
172
+ case 'saved':
173
+ dispatch('saved', { composition: event.composition, savedAt: event.savedAt });
174
+ break;
175
+ case 'error':
176
+ dispatch('error', { during: event.during, failure: event.failure, fatal: event.fatal });
177
+ break;
178
+ }
179
+ });
180
+
181
+ // --- what the page can ask of it ------------------------------------------------------------------
182
+
183
+ /** Saves what is unsaved now. Resolves `true` when nothing is left unsaved afterwards. */
184
+ export function save(): Promise<boolean> {
185
+ return controller.save('manual');
186
+ }
187
+
188
+ /** Saves everything unsaved and waits until it is. Resolves `true` when nothing is left unsaved. Call it before removing the editor. */
189
+ export function flush(): Promise<boolean> {
190
+ return controller.flush();
191
+ }
192
+
193
+ /** Whether there is work that is not saved. */
194
+ export function isDirty(): boolean {
195
+ return controller.getState().dirty;
196
+ }
197
+
198
+ /** What the editor knows now (the composition's values, whether it is saving, the last error...). */
199
+ export function getState(): CompositionEditorState {
200
+ return controller.getState();
201
+ }
202
+
203
+ /** Loads the composition again. Edits that are not saved are lost. */
204
+ export function reload(): Promise<void> {
205
+ return controller.load();
206
+ }
207
+
208
+ /**
209
+ * Looks at the preview as a phone (`'mobile'`) or at the size that is saved (`'desktop'`): what the preview's own
210
+ * buttons do. Nothing becomes unsaved. Does nothing before the editor is ready.
211
+ */
212
+ export function setDeviceView(deviceView: 'desktop' | 'mobile'): void {
213
+ setDevicePreview(deviceView === 'mobile');
214
+ }
215
+
216
+ /**
217
+ * Tells the editor that the page saved the composition's embed settings itself, so a later save does not put
218
+ * the ones the editor loaded back. Call it after `flush()`, with the settings the API now holds.
219
+ */
220
+ export function adoptEmbedSettings(settings: Record<string, unknown>): void {
221
+ controller.adoptEmbedSettings(settings);
222
+ // (The preview may have a new size: the pair is placed again.)
223
+ void tick().then(() => dockFloatingEditorOnce());
224
+ }
225
+
226
+ // --- the preview ----------------------------------------------------------------------------------
227
+
228
+ let previewRef: {
229
+ sendMessageToPreview?: (message: Record<string, unknown>) => void;
230
+ setShellPosition?: (left: number, top: number) => void;
231
+ } | null = null;
232
+ let previewHydrating = false;
233
+ let isContentReady = false;
234
+
235
+ $: previewSrc = state.preview?.previewUrl ?? null;
236
+ $: previewLoading = state.phase === 'idle' || state.phase === 'loading';
237
+ $: isMobilePreview = state.deviceView === 'mobile';
238
+ // "Loading editor…" sits in the middle while the composition loads, and where the editor is about to appear after.
239
+ $: loadingCardPosition =
240
+ state.phase === 'ready' ? 'start-4 top-4' : 'left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2';
241
+ // A preview address that has just arrived is a page still to load: the preview says so until its frame has loaded.
242
+ $: startHydrating(previewSrc);
243
+
244
+ function startHydrating(address: string | null) {
245
+ previewHydrating = Boolean(address);
246
+ isContentReady = false;
247
+ }
248
+
249
+ async function handlePreviewIframeLoad() {
250
+ log('Preview frame loaded');
251
+ controller.syncPreview();
252
+ await tick();
253
+ requestAnimationFrame(() => {
254
+ previewHydrating = false;
255
+ isContentReady = true;
256
+ });
257
+ }
258
+
259
+ function handlePreviewResize(
260
+ event: CustomEvent<{ width: number; height: number; source?: string }>
261
+ ) {
262
+ // A box smaller than the preview shrinks the preview to fit; that is the box's doing, not a size the user chose,
263
+ // so it is not saved (the composition would change just by being opened in a small window).
264
+ if (event.detail.source === 'clamp') {
265
+ log('The preview was shrunk to fit its box; the saved size is unchanged', event.detail);
266
+ return;
267
+ }
268
+ controller.setPreviewSize(event.detail.width, event.detail.height);
269
+ }
270
+
271
+ function setDevicePreview(mobile: boolean) {
272
+ controller.setDeviceView(mobile ? 'mobile' : 'desktop');
273
+ void tick().then(() => dockFloatingEditorOnce());
274
+ }
275
+
276
+ // --- the props editor -----------------------------------------------------------------------------
277
+
278
+ let propsEditorRef: {
279
+ setPosition?: (left: number, top: number) => void;
280
+ getPosition?: () => { left: number; top: number };
281
+ } | null = null;
282
+
283
+ function handleEditorPropertyChange(event: CustomEvent<PropertyChange>) {
284
+ const { schema, propertyId, value, itemId } = event.detail;
285
+ controller.changeProperty({ schema, propertyId, value, itemId });
286
+ }
287
+
288
+ function handleEditorContentItemsChanged(event: CustomEvent<{ items: unknown[] }>) {
289
+ controller.changeContentItems(event.detail.items);
290
+ }
291
+
292
+ // The Widgetic library, as the file picker's source, when the page brings none of its own.
293
+ $: widgetId = state.widgetId;
294
+ $: defaultMediaLibrary =
295
+ !mediaLibrary && widgetId ? createDefaultMediaLibrary(widgetId) : undefined;
296
+
297
+ function createDefaultMediaLibrary(forWidgetId: string): MediaLibraryProvider {
298
+ return createWidgeticMediaLibrary({
299
+ apiBasePath,
300
+ getAccessToken: (reason, refusedToken) => getAccessToken(reason, refusedToken),
301
+ contextType: 'content-property',
302
+ source: 'file-browser-upload',
303
+ widgetId: forWidgetId,
304
+ // An editor session cannot use the resumable upload: files over its limit are refused in words instead of with a 403.
305
+ allowResumableUploads: false
306
+ });
307
+ }
308
+
309
+ $: if (googleFontsApiKey) setGoogleFontsApiKey(googleFontsApiKey);
310
+
311
+ // --- the layout: settings editor beside the middle, preview on the other side of it -------------
312
+ // (the editor on the left and the preview on the right of a left to right screen; the other way round on a right to left one)
313
+
314
+ const EDITOR_WIDTH_PX = 450;
315
+ const EDITOR_PADDING = 20;
316
+ const EDITOR_MIN_HEIGHT = 360;
317
+ const PREVIEW_CHROME_HEIGHT = 36;
318
+
319
+ let editorShellEl: HTMLElement | null = null;
320
+ let floatingEditorHeight = 640;
321
+
322
+ $: schemasAvailable = Boolean(state.designSchema || state.contentSchema);
323
+ $: editorVisible =
324
+ state.phase === 'ready' && !state.schemasLoading && !state.schemasError && schemasAvailable;
325
+ // Placed once the editor is on the screen, not on every change: it must not jump back while it is being dragged.
326
+ // It is placed again when the screen turns around, because the pair is the mirror of itself.
327
+ $: if (editorVisible && $textDirection) requestDock();
328
+
329
+ function log(message: string, details?: unknown) {
330
+ if (!debug) return;
331
+ if (details === undefined) console.log(`[CompositionEditorApp] ${message}`);
332
+ else console.log(`[CompositionEditorApp] ${message}`, details);
333
+ }
334
+
335
+ function clampEditorHeight(containerHeight: number): number {
336
+ return Math.max(EDITOR_MIN_HEIGHT, containerHeight - EDITOR_PADDING * 2);
337
+ }
338
+
339
+ function requestDock() {
340
+ void tick().then(() => requestAnimationFrame(() => dockFloatingEditorOnce()));
341
+ }
342
+
343
+ /** Editor on one side of the middle, preview on the other, each EDITOR_PADDING from the middle: the editor starts the line, the preview follows it. */
344
+ function dockFloatingEditorOnce() {
345
+ if (!editorShellEl) return;
346
+ try {
347
+ const shellWidth = editorShellEl.clientWidth || window.innerWidth;
348
+ const shellHeight = editorShellEl.clientHeight || window.innerHeight;
349
+ floatingEditorHeight = clampEditorHeight(shellHeight);
350
+
351
+ const middleX = Math.round(shellWidth / 2);
352
+ const previewHeight = state.previewHeight + PREVIEW_CHROME_HEIGHT;
353
+ const leftEditor = leftOnThisScreen($textDirection, Math.max(EDITOR_PADDING, middleX - EDITOR_PADDING - EDITOR_WIDTH_PX), EDITOR_WIDTH_PX, shellWidth);
354
+ const topEditor = EDITOR_PADDING;
355
+ const leftPreview = leftOnThisScreen($textDirection, middleX + EDITOR_PADDING, state.previewWidth, shellWidth);
356
+ const topPreview = Math.max(EDITOR_PADDING, Math.round((shellHeight - previewHeight) / 2));
357
+
358
+ void tick().then(() => {
359
+ propsEditorRef?.setPosition?.(leftEditor, topEditor);
360
+ previewRef?.setShellPosition?.(leftPreview, topPreview);
361
+ });
362
+ log('Docked the editor and the preview', {
363
+ direction: $textDirection,
364
+ height: floatingEditorHeight,
365
+ middleX,
366
+ leftEditor,
367
+ topEditor,
368
+ leftPreview,
369
+ topPreview
370
+ });
371
+ } catch (error) {
372
+ console.error('[CompositionEditorApp] Placing the editor and the preview failed:', error);
373
+ }
374
+ }
375
+
376
+ /** When the box the editor is in changes size: the height follows, and the pair is placed again. */
377
+ function syncFloatingEditorHeight() {
378
+ if (!editorShellEl) return;
379
+ floatingEditorHeight = clampEditorHeight(editorShellEl.clientHeight || window.innerHeight);
380
+ dockFloatingEditorOnce();
381
+ }
382
+
383
+ // --- life -----------------------------------------------------------------------------------------
384
+
385
+ let shellObserver: ResizeObserver | null = null;
386
+
387
+ onMount(() => {
388
+ void controller.load();
389
+
390
+ window.addEventListener('resize', syncFloatingEditorHeight);
391
+ if (editorShellEl && typeof ResizeObserver !== 'undefined') {
392
+ shellObserver = new ResizeObserver(() => syncFloatingEditorHeight());
393
+ shellObserver.observe(editorShellEl);
394
+ }
395
+ });
396
+
397
+ onDestroy(() => {
398
+ // (Also runs when the page is rendered on a server, where there is no window.)
399
+ if (typeof window !== 'undefined')
400
+ window.removeEventListener('resize', syncFloatingEditorHeight);
401
+ shellObserver?.disconnect();
402
+ stopWatchingState();
403
+ stopWatchingEvents();
404
+ controller.dispose();
405
+ // The font stylesheets the editor put in the page's <head> go with it (a page of somebody else's is left as found)
406
+ FontManager.releasePageResources();
407
+ });
408
+ </script>
409
+
410
+ <div
411
+ class="composition-editor-ct relative h-full min-h-0 w-full overflow-hidden {className}"
412
+ dir={$textDirection}
413
+ {style}
414
+ bind:this={editorShellEl}
415
+ >
416
+ <!-- Keep preview resize handles in their own layer below the floating editor. -->
417
+ <div
418
+ class="composition-editor-preview-area absolute inset-0 z-0 flex min-h-0 min-w-0 bg-[#f3f4f6]"
419
+ >
420
+ <!--
421
+ This editor can run inside a partner's page, and a widget is code written by somebody else. The preview may
422
+ run its scripts, keep its own storage (allow-same-origin), open links in a new tab and submit forms, and
423
+ nothing more: no top navigation (it cannot send the visitor's tab away), no alert/confirm dialogs, no
424
+ downloads; of the page's permissions it may only write to the clipboard.
425
+ -->
426
+ <WidgetPreview
427
+ bind:this={previewRef}
428
+ class="composition-widget-preview h-full w-full"
429
+ mode="controlled"
430
+ sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
431
+ allow="clipboard-write"
432
+ enableResize={true}
433
+ enableDrag={true}
434
+ showDimensionInputs={true}
435
+ showChromeDeviceToggle={true}
436
+ deviceMobile={isMobilePreview}
437
+ on:deviceView={(event) => setDevicePreview(event.detail.mobile)}
438
+ resizeMode="corner"
439
+ defaultStageAlign="center"
440
+ compactChrome={true}
441
+ showUrlBar={false}
442
+ src={previewSrc}
443
+ displayUrl={previewSrc}
444
+ resizeWidth={state.previewWidth}
445
+ resizeHeight={state.previewHeight}
446
+ {previewLoading}
447
+ {previewHydrating}
448
+ previewError={state.previewError}
449
+ {isContentReady}
450
+ on:iframeLoad={handlePreviewIframeLoad}
451
+ on:resize={handlePreviewResize}
452
+ />
453
+ </div>
454
+
455
+ {#if state.phase === 'failed' && state.loadError}
456
+ <!-- Nothing can be shown behind this one (no composition, so no editor and no preview), so it sits in the middle. -->
457
+ <div
458
+ role="alert"
459
+ class="composition-editor-error absolute left-1/2 top-1/2 z-30 flex w-max max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
460
+ >
461
+ <p class="text-sm text-red-600">{failureMessage(state.loadError, 'load', $t)}</p>
462
+ <button
463
+ type="button"
464
+ class="retry-load-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
465
+ on:click={() => {
466
+ void controller.load();
467
+ }}
468
+ >
469
+ {$t('editor.app.tryAgain')}
470
+ </button>
471
+ </div>
472
+ {:else if state.phase !== 'ready' || state.schemasLoading}
473
+ <div
474
+ role="status"
475
+ class="composition-editor-loading absolute z-30 rounded-lg border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm {loadingCardPosition}"
476
+ >
477
+ {$t('editor.app.loading')}
478
+ </div>
479
+ {:else if state.schemasError}
480
+ <div
481
+ role="alert"
482
+ class="composition-editor-error absolute start-4 top-4 z-30 flex max-w-sm flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
483
+ >
484
+ <span class="text-xs text-red-600">{state.schemasError}</span>
485
+ <button
486
+ type="button"
487
+ class="retry-schemas-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
488
+ on:click={() => {
489
+ void controller.reloadSchemas();
490
+ }}
491
+ >
492
+ {$t('editor.app.retry')}
493
+ </button>
494
+ </div>
495
+ {:else if schemasAvailable}
496
+ <!--
497
+ The panel is placed from the left edge of the editor's area, so it says `position: absolute` itself: the class of the
498
+ same name loses to the `relative` the panel's own root carries, and a relative box moves from where it sits in the flow,
499
+ which is the left edge only on a left to right screen (on a right to left one it sits at the right edge, and a `left` of
500
+ 720px puts it 720px beyond that: out of sight).
501
+ -->
502
+ <PropsEditor
503
+ bind:this={propsEditorRef}
504
+ class="composition-props-editor absolute z-20 shadow-lg"
505
+ style="position: absolute; left: {EDITOR_PADDING}px; top: {EDITOR_PADDING}px; /* rtl-ok: the dock puts the panel in pixels from the left edge as soon as it is on screen (see dockFloatingEditorOnce) */"
506
+ designSchema={state.designSchema}
507
+ contentSchema={propsEditorContentSchema}
508
+ initialDesignValues={state.designValues}
509
+ initialContentValues={propsEditorContentValues}
510
+ mediaLibrary={mediaLibrary ?? defaultMediaLibrary}
511
+ width="{EDITOR_WIDTH_PX}px"
512
+ height="{floatingEditorHeight}px"
513
+ showDragHandle={true}
514
+ enableDrag={true}
515
+ showSaveButton={true}
516
+ saveDirty={state.dirty}
517
+ saveSaving={state.saving}
518
+ defaultTab="content"
519
+ on:propertyChange={handleEditorPropertyChange}
520
+ on:contentItemsChanged={handleEditorContentItemsChanged}
521
+ on:save={() => {
522
+ void controller.save('manual');
523
+ }}
524
+ />
525
+ {/if}
526
+
527
+ {#if state.phase === 'ready' && state.saveError}
528
+ <div
529
+ role="alert"
530
+ class="composition-editor-save-error absolute bottom-4 start-4 z-30 flex max-w-sm flex-col gap-2 rounded-lg border border-red-200 bg-white p-4 shadow-sm"
531
+ >
532
+ <p class="text-sm text-red-600">{failureMessage(state.saveError, 'save', $t)}</p>
533
+ <button
534
+ type="button"
535
+ class="retry-save-btn w-fit cursor-pointer rounded-lg bg-blue-100 px-2 py-1 text-xs text-blue-700 hover:bg-blue-200 focus:outline-none focus-visible:outline-none"
536
+ on:click={() => {
537
+ void controller.save('manual');
538
+ }}
539
+ >
540
+ {$t('editor.app.tryAgain')}
541
+ </button>
542
+ </div>
543
+ {/if}
544
+ </div>
545
+
546
+ <style>
547
+ /* The props editor's own classes are out of reach of this component, so they are styled from the box it sits in. */
548
+ .composition-editor-ct :global(.composition-props-editor .ic-input-ct) {
549
+ width: 152px !important;
550
+ }
551
+ .composition-editor-ct :global(.composition-props-editor .input-controller > div:last-child) {
552
+ width: 152px !important;
553
+ }
554
+ .composition-editor-ct :global(.composition-props-editor .color-input-main-container) {
555
+ width: 152px !important;
556
+ }
557
+ .composition-editor-ct :global(.composition-props-editor .input-controller-header .subtext) {
558
+ overflow: visible;
559
+ text-overflow: unset;
560
+ white-space: nowrap;
561
+ }
562
+ </style>
@@ -0,0 +1,95 @@
1
+ import { type MediaLibraryProvider } from '@widgetic/file-browser';
2
+ import type { MessageOverrides } from '@widgetic/design-system/i18n';
3
+ import { type TextDirection } from '@widgetic/design-system/components/textDirection';
4
+ import { type TokenReason } from '../../composition/compositionApi.js';
5
+ import { type CompositionEditorState, type EditorFailure } from '../../composition/compositionEditorController.js';
6
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
7
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
8
+ $$bindings?: Bindings;
9
+ } & Exports;
10
+ (internal: unknown, props: Props & {
11
+ $$events?: Events;
12
+ $$slots?: Slots;
13
+ }): Exports & {
14
+ $set?: any;
15
+ $on?: any;
16
+ };
17
+ z_$$bindings?: Bindings;
18
+ }
19
+ declare const CompositionEditorApp: $$__sveltets_2_IsomorphicComponent<{
20
+ class?: string;
21
+ style?: string;
22
+ /** The composition to edit. */ compositionId: string;
23
+ /** The Widgetic API's address, with its version: `https://api.widgetic.com/v1`. */ apiBasePath: string;
24
+ /**
25
+ * Gives the token to call the API with. Asked before every request, so a host that keeps its token fresh is
26
+ * always used up to date; asked again with `'expired'` when the API refuses a token, after which the request
27
+ * is sent once more if the token that comes back is a different one. That second call also gets the token that
28
+ * was refused (`refusedToken`), so a page can tell the refusal of its current token from the late refusal of
29
+ * one it has already replaced, when several requests are refused together.
30
+ */ getAccessToken: (reason: TokenReason, refusedToken?: string) => Promise<string> | string;
31
+ /**
32
+ * Where the file picker gets its files, when they are not the Widgetic library: the partner's own media
33
+ * library. Without it the files are the ones of the Widgetic library the token can see.
34
+ */ mediaLibrary?: MediaLibraryProvider | undefined;
35
+ /** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */ googleFontsApiKey?: string;
36
+ /** Wait this long after the last change before saving by itself; `0` turns saving by itself off. Read once, when the editor starts. */ autosaveDelayMs?: number | undefined;
37
+ /** Writes what the layout does to the console. */ debug?: boolean;
38
+ /**
39
+ * The language of the editor: a BCP 47 tag ("de", "pt-BR") that sets how plurals, numbers and dates are written.
40
+ * Without it, English. The words themselves come from `messages`.
41
+ */ locale?: string | null | undefined;
42
+ /**
43
+ * The words of the editor in the language of the page: any of the keys of `EDITOR_MESSAGES` (and of the File
44
+ * Browser's and the rich text editor's catalogs, which are inside the editor). One flat object; a key left out keeps
45
+ * its English text. See `checkEditorMessages` to test it.
46
+ */ messages?: MessageOverrides | null | undefined;
47
+ /**
48
+ * Which way the editor is read: "ltr" or "rtl". The page that puts it on screen sets `dir` on an element around it and the
49
+ * browser turns the layout around; this prop says the same to the parts that wrap a library which writes a `dir` of its own
50
+ * (the menus, the scroll areas, the File Browser and the rich text editor inside it), and to the code that places the settings
51
+ * editor and the preview: with "rtl" the settings editor is on the right of the middle and the preview on its left.
52
+ * It is NOT worked out from `locale`: an editor on a page that is laid out left to right can speak Arabic and still be left
53
+ * to right. Left out, the editor is read the way the screen around it is, and left to right when nothing around it says.
54
+ * Can change while the editor is open.
55
+ */ dir?: TextDirection | undefined;
56
+ save?: () => Promise<boolean>;
57
+ flush?: () => Promise<boolean>;
58
+ isDirty?: () => boolean;
59
+ getState?: () => CompositionEditorState;
60
+ reload?: () => Promise<void>;
61
+ setDeviceView?: (deviceView: "desktop" | "mobile") => void;
62
+ adoptEmbedSettings?: (settings: Record<string, unknown>) => void;
63
+ }, {
64
+ ready: CustomEvent<{
65
+ name: string;
66
+ widgetId: string;
67
+ }>;
68
+ dirty: CustomEvent<{
69
+ dirty: boolean;
70
+ }>;
71
+ saved: CustomEvent<{
72
+ composition: Record<string, unknown>;
73
+ savedAt: number;
74
+ }>;
75
+ error: CustomEvent<{
76
+ during: "load" | "schemas" | "save";
77
+ failure: EditorFailure;
78
+ fatal: boolean;
79
+ }>;
80
+ deviceView: CustomEvent<{
81
+ deviceView: "desktop" | "mobile";
82
+ }>;
83
+ } & {
84
+ [evt: string]: CustomEvent<any>;
85
+ }, {}, {
86
+ save: () => Promise<boolean>;
87
+ flush: () => Promise<boolean>;
88
+ isDirty: () => boolean;
89
+ getState: () => CompositionEditorState;
90
+ reload: () => Promise<void>;
91
+ setDeviceView: (deviceView: "desktop" | "mobile") => void;
92
+ adoptEmbedSettings: (settings: Record<string, unknown>) => void;
93
+ }, string>;
94
+ type CompositionEditorApp = InstanceType<typeof CompositionEditorApp>;
95
+ export default CompositionEditorApp;