@widgetic/editor 4.1.0 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/components/PreviewChromeControls.svelte +84 -0
- package/dist/components/PreviewChromeControls.svelte.d.ts +35 -0
- package/dist/components/WidgetPreview.svelte +39 -77
- package/dist/components/WidgetPreview.svelte.d.ts +1 -0
- package/dist/components/editors/CompositionEditorApp.svelte +14 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +8 -0
- package/dist/components/editors/PropsEditor.svelte +8 -0
- package/dist/components/editors/PropsEditor.svelte.d.ts +4 -0
- package/dist/components/ic/BrowserInputController.svelte +61 -14
- package/dist/composition/compositionEditorController.d.ts +5 -0
- package/dist/composition/compositionEditorController.js +8 -0
- package/dist/fileStorageUrlContext.d.ts +6 -0
- package/dist/fileStorageUrlContext.js +13 -0
- package/dist/i18n/language.d.ts +1 -0
- package/dist/i18n/messages.d.ts +1 -0
- package/dist/i18n/messages.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pickFileContext.d.ts +19 -0
- package/dist/pickFileContext.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
### WIDGET PROPERTIES EDITOR
|
|
2
2
|
|
|
3
|
+
> **Internal building block.** This package is part of Widgetic's own apps and is not a supported integration surface. Partners: use [`@widgetic/ui-sdk`](https://www.npmjs.com/package/@widgetic/ui-sdk) instead.
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
A Svelte Component Module that will be loaded and used to edit the widget properties: design and content.
|
|
5
7
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let className = '';
|
|
3
|
+
export { className as class };
|
|
4
|
+
export let style = '';
|
|
5
|
+
/** Desktop is the unpressed state of the phone icon. */
|
|
6
|
+
export let mobile = false;
|
|
7
|
+
export let showDevices = true;
|
|
8
|
+
export let showDimensions = true;
|
|
9
|
+
export let widthValue = '';
|
|
10
|
+
export let heightValue = '';
|
|
11
|
+
export let minWidth = 120;
|
|
12
|
+
export let minHeight = 80;
|
|
13
|
+
export let onDevice: (mobile: boolean) => void = () => {};
|
|
14
|
+
export let onWidth: (value: string) => void = () => {};
|
|
15
|
+
export let onHeight: (value: string) => void = () => {};
|
|
16
|
+
export let desktopLabel = 'Desktop preview';
|
|
17
|
+
export let mobileLabel = 'Mobile preview';
|
|
18
|
+
export let widthLabel = 'W';
|
|
19
|
+
export let heightLabel = 'H';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<div class="preview-chrome-controls flex items-center gap-2 {className}" style={style}>
|
|
23
|
+
{#if showDevices}
|
|
24
|
+
<div class="preview-chrome-device-toggle flex shrink-0 items-center overflow-hidden rounded border border-gray-300">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class="preview-chrome-device-desktop flex h-6 w-6 items-center justify-center {!mobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
|
|
28
|
+
aria-label={desktopLabel}
|
|
29
|
+
aria-pressed={!mobile}
|
|
30
|
+
onclick={() => onDevice(false)}
|
|
31
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
32
|
+
>
|
|
33
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class="preview-chrome-device-mobile flex h-6 w-6 items-center justify-center border-s border-gray-300 {mobile ? 'bg-gray-100 text-gray-800' : 'bg-white text-gray-400'} hover:text-gray-800 focus:outline-none focus-visible:outline-none"
|
|
38
|
+
aria-label={mobileLabel}
|
|
39
|
+
aria-pressed={mobile}
|
|
40
|
+
onclick={() => onDevice(true)}
|
|
41
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
42
|
+
>
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if showDimensions}
|
|
48
|
+
<div class="preview-chrome-dimensions flex items-center gap-1.5">
|
|
49
|
+
<label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
|
|
50
|
+
<span>{widthLabel}</span>
|
|
51
|
+
<input
|
|
52
|
+
type="number"
|
|
53
|
+
min={minWidth}
|
|
54
|
+
class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
|
|
55
|
+
value={widthValue}
|
|
56
|
+
oninput={(event) => onWidth(event.currentTarget.value)}
|
|
57
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
<span class="text-[10px] text-gray-400">×</span>
|
|
61
|
+
<label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
|
|
62
|
+
<span>{heightLabel}</span>
|
|
63
|
+
<input
|
|
64
|
+
type="number"
|
|
65
|
+
min={minHeight}
|
|
66
|
+
class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
|
|
67
|
+
value={heightValue}
|
|
68
|
+
oninput={(event) => onHeight(event.currentTarget.value)}
|
|
69
|
+
onpointerdown={(event) => event.stopPropagation()}
|
|
70
|
+
/>
|
|
71
|
+
</label>
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<style>
|
|
77
|
+
.preview-chrome-controls button:focus,
|
|
78
|
+
.preview-chrome-controls button:focus-visible,
|
|
79
|
+
.preview-chrome-controls input:focus,
|
|
80
|
+
.preview-chrome-controls input:focus-visible {
|
|
81
|
+
outline: none;
|
|
82
|
+
box-shadow: none;
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
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> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: Props & {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const PreviewChromeControls: $$__sveltets_2_IsomorphicComponent<{
|
|
15
|
+
class?: string;
|
|
16
|
+
style?: string;
|
|
17
|
+
/** Desktop is the unpressed state of the phone icon. */ mobile?: boolean;
|
|
18
|
+
showDevices?: boolean;
|
|
19
|
+
showDimensions?: boolean;
|
|
20
|
+
widthValue?: string;
|
|
21
|
+
heightValue?: string;
|
|
22
|
+
minWidth?: number;
|
|
23
|
+
minHeight?: number;
|
|
24
|
+
onDevice?: (mobile: boolean) => void;
|
|
25
|
+
onWidth?: (value: string) => void;
|
|
26
|
+
onHeight?: (value: string) => void;
|
|
27
|
+
desktopLabel?: string;
|
|
28
|
+
mobileLabel?: string;
|
|
29
|
+
widthLabel?: string;
|
|
30
|
+
heightLabel?: string;
|
|
31
|
+
}, {
|
|
32
|
+
[evt: string]: CustomEvent<any>;
|
|
33
|
+
}, {}, {}, string>;
|
|
34
|
+
type PreviewChromeControls = InstanceType<typeof PreviewChromeControls>;
|
|
35
|
+
export default PreviewChromeControls;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { GenerateLoader } from '@widgetic/design-system/components';
|
|
4
4
|
import { lockPointerStyle } from '../utils/shadowDom';
|
|
5
5
|
import { useEditorLanguage } from '../i18n/language';
|
|
6
|
+
import PreviewChromeControls from './PreviewChromeControls.svelte';
|
|
6
7
|
|
|
7
8
|
// The words of the editor in the language of the page: the editor above this one chose it (see `provideEditorLanguage`).
|
|
8
9
|
const { t } = useEditorLanguage();
|
|
@@ -121,10 +122,8 @@
|
|
|
121
122
|
const MIN_WIDTH = 120;
|
|
122
123
|
const MIN_HEIGHT = 80;
|
|
123
124
|
const HANDLE_MARGIN = 6;
|
|
124
|
-
/** Inset
|
|
125
|
-
|
|
126
|
-
const RESIZE_PAD_TOP = 12;
|
|
127
|
-
const RESIZE_PAD_BOTTOM = 12;
|
|
125
|
+
/** Inset so resize handles stay inside bounds. 0 sits the frame on the stage edge. */
|
|
126
|
+
export let edgeInset = 12;
|
|
128
127
|
const PREVIEW_CHROME_HEADER_H = 36;
|
|
129
128
|
const PREVIEW_STROKE = '#3b82f6';
|
|
130
129
|
const PREVIEW_STROKE_ALT = '#f97316';
|
|
@@ -159,8 +158,8 @@
|
|
|
159
158
|
}
|
|
160
159
|
if (hasFixedBounds && boundsWidth != null && boundsHeight != null) {
|
|
161
160
|
return {
|
|
162
|
-
maxWidth: Math.max(MIN_WIDTH, boundsWidth -
|
|
163
|
-
maxHeight: Math.max(MIN_HEIGHT, boundsHeight -
|
|
161
|
+
maxWidth: Math.max(MIN_WIDTH, boundsWidth - edgeInset * 2),
|
|
162
|
+
maxHeight: Math.max(MIN_HEIGHT, boundsHeight - edgeInset - edgeInset - chromeH),
|
|
164
163
|
};
|
|
165
164
|
}
|
|
166
165
|
return { maxWidth: 720, maxHeight: 420 };
|
|
@@ -176,17 +175,17 @@
|
|
|
176
175
|
shellWidth: number,
|
|
177
176
|
shellHeight: number
|
|
178
177
|
): { left: number; top: number } {
|
|
179
|
-
const maxLeft = Math.max(
|
|
180
|
-
const maxTop = Math.max(
|
|
178
|
+
const maxLeft = Math.max(edgeInset, stageWidth - shellWidth - edgeInset);
|
|
179
|
+
const maxTop = Math.max(edgeInset, stageHeight - shellHeight - edgeInset);
|
|
181
180
|
// rtl-ok: the half of the stage is named by its place on the screen, see the prop
|
|
182
181
|
if (defaultStageAlign === 'right-half') {
|
|
183
182
|
const rightHalfCenterX = stageWidth * 0.75;
|
|
184
|
-
const left = clampDimension(rightHalfCenterX - shellWidth / 2,
|
|
185
|
-
const top = clampDimension((stageHeight - shellHeight) / 2,
|
|
183
|
+
const left = clampDimension(rightHalfCenterX - shellWidth / 2, edgeInset, maxLeft);
|
|
184
|
+
const top = clampDimension((stageHeight - shellHeight) / 2, edgeInset, maxTop);
|
|
186
185
|
return { left, top };
|
|
187
186
|
}
|
|
188
|
-
const left = clampDimension((stageWidth - shellWidth) / 2,
|
|
189
|
-
const top = clampDimension((stageHeight - shellHeight) / 2,
|
|
187
|
+
const left = clampDimension((stageWidth - shellWidth) / 2, edgeInset, maxLeft);
|
|
188
|
+
const top = clampDimension((stageHeight - shellHeight) / 2, edgeInset, maxTop);
|
|
190
189
|
return { left, top };
|
|
191
190
|
}
|
|
192
191
|
|
|
@@ -271,10 +270,10 @@
|
|
|
271
270
|
|
|
272
271
|
function onPointerMove(e: PointerEvent) {
|
|
273
272
|
if (!stageEl) return;
|
|
274
|
-
const maxLeft = Math.max(
|
|
275
|
-
const maxTop = Math.max(
|
|
276
|
-
previewShellLeft = clampDimension(startLeft + (e.clientX - startX),
|
|
277
|
-
previewShellTop = clampDimension(startTop + (e.clientY - startY),
|
|
273
|
+
const maxLeft = Math.max(edgeInset, stageEl.clientWidth - shellWidth - edgeInset);
|
|
274
|
+
const maxTop = Math.max(edgeInset, stageEl.clientHeight - shellHeight - edgeInset);
|
|
275
|
+
previewShellLeft = clampDimension(startLeft + (e.clientX - startX), edgeInset, maxLeft);
|
|
276
|
+
previewShellTop = clampDimension(startTop + (e.clientY - startY), edgeInset, maxTop);
|
|
278
277
|
}
|
|
279
278
|
|
|
280
279
|
function endDrag() {
|
|
@@ -365,7 +364,7 @@
|
|
|
365
364
|
$: showResizeHandles = enableResize && (mode === 'controlled' ? isContentReady : iframeLoaded);
|
|
366
365
|
$: hasFixedBounds = boundsWidth != null && boundsHeight != null;
|
|
367
366
|
$: containerPaddingStyle = enableResize
|
|
368
|
-
? `padding: ${
|
|
367
|
+
? `padding: ${edgeInset}px ${edgeInset}px ${edgeInset}px ${edgeInset}px;`
|
|
369
368
|
: !hasFixedBounds && !compactChrome && (showUrlBar || mode === 'controlled')
|
|
370
369
|
? `padding: ${HANDLE_MARGIN}px;`
|
|
371
370
|
: '';
|
|
@@ -926,66 +925,29 @@
|
|
|
926
925
|
<span class="preview-chrome-drag-grip shrink-0 text-gray-400 text-xs leading-none" title={$t('editor.preview.dragHandle')} aria-hidden="true">⠿</span>
|
|
927
926
|
{/if}
|
|
928
927
|
<span class="preview-chrome-title text-[11px] font-semibold uppercase tracking-wide text-gray-500 shrink-0">{$t('editor.preview.title')}</span>
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
953
|
-
</button>
|
|
954
|
-
</div>
|
|
955
|
-
{/if}
|
|
956
|
-
{#if showDimensionInputs}
|
|
957
|
-
<div class="preview-chrome-dimensions flex items-center gap-1.5 {showChromeDeviceToggle ? '' : 'ms-auto'}">
|
|
958
|
-
<label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
|
|
959
|
-
<span>{$t('editor.preview.widthShort')}</span>
|
|
960
|
-
<input
|
|
961
|
-
type="number"
|
|
962
|
-
min={MIN_WIDTH}
|
|
963
|
-
class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
|
|
964
|
-
value={widthInputValue}
|
|
965
|
-
oninput={(e) => {
|
|
966
|
-
widthInputValue = e.currentTarget.value;
|
|
967
|
-
applyDimensionFromInput('width', e.currentTarget.value);
|
|
968
|
-
}}
|
|
969
|
-
onpointerdown={(e) => e.stopPropagation()}
|
|
970
|
-
/>
|
|
971
|
-
</label>
|
|
972
|
-
<span class="text-[10px] text-gray-400">×</span>
|
|
973
|
-
<label class="preview-dim-label flex items-center gap-1 text-[10px] font-semibold text-gray-500">
|
|
974
|
-
<span>{$t('editor.preview.heightShort')}</span>
|
|
975
|
-
<input
|
|
976
|
-
type="number"
|
|
977
|
-
min={MIN_HEIGHT}
|
|
978
|
-
class="preview-dim-input w-14 h-6 rounded border border-gray-300 bg-white px-1 text-[11px] text-gray-800 focus:outline-none focus:border-blue-400 focus-visible:outline-none"
|
|
979
|
-
value={heightInputValue}
|
|
980
|
-
oninput={(e) => {
|
|
981
|
-
heightInputValue = e.currentTarget.value;
|
|
982
|
-
applyDimensionFromInput('height', e.currentTarget.value);
|
|
983
|
-
}}
|
|
984
|
-
onpointerdown={(e) => e.stopPropagation()}
|
|
985
|
-
/>
|
|
986
|
-
</label>
|
|
987
|
-
</div>
|
|
988
|
-
{/if}
|
|
928
|
+
<PreviewChromeControls
|
|
929
|
+
class="{showChromeDeviceToggle || showDimensionInputs ? 'ms-auto' : ''}"
|
|
930
|
+
mobile={deviceMobile}
|
|
931
|
+
showDevices={showChromeDeviceToggle}
|
|
932
|
+
showDimensions={showDimensionInputs}
|
|
933
|
+
widthValue={widthInputValue}
|
|
934
|
+
heightValue={heightInputValue}
|
|
935
|
+
minWidth={MIN_WIDTH}
|
|
936
|
+
minHeight={MIN_HEIGHT}
|
|
937
|
+
desktopLabel={$t('editor.preview.desktop')}
|
|
938
|
+
mobileLabel={$t('editor.preview.mobile')}
|
|
939
|
+
widthLabel={$t('editor.preview.widthShort')}
|
|
940
|
+
heightLabel={$t('editor.preview.heightShort')}
|
|
941
|
+
onDevice={(nextMobile) => dispatch('deviceView', { mobile: nextMobile })}
|
|
942
|
+
onWidth={(value) => {
|
|
943
|
+
widthInputValue = value;
|
|
944
|
+
applyDimensionFromInput('width', value);
|
|
945
|
+
}}
|
|
946
|
+
onHeight={(value) => {
|
|
947
|
+
heightInputValue = value;
|
|
948
|
+
applyDimensionFromInput('height', value);
|
|
949
|
+
}}
|
|
950
|
+
/>
|
|
989
951
|
</div>
|
|
990
952
|
{/if}
|
|
991
953
|
|
|
@@ -66,6 +66,7 @@ declare const WidgetPreview: $$__sveltets_2_IsomorphicComponent<{
|
|
|
66
66
|
resizeWidth?: number | null;
|
|
67
67
|
resizeHeight?: number | null;
|
|
68
68
|
iframeEl?: HTMLIFrameElement | null;
|
|
69
|
+
/** Inset so resize handles stay inside bounds. 0 sits the frame on the stage edge. */ edgeInset?: number;
|
|
69
70
|
getResizeLimits?: () => {
|
|
70
71
|
maxWidth: number;
|
|
71
72
|
maxHeight: number;
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
import { shareTextDirection, type TextDirection } from '@widgetic/design-system/components/textDirection';
|
|
31
31
|
import { leftOnThisScreen } from '../../utils/dockPlacement';
|
|
32
32
|
import { provideEditorLanguage } from '../../i18n/language.js';
|
|
33
|
+
import type { PartnerPickFileFn } from '../../pickFileContext.js';
|
|
34
|
+
import type { StoreFileUrlFn } from '../../fileStorageUrlContext.js';
|
|
33
35
|
import PropsEditor from './PropsEditor.svelte';
|
|
34
36
|
import WidgetPreview from '../WidgetPreview.svelte';
|
|
35
37
|
import { setGoogleFontsApiKey } from '../../config';
|
|
@@ -74,6 +76,15 @@
|
|
|
74
76
|
* library. Without it the files are the ones of the Widgetic library the token can see.
|
|
75
77
|
*/
|
|
76
78
|
export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
|
|
79
|
+
/** When given, file fields call this instead of opening the built-in File Browser. */
|
|
80
|
+
export let pickFile: PartnerPickFileFn | undefined = undefined;
|
|
81
|
+
/** When given, resolves the url stored with a picked file (partner `mediaLibrary.urlResolver`). */
|
|
82
|
+
export let storeFileUrl: StoreFileUrlFn | undefined = undefined;
|
|
83
|
+
/**
|
|
84
|
+
* When true, dragging or typing a preview size changes the preview on screen but does not change the embed width and
|
|
85
|
+
* height saved with the composition.
|
|
86
|
+
*/
|
|
87
|
+
export let lockPreviewSize = false;
|
|
77
88
|
/** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */
|
|
78
89
|
export let googleFontsApiKey = '';
|
|
79
90
|
/** Wait this long after the last change before saving by itself; `0` turns saving by itself off. Read once, when the editor starts. */
|
|
@@ -130,6 +141,7 @@
|
|
|
130
141
|
compositionId,
|
|
131
142
|
api,
|
|
132
143
|
autosaveDelayMs,
|
|
144
|
+
lockPreviewSize,
|
|
133
145
|
// The sentences the controller makes (why there is no preview) are made in the language of the moment.
|
|
134
146
|
translate: (key, values) => get(language.t)(key, values),
|
|
135
147
|
// The preview page hears about every change as it happens (and everything, once its page has loaded).
|
|
@@ -508,6 +520,8 @@
|
|
|
508
520
|
initialDesignValues={state.designValues}
|
|
509
521
|
initialContentValues={propsEditorContentValues}
|
|
510
522
|
mediaLibrary={mediaLibrary ?? defaultMediaLibrary}
|
|
523
|
+
{pickFile}
|
|
524
|
+
{storeFileUrl}
|
|
511
525
|
width="{EDITOR_WIDTH_PX}px"
|
|
512
526
|
height="{floatingEditorHeight}px"
|
|
513
527
|
showDragHandle={true}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type MediaLibraryProvider } from '@widgetic/file-browser';
|
|
2
2
|
import type { MessageOverrides } from '@widgetic/design-system/i18n';
|
|
3
3
|
import { type TextDirection } from '@widgetic/design-system/components/textDirection';
|
|
4
|
+
import type { PartnerPickFileFn } from '../../pickFileContext.js';
|
|
5
|
+
import type { StoreFileUrlFn } from '../../fileStorageUrlContext.js';
|
|
4
6
|
import { type TokenReason } from '../../composition/compositionApi.js';
|
|
5
7
|
import { type CompositionEditorState, type EditorFailure } from '../../composition/compositionEditorController.js';
|
|
6
8
|
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> {
|
|
@@ -32,6 +34,12 @@ declare const CompositionEditorApp: $$__sveltets_2_IsomorphicComponent<{
|
|
|
32
34
|
* Where the file picker gets its files, when they are not the Widgetic library: the partner's own media
|
|
33
35
|
* library. Without it the files are the ones of the Widgetic library the token can see.
|
|
34
36
|
*/ mediaLibrary?: MediaLibraryProvider | undefined;
|
|
37
|
+
/** When given, file fields call this instead of opening the built-in File Browser. */ pickFile?: PartnerPickFileFn | undefined;
|
|
38
|
+
/** When given, resolves the url stored with a picked file (partner `mediaLibrary.urlResolver`). */ storeFileUrl?: StoreFileUrlFn | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* When true, dragging or typing a preview size changes the preview on screen but does not change the embed width and
|
|
41
|
+
* height saved with the composition.
|
|
42
|
+
*/ lockPreviewSize?: boolean;
|
|
35
43
|
/** The key the font pickers list Google Fonts with. Without one only the fonts that need no key are listed. */ googleFontsApiKey?: string;
|
|
36
44
|
/** 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
45
|
/** Writes what the layout does to the console. */ debug?: boolean;
|
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
} from '../../utils/shadowDom';
|
|
91
91
|
import { config as appConfig } from '../../config';
|
|
92
92
|
import { provideEditorMediaLibrary } from '../../mediaLibraryContext';
|
|
93
|
+
import { provideEditorPickFile, type PartnerPickFileFn } from '../../pickFileContext';
|
|
94
|
+
import { provideEditorStoreFileUrl, type StoreFileUrlFn } from '../../fileStorageUrlContext';
|
|
93
95
|
import { useEditorLanguage } from '../../i18n/language';
|
|
94
96
|
import type { MediaLibraryProvider } from '@widgetic/file-browser';
|
|
95
97
|
|
|
@@ -181,8 +183,14 @@
|
|
|
181
183
|
* storage (a CMS, an S3 bucket, a DAM) can be used with no Widgetic storage at all.
|
|
182
184
|
*/
|
|
183
185
|
export let mediaLibrary: MediaLibraryProvider | undefined = undefined;
|
|
186
|
+
/** When given, file fields call this instead of opening the built-in File Browser. */
|
|
187
|
+
export let pickFile: PartnerPickFileFn | undefined = undefined;
|
|
188
|
+
/** When given, resolves the url stored with a picked file (partner `mediaLibrary.urlResolver`). */
|
|
189
|
+
export let storeFileUrl: StoreFileUrlFn | undefined = undefined;
|
|
184
190
|
// The file controls are created from the schema several components down: give them the library through context.
|
|
185
191
|
provideEditorMediaLibrary(() => mediaLibrary);
|
|
192
|
+
provideEditorPickFile(() => pickFile);
|
|
193
|
+
provideEditorStoreFileUrl(() => storeFileUrl);
|
|
186
194
|
|
|
187
195
|
// The words of the editor in the language of the page: the editor above this one chose it (see `provideEditorLanguage`).
|
|
188
196
|
const { t } = useEditorLanguage();
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ContentSchemaView, DesignSchemaView } from '../../schemaViews.js';
|
|
2
|
+
import { type PartnerPickFileFn } from '../../pickFileContext';
|
|
3
|
+
import { type StoreFileUrlFn } from '../../fileStorageUrlContext';
|
|
2
4
|
import type { MediaLibraryProvider } from '@widgetic/file-browser';
|
|
3
5
|
/**
|
|
4
6
|
* STYLES from Widgetic Design System
|
|
@@ -52,6 +54,8 @@ declare const PropsEditor: $$__sveltets_2_IsomorphicComponent<{
|
|
|
52
54
|
* `MediaLibraryProvider` they list, upload, rename and delete through its functions instead, so a partner's own
|
|
53
55
|
* storage (a CMS, an S3 bucket, a DAM) can be used with no Widgetic storage at all.
|
|
54
56
|
*/ mediaLibrary?: MediaLibraryProvider | undefined;
|
|
57
|
+
/** When given, file fields call this instead of opening the built-in File Browser. */ pickFile?: PartnerPickFileFn | undefined;
|
|
58
|
+
/** When given, resolves the url stored with a picked file (partner `mediaLibrary.urlResolver`). */ storeFileUrl?: StoreFileUrlFn | undefined;
|
|
55
59
|
updateDesignValue?: (propertyId: string, value: any) => void;
|
|
56
60
|
setPosition?: (left: number, top: number) => void;
|
|
57
61
|
getPosition?: () => {
|
|
@@ -32,10 +32,12 @@
|
|
|
32
32
|
import { onDestroy, tick } from 'svelte';
|
|
33
33
|
|
|
34
34
|
// Shadcn UI Components
|
|
35
|
-
import { Portal, Tooltip } from '@widgetic/design-system/components';
|
|
35
|
+
import { Portal, Tooltip, showToast } from '@widgetic/design-system/components';
|
|
36
36
|
import { ThumbHashImage } from "@widgetic/design-system/components";
|
|
37
37
|
import { FileBrowser, setFileBrowserUploadConfig, persistentSelectStore } from '@widgetic/file-browser';
|
|
38
38
|
import { useEditorMediaLibrary } from '../../mediaLibraryContext';
|
|
39
|
+
import { useEditorPickFile, type PartnerPickFile } from '../../pickFileContext';
|
|
40
|
+
import { useEditorStoreFileUrl } from '../../fileStorageUrlContext';
|
|
39
41
|
import { useEditorLanguage } from '../../i18n/language';
|
|
40
42
|
import type { EditorMessageKey } from '../../i18n/messages';
|
|
41
43
|
import type { Translate } from '@widgetic/design-system/i18n';
|
|
@@ -74,6 +76,9 @@
|
|
|
74
76
|
// The media library the editor above was given (a partner's own storage, for instance), read through a getter
|
|
75
77
|
// because it reaches this component through context.
|
|
76
78
|
const getEditorMediaLibrary = useEditorMediaLibrary();
|
|
79
|
+
const getEditorPickFile = useEditorPickFile();
|
|
80
|
+
const getEditorStoreFileUrl = useEditorStoreFileUrl();
|
|
81
|
+
let pickAbortController: AbortController | null = null;
|
|
77
82
|
|
|
78
83
|
// The words of the editor in the language of the page, and the share of them that belongs to the File Browser inside it.
|
|
79
84
|
const { t, fileBrowser: fileBrowserLanguage } = useEditorLanguage();
|
|
@@ -218,7 +223,56 @@
|
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
function acceptPatternsForField(): string[] {
|
|
227
|
+
if (!fileTypes?.length || fileTypes.includes('*')) return ['*/*'];
|
|
228
|
+
return fileTypes;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function storedUrlFor(file: PartnerPickFile): Promise<string> {
|
|
232
|
+
const storeFileUrl = getEditorStoreFileUrl();
|
|
233
|
+
if (!storeFileUrl) return file.url;
|
|
234
|
+
try {
|
|
235
|
+
return await storeFileUrl(file);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.warn('[editor] storeFileUrl failed; using the file url.', error);
|
|
238
|
+
return file.url;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function applyPickedFile(selectedFile: PartnerPickFile) {
|
|
243
|
+
const url = await storedUrlFor(selectedFile);
|
|
244
|
+
const nextValue: FileValue = {
|
|
245
|
+
id: selectedFile.id,
|
|
246
|
+
file: selectedFile.name || '',
|
|
247
|
+
url,
|
|
248
|
+
hash: selectedFile.hash
|
|
249
|
+
};
|
|
250
|
+
value = nextValue;
|
|
251
|
+
onChange(nextValue);
|
|
252
|
+
}
|
|
253
|
+
|
|
221
254
|
async function openFileBrowser() {
|
|
255
|
+
const pickFile = getEditorPickFile();
|
|
256
|
+
if (pickFile) {
|
|
257
|
+
pickAbortController?.abort();
|
|
258
|
+
pickAbortController = new AbortController();
|
|
259
|
+
const signal = pickAbortController.signal;
|
|
260
|
+
try {
|
|
261
|
+
const picked = await pickFile({ accept: acceptPatternsForField(), multiple: false, signal });
|
|
262
|
+
if (signal.aborted) return;
|
|
263
|
+
const selectedFile = picked?.[0];
|
|
264
|
+
if (!selectedFile) return;
|
|
265
|
+
await applyPickedFile(selectedFile);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (signal.aborted) return;
|
|
268
|
+
const name = error instanceof Error ? error.name : '';
|
|
269
|
+
if (name === 'AbortError') return;
|
|
270
|
+
console.error('[editor] pickFile failed:', error);
|
|
271
|
+
showToast('error', $t('editor.file.pickFailed'));
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
222
276
|
try {
|
|
223
277
|
syncFileBrowserUploadConfigIfEmbedded();
|
|
224
278
|
// console.log('BrowserInputController: opening file browser', {
|
|
@@ -240,7 +294,7 @@
|
|
|
240
294
|
setPortalHostInteractive(false);
|
|
241
295
|
}
|
|
242
296
|
|
|
243
|
-
function handleFileBrowserSelect(event: CustomEvent<{ files?: Array<FileValue & { name?: string; thumbnail?: string }> }>) {
|
|
297
|
+
async function handleFileBrowserSelect(event: CustomEvent<{ files?: Array<FileValue & { name?: string; thumbnail?: string }> }>) {
|
|
244
298
|
try {
|
|
245
299
|
const selectedFile = event.detail?.files?.[0];
|
|
246
300
|
if (!selectedFile) {
|
|
@@ -248,21 +302,12 @@
|
|
|
248
302
|
return;
|
|
249
303
|
}
|
|
250
304
|
|
|
251
|
-
|
|
305
|
+
await applyPickedFile({
|
|
252
306
|
id: selectedFile.id,
|
|
253
|
-
|
|
307
|
+
name: selectedFile.name || selectedFile.file || '',
|
|
254
308
|
url: selectedFile.url,
|
|
255
309
|
hash: selectedFile.hash
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
// console.log('BrowserInputController: selected file', {
|
|
259
|
-
// id: nextValue.id,
|
|
260
|
-
// file: nextValue.file,
|
|
261
|
-
// url: nextValue.url
|
|
262
|
-
// });
|
|
263
|
-
|
|
264
|
-
value = nextValue;
|
|
265
|
-
onChange(nextValue);
|
|
310
|
+
});
|
|
266
311
|
closeFileBrowser();
|
|
267
312
|
} catch (error) {
|
|
268
313
|
console.error('BrowserInputController: failed to select file', error);
|
|
@@ -292,6 +337,8 @@
|
|
|
292
337
|
}
|
|
293
338
|
|
|
294
339
|
onDestroy(() => {
|
|
340
|
+
pickAbortController?.abort();
|
|
341
|
+
pickAbortController = null;
|
|
295
342
|
clearTimeout(showHelpTimeout);
|
|
296
343
|
setPortalHostInteractive(false);
|
|
297
344
|
if (audioElement) {
|
|
@@ -148,6 +148,11 @@ export interface CompositionEditorOptions {
|
|
|
148
148
|
* Default: English.
|
|
149
149
|
*/
|
|
150
150
|
translate?: Translate<EditorMessageKey>;
|
|
151
|
+
/**
|
|
152
|
+
* When true, resizing the preview changes how it is drawn but never updates the embed width and height that are saved
|
|
153
|
+
* with the composition (the partner owns embed size elsewhere).
|
|
154
|
+
*/
|
|
155
|
+
lockPreviewSize?: boolean;
|
|
151
156
|
}
|
|
152
157
|
/** What went wrong, from whatever was thrown. */
|
|
153
158
|
export declare function describeFailure(error: unknown): EditorFailure;
|
|
@@ -515,6 +515,14 @@ export class CompositionEditorController {
|
|
|
515
515
|
// (The preview reports the size it was just given, too: that is not a new size.)
|
|
516
516
|
if (previewWidth === this.state.previewWidth && previewHeight === this.state.previewHeight)
|
|
517
517
|
return;
|
|
518
|
+
if (this.options.lockPreviewSize === true) {
|
|
519
|
+
this.patch({
|
|
520
|
+
previewWidth,
|
|
521
|
+
previewHeight,
|
|
522
|
+
deviceView: 'desktop'
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
518
526
|
const embedChanged = previewWidth !== this.state.embedWidth || previewHeight !== this.state.embedHeight;
|
|
519
527
|
this.patch({
|
|
520
528
|
previewWidth,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PartnerPickFile } from './pickFileContext';
|
|
2
|
+
/** Resolves the address stored with a composition for a picked file (partner `mediaLibrary.urlResolver`). */
|
|
3
|
+
export type StoreFileUrlFn = (file: PartnerPickFile) => Promise<string>;
|
|
4
|
+
export type StoreFileUrlGetter = () => StoreFileUrlFn | undefined;
|
|
5
|
+
export declare function provideEditorStoreFileUrl(getStoreFileUrl: StoreFileUrlGetter): void;
|
|
6
|
+
export declare function useEditorStoreFileUrl(): StoreFileUrlGetter;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
const STORE_FILE_URL_CONTEXT_KEY = Symbol.for('widgetic.editor.storeFileUrl');
|
|
3
|
+
export function provideEditorStoreFileUrl(getStoreFileUrl) {
|
|
4
|
+
setContext(STORE_FILE_URL_CONTEXT_KEY, getStoreFileUrl);
|
|
5
|
+
}
|
|
6
|
+
export function useEditorStoreFileUrl() {
|
|
7
|
+
try {
|
|
8
|
+
return getContext(STORE_FILE_URL_CONTEXT_KEY) ?? (() => undefined);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return () => undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/i18n/language.d.ts
CHANGED
|
@@ -145,6 +145,7 @@ export declare const EDITOR_CATALOGS: {
|
|
|
145
145
|
readonly 'editor.file.notFound': "File not found";
|
|
146
146
|
readonly 'editor.file.emptyFolder': "Empty folder";
|
|
147
147
|
readonly 'editor.file.clear': "Clear file";
|
|
148
|
+
readonly 'editor.file.pickFailed': "Could not choose a file. Try again.";
|
|
148
149
|
readonly 'editor.file.dialogTitle': "Select file";
|
|
149
150
|
readonly 'editor.file.closeDialog': "Close file browser";
|
|
150
151
|
readonly 'editor.color.noRecent': "No recent colors";
|
package/dist/i18n/messages.d.ts
CHANGED
|
@@ -164,6 +164,7 @@ export declare const EDITOR_MESSAGES: {
|
|
|
164
164
|
readonly 'editor.file.notFound': "File not found";
|
|
165
165
|
readonly 'editor.file.emptyFolder': "Empty folder";
|
|
166
166
|
readonly 'editor.file.clear': "Clear file";
|
|
167
|
+
readonly 'editor.file.pickFailed': "Could not choose a file. Try again.";
|
|
167
168
|
readonly 'editor.file.dialogTitle': "Select file";
|
|
168
169
|
readonly 'editor.file.closeDialog': "Close file browser";
|
|
169
170
|
readonly 'editor.color.noRecent': "No recent colors";
|
package/dist/i18n/messages.js
CHANGED
|
@@ -173,6 +173,7 @@ export const EDITOR_MESSAGES = {
|
|
|
173
173
|
'editor.file.notFound': 'File not found',
|
|
174
174
|
'editor.file.emptyFolder': 'Empty folder',
|
|
175
175
|
'editor.file.clear': 'Clear file',
|
|
176
|
+
'editor.file.pickFailed': 'Could not choose a file. Try again.',
|
|
176
177
|
'editor.file.dialogTitle': 'Select file',
|
|
177
178
|
'editor.file.closeDialog': 'Close file browser',
|
|
178
179
|
// ===== Colours =====
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type { ContentSchemaItemView, ContentSchemaPropertyView, ContentSchemaVie
|
|
|
2
2
|
export { default as PropsEditor } from './components/editors/PropsEditor.svelte';
|
|
3
3
|
export { default as FontInputController } from './components/ic/FontInputController.svelte';
|
|
4
4
|
export { default as WidgetPreview } from './components/WidgetPreview.svelte';
|
|
5
|
+
export { default as PreviewChromeControls } from './components/PreviewChromeControls.svelte';
|
|
5
6
|
export { setFileBrowserUploadConfig } from '@widgetic/file-browser';
|
|
6
7
|
export { setGoogleFontsApiKey, getGoogleFontsKey } from './config';
|
|
7
8
|
export { default as EmbedEditor } from './components/editors/EmbedEditor.svelte';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
export { default as PropsEditor } from './components/editors/PropsEditor.svelte';
|
|
5
5
|
export { default as FontInputController } from './components/ic/FontInputController.svelte';
|
|
6
6
|
export { default as WidgetPreview } from './components/WidgetPreview.svelte';
|
|
7
|
+
export { default as PreviewChromeControls } from './components/PreviewChromeControls.svelte';
|
|
7
8
|
export { setFileBrowserUploadConfig } from '@widgetic/file-browser';
|
|
8
9
|
export { setGoogleFontsApiKey, getGoogleFontsKey } from './config';
|
|
9
10
|
// Embed Editor
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** One file the partner's picker returns (same shape as the media library's `MediaLibraryFile`). */
|
|
2
|
+
export interface PartnerPickFile {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
mimeType?: string;
|
|
7
|
+
size?: number;
|
|
8
|
+
thumbnail?: string;
|
|
9
|
+
hash?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface PartnerPickFileRequest {
|
|
12
|
+
accept: string[];
|
|
13
|
+
multiple: boolean;
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
export type PartnerPickFileFn = (request: PartnerPickFileRequest) => Promise<PartnerPickFile[]>;
|
|
17
|
+
export type PickFileGetter = () => PartnerPickFileFn | undefined;
|
|
18
|
+
export declare function provideEditorPickFile(getPickFile: PickFileGetter): void;
|
|
19
|
+
export declare function useEditorPickFile(): PickFileGetter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hands the partner's `pickFile` hook from the editor down to file field controls.
|
|
3
|
+
*
|
|
4
|
+
* Same pattern as {@link mediaLibraryContext}: `PropsEditor` receives `pickFile`, and the file controls read it through
|
|
5
|
+
* context instead of threading a prop through the controller renderer.
|
|
6
|
+
*/
|
|
7
|
+
import { getContext, setContext } from 'svelte';
|
|
8
|
+
const PICK_FILE_CONTEXT_KEY = Symbol.for('widgetic.editor.pickFile');
|
|
9
|
+
export function provideEditorPickFile(getPickFile) {
|
|
10
|
+
setContext(PICK_FILE_CONTEXT_KEY, getPickFile);
|
|
11
|
+
}
|
|
12
|
+
export function useEditorPickFile() {
|
|
13
|
+
try {
|
|
14
|
+
return getContext(PICK_FILE_CONTEXT_KEY) ?? (() => undefined);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return () => undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|