@contentful/experiences-core 0.0.1-alpha.2

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/dist/communication/sendMessage.d.ts +6 -0
  3. package/dist/constants.d.ts +122 -0
  4. package/dist/constants.js +159 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/deep-binding/DeepReference.d.ts +28 -0
  7. package/dist/definitions/components.d.ts +8 -0
  8. package/dist/definitions/styles.d.ts +9 -0
  9. package/dist/entity/EditorEntityStore.d.ts +34 -0
  10. package/dist/entity/EditorModeEntityStore.d.ts +29 -0
  11. package/dist/entity/EntityStore.d.ts +54 -0
  12. package/dist/entity/EntityStoreBase.d.ts +39 -0
  13. package/dist/enums.d.ts +6 -0
  14. package/dist/exports.d.ts +3 -0
  15. package/dist/exports.js +2 -0
  16. package/dist/exports.js.map +1 -0
  17. package/dist/fetchers/createExperience.d.ts +20 -0
  18. package/dist/fetchers/fetchById.d.ts +20 -0
  19. package/dist/fetchers/fetchBySlug.d.ts +20 -0
  20. package/dist/index.d.ts +24 -0
  21. package/dist/index.js +2169 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/registries/designTokenRegistry.d.ts +12 -0
  24. package/dist/types.d.ts +223 -0
  25. package/dist/utils/breakpoints.d.ts +12 -0
  26. package/dist/utils/components.d.ts +4 -0
  27. package/dist/utils/domValues.d.ts +15 -0
  28. package/dist/utils/isLink.d.ts +5 -0
  29. package/dist/utils/isLinkToAsset.d.ts +5 -0
  30. package/dist/utils/pathSchema.d.ts +30 -0
  31. package/dist/utils/stylesUtils.d.ts +20 -0
  32. package/dist/utils/supportedModes.d.ts +5 -0
  33. package/dist/utils/transformers.d.ts +24 -0
  34. package/dist/utils/typeguards.d.ts +7 -0
  35. package/dist/utils/utils.d.ts +46 -0
  36. package/dist/utils/validations.d.ts +15 -0
  37. package/package.json +75 -0
package/dist/index.js ADDED
@@ -0,0 +1,2169 @@
1
+ import md5 from 'md5';
2
+ import { BLOCKS } from '@contentful/rich-text-types';
3
+
4
+ const INCOMING_EVENTS = {
5
+ RequestEditorMode: 'requestEditorMode',
6
+ CompositionUpdated: 'componentTreeUpdated',
7
+ ComponentDraggingChanged: 'componentDraggingChanged',
8
+ ComponentDragCanceled: 'componentDragCanceled',
9
+ ComponentDragStarted: 'componentDragStarted',
10
+ ComponentDragEnded: 'componentDragEnded',
11
+ CanvasResized: 'canvasResized',
12
+ SelectComponent: 'selectComponent',
13
+ HoverComponent: 'hoverComponent',
14
+ UpdatedEntity: 'updatedEntity',
15
+ /**
16
+ * @deprecated use `AssembliesAdded` instead. This will be removed in version 5.
17
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
18
+ */
19
+ DesignComponentsAdded: 'designComponentsAdded',
20
+ /**
21
+ * @deprecated use `AssembliesRegistered` instead. This will be removed in version 5.
22
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
23
+ */
24
+ DesignComponentsRegistered: 'designComponentsRegistered',
25
+ AssembliesAdded: 'assembliesAdded',
26
+ AssembliesRegistered: 'assembliesRegistered',
27
+ InitEditor: 'initEditor',
28
+ };
29
+ const CONTENTFUL_COMPONENT_CATEGORY = 'contentful-component';
30
+ const CONTENTFUL_COMPONENTS = {
31
+ section: {
32
+ id: 'contentful-section',
33
+ name: 'Section',
34
+ },
35
+ container: {
36
+ id: 'contentful-container',
37
+ name: 'Container',
38
+ },
39
+ columns: {
40
+ id: 'contentful-columns',
41
+ name: 'Columns',
42
+ },
43
+ singleColumn: {
44
+ id: 'contentful-single-column',
45
+ name: 'Column',
46
+ },
47
+ button: {
48
+ id: 'button',
49
+ name: 'Button',
50
+ },
51
+ heading: {
52
+ id: 'heading',
53
+ name: 'Heading',
54
+ },
55
+ image: {
56
+ id: 'image',
57
+ name: 'Image',
58
+ },
59
+ richText: {
60
+ id: 'richText',
61
+ name: 'Rich Text',
62
+ },
63
+ text: {
64
+ id: 'text',
65
+ name: 'Text',
66
+ },
67
+ };
68
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
69
+ /** @deprecated use `ASSEMBLY_DEFAULT_CATEGORY` instead. This will be removed in version 5. */
70
+ const DESIGN_COMPONENT_DEFAULT_CATEGORY = 'Design Components';
71
+ const EMPTY_CONTAINER_HEIGHT = '80px';
72
+ var PostMessageMethods;
73
+ (function (PostMessageMethods) {
74
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
75
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
76
+ })(PostMessageMethods || (PostMessageMethods = {}));
77
+
78
+ const structureComponents = new Set([
79
+ CONTENTFUL_COMPONENTS.section.id,
80
+ CONTENTFUL_COMPONENTS.columns.id,
81
+ CONTENTFUL_COMPONENTS.container.id,
82
+ CONTENTFUL_COMPONENTS.singleColumn.id,
83
+ ]);
84
+ const isContentfulStructureComponent = (componentId) => structureComponents.has(componentId ?? '');
85
+ const isEmptyStructureWithRelativeHeight = (children, componentId, height) => {
86
+ return (children === 0 &&
87
+ isContentfulStructureComponent(componentId) &&
88
+ !height?.toString().endsWith('px'));
89
+ };
90
+
91
+ const findOutermostCoordinates = (first, second) => {
92
+ return {
93
+ top: Math.min(first.top, second.top),
94
+ right: Math.max(first.right, second.right),
95
+ bottom: Math.max(first.bottom, second.bottom),
96
+ left: Math.min(first.left, second.left),
97
+ };
98
+ };
99
+ const getElementCoordinates = (element) => {
100
+ const rect = element.getBoundingClientRect();
101
+ /**
102
+ * If element does not have children, or element has it's own width or height,
103
+ * return the element's coordinates.
104
+ */
105
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
106
+ return rect;
107
+ }
108
+ const rects = [];
109
+ /**
110
+ * If element has children, or element does not have it's own width and height,
111
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
112
+ * as the coordinate of the element.
113
+ *
114
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
115
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
116
+ */
117
+ for (const child of element.children) {
118
+ const childRect = getElementCoordinates(child);
119
+ if (childRect.width !== 0 || childRect.height !== 0) {
120
+ const { top, right, bottom, left } = childRect;
121
+ rects.push({ top, right, bottom, left });
122
+ }
123
+ }
124
+ if (rects.length === 0) {
125
+ return rect;
126
+ }
127
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
128
+ return DOMRect.fromRect({
129
+ x: left,
130
+ y: top,
131
+ height: bottom - top,
132
+ width: right - left,
133
+ });
134
+ };
135
+
136
+ class ParseError extends Error {
137
+ constructor(message) {
138
+ super(message);
139
+ }
140
+ }
141
+ const isValidJsonObject = (s) => {
142
+ try {
143
+ const result = JSON.parse(s);
144
+ if ('object' !== typeof result) {
145
+ return false;
146
+ }
147
+ return true;
148
+ }
149
+ catch (e) {
150
+ return false;
151
+ }
152
+ };
153
+ const doesMismatchMessageSchema = (event) => {
154
+ try {
155
+ tryParseMessage(event);
156
+ return false;
157
+ }
158
+ catch (e) {
159
+ if (e instanceof ParseError) {
160
+ return e.message;
161
+ }
162
+ throw e;
163
+ }
164
+ };
165
+ const tryParseMessage = (event) => {
166
+ if (!event.data) {
167
+ throw new ParseError('Field event.data is missing');
168
+ }
169
+ if ('string' !== typeof event.data) {
170
+ throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
171
+ }
172
+ if (!isValidJsonObject(event.data)) {
173
+ throw new ParseError('Field event.data must be a valid JSON object serialized as string');
174
+ }
175
+ const eventData = JSON.parse(event.data);
176
+ if (!eventData.source) {
177
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
178
+ }
179
+ if ('composability-app' !== eventData.source) {
180
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
181
+ }
182
+ // check eventData.eventType
183
+ const supportedEventTypes = Object.values(INCOMING_EVENTS);
184
+ if (!supportedEventTypes.includes(eventData.eventType)) {
185
+ // Expected message: This message is handled in the EntityStore to store fetched entities
186
+ if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
187
+ throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
188
+ }
189
+ }
190
+ return eventData;
191
+ };
192
+ const validateExperienceBuilderConfig = ({ locale, isEditorMode, }) => {
193
+ if (isEditorMode) {
194
+ return;
195
+ }
196
+ if (!locale) {
197
+ throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
198
+ }
199
+ };
200
+
201
+ const transformFill = (value) => (value === 'fill' ? '100%' : value);
202
+ const transformGridColumn = (span) => {
203
+ if (!span) {
204
+ return {};
205
+ }
206
+ return {
207
+ gridColumn: `span ${span}`,
208
+ };
209
+ };
210
+ const transformBorderStyle = (value) => {
211
+ if (!value)
212
+ return {};
213
+ const parts = value.split(' ');
214
+ // Just accept the passed value
215
+ if (parts.length < 3)
216
+ return { border: value };
217
+ // Replace the second part always with `solid` and set the box sizing accordingly
218
+ const [borderSize, borderStyle, ...borderColorParts] = parts;
219
+ const borderColor = borderColorParts.join(' ');
220
+ return {
221
+ border: `${borderSize} ${borderStyle} ${borderColor}`,
222
+ };
223
+ };
224
+ const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
225
+ ? {
226
+ alignItems: cfHorizontalAlignment,
227
+ justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
228
+ }
229
+ : {
230
+ alignItems: cfVerticalAlignment,
231
+ justifyContent: cfHorizontalAlignment === 'center'
232
+ ? `safe ${cfHorizontalAlignment}`
233
+ : cfHorizontalAlignment,
234
+ };
235
+ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageScaling, cfBackgroundImageAlignment) => {
236
+ const matchBackgroundSize = (backgroundImageScaling) => {
237
+ if ('fill' === backgroundImageScaling)
238
+ return 'cover';
239
+ if ('fit' === backgroundImageScaling)
240
+ return 'contain';
241
+ return undefined;
242
+ };
243
+ const matchBackgroundPosition = (cfBackgroundImageAlignment) => {
244
+ if (!cfBackgroundImageAlignment) {
245
+ return undefined;
246
+ }
247
+ if ('string' !== typeof cfBackgroundImageAlignment) {
248
+ return undefined;
249
+ }
250
+ let [horizontalAlignment, verticalAlignment] = cfBackgroundImageAlignment
251
+ .trim()
252
+ .split(/\s+/, 2);
253
+ // Special case for handling single values
254
+ // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
255
+ if (horizontalAlignment && !verticalAlignment) {
256
+ const singleValue = horizontalAlignment;
257
+ switch (singleValue) {
258
+ case 'left':
259
+ horizontalAlignment = 'left';
260
+ verticalAlignment = 'center';
261
+ break;
262
+ case 'right':
263
+ horizontalAlignment = 'right';
264
+ verticalAlignment = 'center';
265
+ break;
266
+ case 'center':
267
+ horizontalAlignment = 'center';
268
+ verticalAlignment = 'center';
269
+ break;
270
+ case 'top':
271
+ horizontalAlignment = 'center';
272
+ verticalAlignment = 'top';
273
+ break;
274
+ case 'bottom':
275
+ horizontalAlignment = 'center';
276
+ verticalAlignment = 'bottom';
277
+ break;
278
+ // just fall down to the normal validation logic for horiz and vert
279
+ }
280
+ }
281
+ const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
282
+ const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
283
+ horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
284
+ verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
285
+ return `${horizontalAlignment} ${verticalAlignment}`;
286
+ };
287
+ if (!cfBackgroundImageUrl) {
288
+ return undefined;
289
+ }
290
+ return {
291
+ backgroundImage: `url(${cfBackgroundImageUrl})`,
292
+ backgroundRepeat: cfBackgroundImageScaling === 'tile' ? 'repeat' : 'no-repeat',
293
+ backgroundPosition: matchBackgroundPosition(cfBackgroundImageAlignment),
294
+ backgroundSize: matchBackgroundSize(cfBackgroundImageScaling),
295
+ };
296
+ };
297
+ const transformContentValue = (value, variableDefinition) => {
298
+ if (variableDefinition.type === 'RichText') {
299
+ return transformRichText(value);
300
+ }
301
+ return value;
302
+ };
303
+ const transformRichText = (value) => {
304
+ if (typeof value === 'string') {
305
+ return {
306
+ data: {},
307
+ content: [
308
+ {
309
+ nodeType: BLOCKS.PARAGRAPH,
310
+ data: {},
311
+ content: [
312
+ {
313
+ data: {},
314
+ nodeType: 'text',
315
+ value: value,
316
+ marks: [],
317
+ },
318
+ ],
319
+ },
320
+ ],
321
+ nodeType: BLOCKS.DOCUMENT,
322
+ };
323
+ }
324
+ if (typeof value === 'object' && value.nodeType === BLOCKS.DOCUMENT) {
325
+ return value;
326
+ }
327
+ return undefined;
328
+ };
329
+ const transformWidthSizing = ({ value, cfMargin, }) => {
330
+ if (!value || !cfMargin)
331
+ return undefined;
332
+ const transformedValue = transformFill(value);
333
+ const marginValues = cfMargin.split(' ');
334
+ const rightMargin = marginValues[1] || '0px';
335
+ const leftMargin = marginValues[3] || '0px';
336
+ const calcValue = `calc(${transformedValue} - ${leftMargin} - ${rightMargin})`;
337
+ /**
338
+ * We want to check if the calculated value is valid CSS. If this fails,
339
+ * this means the `transformedValue` is not a calculable value (not a px, rem, or %).
340
+ * The value may instead be a string such as `min-content` or `max-content`. In
341
+ * that case we don't want to use calc and instead return the raw value.
342
+ */
343
+ if (typeof window !== 'undefined' && CSS.supports('width', calcValue)) {
344
+ return calcValue;
345
+ }
346
+ return transformedValue;
347
+ };
348
+
349
+ const toCSSAttribute = (key) => key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
350
+ const buildStyleTag = ({ styles, nodeId }) => {
351
+ const stylesStr = Object.entries(styles)
352
+ .filter(([, value]) => value !== undefined)
353
+ .reduce((acc, [key, value]) => `${acc}
354
+ ${toCSSAttribute(key)}: ${value};`, '');
355
+ const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
356
+ const styleRule = `.${className}{ ${stylesStr} }`;
357
+ return [className, styleRule];
358
+ };
359
+ const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfGap, cfBackgroundImageUrl, cfBackgroundImageAlignment, cfBackgroundImageScaling, cfFontSize, cfFontWeight, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, }) => {
360
+ return {
361
+ margin: cfMargin,
362
+ padding: cfPadding,
363
+ backgroundColor: cfBackgroundColor,
364
+ width: transformWidthSizing({ value: cfWidth, cfMargin }),
365
+ height: transformFill(cfHeight),
366
+ maxWidth: cfMaxWidth,
367
+ ...transformGridColumn(cfColumnSpan),
368
+ ...transformBorderStyle(cfBorder),
369
+ gap: cfGap,
370
+ ...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
371
+ flexDirection: cfFlexDirection,
372
+ flexWrap: cfFlexWrap,
373
+ ...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageScaling, cfBackgroundImageAlignment),
374
+ fontSize: cfFontSize,
375
+ fontWeight: cfTextBold ? 'bold' : cfFontWeight,
376
+ fontStyle: cfTextItalic ? 'italic' : 'normal',
377
+ lineHeight: cfLineHeight,
378
+ letterSpacing: cfLetterSpacing,
379
+ color: cfTextColor,
380
+ textAlign: cfTextAlign,
381
+ textTransform: cfTextTransform,
382
+ textDecoration: cfTextUnderline ? 'underline' : 'none',
383
+ boxSizing: 'border-box',
384
+ };
385
+ };
386
+ /**
387
+ * Container/section default behaviour:
388
+ * Default height => height: EMPTY_CONTAINER_HEIGHT (120px)
389
+ * If a container component has children => height: 'fit-content'
390
+ */
391
+ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
392
+ if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
393
+ return value;
394
+ }
395
+ if (children.length) {
396
+ return '100%';
397
+ }
398
+ return EMPTY_CONTAINER_HEIGHT;
399
+ };
400
+
401
+ const getDataFromTree = (tree) => {
402
+ let dataSource = {};
403
+ let unboundValues = {};
404
+ const queue = [...tree.root.children];
405
+ while (queue.length) {
406
+ const node = queue.shift();
407
+ if (!node) {
408
+ continue;
409
+ }
410
+ dataSource = { ...dataSource, ...node.data.dataSource };
411
+ unboundValues = { ...unboundValues, ...node.data.unboundValues };
412
+ if (node.children.length) {
413
+ queue.push(...node.children);
414
+ }
415
+ }
416
+ return {
417
+ dataSource,
418
+ unboundValues,
419
+ };
420
+ };
421
+ /**
422
+ * Gets calculates the index to drop the dragged component based on the mouse position
423
+ * @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
424
+ */
425
+ const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
426
+ const APPEND_INSIDE = dropReceiverNode.children.length;
427
+ const PREPEND_INSIDE = 0;
428
+ if (isMouseAtTopBorder || isMouseAtBottomBorder) {
429
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
430
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
431
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
432
+ return {
433
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
434
+ node: dropReceiverParentNode,
435
+ index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
436
+ };
437
+ }
438
+ // if over one of the section indicators
439
+ if (isOverTopIndicator || isOverBottomIndicator) {
440
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
441
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
442
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
443
+ return {
444
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
445
+ node: dropReceiverParentNode,
446
+ index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
447
+ };
448
+ }
449
+ if (flexDirection === undefined || flexDirection === 'row') {
450
+ return {
451
+ node: dropReceiverNode,
452
+ index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
453
+ };
454
+ }
455
+ else {
456
+ return {
457
+ node: dropReceiverNode,
458
+ index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
459
+ };
460
+ }
461
+ };
462
+ const generateRandomId = (letterCount) => {
463
+ const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
464
+ const NUMS = '0123456789';
465
+ const ALNUM = NUMS + LETTERS;
466
+ const times = (n, callback) => Array.from({ length: n }, callback);
467
+ const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
468
+ return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
469
+ };
470
+ const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
471
+ if (!usedComponents?.length)
472
+ return false;
473
+ return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
474
+ };
475
+ /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
476
+ const checkIsAssembly = checkIsAssemblyNode;
477
+ /**
478
+ * This check assumes that the entry is already ensured to be an experience, i.e. the
479
+ * content type of the entry is an experience type with the necessary annotations.
480
+ **/
481
+ const checkIsAssemblyEntry = (entry) => {
482
+ return Boolean(entry.fields?.componentSettings);
483
+ };
484
+ const checkIsAssemblyDefinition = (component) => component?.category === DESIGN_COMPONENT_DEFAULT_CATEGORY ||
485
+ component?.category === ASSEMBLY_DEFAULT_CATEGORY;
486
+
487
+ const isExperienceEntry = (entry) => {
488
+ return (entry?.sys?.type === 'Entry' &&
489
+ !!entry.fields?.title &&
490
+ !!entry.fields?.slug &&
491
+ !!entry.fields?.componentTree &&
492
+ Array.isArray(entry.fields.componentTree.breakpoints) &&
493
+ Array.isArray(entry.fields.componentTree.children) &&
494
+ typeof entry.fields.componentTree.schemaVersion === 'string');
495
+ };
496
+ const isDeprecatedExperience = (experience) => {
497
+ return (experience &&
498
+ {}.hasOwnProperty.call(experience, 'client') &&
499
+ {}.hasOwnProperty.call(experience, 'experienceTypeId') &&
500
+ {}.hasOwnProperty.call(experience, 'mode'));
501
+ };
502
+
503
+ const supportedModes = ['delivery', 'preview', 'editor'];
504
+
505
+ const builtInStyles = {
506
+ cfVerticalAlignment: {
507
+ validations: {
508
+ in: [
509
+ {
510
+ value: 'start',
511
+ displayName: 'Align left',
512
+ },
513
+ {
514
+ value: 'center',
515
+ displayName: 'Align center',
516
+ },
517
+ {
518
+ value: 'end',
519
+ displayName: 'Align right',
520
+ },
521
+ ],
522
+ },
523
+ type: 'Text',
524
+ group: 'style',
525
+ description: 'The horizontal alignment of the section',
526
+ defaultValue: 'center',
527
+ displayName: 'Vertical alignment',
528
+ },
529
+ cfHorizontalAlignment: {
530
+ validations: {
531
+ in: [
532
+ {
533
+ value: 'start',
534
+ displayName: 'Align top',
535
+ },
536
+ {
537
+ value: 'center',
538
+ displayName: 'Align center',
539
+ },
540
+ {
541
+ value: 'end',
542
+ displayName: 'Align bottom',
543
+ },
544
+ ],
545
+ },
546
+ type: 'Text',
547
+ group: 'style',
548
+ description: 'The horizontal alignment of the section',
549
+ defaultValue: 'center',
550
+ displayName: 'Horizontal alignment',
551
+ },
552
+ cfMargin: {
553
+ displayName: 'Margin',
554
+ type: 'Text',
555
+ group: 'style',
556
+ description: 'The margin of the section',
557
+ defaultValue: '0 0 0 0',
558
+ },
559
+ cfPadding: {
560
+ displayName: 'Padding',
561
+ type: 'Text',
562
+ group: 'style',
563
+ description: 'The padding of the section',
564
+ defaultValue: '0 0 0 0',
565
+ },
566
+ cfBackgroundColor: {
567
+ displayName: 'Background color',
568
+ type: 'Text',
569
+ group: 'style',
570
+ description: 'The background color of the section',
571
+ defaultValue: 'rgba(255, 255, 255, 0)',
572
+ },
573
+ cfWidth: {
574
+ displayName: 'Width',
575
+ type: 'Text',
576
+ group: 'style',
577
+ description: 'The width of the section',
578
+ defaultValue: 'fill',
579
+ },
580
+ cfHeight: {
581
+ displayName: 'Height',
582
+ type: 'Text',
583
+ group: 'style',
584
+ description: 'The height of the section',
585
+ defaultValue: 'fit-content',
586
+ },
587
+ cfMaxWidth: {
588
+ displayName: 'Max width',
589
+ type: 'Text',
590
+ group: 'style',
591
+ description: 'The max-width of the section',
592
+ defaultValue: 'none',
593
+ },
594
+ cfFlexDirection: {
595
+ displayName: 'Direction',
596
+ type: 'Text',
597
+ group: 'style',
598
+ description: 'The orientation of the section',
599
+ defaultValue: 'column',
600
+ },
601
+ cfFlexWrap: {
602
+ displayName: 'Wrap objects',
603
+ type: 'Text',
604
+ group: 'style',
605
+ description: 'Wrap objects',
606
+ defaultValue: 'nowrap',
607
+ },
608
+ cfBorder: {
609
+ displayName: 'Border',
610
+ type: 'Text',
611
+ group: 'style',
612
+ description: 'The border of the section',
613
+ defaultValue: '1px solid rgba(0, 0, 0, 0)',
614
+ },
615
+ cfGap: {
616
+ displayName: 'Gap',
617
+ type: 'Text',
618
+ group: 'style',
619
+ description: 'The spacing between the elements of the section',
620
+ defaultValue: '0px',
621
+ },
622
+ cfBackgroundImageUrl: {
623
+ displayName: 'Background image',
624
+ type: 'Text',
625
+ defaultValue: '',
626
+ description: 'Background image for section or container',
627
+ },
628
+ cfBackgroundImageScaling: {
629
+ displayName: 'Image scaling',
630
+ type: 'Text',
631
+ group: 'style',
632
+ description: 'Adjust background image to fit, fill or tile the container',
633
+ defaultValue: 'fit',
634
+ validations: {
635
+ in: [
636
+ {
637
+ value: 'fill',
638
+ displayName: 'Fill',
639
+ },
640
+ {
641
+ value: 'fit',
642
+ displayName: 'Fit',
643
+ },
644
+ {
645
+ value: 'tile',
646
+ displayName: 'Tile',
647
+ },
648
+ ],
649
+ },
650
+ },
651
+ cfBackgroundImageAlignment: {
652
+ displayName: 'Image alignment',
653
+ type: 'Text',
654
+ group: 'style',
655
+ description: 'Align background image to the edges of the container',
656
+ defaultValue: 'left top',
657
+ },
658
+ cfHyperlink: {
659
+ displayName: 'Hyperlink',
660
+ type: 'Text',
661
+ defaultValue: '',
662
+ validations: {
663
+ format: 'URL',
664
+ },
665
+ description: 'hyperlink for section or container',
666
+ },
667
+ cfOpenInNewTab: {
668
+ displayName: 'Hyperlink behaviour',
669
+ type: 'Boolean',
670
+ defaultValue: false,
671
+ description: 'To open hyperlink in new Tab or not',
672
+ },
673
+ };
674
+ const optionalBuiltInStyles = {
675
+ cfFontSize: {
676
+ displayName: 'Font Size',
677
+ type: 'Text',
678
+ group: 'style',
679
+ description: 'The font size of the element',
680
+ defaultValue: '16px',
681
+ },
682
+ cfFontWeight: {
683
+ validations: {
684
+ in: [
685
+ {
686
+ value: '400',
687
+ displayName: 'Normal',
688
+ },
689
+ {
690
+ value: '500',
691
+ displayName: 'Medium',
692
+ },
693
+ {
694
+ value: '600',
695
+ displayName: 'Semi Bold',
696
+ },
697
+ ],
698
+ },
699
+ displayName: 'Font Weight',
700
+ type: 'Text',
701
+ group: 'style',
702
+ description: 'The font weight of the element',
703
+ defaultValue: '400',
704
+ },
705
+ cfLineHeight: {
706
+ displayName: 'Line Height',
707
+ type: 'Text',
708
+ group: 'style',
709
+ description: 'The line height of the element',
710
+ defaultValue: '20px',
711
+ },
712
+ cfLetterSpacing: {
713
+ displayName: 'Letter Spacing',
714
+ type: 'Text',
715
+ group: 'style',
716
+ description: 'The letter spacing of the element',
717
+ defaultValue: '0px',
718
+ },
719
+ cfTextColor: {
720
+ displayName: 'Text Color',
721
+ type: 'Text',
722
+ group: 'style',
723
+ description: 'The text color of the element',
724
+ defaultValue: 'rgba(0, 0, 0, 1)',
725
+ },
726
+ cfTextAlign: {
727
+ validations: {
728
+ in: [
729
+ {
730
+ value: 'left',
731
+ displayName: 'Align left',
732
+ },
733
+ {
734
+ value: 'center',
735
+ displayName: 'Align center',
736
+ },
737
+ {
738
+ value: 'right',
739
+ displayName: 'Align right',
740
+ },
741
+ ],
742
+ },
743
+ displayName: 'Text Align',
744
+ type: 'Text',
745
+ group: 'style',
746
+ description: 'The text alignment of the element',
747
+ defaultValue: 'left',
748
+ },
749
+ cfTextTransform: {
750
+ validations: {
751
+ in: [
752
+ {
753
+ value: 'none',
754
+ displayName: 'Normal',
755
+ },
756
+ {
757
+ value: 'capitalize',
758
+ displayName: 'Capitalize',
759
+ },
760
+ {
761
+ value: 'uppercase',
762
+ displayName: 'Uppercase',
763
+ },
764
+ {
765
+ value: 'lowercase',
766
+ displayName: 'Lowercase',
767
+ },
768
+ ],
769
+ },
770
+ displayName: 'Text Transform',
771
+ type: 'Text',
772
+ group: 'style',
773
+ description: 'The text transform of the element',
774
+ defaultValue: 'none',
775
+ },
776
+ cfTextBold: {
777
+ displayName: 'Bold',
778
+ type: 'Boolean',
779
+ group: 'style',
780
+ description: 'The text bold of the element',
781
+ defaultValue: false,
782
+ },
783
+ cfTextItalic: {
784
+ displayName: 'Italic',
785
+ type: 'Boolean',
786
+ group: 'style',
787
+ description: 'The text italic of the element',
788
+ defaultValue: false,
789
+ },
790
+ cfTextUnderline: {
791
+ displayName: 'Underline',
792
+ type: 'Boolean',
793
+ group: 'style',
794
+ description: 'The text underline of the element',
795
+ defaultValue: false,
796
+ },
797
+ };
798
+ const containerBuiltInStyles = {
799
+ ...builtInStyles,
800
+ cfMaxWidth: {
801
+ displayName: 'Max Width',
802
+ type: 'Text',
803
+ group: 'style',
804
+ description: 'The max-width of the section',
805
+ defaultValue: '1192px',
806
+ },
807
+ cfMargin: {
808
+ displayName: 'Margin',
809
+ type: 'Text',
810
+ group: 'style',
811
+ description: 'The margin of the container',
812
+ defaultValue: '0 Auto 0 Auto',
813
+ },
814
+ };
815
+ const singleColumnBuiltInStyles = {
816
+ cfVerticalAlignment: {
817
+ validations: {
818
+ in: [
819
+ {
820
+ value: 'start',
821
+ displayName: 'Align left',
822
+ },
823
+ {
824
+ value: 'center',
825
+ displayName: 'Align center',
826
+ },
827
+ {
828
+ value: 'end',
829
+ displayName: 'Align right',
830
+ },
831
+ ],
832
+ },
833
+ type: 'Text',
834
+ group: 'style',
835
+ description: 'The horizontal alignment of the column',
836
+ defaultValue: 'center',
837
+ displayName: 'Vertical alignment',
838
+ },
839
+ cfHorizontalAlignment: {
840
+ validations: {
841
+ in: [
842
+ {
843
+ value: 'start',
844
+ displayName: 'Align top',
845
+ },
846
+ {
847
+ value: 'center',
848
+ displayName: 'Align center',
849
+ },
850
+ {
851
+ value: 'end',
852
+ displayName: 'Align bottom',
853
+ },
854
+ ],
855
+ },
856
+ type: 'Text',
857
+ group: 'style',
858
+ description: 'The horizontal alignment of the column',
859
+ defaultValue: 'center',
860
+ displayName: 'Horizontal alignment',
861
+ },
862
+ cfPadding: {
863
+ displayName: 'Padding',
864
+ type: 'Text',
865
+ group: 'style',
866
+ description: 'The padding of the column',
867
+ defaultValue: '0 0 0 0',
868
+ },
869
+ cfBackgroundColor: {
870
+ displayName: 'Background color',
871
+ type: 'Text',
872
+ group: 'style',
873
+ description: 'The background color of the column',
874
+ defaultValue: 'rgba(255, 255, 255, 0)',
875
+ },
876
+ cfFlexDirection: {
877
+ displayName: 'Direction',
878
+ type: 'Text',
879
+ group: 'style',
880
+ description: 'The orientation of the column',
881
+ defaultValue: 'column',
882
+ },
883
+ cfFlexWrap: {
884
+ displayName: 'Wrap objects',
885
+ type: 'Text',
886
+ group: 'style',
887
+ description: 'Wrap objects',
888
+ defaultValue: 'nowrap',
889
+ },
890
+ cfBorder: {
891
+ displayName: 'Border',
892
+ type: 'Text',
893
+ group: 'style',
894
+ description: 'The border of the column',
895
+ defaultValue: '1px solid rgba(0, 0, 0, 0)',
896
+ },
897
+ cfGap: {
898
+ displayName: 'Gap',
899
+ type: 'Text',
900
+ group: 'style',
901
+ description: 'The spacing between the elements of the column',
902
+ defaultValue: '0px',
903
+ },
904
+ cfBackgroundImageUrl: {
905
+ displayName: 'Background image',
906
+ type: 'Text',
907
+ defaultValue: '',
908
+ description: 'Background image for section or container',
909
+ },
910
+ cfBackgroundImageScaling: {
911
+ displayName: 'Image scaling',
912
+ type: 'Text',
913
+ group: 'style',
914
+ description: 'Adjust background image to fit, fill or tile the column',
915
+ defaultValue: 'fit',
916
+ validations: {
917
+ in: [
918
+ {
919
+ value: 'fill',
920
+ displayName: 'Fill',
921
+ },
922
+ {
923
+ value: 'fit',
924
+ displayName: 'Fit',
925
+ },
926
+ {
927
+ value: 'tile',
928
+ displayName: 'Tile',
929
+ },
930
+ ],
931
+ },
932
+ },
933
+ cfBackgroundImageAlignment: {
934
+ displayName: 'Image alignment',
935
+ type: 'Text',
936
+ group: 'style',
937
+ description: 'Align background image to the edges of the column',
938
+ defaultValue: 'left top',
939
+ },
940
+ cfColumnSpan: {
941
+ type: 'Text',
942
+ defaultValue: '6',
943
+ group: 'style',
944
+ },
945
+ cfColumnSpanLock: {
946
+ type: 'Boolean',
947
+ defaultValue: false,
948
+ group: 'style',
949
+ },
950
+ };
951
+ const columnsBuiltInStyles = {
952
+ cfMargin: {
953
+ displayName: 'Margin',
954
+ type: 'Text',
955
+ group: 'style',
956
+ description: 'The margin of the columns',
957
+ defaultValue: '0 Auto 0 Auto',
958
+ },
959
+ cfWidth: {
960
+ displayName: 'Width',
961
+ type: 'Text',
962
+ group: 'style',
963
+ description: 'The width of the columns',
964
+ defaultValue: 'fill',
965
+ },
966
+ cfMaxWidth: {
967
+ displayName: 'Max width',
968
+ type: 'Text',
969
+ group: 'style',
970
+ description: 'The max-width of the columns',
971
+ defaultValue: '1192px',
972
+ },
973
+ cfPadding: {
974
+ displayName: 'Padding',
975
+ type: 'Text',
976
+ group: 'style',
977
+ description: 'The padding of the columns',
978
+ defaultValue: '10px 10px 10px 10px',
979
+ },
980
+ cfBackgroundColor: {
981
+ displayName: 'Background color',
982
+ type: 'Text',
983
+ group: 'style',
984
+ description: 'The background color of the columns',
985
+ defaultValue: 'rgba(255, 255, 255, 0)',
986
+ },
987
+ cfBorder: {
988
+ displayName: 'Border',
989
+ type: 'Text',
990
+ group: 'style',
991
+ description: 'The border of the columns',
992
+ defaultValue: '1px solid rgba(0, 0, 0, 0)',
993
+ },
994
+ cfBackgroundImageUrl: {
995
+ displayName: 'Background image',
996
+ type: 'Text',
997
+ defaultValue: '',
998
+ description: 'Background image for section or container',
999
+ },
1000
+ cfGap: {
1001
+ displayName: 'Gap',
1002
+ type: 'Text',
1003
+ group: 'style',
1004
+ description: 'The spacing between the elements of the columns',
1005
+ defaultValue: '10px 10px',
1006
+ },
1007
+ cfBackgroundImageScaling: {
1008
+ displayName: 'Image scaling',
1009
+ type: 'Text',
1010
+ group: 'style',
1011
+ description: 'Adjust background image to fit, fill or tile the columns',
1012
+ defaultValue: 'fit',
1013
+ validations: {
1014
+ in: [
1015
+ {
1016
+ value: 'fill',
1017
+ displayName: 'Fill',
1018
+ },
1019
+ {
1020
+ value: 'fit',
1021
+ displayName: 'Fit',
1022
+ },
1023
+ {
1024
+ value: 'tile',
1025
+ displayName: 'Tile',
1026
+ },
1027
+ ],
1028
+ },
1029
+ },
1030
+ cfBackgroundImageAlignment: {
1031
+ displayName: 'Image alignment',
1032
+ type: 'Text',
1033
+ group: 'style',
1034
+ description: 'Align background image to the edges of the columns',
1035
+ defaultValue: 'left top',
1036
+ },
1037
+ cfColumns: {
1038
+ type: 'Text',
1039
+ defaultValue: '[6,6]',
1040
+ group: 'style',
1041
+ },
1042
+ cfWrapColumns: {
1043
+ type: 'Boolean',
1044
+ defaultValue: false,
1045
+ group: 'style',
1046
+ },
1047
+ cfWrapColumnsCount: {
1048
+ type: 'Text',
1049
+ defaultValue: '2',
1050
+ group: 'style',
1051
+ },
1052
+ };
1053
+
1054
+ const designTokensRegistry = {};
1055
+ /**
1056
+ * Register design tokens styling
1057
+ * @param designTokenDefinition - {[key:string]: Record<string, string>}
1058
+ * @returns void
1059
+ */
1060
+ const defineDesignTokens = (designTokenDefinition) => {
1061
+ Object.assign(designTokensRegistry, designTokenDefinition);
1062
+ };
1063
+ const templateStringRegex = /\${(.+?)}/g;
1064
+ const getDesignTokenRegistration = (breakpointValue, variableName) => {
1065
+ if (!breakpointValue)
1066
+ return breakpointValue;
1067
+ let resolvedValue = '';
1068
+ for (const part of breakpointValue.split(' ')) {
1069
+ const tokenValue = templateStringRegex.test(part)
1070
+ ? resolveSimpleDesignToken(part, variableName)
1071
+ : part;
1072
+ resolvedValue += `${tokenValue} `;
1073
+ }
1074
+ // Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
1075
+ return resolvedValue.trim();
1076
+ };
1077
+ const resolveSimpleDesignToken = (templateString, variableName) => {
1078
+ const nonTemplateValue = templateString.replace(templateStringRegex, '$1');
1079
+ const [tokenCategory, tokenName] = nonTemplateValue.split('.');
1080
+ const tokenValues = designTokensRegistry[tokenCategory];
1081
+ if (tokenValues && tokenValues[tokenName]) {
1082
+ if (variableName === 'cfBorder') {
1083
+ const { width, style, color } = tokenValues[tokenName];
1084
+ return `${width} ${style} ${color}`;
1085
+ }
1086
+ return tokenValues[tokenName];
1087
+ }
1088
+ if (builtInStyles[variableName]) {
1089
+ return builtInStyles[variableName].defaultValue;
1090
+ }
1091
+ if (optionalBuiltInStyles[variableName]) {
1092
+ return optionalBuiltInStyles[variableName].defaultValue;
1093
+ }
1094
+ return '0px';
1095
+ };
1096
+
1097
+ const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
1098
+ const toCSSMediaQuery = ({ query }) => {
1099
+ if (query === '*')
1100
+ return undefined;
1101
+ const match = query.match(MEDIA_QUERY_REGEXP);
1102
+ if (!match)
1103
+ return undefined;
1104
+ const [, operator, value, unit] = match;
1105
+ if (operator === '<') {
1106
+ const maxScreenWidth = Number(value) - 1;
1107
+ return `(max-width: ${maxScreenWidth}${unit})`;
1108
+ }
1109
+ else if (operator === '>') {
1110
+ const minScreenWidth = Number(value) + 1;
1111
+ return `(min-width: ${minScreenWidth}${unit})`;
1112
+ }
1113
+ return undefined;
1114
+ };
1115
+ // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
1116
+ const findLast = (array, predicate) => {
1117
+ return array.reverse().find(predicate);
1118
+ };
1119
+ // Initialise media query matchers. This won't include the always matching fallback breakpoint.
1120
+ const mediaQueryMatcher = (breakpoints) => {
1121
+ const mediaQueryMatches = {};
1122
+ const mediaQueryMatchers = breakpoints
1123
+ .map((breakpoint) => {
1124
+ const cssMediaQuery = toCSSMediaQuery(breakpoint);
1125
+ if (!cssMediaQuery)
1126
+ return undefined;
1127
+ if (typeof window === 'undefined')
1128
+ return undefined;
1129
+ const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
1130
+ mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
1131
+ return { id: breakpoint.id, signal: mediaQueryMatcher };
1132
+ })
1133
+ .filter((matcher) => !!matcher);
1134
+ return [mediaQueryMatchers, mediaQueryMatches];
1135
+ };
1136
+ const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
1137
+ // The breakpoints are ordered (desktop-first: descending by screen width)
1138
+ const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1139
+ id,
1140
+ index,
1141
+ // The fallback breakpoint with wildcard query will always match
1142
+ isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1143
+ }));
1144
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1145
+ const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1146
+ return mostSpecificIndex ?? fallbackBreakpointIndex;
1147
+ };
1148
+ const getFallbackBreakpointIndex = (breakpoints) => {
1149
+ // We assume that there will be a single breakpoint which uses the wildcard query.
1150
+ // If there is none, we just take the first one in the list.
1151
+ return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1152
+ };
1153
+ const builtInStylesWithDesignTokens = [
1154
+ 'cfMargin',
1155
+ 'cfPadding',
1156
+ 'cfGap',
1157
+ 'cfWidth',
1158
+ 'cfHeight',
1159
+ 'cfBackgroundColor',
1160
+ 'cfBorder',
1161
+ 'cfFontSize',
1162
+ 'cfLineHeight',
1163
+ 'cfLetterSpacing',
1164
+ 'cfTextColor',
1165
+ ];
1166
+ const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName) => {
1167
+ const eventuallyResolveDesignTokens = (value) => {
1168
+ // For some built-in design propertier, we support design tokens
1169
+ if (builtInStylesWithDesignTokens.includes(variableName)) {
1170
+ return getDesignTokenRegistration(value, variableName);
1171
+ }
1172
+ // For all other properties, we just return the breakpoint-specific value
1173
+ return value;
1174
+ };
1175
+ if (valuesByBreakpoint instanceof Object) {
1176
+ // Assume that the values are sorted by media query to apply the cascading CSS logic
1177
+ for (let index = activeBreakpointIndex; index >= 0; index--) {
1178
+ const breakpointId = breakpoints[index].id;
1179
+ if (valuesByBreakpoint[breakpointId]) {
1180
+ // If the value is defined, we use it and stop the breakpoints cascade
1181
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
1182
+ }
1183
+ }
1184
+ // If no breakpoint matched, we search and apply the fallback breakpoint
1185
+ const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
1186
+ const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex].id;
1187
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
1188
+ }
1189
+ else {
1190
+ // Old design properties did not support breakpoints, keep for backward compatibility
1191
+ return valuesByBreakpoint;
1192
+ }
1193
+ };
1194
+
1195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1196
+ const isLinkToAsset = (variable) => {
1197
+ if (!variable)
1198
+ return false;
1199
+ if (typeof variable !== 'object')
1200
+ return false;
1201
+ return (variable.sys?.linkType === 'Asset' &&
1202
+ typeof variable.sys?.id === 'string' &&
1203
+ !!variable.sys?.id &&
1204
+ variable.sys?.type === 'Link');
1205
+ };
1206
+
1207
+ const isLink = (maybeLink) => {
1208
+ if (maybeLink === null)
1209
+ return false;
1210
+ if (typeof maybeLink !== 'object')
1211
+ return false;
1212
+ const link = maybeLink;
1213
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1214
+ };
1215
+
1216
+ /**
1217
+ * This module encapsulates format of the path to a deep reference.
1218
+ */
1219
+ const parseDataSourcePathIntoFieldset = (path) => {
1220
+ const parsedPath = parseDeepPath(path);
1221
+ if (null === parsedPath) {
1222
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1223
+ }
1224
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
1225
+ };
1226
+ /**
1227
+ * Parse path into components, supports L1 references (one reference follow) atm.
1228
+ * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
1229
+ * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
1230
+ * @returns
1231
+ */
1232
+ const parseDataSourcePathWithL1DeepBindings = (path) => {
1233
+ const parsedPath = parseDeepPath(path);
1234
+ if (null === parsedPath) {
1235
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1236
+ }
1237
+ return {
1238
+ key: parsedPath.key,
1239
+ field: parsedPath.fields[0],
1240
+ referentField: parsedPath.fields[1],
1241
+ };
1242
+ };
1243
+ /**
1244
+ * Detects if paths is valid deep-path, like:
1245
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1246
+ * or regular, like:
1247
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1248
+ * @returns
1249
+ */
1250
+ const isDeepPath = (deepPathCandidate) => {
1251
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
1252
+ if (!deepPathParsed) {
1253
+ return false;
1254
+ }
1255
+ return deepPathParsed.fields.length > 1;
1256
+ };
1257
+ const parseDeepPath = (deepPathCandidate) => {
1258
+ // ALGORITHM:
1259
+ // We start with deep path in form:
1260
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1261
+ // First turn string into array of segments
1262
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1263
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1264
+ // [
1265
+ // [ "", "uuid123" ],
1266
+ // [ "fields", "mainStory", "~locale" ],
1267
+ // [ "fields", "cover", "~locale" ],
1268
+ // [ "fields", "title", "~locale" ]
1269
+ // ]
1270
+ // Then check "initial" chunk for corretness
1271
+ // Then check all "field-leading" chunks for correctness
1272
+ const isValidInitialChunk = (initialChunk) => {
1273
+ // must have start with '' and have at least 2 segments, second non-empty
1274
+ // eg. /-_432uuid123123
1275
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1276
+ };
1277
+ const isValidFieldChunk = (fieldChunk) => {
1278
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1279
+ // eg. fields/-32234mainStory/~locale
1280
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1281
+ };
1282
+ const deepPathSegments = deepPathCandidate.split('/');
1283
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1284
+ if (chunks.length <= 1) {
1285
+ return null; // malformed path, even regular paths have at least 2 chunks
1286
+ }
1287
+ else if (chunks.length === 2) {
1288
+ return null; // deep paths have at least 3 chunks
1289
+ }
1290
+ // With 3+ chunks we can now check for deep path correctness
1291
+ const [initialChunk, ...fieldChunks] = chunks;
1292
+ if (!isValidInitialChunk(initialChunk)) {
1293
+ return null;
1294
+ }
1295
+ if (!fieldChunks.every(isValidFieldChunk)) {
1296
+ return null;
1297
+ }
1298
+ return {
1299
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1300
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1301
+ };
1302
+ };
1303
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1304
+ const chunks = [];
1305
+ let currentChunk = [];
1306
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1307
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
1308
+ for (let i = 0; i < segments.length; i++) {
1309
+ const isInitialElement = i === 0;
1310
+ const segment = segments[i];
1311
+ if (isInitialElement) {
1312
+ currentChunk = [segment];
1313
+ }
1314
+ else if (isSegmentBeginningOfChunk(segment)) {
1315
+ chunks.push(currentChunk);
1316
+ currentChunk = [segment];
1317
+ }
1318
+ else {
1319
+ currentChunk.push(segment);
1320
+ }
1321
+ }
1322
+ chunks.push(currentChunk);
1323
+ return chunks.filter(excludeEmptyChunks);
1324
+ };
1325
+
1326
+ const sectionDefinition = {
1327
+ id: CONTENTFUL_COMPONENTS.section.id,
1328
+ name: CONTENTFUL_COMPONENTS.section.name,
1329
+ category: CONTENTFUL_COMPONENT_CATEGORY,
1330
+ children: true,
1331
+ variables: builtInStyles,
1332
+ tooltip: {
1333
+ description: 'Create a new full width section of your experience by dragging this element onto the canvas. Elements and patterns can be added into a section.',
1334
+ },
1335
+ };
1336
+ const containerDefinition = {
1337
+ id: CONTENTFUL_COMPONENTS.container.id,
1338
+ name: CONTENTFUL_COMPONENTS.container.name,
1339
+ category: CONTENTFUL_COMPONENT_CATEGORY,
1340
+ children: true,
1341
+ variables: containerBuiltInStyles,
1342
+ tooltip: {
1343
+ description: 'Create a new area or pattern within your page layout by dragging a container onto the canvas. Elements and patterns can be added into a container.',
1344
+ },
1345
+ };
1346
+ const columnsDefinition = {
1347
+ id: CONTENTFUL_COMPONENTS.columns.id,
1348
+ name: CONTENTFUL_COMPONENTS.columns.name,
1349
+ category: CONTENTFUL_COMPONENT_CATEGORY,
1350
+ children: true,
1351
+ variables: columnsBuiltInStyles,
1352
+ tooltip: {
1353
+ description: 'Add columns to a container to create your desired layout and ensure that the experience is responsive across different screen sizes.',
1354
+ },
1355
+ };
1356
+ const singleColumnDefinition = {
1357
+ id: CONTENTFUL_COMPONENTS.singleColumn.id,
1358
+ name: CONTENTFUL_COMPONENTS.singleColumn.name,
1359
+ category: CONTENTFUL_COMPONENT_CATEGORY,
1360
+ children: true,
1361
+ variables: singleColumnBuiltInStyles,
1362
+ };
1363
+
1364
+ const sendMessage = (eventType, data) => {
1365
+ if (typeof window === 'undefined') {
1366
+ return;
1367
+ }
1368
+ console.debug(`[experiences-sdk-react::sendMessage] Sending message [${eventType}]`, {
1369
+ source: 'customer-app',
1370
+ eventType,
1371
+ payload: data,
1372
+ });
1373
+ window.parent?.postMessage({
1374
+ source: 'customer-app',
1375
+ eventType,
1376
+ payload: data,
1377
+ }, '*');
1378
+ };
1379
+
1380
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1381
+ function get(obj, path) {
1382
+ if (!path.length) {
1383
+ return obj;
1384
+ }
1385
+ try {
1386
+ const [currentPath, ...nextPath] = path;
1387
+ return get(obj[currentPath], nextPath);
1388
+ }
1389
+ catch (err) {
1390
+ return undefined;
1391
+ }
1392
+ }
1393
+
1394
+ function transformAssetFileToUrl(fieldValue) {
1395
+ return fieldValue && typeof fieldValue == 'object' && fieldValue.url
1396
+ ? fieldValue.url
1397
+ : fieldValue;
1398
+ }
1399
+
1400
+ /**
1401
+ * Base Store for entities
1402
+ * Can be extened for the different loading behaviours (editor, production, ..)
1403
+ */
1404
+ class EntityStoreBase {
1405
+ constructor({ entities, locale }) {
1406
+ this.entryMap = new Map();
1407
+ this.assetMap = new Map();
1408
+ this.locale = locale;
1409
+ for (const entity of entities) {
1410
+ this.addEntity(entity);
1411
+ }
1412
+ }
1413
+ get entities() {
1414
+ return [...this.entryMap.values(), ...this.assetMap.values()];
1415
+ }
1416
+ updateEntity(entity) {
1417
+ this.addEntity(entity);
1418
+ }
1419
+ getValueDeep(headLinkOrEntity, deepPath) {
1420
+ const resolveFieldset = (unresolvedFieldset, headEntity) => {
1421
+ const resolvedFieldset = [];
1422
+ let entityToResolveFieldsFrom = headEntity;
1423
+ for (let i = 0; i < unresolvedFieldset.length; i++) {
1424
+ const isLeaf = i === unresolvedFieldset.length - 1; // with last row, we are not expecting a link, but a value
1425
+ const row = unresolvedFieldset[i];
1426
+ const [, field, _localeQualifier] = row;
1427
+ if (!entityToResolveFieldsFrom) {
1428
+ throw new Error(`Logic Error: Cannot resolve field ${field} of a fieldset as there is no entity to resolve it from.`);
1429
+ }
1430
+ if (isLeaf) {
1431
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1432
+ break;
1433
+ }
1434
+ const fieldValue = get(entityToResolveFieldsFrom, ['fields', field]);
1435
+ if (undefined === fieldValue) {
1436
+ return {
1437
+ resolvedFieldset,
1438
+ isFullyResolved: false,
1439
+ reason: `Cannot resolve field Link<${entityToResolveFieldsFrom.sys.type}>(sys.id=${entityToResolveFieldsFrom.sys.id}).fields[${field}] as field value is not defined`,
1440
+ };
1441
+ }
1442
+ else if (isLink(fieldValue)) {
1443
+ const entity = this.getEntityFromLink(fieldValue);
1444
+ if (entity === undefined) {
1445
+ throw new Error(`Logic Error: Broken Precondition [by the time resolution of deep path happens all referents should be in EntityStore]: Cannot resolve field ${field} of a fieldset row [${JSON.stringify(row)}] as linked entity not found in the EntityStore. ${JSON.stringify({
1446
+ link: fieldValue,
1447
+ })}`);
1448
+ }
1449
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1450
+ entityToResolveFieldsFrom = entity; // we move up
1451
+ }
1452
+ else {
1453
+ // TODO: Eg. when someone changed the schema and the field is not a link anymore, what should we return then?
1454
+ throw new Error(`LogicError: Invalid value of a field we consider a reference field. Cannot resolve field ${field} of a fieldset as it is not a link, neither undefined.`);
1455
+ }
1456
+ }
1457
+ return {
1458
+ resolvedFieldset,
1459
+ isFullyResolved: true,
1460
+ };
1461
+ };
1462
+ const headEntity = isLink(headLinkOrEntity)
1463
+ ? this.getEntityFromLink(headLinkOrEntity)
1464
+ : headLinkOrEntity;
1465
+ if (undefined === headEntity) {
1466
+ return;
1467
+ }
1468
+ const unresolvedFieldset = parseDataSourcePathIntoFieldset(deepPath);
1469
+ // The purpose here is to take this intermediate representation of the deep-path
1470
+ // and to follow the links to the leaf-entity and field
1471
+ // in case we can't follow till the end, we should signal that there was null-reference in the path
1472
+ const { resolvedFieldset, isFullyResolved, reason } = resolveFieldset(unresolvedFieldset, headEntity);
1473
+ if (!isFullyResolved) {
1474
+ reason &&
1475
+ console.debug(`[experiences-sdk-react::EntityStoreBased::getValueDeep()] Deep path wasn't resolved till leaf node, falling back to undefined, because: ${reason}`);
1476
+ return undefined;
1477
+ }
1478
+ const [leafEntity, field /* localeQualifier */] = resolvedFieldset[resolvedFieldset.length - 1];
1479
+ const fieldValue = get(leafEntity, ['fields', field]); // is allowed to be undefined (when non-required field not set; or even when field does NOT exist on the type)
1480
+ return transformAssetFileToUrl(fieldValue);
1481
+ }
1482
+ /**
1483
+ * @deprecated in the base class this should be simply an abstract method
1484
+ * @param entityLink
1485
+ * @param path
1486
+ * @returns
1487
+ */
1488
+ getValue(entityLink, path) {
1489
+ const entity = this.getEntity(entityLink.sys.linkType, entityLink.sys.id);
1490
+ if (!entity) {
1491
+ // TODO: move to `debug` utils once it is extracted
1492
+ console.warn(`Unresolved entity reference: ${entityLink.sys.linkType} with ID ${entityLink.sys.id}`);
1493
+ return;
1494
+ }
1495
+ return get(entity, path);
1496
+ }
1497
+ getEntityFromLink(link) {
1498
+ const resolvedEntity = link.sys.linkType === 'Entry'
1499
+ ? this.entryMap.get(link.sys.id)
1500
+ : this.assetMap.get(link.sys.id);
1501
+ if (!resolvedEntity || resolvedEntity.sys.type !== link.sys.linkType) {
1502
+ console.warn(`Experience references unresolved entity: ${JSON.stringify(link)}`);
1503
+ return;
1504
+ }
1505
+ return resolvedEntity;
1506
+ }
1507
+ getEntitiesFromMap(type, ids) {
1508
+ const resolved = [];
1509
+ const missing = [];
1510
+ for (const id of ids) {
1511
+ const entity = this.getEntity(type, id);
1512
+ if (entity) {
1513
+ resolved.push(entity);
1514
+ }
1515
+ else {
1516
+ missing.push(id);
1517
+ }
1518
+ }
1519
+ return {
1520
+ resolved,
1521
+ missing,
1522
+ };
1523
+ }
1524
+ addEntity(entity) {
1525
+ if (this.isAsset(entity)) {
1526
+ this.assetMap.set(entity.sys.id, entity);
1527
+ }
1528
+ else {
1529
+ this.entryMap.set(entity.sys.id, entity);
1530
+ }
1531
+ }
1532
+ async fetchAsset(id) {
1533
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', [id]);
1534
+ if (missing.length) {
1535
+ // TODO: move to `debug` utils once it is extracted
1536
+ console.warn(`Asset "${id}" is not in the store`);
1537
+ return undefined;
1538
+ }
1539
+ return resolved[0];
1540
+ }
1541
+ async fetchAssets(ids) {
1542
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', ids);
1543
+ if (missing.length) {
1544
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1545
+ }
1546
+ return resolved;
1547
+ }
1548
+ async fetchEntry(id) {
1549
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', [id]);
1550
+ if (missing.length) {
1551
+ // TODO: move to `debug` utils once it is extracted
1552
+ console.warn(`Entry "${id}" is not in the store`);
1553
+ return undefined;
1554
+ }
1555
+ return resolved[0];
1556
+ }
1557
+ async fetchEntries(ids) {
1558
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', ids);
1559
+ if (missing.length) {
1560
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1561
+ }
1562
+ return resolved;
1563
+ }
1564
+ isAsset(entity) {
1565
+ return entity.sys.type === 'Asset';
1566
+ }
1567
+ getEntity(type, id) {
1568
+ if (type === 'Asset') {
1569
+ return this.assetMap.get(id);
1570
+ }
1571
+ return this.entryMap.get(id);
1572
+ }
1573
+ }
1574
+
1575
+ /**
1576
+ * EntityStore which resolves entries and assets from the editor
1577
+ * over the sendMessage and subscribe functions.
1578
+ */
1579
+ class EditorEntityStore extends EntityStoreBase {
1580
+ constructor({ entities, locale, sendMessage, subscribe, timeoutDuration = 3000, }) {
1581
+ super({ entities, locale });
1582
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1583
+ this.requestCache = new Map();
1584
+ this.cacheIdSeperator = ',';
1585
+ this.sendMessage = sendMessage;
1586
+ this.subscribe = subscribe;
1587
+ this.timeoutDuration = timeoutDuration;
1588
+ }
1589
+ cleanupPromise(referenceId) {
1590
+ setTimeout(() => {
1591
+ this.requestCache.delete(referenceId);
1592
+ }, 300);
1593
+ }
1594
+ getCacheId(id) {
1595
+ return id.length === 1 ? id[0] : id.join(this.cacheIdSeperator);
1596
+ }
1597
+ async fetchEntity(type, ids, skipCache = false) {
1598
+ let missing;
1599
+ if (!skipCache) {
1600
+ const { missing: missingFromCache, resolved } = this.getEntitiesFromMap(type, ids);
1601
+ if (missingFromCache.length === 0) {
1602
+ // everything is already in cache
1603
+ return resolved;
1604
+ }
1605
+ missing = missingFromCache;
1606
+ }
1607
+ else {
1608
+ missing = [...ids];
1609
+ }
1610
+ const cacheId = this.getCacheId(missing);
1611
+ const openRequest = this.requestCache.get(cacheId);
1612
+ if (openRequest) {
1613
+ return openRequest;
1614
+ }
1615
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1616
+ const newPromise = new Promise((resolve, reject) => {
1617
+ const unsubscribe = this.subscribe(PostMessageMethods.REQUESTED_ENTITIES, (message) => {
1618
+ const messageIds = [
1619
+ ...message.entities.map((entity) => entity.sys.id),
1620
+ ...(message.missingEntityIds ?? []),
1621
+ ];
1622
+ if (missing.every((id) => messageIds.find((entityId) => entityId === id))) {
1623
+ clearTimeout(timeout);
1624
+ resolve(message.entities);
1625
+ this.cleanupPromise(cacheId);
1626
+ ids.forEach((id) => this.cleanupPromise(id));
1627
+ unsubscribe();
1628
+ }
1629
+ else {
1630
+ console.warn('Unexpected entities received in REQUESTED_ENTITIES. Ignoring this response.');
1631
+ }
1632
+ });
1633
+ const timeout = setTimeout(() => {
1634
+ reject(new Error(`Request for entities timed out ${this.timeoutDuration}ms} for ${cacheId}`));
1635
+ this.cleanupPromise(cacheId);
1636
+ ids.forEach((id) => this.cleanupPromise(id));
1637
+ unsubscribe();
1638
+ }, this.timeoutDuration);
1639
+ this.sendMessage(PostMessageMethods.REQUEST_ENTITIES, {
1640
+ entityIds: missing,
1641
+ entityType: type,
1642
+ locale: this.locale,
1643
+ });
1644
+ });
1645
+ this.requestCache.set(cacheId, newPromise);
1646
+ ids.forEach((cid) => {
1647
+ this.requestCache.set(cid, newPromise);
1648
+ });
1649
+ const result = (await newPromise);
1650
+ result.forEach((value) => {
1651
+ this.addEntity(value);
1652
+ });
1653
+ return this.getEntitiesFromMap(type, ids).resolved;
1654
+ }
1655
+ async fetchAsset(id, skipCache = false) {
1656
+ try {
1657
+ return (await this.fetchAssets([id], skipCache))[0];
1658
+ }
1659
+ catch (err) {
1660
+ // TODO: move to debug utils once it is extracted
1661
+ console.warn(`Failed to request asset ${id}`);
1662
+ return undefined;
1663
+ }
1664
+ }
1665
+ fetchAssets(ids, skipCache = false) {
1666
+ return this.fetchEntity('Asset', ids, skipCache);
1667
+ }
1668
+ async fetchEntry(id, skipCache = false) {
1669
+ try {
1670
+ return (await this.fetchEntries([id], skipCache))[0];
1671
+ }
1672
+ catch (err) {
1673
+ // TODO: move to debug utils once it is extracted
1674
+ console.warn(`Failed to request entry ${id}`, err);
1675
+ return undefined;
1676
+ }
1677
+ }
1678
+ fetchEntries(ids, skipCache = false) {
1679
+ return this.fetchEntity('Entry', ids, skipCache);
1680
+ }
1681
+ }
1682
+
1683
+ // The default of 3s in the EditorEntityStore is sometimes timing out and
1684
+ // leads to not rendering bound content and assemblies.
1685
+ const REQUEST_TIMEOUT = 10000;
1686
+ class EditorModeEntityStore extends EditorEntityStore {
1687
+ constructor({ entities, locale }) {
1688
+ console.debug(`[experiences-sdk-react] Initializing editor entity store with ${entities.length} entities for locale ${locale}.`, { entities });
1689
+ const subscribe = (method, cb) => {
1690
+ const handleMessage = (event) => {
1691
+ const data = JSON.parse(event.data);
1692
+ if (typeof data !== 'object' || !data)
1693
+ return;
1694
+ if (data.source !== 'composability-app')
1695
+ return;
1696
+ if (data.eventType === method) {
1697
+ cb(data.payload);
1698
+ }
1699
+ };
1700
+ if (typeof window !== 'undefined') {
1701
+ window.addEventListener('message', handleMessage);
1702
+ }
1703
+ return () => {
1704
+ if (typeof window !== 'undefined') {
1705
+ window.removeEventListener('message', handleMessage);
1706
+ }
1707
+ };
1708
+ };
1709
+ super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
1710
+ this.locale = locale;
1711
+ }
1712
+ /**
1713
+ * This function collects and returns the list of requested entries and assets. Additionally, it checks
1714
+ * upfront whether any async fetching logic is actually happening. If not, it returns a plain `false` value, so we
1715
+ * can detect this early and avoid unnecessary re-renders.
1716
+ * @param entityLinks
1717
+ * @returns false if no async fetching is happening, otherwise a promise that resolves when all entities are fetched
1718
+ */
1719
+ async fetchEntities({ missingEntryIds, missingAssetIds, skipCache = false, }) {
1720
+ // Entries and assets will be stored in entryMap and assetMap
1721
+ await Promise.all([
1722
+ this.fetchEntries(missingEntryIds, skipCache),
1723
+ this.fetchAssets(missingAssetIds, skipCache),
1724
+ ]);
1725
+ }
1726
+ getMissingEntityIds(entityLinks) {
1727
+ const entryLinks = entityLinks.filter((link) => link.sys?.linkType === 'Entry');
1728
+ const assetLinks = entityLinks.filter((link) => link.sys?.linkType === 'Asset');
1729
+ const uniqueEntryIds = [...new Set(entryLinks.map((link) => link.sys.id))];
1730
+ const uniqueAssetIds = [...new Set(assetLinks.map((link) => link.sys.id))];
1731
+ const { missing: missingEntryIds } = this.getEntitiesFromMap('Entry', uniqueEntryIds);
1732
+ const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
1733
+ return { missingEntryIds, missingAssetIds };
1734
+ }
1735
+ getValue(entityLink, path) {
1736
+ if (!entityLink || !entityLink.sys)
1737
+ return;
1738
+ const fieldValue = super.getValue(entityLink, path);
1739
+ return transformAssetFileToUrl(fieldValue);
1740
+ }
1741
+ }
1742
+
1743
+ class EntityStore extends EntityStoreBase {
1744
+ constructor(options) {
1745
+ if (typeof options === 'string') {
1746
+ const data = JSON.parse(options);
1747
+ const { _experienceEntry, _unboundValues, locale, entryMap, assetMap } = data.entityStore;
1748
+ super({
1749
+ entities: [
1750
+ ...Object.values(entryMap),
1751
+ ...Object.values(assetMap),
1752
+ ],
1753
+ locale,
1754
+ });
1755
+ this._experienceEntry = _experienceEntry;
1756
+ this._unboundValues = _unboundValues;
1757
+ }
1758
+ else {
1759
+ const { experienceEntry, entities, locale } = options;
1760
+ super({ entities, locale });
1761
+ if (isExperienceEntry(experienceEntry)) {
1762
+ this._experienceEntry = experienceEntry.fields;
1763
+ this._unboundValues = experienceEntry.fields.unboundValues;
1764
+ }
1765
+ else {
1766
+ throw new Error('Provided entry is not experience entry');
1767
+ }
1768
+ }
1769
+ }
1770
+ getCurrentLocale() {
1771
+ return this.locale;
1772
+ }
1773
+ get experienceEntryFields() {
1774
+ return this._experienceEntry;
1775
+ }
1776
+ get schemaVersion() {
1777
+ return this._experienceEntry?.componentTree.schemaVersion;
1778
+ }
1779
+ get breakpoints() {
1780
+ return this._experienceEntry?.componentTree.breakpoints ?? [];
1781
+ }
1782
+ get dataSource() {
1783
+ return this._experienceEntry?.dataSource ?? {};
1784
+ }
1785
+ get unboundValues() {
1786
+ return this._unboundValues ?? {};
1787
+ }
1788
+ get usedComponents() {
1789
+ return this._experienceEntry?.usedComponents ?? [];
1790
+ }
1791
+ /**
1792
+ * Extend the existing set of unbound values with the ones from the assembly definition.
1793
+ * When creating a new assembly out of a container, the unbound value keys are copied and
1794
+ * thus the existing and the added ones have colliding keys. In the case of overlapping value
1795
+ * keys, the ones from the experience overrule the ones from the assembly definition as
1796
+ * the latter one is certainly just a default value while the other one is from the actual instance.
1797
+ * @param unboundValues set of unbound values defined in the assembly definition
1798
+ */
1799
+ addAssemblyUnboundValues(unboundValues) {
1800
+ this._unboundValues = { ...unboundValues, ...(this._unboundValues ?? {}) };
1801
+ }
1802
+ getValue(entityLinkOrEntity, path) {
1803
+ const entity = isLink(entityLinkOrEntity)
1804
+ ? this.getEntityFromLink(entityLinkOrEntity)
1805
+ : entityLinkOrEntity;
1806
+ if (entity === undefined) {
1807
+ return;
1808
+ }
1809
+ const fieldValue = get(entity, path);
1810
+ return transformAssetFileToUrl(fieldValue);
1811
+ }
1812
+ }
1813
+
1814
+ var VisualEditorMode;
1815
+ (function (VisualEditorMode) {
1816
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
1817
+ VisualEditorMode["InjectScript"] = "injectScript";
1818
+ })(VisualEditorMode || (VisualEditorMode = {}));
1819
+
1820
+ function createExperience(options) {
1821
+ if (typeof options === 'string') {
1822
+ const entityStore = new EntityStore(options);
1823
+ return {
1824
+ entityStore,
1825
+ };
1826
+ }
1827
+ else {
1828
+ const { experienceEntry, referencedAssets, referencedEntries, locale } = options;
1829
+ if (!isExperienceEntry(experienceEntry)) {
1830
+ throw new Error('Provided entry is not experience entry');
1831
+ }
1832
+ const entityStore = new EntityStore({
1833
+ experienceEntry,
1834
+ entities: [...referencedEntries, ...referencedAssets],
1835
+ locale,
1836
+ });
1837
+ return {
1838
+ entityStore,
1839
+ };
1840
+ }
1841
+ }
1842
+
1843
+ const fetchExperienceEntry = async ({ client, experienceTypeId, locale, identifier, }) => {
1844
+ if (!client) {
1845
+ throw new Error('Failed to fetch experience entities. Required "client" parameter was not provided');
1846
+ }
1847
+ if (!locale) {
1848
+ throw new Error('Failed to fetch experience entities. Required "locale" parameter was not provided');
1849
+ }
1850
+ if (!experienceTypeId) {
1851
+ throw new Error('Failed to fetch experience entities. Required "experienceTypeId" parameter was not provided');
1852
+ }
1853
+ if (!identifier.slug && !identifier.id) {
1854
+ throw new Error(`Failed to fetch experience entities. At least one identifier must be provided. Received: ${JSON.stringify(identifier)}`);
1855
+ }
1856
+ const filter = identifier.slug ? { 'fields.slug': identifier.slug } : { 'sys.id': identifier.id };
1857
+ const entries = await client.getEntries({
1858
+ content_type: experienceTypeId,
1859
+ locale,
1860
+ ...filter,
1861
+ });
1862
+ if (entries.items.length > 1) {
1863
+ throw new Error(`More than one experience with identifier: ${JSON.stringify(identifier)} was found`);
1864
+ }
1865
+ return entries.items[0];
1866
+ };
1867
+
1868
+ function treeVisit(initialNode, onNode) {
1869
+ // returns last used index
1870
+ const _treeVisit = (currentNode, currentIndex, currentDepth) => {
1871
+ // Copy children in case of onNode removing it as we pass the node by reference
1872
+ const children = [...currentNode.children];
1873
+ onNode(currentNode, currentIndex, currentDepth);
1874
+ let nextAvailableIndex = currentIndex + 1;
1875
+ const lastUsedIndex = currentIndex;
1876
+ for (const child of children) {
1877
+ const lastUsedIndex = _treeVisit(child, nextAvailableIndex, currentDepth + 1);
1878
+ nextAvailableIndex = lastUsedIndex + 1;
1879
+ }
1880
+ return lastUsedIndex;
1881
+ };
1882
+ _treeVisit(initialNode, 0, 0);
1883
+ }
1884
+
1885
+ class DeepReference {
1886
+ constructor({ path, dataSource }) {
1887
+ const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
1888
+ this.originalPath = path;
1889
+ this.entityId = dataSource[key].sys.id;
1890
+ this.entityLink = dataSource[key];
1891
+ this.field = field;
1892
+ this.referentField = referentField;
1893
+ }
1894
+ get headEntityId() {
1895
+ return this.entityId;
1896
+ }
1897
+ /**
1898
+ * Extracts referent from the path, using EntityStore as source of
1899
+ * entities during the resolution path.
1900
+ * TODO: should it be called `extractLeafReferent` ? or `followToLeafReferent`
1901
+ */
1902
+ extractReferent(entityStore) {
1903
+ const headEntity = entityStore.getEntityFromLink(this.entityLink);
1904
+ const maybeReferentLink = headEntity.fields[this.field];
1905
+ if (undefined === maybeReferentLink) {
1906
+ // field references nothing (or even field doesn't exist)
1907
+ return undefined;
1908
+ }
1909
+ if (!isLink(maybeReferentLink)) {
1910
+ // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
1911
+ // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
1912
+ // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
1913
+ return undefined;
1914
+ }
1915
+ return maybeReferentLink;
1916
+ }
1917
+ static from(opt) {
1918
+ return new DeepReference(opt);
1919
+ }
1920
+ }
1921
+ function gatherDeepReferencesFromExperienceEntry(experienceEntry) {
1922
+ const deepReferences = [];
1923
+ const dataSource = experienceEntry.fields.dataSource;
1924
+ const { children } = experienceEntry.fields.componentTree;
1925
+ treeVisit({
1926
+ definitionId: 'root',
1927
+ variables: {},
1928
+ children,
1929
+ }, (node) => {
1930
+ if (!node.variables)
1931
+ return;
1932
+ for (const [, variableMapping] of Object.entries(node.variables)) {
1933
+ if (variableMapping.type !== 'BoundValue')
1934
+ continue;
1935
+ if (!isDeepPath(variableMapping.path))
1936
+ continue;
1937
+ deepReferences.push(DeepReference.from({
1938
+ path: variableMapping.path,
1939
+ dataSource,
1940
+ }));
1941
+ }
1942
+ });
1943
+ return deepReferences;
1944
+ }
1945
+ function gatherDeepReferencesFromTree(startingNode, dataSource) {
1946
+ const deepReferences = [];
1947
+ treeVisit(startingNode, (node) => {
1948
+ if (!node.data.props)
1949
+ return;
1950
+ for (const [, variableMapping] of Object.entries(node.data.props)) {
1951
+ if (variableMapping.type !== 'BoundValue')
1952
+ continue;
1953
+ if (!isDeepPath(variableMapping.path))
1954
+ continue;
1955
+ deepReferences.push(DeepReference.from({
1956
+ path: variableMapping.path,
1957
+ dataSource,
1958
+ }));
1959
+ }
1960
+ });
1961
+ return deepReferences;
1962
+ }
1963
+
1964
+ /**
1965
+ * Traverses deep-references and extracts referents from valid deep-paths.
1966
+ * The referents are received from the CDA/CPA response `.includes` field.
1967
+ *
1968
+ * In case deep-paths not resolving till the end, eg.:
1969
+ * - non-link referents: are ignored
1970
+ * - unset references: are ignored
1971
+ *
1972
+ * Errors are thrown in case of deep-paths being correct,
1973
+ * but referents not found. Because if we don't throw now, the EntityStore will
1974
+ * be missing entities and upon rendering will not be able to render bindings.
1975
+ */
1976
+ function gatherAutoFetchedReferentsFromIncludes(deepReferences, entriesResponse) {
1977
+ const autoFetchedReferentEntries = [];
1978
+ const autoFetchedReferentAssets = [];
1979
+ for (const reference of deepReferences) {
1980
+ const headEntry = entriesResponse.items.find((entry) => entry.sys.id === reference.headEntityId);
1981
+ if (!headEntry) {
1982
+ throw new Error(`LogicError: When resolving deep-references could not find headEntry (id=${reference.entityId})`);
1983
+ }
1984
+ const linkToReferent = headEntry.fields[reference.field];
1985
+ if (undefined === linkToReferent) {
1986
+ console.debug(`[experiences-sdk-react::gatherAutoFetchedReferentsFromIncludes] Empty reference in headEntity. Probably reference is simply not set.`);
1987
+ continue;
1988
+ }
1989
+ if (!isLink(linkToReferent)) {
1990
+ console.debug(`[experiences-sdk-react::gatherAutoFetchedReferentsFromIncludes] Non-link value in headEntity. Probably broken path '${reference.originalPath}'`);
1991
+ continue;
1992
+ }
1993
+ if (linkToReferent.sys.linkType === 'Entry') {
1994
+ const referentEntry = entriesResponse.includes?.Entry?.find((entry) => entry.sys.id === linkToReferent.sys.id);
1995
+ if (!referentEntry) {
1996
+ throw new Error(`Logic Error: L2-referent Entry was not found within .includes (${JSON.stringify({
1997
+ linkToReferent,
1998
+ })})`);
1999
+ }
2000
+ autoFetchedReferentEntries.push(referentEntry);
2001
+ }
2002
+ else if (linkToReferent.sys.linkType === 'Asset') {
2003
+ const referentAsset = entriesResponse.includes?.Asset?.find((entry) => entry.sys.id === linkToReferent.sys.id);
2004
+ if (!referentAsset) {
2005
+ throw new Error(`Logic Error: L2-referent Asset was not found within includes (${JSON.stringify({
2006
+ linkToReferent,
2007
+ })})`);
2008
+ }
2009
+ autoFetchedReferentAssets.push(referentAsset);
2010
+ }
2011
+ else {
2012
+ console.debug(`[experiences-sdk-react::gatherAutoFetchedReferentsFromIncludes] Unhandled linkType :${JSON.stringify(linkToReferent)}`);
2013
+ }
2014
+ } // for (reference of deepReferences)
2015
+ return { autoFetchedReferentAssets, autoFetchedReferentEntries };
2016
+ }
2017
+
2018
+ const fetchReferencedEntities = async ({ client, experienceEntry, locale, }) => {
2019
+ if (!client) {
2020
+ throw new Error('Failed to fetch experience entities. Required "client" parameter was not provided');
2021
+ }
2022
+ if (!locale) {
2023
+ throw new Error('Failed to fetch experience entities. Required "locale" parameter was not provided');
2024
+ }
2025
+ if (!isExperienceEntry(experienceEntry)) {
2026
+ throw new Error('Failed to fetch experience entities. Provided "experienceEntry" does not match experience entry schema');
2027
+ }
2028
+ const deepReferences = gatherDeepReferencesFromExperienceEntry(experienceEntry);
2029
+ const entryIds = [];
2030
+ const assetIds = [];
2031
+ for (const dataBinding of Object.values(experienceEntry.fields.dataSource)) {
2032
+ if (!('sys' in dataBinding)) {
2033
+ continue;
2034
+ }
2035
+ if (dataBinding.sys.linkType === 'Entry') {
2036
+ entryIds.push(dataBinding.sys.id);
2037
+ }
2038
+ if (dataBinding.sys.linkType === 'Asset') {
2039
+ assetIds.push(dataBinding.sys.id);
2040
+ }
2041
+ }
2042
+ const [entriesResponse, assetsResponse] = (await Promise.all([
2043
+ entryIds.length > 0
2044
+ ? client.withoutLinkResolution.getEntries({ 'sys.id[in]': entryIds, locale })
2045
+ : { items: [], includes: [] },
2046
+ assetIds.length > 0 ? client.getAssets({ 'sys.id[in]': assetIds, locale }) : { items: [] },
2047
+ ]));
2048
+ const { autoFetchedReferentAssets, autoFetchedReferentEntries } = gatherAutoFetchedReferentsFromIncludes(deepReferences, entriesResponse);
2049
+ // Using client getEntries resolves all linked entry references, so we do not need to resolve entries in usedComponents
2050
+ const allResolvedEntries = [
2051
+ ...(entriesResponse.items ?? []),
2052
+ ...(experienceEntry.fields.usedComponents || []),
2053
+ ...autoFetchedReferentEntries,
2054
+ ];
2055
+ const allResolvedAssets = [
2056
+ ...(assetsResponse.items ?? []),
2057
+ ...autoFetchedReferentAssets,
2058
+ ];
2059
+ return {
2060
+ entries: allResolvedEntries,
2061
+ assets: allResolvedAssets,
2062
+ };
2063
+ };
2064
+
2065
+ const errorMessagesWhileFetching$1 = {
2066
+ experience: 'Failed to fetch experience',
2067
+ experienceReferences: 'Failed to fetch entities, referenced in experience',
2068
+ };
2069
+ const handleError$1 = (generalMessage, error) => {
2070
+ const message = error instanceof Error ? error.message : `Unknown error: ${error}`;
2071
+ throw Error(message);
2072
+ };
2073
+ /**
2074
+ * Fetch experience entry using slug as the identifier
2075
+ * @param {string} experienceTypeId - id of the content type associated with the experience
2076
+ * @param {string} slug - slug of the experience (defined in entry settings)
2077
+ * @param {string} localeCode - locale code to fetch the experience. Falls back to the currently active locale in the state
2078
+ */
2079
+ // Promise<Experience<EntityStore> | undefined> =>
2080
+ async function fetchBySlug({ client, experienceTypeId, slug, localeCode, }) {
2081
+ let experienceEntry = undefined;
2082
+ try {
2083
+ experienceEntry = await fetchExperienceEntry({
2084
+ client,
2085
+ experienceTypeId,
2086
+ locale: localeCode,
2087
+ identifier: {
2088
+ slug,
2089
+ },
2090
+ });
2091
+ if (!experienceEntry) {
2092
+ throw new Error(`No experience entry with slug: ${slug} exists`);
2093
+ }
2094
+ try {
2095
+ const { entries, assets } = await fetchReferencedEntities({
2096
+ client,
2097
+ experienceEntry,
2098
+ locale: localeCode,
2099
+ });
2100
+ const experience = createExperience({
2101
+ experienceEntry,
2102
+ referencedAssets: assets,
2103
+ referencedEntries: entries,
2104
+ locale: localeCode,
2105
+ });
2106
+ return experience;
2107
+ }
2108
+ catch (error) {
2109
+ handleError$1(errorMessagesWhileFetching$1.experienceReferences, error);
2110
+ }
2111
+ }
2112
+ catch (error) {
2113
+ handleError$1(errorMessagesWhileFetching$1.experience, error);
2114
+ }
2115
+ }
2116
+
2117
+ const errorMessagesWhileFetching = {
2118
+ experience: 'Failed to fetch experience',
2119
+ experienceReferences: 'Failed to fetch entities, referenced in experience',
2120
+ };
2121
+ const handleError = (generalMessage, error) => {
2122
+ const message = error instanceof Error ? error.message : `Unknown error: ${error}`;
2123
+ throw Error(message);
2124
+ };
2125
+ /**
2126
+ * Fetch experience entry using slug as the identifier
2127
+ * @param {string} experienceTypeId - id of the content type associated with the experience
2128
+ * @param {string} slug - slug of the experience (defined in entry settings)
2129
+ * @param {string} localeCode - locale code to fetch the experience. Falls back to the currently active locale in the state
2130
+ */
2131
+ async function fetchById({ client, experienceTypeId, id, localeCode, }) {
2132
+ let experienceEntry = undefined;
2133
+ try {
2134
+ experienceEntry = await fetchExperienceEntry({
2135
+ client,
2136
+ experienceTypeId,
2137
+ locale: localeCode,
2138
+ identifier: {
2139
+ id,
2140
+ },
2141
+ });
2142
+ if (!experienceEntry) {
2143
+ throw new Error(`No experience entry with id: ${id} exists`);
2144
+ }
2145
+ try {
2146
+ const { entries, assets } = await fetchReferencedEntities({
2147
+ client,
2148
+ experienceEntry,
2149
+ locale: localeCode,
2150
+ });
2151
+ const experience = createExperience({
2152
+ experienceEntry,
2153
+ referencedAssets: assets,
2154
+ referencedEntries: entries,
2155
+ locale: localeCode,
2156
+ });
2157
+ return experience;
2158
+ }
2159
+ catch (error) {
2160
+ handleError(errorMessagesWhileFetching.experienceReferences, error);
2161
+ }
2162
+ }
2163
+ catch (error) {
2164
+ handleError(errorMessagesWhileFetching.experience, error);
2165
+ }
2166
+ }
2167
+
2168
+ export { DeepReference, EditorModeEntityStore, EntityStore, EntityStoreBase, MEDIA_QUERY_REGEXP, VisualEditorMode, buildCfStyles, buildStyleTag, builtInStyles, calculateNodeDefaultHeight, checkIsAssembly, checkIsAssemblyDefinition, checkIsAssemblyEntry, checkIsAssemblyNode, columnsBuiltInStyles, columnsDefinition, containerBuiltInStyles, containerDefinition, createExperience, defineDesignTokens, designTokensRegistry, doesMismatchMessageSchema, fetchById, fetchBySlug, findOutermostCoordinates, gatherDeepReferencesFromExperienceEntry, gatherDeepReferencesFromTree, generateRandomId, getActiveBreakpointIndex, getDataFromTree, getDesignTokenRegistration, getElementCoordinates, getFallbackBreakpointIndex, getInsertionData, getValueForBreakpoint, isContentfulStructureComponent, isDeepPath, isDeprecatedExperience, isEmptyStructureWithRelativeHeight, isExperienceEntry, isLink, isLinkToAsset, mediaQueryMatcher, optionalBuiltInStyles, parseDataSourcePathIntoFieldset, parseDataSourcePathWithL1DeepBindings, sectionDefinition, sendMessage, singleColumnBuiltInStyles, singleColumnDefinition, supportedModes, transformAlignment, transformBackgroundImage, transformBorderStyle, transformContentValue, transformFill, transformGridColumn, transformRichText, transformWidthSizing, tryParseMessage, validateExperienceBuilderConfig };
2169
+ //# sourceMappingURL=index.js.map