@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.
- package/README.md +242 -0
- package/dist/components/WidgetPreview.svelte +123 -86
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/color-select/ColorSelect.svelte +4 -4
- package/dist/components/editors/CompositionEditorApp.svelte +562 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
- package/dist/components/editors/EmbedEditor.svelte +33 -22
- package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editors/PropsEditor.svelte +267 -850
- package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
- package/dist/components/ic/AngleInputController.svelte +1 -1
- package/dist/components/ic/BrowserInputController.svelte +58 -31
- package/dist/components/ic/ColorPickerInputController.svelte +55 -21
- package/dist/components/ic/DateFieldInputController.svelte +34 -14
- package/dist/components/ic/DatePickerInputController.svelte +37 -21
- package/dist/components/ic/DropdownInputController.svelte +21 -9
- package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
- package/dist/components/ic/FixedPositionInputController.svelte +12 -6
- package/dist/components/ic/FontInputController.svelte +100 -76
- package/dist/components/ic/InputControllerHeader.svelte +16 -9
- package/dist/components/ic/NumberControls.svelte +4 -4
- package/dist/components/ic/NumberInputController.svelte +6 -4
- package/dist/components/ic/PositionInputController.svelte +23 -15
- package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
- package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
- package/dist/components/ic/SliderInputController.svelte +7 -2
- package/dist/components/ic/SwitchInputController.svelte +12 -3
- package/dist/components/ic/TagsInputController.svelte +19 -4
- package/dist/components/ic/TextAreaInputController.svelte +79 -51
- package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
- package/dist/components/ic/TextInputController.svelte +17 -1
- package/dist/components/ic/TimeInputController.svelte +11 -4
- package/dist/components/ic/VectorInputController.svelte +101 -70
- package/dist/components/ic/shared/ItemGrid.svelte +12 -4
- package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
- package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +263 -0
- package/dist/composition/compositionEditorController.js +746 -0
- package/dist/composition/compositionHelpers.d.ts +74 -0
- package/dist/composition/compositionHelpers.js +405 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/labels.d.ts +15 -0
- package/dist/i18n/labels.js +57 -0
- package/dist/i18n/language.d.ts +228 -0
- package/dist/i18n/language.js +34 -0
- package/dist/i18n/languageKit.d.ts +54 -0
- package/dist/i18n/languageKit.js +102 -0
- package/dist/i18n/messages.d.ts +241 -0
- package/dist/i18n/messages.js +253 -0
- package/dist/i18n/translate.d.ts +6 -0
- package/dist/i18n/translate.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/mediaLibraryContext.d.ts +10 -0
- package/dist/mediaLibraryContext.js +30 -0
- package/dist/utils/FontManager.d.ts +21 -5
- package/dist/utils/FontManager.js +62 -67
- package/dist/utils/VectorManager.d.ts +0 -1
- package/dist/utils/VectorManager.js +10 -12
- package/dist/utils/dockPlacement.d.ts +12 -0
- package/dist/utils/dockPlacement.js +13 -0
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fieldDirection.d.ts +13 -0
- package/dist/utils/fieldDirection.js +25 -0
- package/dist/utils/fontUtils.js +5 -3
- package/dist/utils/htmlSafety.d.ts +21 -0
- package/dist/utils/htmlSafety.js +38 -0
- package/dist/utils/itemReorder.d.ts +90 -0
- package/dist/utils/itemReorder.js +354 -0
- package/dist/utils/recentColors.js +4 -2
- package/dist/utils/richTextPreview.d.ts +38 -0
- package/dist/utils/richTextPreview.js +207 -0
- package/dist/utils/scrollActivity.d.ts +14 -0
- package/dist/utils/scrollActivity.js +63 -0
- package/dist/utils/shadowDom.d.ts +67 -0
- package/dist/utils/shadowDom.js +211 -0
- package/dist/utils/vectorUtils.js +5 -3
- package/dist/utils/visibilityCondition.d.ts +40 -0
- package/dist/utils/visibilityCondition.js +535 -0
- package/package.json +7 -6
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for editing a composition: reading what the API sends, merging it with the widget's schemas, and
|
|
3
|
+
* turning the editor's values into what the widget's preview page understands.
|
|
4
|
+
*
|
|
5
|
+
* They hold no state and touch no browser API, so they behave the same wherever the editor runs: in Widgetic's
|
|
6
|
+
* own site and in a partner's page. They were written for the site's composition page and moved here unchanged in
|
|
7
|
+
* behavior, so the site and an embedded editor read and write a composition in exactly the same way (see
|
|
8
|
+
* `tests/composition-helpers.test.mjs`, which pins what they do).
|
|
9
|
+
*
|
|
10
|
+
* "Schema" is the widget's description of what can be edited. Its shape is the producer's (the API's), so the
|
|
11
|
+
* helpers read it loosely and keep unknown fields as they are.
|
|
12
|
+
*/
|
|
13
|
+
/** A JSON object whose fields are not known to the editor. */
|
|
14
|
+
export type JsonRecord = Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* The payload of an API answer. The API wraps what it returns as `{ success, data }`; code that already holds the
|
|
17
|
+
* payload (or an older, unwrapped answer) passes through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unwrapApiData<T = any>(response: unknown): T;
|
|
20
|
+
/** The widget a composition is made of, or `''` when the record does not name one. */
|
|
21
|
+
export declare function compositionWidgetId(composition: JsonRecord | null | undefined): string;
|
|
22
|
+
/** The project a composition is filed in, or `null` when it is in none. */
|
|
23
|
+
export declare function compositionRecordProjectId(composition: JsonRecord | null | undefined): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The design schema as the editor draws it: a list of component groups, or `null` when there is nothing to edit.
|
|
26
|
+
* A schema with several groups becomes ONE group (the editor draws a single design tab) holding all their
|
|
27
|
+
* components; the first group names it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeDesignSchemaFromApiResponse(designResponse: unknown): any;
|
|
30
|
+
/**
|
|
31
|
+
* Gives a design schema that has a title font but no subtitle font a subtitle font beside the title's (a copy of
|
|
32
|
+
* it), so widgets that read `subtitleFont` can be edited. A schema that already has one, or has no title font to
|
|
33
|
+
* copy, is returned as it is.
|
|
34
|
+
*
|
|
35
|
+
* `description` is what the new font is called on the screen: the editor's own word, so the page's language can give it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureSubtitleFontInDesignSchema(schema: unknown, description?: string): unknown;
|
|
38
|
+
/** The content schema as the editor draws it, or `null` when there is none. */
|
|
39
|
+
export declare function normalizeContentSchemaFromApiResponse(contentResponse: unknown): any;
|
|
40
|
+
/**
|
|
41
|
+
* The design values as the widget's preview page reads them: every value a string. A font becomes the CSS family
|
|
42
|
+
* (`"Roboto", sans-serif`) plus `<name>Family`, `<name>Weight` and `<name>Style`; a `{ value }` option becomes its
|
|
43
|
+
* value; an aspect ratio is written as a CSS ratio.
|
|
44
|
+
*/
|
|
45
|
+
export declare function flattenDesignValuesForPreview(design: JsonRecord): Record<string, string>;
|
|
46
|
+
/** The default of every generic content property the schema declares, by property name. */
|
|
47
|
+
export declare function extractContentDefaultsFromSchema(schema: unknown): JsonRecord;
|
|
48
|
+
/** The saved content values, with the schema's defaults filled in for whatever was never saved. Saved values win. */
|
|
49
|
+
export declare function mergeMissingContentValues(existing: JsonRecord, schema: unknown): JsonRecord;
|
|
50
|
+
/** The flat content values (not the items) out of what a composition saved as its `content`. */
|
|
51
|
+
export declare function extractContentValuesFromCompositionRaw(rawContent: JsonRecord): JsonRecord;
|
|
52
|
+
/** The content items (the repeated entries: slides, rows, cards…) out of what a composition saved as its `content`. */
|
|
53
|
+
export declare function extractContentItemsFromCompositionRaw(rawContent: JsonRecord): any[];
|
|
54
|
+
/** The content items as the widget's preview page reads them: flat, with each property's value on the item itself. */
|
|
55
|
+
export declare function flattenContentItemsForPreview(items: any[]): any[];
|
|
56
|
+
/** The content items as the `items` map the props editor starts from: flat items by id. */
|
|
57
|
+
export declare function contentItemsToEditorValueMap(items: any[]): Record<string, Record<string, unknown>>;
|
|
58
|
+
/**
|
|
59
|
+
* The content items the composition holds, with the schema's definitions attached. The composition decides which
|
|
60
|
+
* items there are and in which order: the user can delete the items the widget starts with and drag them into another
|
|
61
|
+
* order, and that must still be so after the items have been through here (they used to come back in the schema's
|
|
62
|
+
* order, with the deleted ones among them). An item the schema lists carries the schema's definition and its saved
|
|
63
|
+
* values; one it does not list is shaped like the schema's template item. With no saved items (a composition nobody
|
|
64
|
+
* has edited yet) the schema's own are used; with no schema items the saved ones are returned as they are.
|
|
65
|
+
*/
|
|
66
|
+
export declare function mergeContentItemsWithSchema(savedItems: any[], schemaItems: any[]): any[];
|
|
67
|
+
/** The content schema the props editor is given: the widget's, with the composition's items merged into it. */
|
|
68
|
+
export declare function buildPropsEditorContentSchema(schema: any, items: any[]): any;
|
|
69
|
+
/** A design with a title font but no subtitle font gets the title's as its subtitle font. */
|
|
70
|
+
export declare function seedMissingFontFaces(design: JsonRecord): JsonRecord;
|
|
71
|
+
/** The embed settings a composition saved, or `{}` when it saved none. */
|
|
72
|
+
export declare function getCompositionEmbedSettings(composition: JsonRecord): JsonRecord;
|
|
73
|
+
/** A size saved as text or number ("800", 800, "800px") as a positive whole number; anything else is the fallback. */
|
|
74
|
+
export declare function parseEmbedDimension(value: unknown, fallback: number): number;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for editing a composition: reading what the API sends, merging it with the widget's schemas, and
|
|
3
|
+
* turning the editor's values into what the widget's preview page understands.
|
|
4
|
+
*
|
|
5
|
+
* They hold no state and touch no browser API, so they behave the same wherever the editor runs: in Widgetic's
|
|
6
|
+
* own site and in a partner's page. They were written for the site's composition page and moved here unchanged in
|
|
7
|
+
* behavior, so the site and an embedded editor read and write a composition in exactly the same way (see
|
|
8
|
+
* `tests/composition-helpers.test.mjs`, which pins what they do).
|
|
9
|
+
*
|
|
10
|
+
* "Schema" is the widget's description of what can be edited. Its shape is the producer's (the API's), so the
|
|
11
|
+
* helpers read it loosely and keep unknown fields as they are.
|
|
12
|
+
*/
|
|
13
|
+
/** The font family a font value falls back to in the preview when it names none. */
|
|
14
|
+
const DEFAULT_PREVIEW_FONT_FAMILY = 'Inter';
|
|
15
|
+
/**
|
|
16
|
+
* The payload of an API answer. The API wraps what it returns as `{ success, data }`; code that already holds the
|
|
17
|
+
* payload (or an older, unwrapped answer) passes through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export function unwrapApiData(response) {
|
|
20
|
+
if (response && typeof response === 'object' && 'data' in response) {
|
|
21
|
+
return (response.data ?? response);
|
|
22
|
+
}
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
/** The widget a composition is made of, or `''` when the record does not name one. */
|
|
26
|
+
export function compositionWidgetId(composition) {
|
|
27
|
+
if (!composition)
|
|
28
|
+
return '';
|
|
29
|
+
const id = composition.widgetId ?? composition.widget_id;
|
|
30
|
+
return typeof id === 'string' ? id : '';
|
|
31
|
+
}
|
|
32
|
+
/** The project a composition is filed in, or `null` when it is in none. */
|
|
33
|
+
export function compositionRecordProjectId(composition) {
|
|
34
|
+
if (!composition)
|
|
35
|
+
return null;
|
|
36
|
+
const projectId = composition.projectId ?? composition.project_id;
|
|
37
|
+
return typeof projectId === 'string' && projectId ? projectId : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The design schema as the editor draws it: a list of component groups, or `null` when there is nothing to edit.
|
|
41
|
+
* A schema with several groups becomes ONE group (the editor draws a single design tab) holding all their
|
|
42
|
+
* components; the first group names it.
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeDesignSchemaFromApiResponse(designResponse) {
|
|
45
|
+
if (!designResponse)
|
|
46
|
+
return null;
|
|
47
|
+
const payload = designResponse?.data ?? designResponse;
|
|
48
|
+
if (!payload || typeof payload !== 'object')
|
|
49
|
+
return null;
|
|
50
|
+
if (Array.isArray(payload))
|
|
51
|
+
return payload;
|
|
52
|
+
const record = payload;
|
|
53
|
+
const groups = record.componentGroups ?? record.component_groups;
|
|
54
|
+
if (Array.isArray(groups)) {
|
|
55
|
+
if (groups.length > 1) {
|
|
56
|
+
const mergedComponents = groups.flatMap((group) => group.components || []);
|
|
57
|
+
return [
|
|
58
|
+
{ name: groups[0]?.name || 'Design', components: mergedComponents }
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
return groups;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Gives a design schema that has a title font but no subtitle font a subtitle font beside the title's (a copy of
|
|
67
|
+
* it), so widgets that read `subtitleFont` can be edited. A schema that already has one, or has no title font to
|
|
68
|
+
* copy, is returned as it is.
|
|
69
|
+
*
|
|
70
|
+
* `description` is what the new font is called on the screen: the editor's own word, so the page's language can give it.
|
|
71
|
+
*/
|
|
72
|
+
export function ensureSubtitleFontInDesignSchema(schema, description = 'Subtitle Font') {
|
|
73
|
+
if (!schema)
|
|
74
|
+
return schema;
|
|
75
|
+
try {
|
|
76
|
+
const clone = structuredClone(schema);
|
|
77
|
+
const groups = Array.isArray(clone) ? clone : clone.componentGroups;
|
|
78
|
+
if (!Array.isArray(groups) || groups.length === 0)
|
|
79
|
+
return schema;
|
|
80
|
+
const allProps = groups.flatMap((group) => (group.components || []).flatMap((component) => component.properties || []));
|
|
81
|
+
const hasSubtitle = allProps.some((prop) => /subtitleFont|captionFont/i.test(String(prop?.name || prop?.id || '')));
|
|
82
|
+
if (hasSubtitle)
|
|
83
|
+
return clone;
|
|
84
|
+
const titleProp = allProps.find((prop) => {
|
|
85
|
+
const key = String(prop?.name || prop?.id || '');
|
|
86
|
+
const type = String(prop?.inputController?.type || '').toLowerCase();
|
|
87
|
+
return type === 'font' && /titleFont|^font$/i.test(key);
|
|
88
|
+
});
|
|
89
|
+
if (!titleProp || typeof titleProp !== 'object')
|
|
90
|
+
return clone;
|
|
91
|
+
const firstComponent = groups[0]?.components?.[0];
|
|
92
|
+
if (!firstComponent)
|
|
93
|
+
return clone;
|
|
94
|
+
firstComponent.properties = Array.isArray(firstComponent.properties)
|
|
95
|
+
? firstComponent.properties
|
|
96
|
+
: [];
|
|
97
|
+
const subtitleProp = {
|
|
98
|
+
...structuredClone(titleProp),
|
|
99
|
+
id: 'subtitleFont',
|
|
100
|
+
name: 'subtitleFont',
|
|
101
|
+
description
|
|
102
|
+
};
|
|
103
|
+
const titleIndex = firstComponent.properties.findIndex((prop) => (prop?.name || prop?.id) ===
|
|
104
|
+
(titleProp.name ||
|
|
105
|
+
titleProp.id));
|
|
106
|
+
if (titleIndex >= 0)
|
|
107
|
+
firstComponent.properties.splice(titleIndex + 1, 0, subtitleProp);
|
|
108
|
+
else
|
|
109
|
+
firstComponent.properties.push(subtitleProp);
|
|
110
|
+
return clone;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
console.warn('[CompositionEditor] The subtitle font could not be added to the design schema:', error);
|
|
114
|
+
return schema;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** The content schema as the editor draws it, or `null` when there is none. */
|
|
118
|
+
export function normalizeContentSchemaFromApiResponse(contentResponse) {
|
|
119
|
+
if (!contentResponse)
|
|
120
|
+
return null;
|
|
121
|
+
const payload = contentResponse?.data ?? contentResponse;
|
|
122
|
+
return payload && typeof payload === 'object' ? payload : null;
|
|
123
|
+
}
|
|
124
|
+
/** A font weight name ("Bold Italic", "600") as the CSS weight and style a page needs. Unknown names are 400/normal. */
|
|
125
|
+
function parseFontFace(weight) {
|
|
126
|
+
const raw = String(weight || 'Regular');
|
|
127
|
+
const cssStyle = /italic|oblique/i.test(raw) ? 'italic' : 'normal';
|
|
128
|
+
const name = raw
|
|
129
|
+
.toLowerCase()
|
|
130
|
+
.replace(/italic|oblique/g, '')
|
|
131
|
+
.replace(/[\s_-]/g, '');
|
|
132
|
+
const table = {
|
|
133
|
+
thin: '100',
|
|
134
|
+
hairline: '100',
|
|
135
|
+
extralight: '200',
|
|
136
|
+
ultralight: '200',
|
|
137
|
+
light: '300',
|
|
138
|
+
regular: '400',
|
|
139
|
+
normal: '400',
|
|
140
|
+
'': '400',
|
|
141
|
+
medium: '500',
|
|
142
|
+
semibold: '600',
|
|
143
|
+
demibold: '600',
|
|
144
|
+
bold: '700',
|
|
145
|
+
extrabold: '800',
|
|
146
|
+
ultrabold: '800',
|
|
147
|
+
black: '900',
|
|
148
|
+
heavy: '900'
|
|
149
|
+
};
|
|
150
|
+
if (/^\d{3}$/.test(name))
|
|
151
|
+
return { cssWeight: name, cssStyle };
|
|
152
|
+
return { cssWeight: table[name] || '400', cssStyle };
|
|
153
|
+
}
|
|
154
|
+
/** An aspect ratio the way a stylesheet writes it ("landscape", "16:9" → "4/3", "16/9"). Unknown values stay as they are. */
|
|
155
|
+
function mapAspectRatioToCss(raw) {
|
|
156
|
+
const key = raw.trim().toLowerCase();
|
|
157
|
+
const table = {
|
|
158
|
+
square: '1/1',
|
|
159
|
+
'1:1': '1/1',
|
|
160
|
+
'1/1': '1/1',
|
|
161
|
+
landscape: '4/3',
|
|
162
|
+
'4:3': '4/3',
|
|
163
|
+
'4/3': '4/3',
|
|
164
|
+
wide: '16/9',
|
|
165
|
+
'16:9': '16/9',
|
|
166
|
+
'16/9': '16/9',
|
|
167
|
+
portrait: '3/4',
|
|
168
|
+
'3:4': '3/4',
|
|
169
|
+
'3/4': '3/4'
|
|
170
|
+
};
|
|
171
|
+
return table[key] || raw;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The design values as the widget's preview page reads them: every value a string. A font becomes the CSS family
|
|
175
|
+
* (`"Roboto", sans-serif`) plus `<name>Family`, `<name>Weight` and `<name>Style`; a `{ value }` option becomes its
|
|
176
|
+
* value; an aspect ratio is written as a CSS ratio.
|
|
177
|
+
*/
|
|
178
|
+
export function flattenDesignValuesForPreview(design) {
|
|
179
|
+
const out = {};
|
|
180
|
+
for (const [key, value] of Object.entries(design || {})) {
|
|
181
|
+
if (value && typeof value === 'object' && 'family' in value) {
|
|
182
|
+
const font = value;
|
|
183
|
+
const family = String(font.family || DEFAULT_PREVIEW_FONT_FAMILY);
|
|
184
|
+
const face = parseFontFace(font.weight);
|
|
185
|
+
out[key] = `"${family}", sans-serif`;
|
|
186
|
+
out[`${key}Family`] = family;
|
|
187
|
+
out[`${key}Weight`] = face.cssWeight;
|
|
188
|
+
out[`${key}Style`] = face.cssStyle;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
192
|
+
out[key] = mapAspectRatioToCss(String(value.value ?? ''));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (typeof value === 'string') {
|
|
196
|
+
out[key] = /aspect/i.test(key) ? mapAspectRatioToCss(value) : value;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (value == null)
|
|
200
|
+
continue;
|
|
201
|
+
out[key] = String(value);
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
/** The default of every generic content property the schema declares, by property name. */
|
|
206
|
+
export function extractContentDefaultsFromSchema(schema) {
|
|
207
|
+
const values = {};
|
|
208
|
+
if (!schema || typeof schema !== 'object')
|
|
209
|
+
return values;
|
|
210
|
+
const record = schema;
|
|
211
|
+
const props = record.genericContent?.properties ?? record.data?.genericContent?.properties ?? [];
|
|
212
|
+
if (!Array.isArray(props))
|
|
213
|
+
return values;
|
|
214
|
+
for (const prop of props) {
|
|
215
|
+
const p = prop;
|
|
216
|
+
const name = p?.name || p?.id;
|
|
217
|
+
if (name && p.defaultValue !== undefined && p.defaultValue !== null) {
|
|
218
|
+
values[name] = p.defaultValue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return values;
|
|
222
|
+
}
|
|
223
|
+
/** The saved content values, with the schema's defaults filled in for whatever was never saved. Saved values win. */
|
|
224
|
+
export function mergeMissingContentValues(existing, schema) {
|
|
225
|
+
return { ...extractContentDefaultsFromSchema(schema), ...existing };
|
|
226
|
+
}
|
|
227
|
+
/** The flat content values (not the items) out of what a composition saved as its `content`. */
|
|
228
|
+
export function extractContentValuesFromCompositionRaw(rawContent) {
|
|
229
|
+
const nested = rawContent.content;
|
|
230
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
231
|
+
const { items: _items, contentItemsSnapshot: _snapshot, contentItems: _contentItems, content_items: _contentItemsSnake, ...flatContent } = nested;
|
|
232
|
+
return flatContent;
|
|
233
|
+
}
|
|
234
|
+
return (rawContent.content ?? {});
|
|
235
|
+
}
|
|
236
|
+
/** The content items (the repeated entries: slides, rows, cards…) out of what a composition saved as its `content`. */
|
|
237
|
+
export function extractContentItemsFromCompositionRaw(rawContent) {
|
|
238
|
+
if (Array.isArray(rawContent.contentItems))
|
|
239
|
+
return [...rawContent.contentItems];
|
|
240
|
+
if (Array.isArray(rawContent.content_items))
|
|
241
|
+
return [...rawContent.content_items];
|
|
242
|
+
const nested = rawContent.content;
|
|
243
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
244
|
+
const nestedContent = nested;
|
|
245
|
+
if (Array.isArray(nestedContent.contentItemsSnapshot)) {
|
|
246
|
+
return [...nestedContent.contentItemsSnapshot];
|
|
247
|
+
}
|
|
248
|
+
if (Array.isArray(nestedContent.contentItems)) {
|
|
249
|
+
return [...nestedContent.contentItems];
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(nestedContent.content_items)) {
|
|
252
|
+
return [...nestedContent.content_items];
|
|
253
|
+
}
|
|
254
|
+
const itemsMap = nestedContent.items;
|
|
255
|
+
if (itemsMap && typeof itemsMap === 'object' && !Array.isArray(itemsMap)) {
|
|
256
|
+
return Object.entries(itemsMap).map(([id, item]) => ({
|
|
257
|
+
id,
|
|
258
|
+
...(typeof item === 'object' && item !== null ? item : {})
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
/** A picked file or picture is stored as `{ url, … }`; the preview wants only its address. */
|
|
265
|
+
function unwrapContentMediaValue(raw) {
|
|
266
|
+
if (raw && typeof raw === 'object' && 'url' in raw) {
|
|
267
|
+
return raw.url ?? '';
|
|
268
|
+
}
|
|
269
|
+
return raw;
|
|
270
|
+
}
|
|
271
|
+
/** The first candidate that holds something (not undefined, null or an empty string), as its plain value; else `''`. */
|
|
272
|
+
function pickLiveContentValue(...candidates) {
|
|
273
|
+
for (const candidate of candidates) {
|
|
274
|
+
const unwrapped = unwrapContentMediaValue(candidate);
|
|
275
|
+
if (unwrapped === undefined || unwrapped === null || unwrapped === '')
|
|
276
|
+
continue;
|
|
277
|
+
return unwrapped;
|
|
278
|
+
}
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
/** The content items as the widget's preview page reads them: flat, with each property's value on the item itself. */
|
|
282
|
+
export function flattenContentItemsForPreview(items) {
|
|
283
|
+
return items.map((item) => {
|
|
284
|
+
if (!item || typeof item !== 'object')
|
|
285
|
+
return item;
|
|
286
|
+
const flat = { id: item.id, title: item.title };
|
|
287
|
+
for (const [key, value] of Object.entries(item)) {
|
|
288
|
+
if (key === 'properties' || key === 'inputController' || key === 'id' || key === 'title')
|
|
289
|
+
continue;
|
|
290
|
+
const unwrapped = unwrapContentMediaValue(value);
|
|
291
|
+
if (unwrapped === undefined)
|
|
292
|
+
continue;
|
|
293
|
+
flat[key] = unwrapped;
|
|
294
|
+
}
|
|
295
|
+
const props = item.properties;
|
|
296
|
+
if (Array.isArray(props)) {
|
|
297
|
+
for (const prop of props) {
|
|
298
|
+
const name = prop?.name || prop?.id;
|
|
299
|
+
if (!name)
|
|
300
|
+
continue;
|
|
301
|
+
if (flat[name] !== undefined && flat[name] !== '')
|
|
302
|
+
continue;
|
|
303
|
+
const picked = pickLiveContentValue(prop.value, prop.defaultValue);
|
|
304
|
+
if (picked !== '')
|
|
305
|
+
flat[name] = picked;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return flat;
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** The content items as the `items` map the props editor starts from: flat items by id. */
|
|
312
|
+
export function contentItemsToEditorValueMap(items) {
|
|
313
|
+
const map = {};
|
|
314
|
+
for (const item of flattenContentItemsForPreview(items || [])) {
|
|
315
|
+
if (!item?.id)
|
|
316
|
+
continue;
|
|
317
|
+
map[String(item.id)] = item;
|
|
318
|
+
}
|
|
319
|
+
return map;
|
|
320
|
+
}
|
|
321
|
+
function contentItemHasProperties(item) {
|
|
322
|
+
return Array.isArray(item?.properties) && item.properties.length > 0;
|
|
323
|
+
}
|
|
324
|
+
/** Gives a saved item the schema's property definitions, holding the item's own value for each. */
|
|
325
|
+
function attachSchemaProperties(item, template) {
|
|
326
|
+
if (!item || typeof item !== 'object')
|
|
327
|
+
return item;
|
|
328
|
+
const templateHasProps = contentItemHasProperties(template);
|
|
329
|
+
if (!contentItemHasProperties(item) && !templateHasProps)
|
|
330
|
+
return item;
|
|
331
|
+
const sourceProperties = contentItemHasProperties(item)
|
|
332
|
+
? item.properties
|
|
333
|
+
: structuredClone(template.properties);
|
|
334
|
+
const nextProperties = sourceProperties.map((prop) => {
|
|
335
|
+
const key = prop.id || prop.name;
|
|
336
|
+
const hasOwn = key != null && Object.prototype.hasOwnProperty.call(item, key);
|
|
337
|
+
const incoming = hasOwn ? item[key] : undefined;
|
|
338
|
+
const picked = hasOwn
|
|
339
|
+
? (unwrapContentMediaValue(incoming) ?? '')
|
|
340
|
+
: pickLiveContentValue(prop.value, prop.defaultValue);
|
|
341
|
+
return {
|
|
342
|
+
...prop,
|
|
343
|
+
value: picked
|
|
344
|
+
};
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
...(templateHasProps ? structuredClone(template) : {}),
|
|
348
|
+
...item,
|
|
349
|
+
id: item.id,
|
|
350
|
+
title: item.title || template?.title,
|
|
351
|
+
properties: nextProperties
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The content items the composition holds, with the schema's definitions attached. The composition decides which
|
|
356
|
+
* items there are and in which order: the user can delete the items the widget starts with and drag them into another
|
|
357
|
+
* order, and that must still be so after the items have been through here (they used to come back in the schema's
|
|
358
|
+
* order, with the deleted ones among them). An item the schema lists carries the schema's definition and its saved
|
|
359
|
+
* values; one it does not list is shaped like the schema's template item. With no saved items (a composition nobody
|
|
360
|
+
* has edited yet) the schema's own are used; with no schema items the saved ones are returned as they are.
|
|
361
|
+
*/
|
|
362
|
+
export function mergeContentItemsWithSchema(savedItems, schemaItems) {
|
|
363
|
+
if (!Array.isArray(schemaItems) || schemaItems.length === 0) {
|
|
364
|
+
return Array.isArray(savedItems) ? [...savedItems] : [];
|
|
365
|
+
}
|
|
366
|
+
if (!Array.isArray(savedItems) || savedItems.length === 0) {
|
|
367
|
+
return structuredClone(schemaItems);
|
|
368
|
+
}
|
|
369
|
+
const template = schemaItems.find(contentItemHasProperties) || schemaItems[0];
|
|
370
|
+
const schemaItemsById = new Map(schemaItems.map((schemaItem) => [schemaItem.id, schemaItem]));
|
|
371
|
+
return savedItems.map((savedItem) => {
|
|
372
|
+
const schemaItem = schemaItemsById.get(savedItem?.id);
|
|
373
|
+
if (!schemaItem)
|
|
374
|
+
return attachSchemaProperties(savedItem, template);
|
|
375
|
+
const clonedSchema = structuredClone(schemaItem);
|
|
376
|
+
return attachSchemaProperties({ ...clonedSchema, ...savedItem }, clonedSchema);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** The content schema the props editor is given: the widget's, with the composition's items merged into it. */
|
|
380
|
+
export function buildPropsEditorContentSchema(schema, items) {
|
|
381
|
+
if (!schema)
|
|
382
|
+
return null;
|
|
383
|
+
const cloned = structuredClone(schema);
|
|
384
|
+
const schemaItems = Array.isArray(cloned.contentItems) ? cloned.contentItems : [];
|
|
385
|
+
cloned.contentItems = mergeContentItemsWithSchema(items ?? [], schemaItems);
|
|
386
|
+
return cloned;
|
|
387
|
+
}
|
|
388
|
+
/** A design with a title font but no subtitle font gets the title's as its subtitle font. */
|
|
389
|
+
export function seedMissingFontFaces(design) {
|
|
390
|
+
const next = { ...design };
|
|
391
|
+
if (!next.subtitleFont && next.titleFont) {
|
|
392
|
+
next.subtitleFont = next.titleFont;
|
|
393
|
+
}
|
|
394
|
+
return next;
|
|
395
|
+
}
|
|
396
|
+
/** The embed settings a composition saved, or `{}` when it saved none. */
|
|
397
|
+
export function getCompositionEmbedSettings(composition) {
|
|
398
|
+
const settings = composition.embedSettings ?? composition.embed_settings;
|
|
399
|
+
return settings && typeof settings === 'object' ? settings : {};
|
|
400
|
+
}
|
|
401
|
+
/** A size saved as text or number ("800", 800, "800px") as a positive whole number; anything else is the fallback. */
|
|
402
|
+
export function parseEmbedDimension(value, fallback) {
|
|
403
|
+
const parsed = parseInt(String(value ?? ''), 10);
|
|
404
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
405
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { EDITOR_MESSAGES, type EditorMessageKey } from './messages.js';
|
|
2
|
+
export { ALL_EDITOR_MESSAGES, EDITOR_CATALOGS, checkEditorMessages, provideEditorLanguage, useEditorLanguage, type EditorLanguage, type EditorLanguageHandle } from './language.js';
|
|
3
|
+
export { translateEnglish } from './translate.js';
|
|
4
|
+
export { categoryLabel, fixedPositionLabel, fontWeightLabel } from './labels.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { EDITOR_MESSAGES } from './messages.js';
|
|
2
|
+
export { ALL_EDITOR_MESSAGES, EDITOR_CATALOGS, checkEditorMessages, provideEditorLanguage, useEditorLanguage } from './language.js';
|
|
3
|
+
export { translateEnglish } from './translate.js';
|
|
4
|
+
export { categoryLabel, fixedPositionLabel, fontWeightLabel } from './labels.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Translate } from '@widgetic/design-system/i18n';
|
|
2
|
+
import type { EditorMessageKey } from './messages.js';
|
|
3
|
+
/**
|
|
4
|
+
* Some things the editor keeps by an English name (the tab "Sans Serif" that was last open, the weight "Bold Italic" that
|
|
5
|
+
* is saved with a font) and shows to a person by a name in their language. The name that is kept does not change with the
|
|
6
|
+
* language: these functions only turn it into what is shown, and show a name they do not know as it is.
|
|
7
|
+
*/
|
|
8
|
+
type Translator = Translate<EditorMessageKey>;
|
|
9
|
+
/** The tab of a list of fonts or vectors, by the name the list keeps it under ("Sans Serif"). */
|
|
10
|
+
export declare function categoryLabel(category: string, t: Translator): string;
|
|
11
|
+
/** A weight of a font, by the name that is saved ("Bold", "Medium Italic", "Italic"). A name the editor does not know is shown as it is. */
|
|
12
|
+
export declare function fontWeightLabel(weight: string, t: Translator): string;
|
|
13
|
+
/** The name of one of the nine places on a screen, by the name the widget keeps it under ("top-left"). */
|
|
14
|
+
export declare function fixedPositionLabel(position: string, t: Translator): string;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** `own[name]`, only when `name` is the table's own entry (a weight called "constructor" is not a method of the table). */
|
|
2
|
+
function ownEntry(table, name) {
|
|
3
|
+
return Object.prototype.hasOwnProperty.call(table, name) ? table[name] : undefined;
|
|
4
|
+
}
|
|
5
|
+
const CATEGORY_KEYS = {
|
|
6
|
+
All: 'editor.category.all',
|
|
7
|
+
Mine: 'editor.category.mine',
|
|
8
|
+
'Sans Serif': 'editor.category.sansSerif',
|
|
9
|
+
Serif: 'editor.category.serif',
|
|
10
|
+
Script: 'editor.category.script',
|
|
11
|
+
Other: 'editor.category.other',
|
|
12
|
+
Icons: 'editor.category.icons',
|
|
13
|
+
Emojis: 'editor.category.emojis',
|
|
14
|
+
Clipart: 'editor.category.clipart'
|
|
15
|
+
};
|
|
16
|
+
/** The tab of a list of fonts or vectors, by the name the list keeps it under ("Sans Serif"). */
|
|
17
|
+
export function categoryLabel(category, t) {
|
|
18
|
+
const key = ownEntry(CATEGORY_KEYS, category);
|
|
19
|
+
return key ? t(key) : category;
|
|
20
|
+
}
|
|
21
|
+
const WEIGHT_KEYS = {
|
|
22
|
+
Thin: 'editor.fontWeight.thin',
|
|
23
|
+
ExtraLight: 'editor.fontWeight.extraLight',
|
|
24
|
+
Light: 'editor.fontWeight.light',
|
|
25
|
+
Regular: 'editor.fontWeight.regular',
|
|
26
|
+
Medium: 'editor.fontWeight.medium',
|
|
27
|
+
SemiBold: 'editor.fontWeight.semiBold',
|
|
28
|
+
Bold: 'editor.fontWeight.bold',
|
|
29
|
+
ExtraBold: 'editor.fontWeight.extraBold',
|
|
30
|
+
Black: 'editor.fontWeight.black'
|
|
31
|
+
};
|
|
32
|
+
/** A weight of a font, by the name that is saved ("Bold", "Medium Italic", "Italic"). A name the editor does not know is shown as it is. */
|
|
33
|
+
export function fontWeightLabel(weight, t) {
|
|
34
|
+
if (weight === 'Italic')
|
|
35
|
+
return t('editor.fontWeight.italic');
|
|
36
|
+
const italic = /^(\S+) Italic$/.exec(weight);
|
|
37
|
+
const baseKey = ownEntry(WEIGHT_KEYS, italic ? italic[1] : weight);
|
|
38
|
+
if (!baseKey)
|
|
39
|
+
return weight;
|
|
40
|
+
return italic ? t('editor.fontWeight.withItalic', { weight: t(baseKey) }) : t(baseKey);
|
|
41
|
+
}
|
|
42
|
+
const POSITION_KEYS = {
|
|
43
|
+
'top-left': 'editor.fixedPosition.topLeft',
|
|
44
|
+
'top-center': 'editor.fixedPosition.topCenter',
|
|
45
|
+
'top-right': 'editor.fixedPosition.topRight',
|
|
46
|
+
'center-left': 'editor.fixedPosition.centerLeft',
|
|
47
|
+
'center-center': 'editor.fixedPosition.centerCenter',
|
|
48
|
+
'center-right': 'editor.fixedPosition.centerRight',
|
|
49
|
+
'bottom-left': 'editor.fixedPosition.bottomLeft',
|
|
50
|
+
'bottom-center': 'editor.fixedPosition.bottomCenter',
|
|
51
|
+
'bottom-right': 'editor.fixedPosition.bottomRight'
|
|
52
|
+
};
|
|
53
|
+
/** The name of one of the nine places on a screen, by the name the widget keeps it under ("top-left"). */
|
|
54
|
+
export function fixedPositionLabel(position, t) {
|
|
55
|
+
const key = ownEntry(POSITION_KEYS, position);
|
|
56
|
+
return key ? t(key) : position;
|
|
57
|
+
}
|