@contentful/experiences-core 1.36.0-dev-20250417T1301-b6204ec.0 → 1.36.0-dev-20250422T1327-c14ac85.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import md5 from 'md5';
2
1
  import { z, ZodIssueCode } from 'zod';
2
+ import { omit, isArray, uniqBy } from 'lodash-es';
3
+ import md5 from 'md5';
3
4
  import { BLOCKS } from '@contentful/rich-text-types';
4
- import { isArray, omit, uniqBy } from 'lodash-es';
5
5
 
6
6
  const INCOMING_EVENTS = {
7
7
  RequestEditorMode: 'requestEditorMode',
@@ -151,514 +151,173 @@ const isStructureWithRelativeHeight = (componentId, height) => {
151
151
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
152
152
  };
153
153
 
154
- const findOutermostCoordinates = (first, second) => {
155
- return {
156
- top: Math.min(first.top, second.top),
157
- right: Math.max(first.right, second.right),
158
- bottom: Math.max(first.bottom, second.bottom),
159
- left: Math.min(first.left, second.left),
160
- };
161
- };
162
- const getElementCoordinates = (element) => {
163
- const rect = element.getBoundingClientRect();
164
- /**
165
- * If element does not have children, or element has it's own width or height,
166
- * return the element's coordinates.
167
- */
168
- if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
169
- return rect;
170
- }
171
- const rects = [];
172
- /**
173
- * If element has children, or element does not have it's own width and height,
174
- * we find the cordinates of the children, and assume the outermost coordinates of the children
175
- * as the coordinate of the element.
176
- *
177
- * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
178
- * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
179
- */
180
- for (const child of element.children) {
181
- const childRect = getElementCoordinates(child);
182
- if (childRect.width !== 0 || childRect.height !== 0) {
183
- const { top, right, bottom, left } = childRect;
184
- rects.push({ top, right, bottom, left });
185
- }
186
- }
187
- if (rects.length === 0) {
188
- return rect;
189
- }
190
- const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
191
- return DOMRect.fromRect({
192
- x: left,
193
- y: top,
194
- height: bottom - top,
195
- width: right - left,
196
- });
197
- };
198
-
199
- class ParseError extends Error {
200
- constructor(message) {
201
- super(message);
202
- }
203
- }
204
- const isValidJsonObject = (s) => {
205
- try {
206
- const result = JSON.parse(s);
207
- if ('object' !== typeof result) {
208
- return false;
209
- }
210
- return true;
211
- }
212
- catch (e) {
213
- return false;
214
- }
215
- };
216
- const doesMismatchMessageSchema = (event) => {
217
- try {
218
- tryParseMessage(event);
219
- return false;
220
- }
221
- catch (e) {
222
- if (e instanceof ParseError) {
223
- return e.message;
224
- }
225
- throw e;
226
- }
227
- };
228
- const tryParseMessage = (event) => {
229
- if (!event.data) {
230
- throw new ParseError('Field event.data is missing');
231
- }
232
- if ('string' !== typeof event.data) {
233
- throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
234
- }
235
- if (!isValidJsonObject(event.data)) {
236
- throw new ParseError('Field event.data must be a valid JSON object serialized as string');
237
- }
238
- const eventData = JSON.parse(event.data);
239
- if (!eventData.source) {
240
- throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
241
- }
242
- if ('composability-app' !== eventData.source) {
243
- throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
244
- }
245
- // check eventData.eventType
246
- const supportedEventTypes = Object.values(INCOMING_EVENTS);
247
- if (!supportedEventTypes.includes(eventData.eventType)) {
248
- // Expected message: This message is handled in the EntityStore to store fetched entities
249
- if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
250
- throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
251
- }
252
- }
253
- return eventData;
154
+ // These styles get added to every component, user custom or contentful provided
155
+ const builtInStyles = {
156
+ cfVerticalAlignment: {
157
+ validations: {
158
+ in: [
159
+ {
160
+ value: 'start',
161
+ displayName: 'Align left',
162
+ },
163
+ {
164
+ value: 'center',
165
+ displayName: 'Align center',
166
+ },
167
+ {
168
+ value: 'end',
169
+ displayName: 'Align right',
170
+ },
171
+ ],
172
+ },
173
+ type: 'Text',
174
+ group: 'style',
175
+ description: 'The vertical alignment of the section',
176
+ defaultValue: 'center',
177
+ displayName: 'Vertical alignment',
178
+ },
179
+ cfHorizontalAlignment: {
180
+ validations: {
181
+ in: [
182
+ {
183
+ value: 'start',
184
+ displayName: 'Align top',
185
+ },
186
+ {
187
+ value: 'center',
188
+ displayName: 'Align center',
189
+ },
190
+ {
191
+ value: 'end',
192
+ displayName: 'Align bottom',
193
+ },
194
+ ],
195
+ },
196
+ type: 'Text',
197
+ group: 'style',
198
+ description: 'The horizontal alignment of the section',
199
+ defaultValue: 'center',
200
+ displayName: 'Horizontal alignment',
201
+ },
202
+ cfVisibility: {
203
+ displayName: 'Visibility toggle',
204
+ type: 'Boolean',
205
+ group: 'style',
206
+ defaultValue: true,
207
+ description: 'The visibility of the component',
208
+ },
209
+ cfMargin: {
210
+ displayName: 'Margin',
211
+ type: 'Text',
212
+ group: 'style',
213
+ description: 'The margin of the section',
214
+ defaultValue: '0 0 0 0',
215
+ },
216
+ cfPadding: {
217
+ displayName: 'Padding',
218
+ type: 'Text',
219
+ group: 'style',
220
+ description: 'The padding of the section',
221
+ defaultValue: '0 0 0 0',
222
+ },
223
+ cfBackgroundColor: {
224
+ displayName: 'Background color',
225
+ type: 'Text',
226
+ group: 'style',
227
+ description: 'The background color of the section',
228
+ defaultValue: 'rgba(0, 0, 0, 0)',
229
+ },
230
+ cfWidth: {
231
+ displayName: 'Width',
232
+ type: 'Text',
233
+ group: 'style',
234
+ description: 'The width of the section',
235
+ defaultValue: '100%',
236
+ },
237
+ cfHeight: {
238
+ displayName: 'Height',
239
+ type: 'Text',
240
+ group: 'style',
241
+ description: 'The height of the section',
242
+ defaultValue: 'fit-content',
243
+ },
244
+ cfMaxWidth: {
245
+ displayName: 'Max width',
246
+ type: 'Text',
247
+ group: 'style',
248
+ description: 'The max-width of the section',
249
+ defaultValue: 'none',
250
+ },
251
+ cfFlexDirection: {
252
+ displayName: 'Direction',
253
+ type: 'Text',
254
+ group: 'style',
255
+ description: 'The orientation of the section',
256
+ defaultValue: 'column',
257
+ },
258
+ cfFlexReverse: {
259
+ displayName: 'Reverse Direction',
260
+ type: 'Boolean',
261
+ group: 'style',
262
+ description: 'Toggle the flex direction to be reversed',
263
+ defaultValue: false,
264
+ },
265
+ cfFlexWrap: {
266
+ displayName: 'Wrap objects',
267
+ type: 'Text',
268
+ group: 'style',
269
+ description: 'Wrap objects',
270
+ defaultValue: 'nowrap',
271
+ },
272
+ cfBorder: {
273
+ displayName: 'Border',
274
+ type: 'Text',
275
+ group: 'style',
276
+ description: 'The border of the section',
277
+ defaultValue: '0px solid rgba(0, 0, 0, 0)',
278
+ },
279
+ cfGap: {
280
+ displayName: 'Gap',
281
+ type: 'Text',
282
+ group: 'style',
283
+ description: 'The spacing between the elements of the section',
284
+ defaultValue: '0px',
285
+ },
286
+ cfHyperlink: {
287
+ displayName: 'URL',
288
+ type: 'Hyperlink',
289
+ defaultValue: '',
290
+ validations: {
291
+ format: 'URL',
292
+ bindingSourceType: ['entry', 'experience', 'manual'],
293
+ },
294
+ description: 'hyperlink for section or container',
295
+ },
296
+ cfOpenInNewTab: {
297
+ displayName: 'URL behaviour',
298
+ type: 'Boolean',
299
+ defaultValue: false,
300
+ description: 'Open in new tab',
301
+ },
254
302
  };
255
- const validateExperienceBuilderConfig = ({ locale, mode, }) => {
256
- if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
257
- return;
258
- }
259
- if (!locale) {
260
- throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
261
- }
262
- };
263
-
264
- const transformVisibility = (value) => {
265
- if (value === false) {
266
- return {
267
- display: 'none !important',
268
- };
269
- }
270
- // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
271
- return {};
272
- };
273
- // Keep this for backwards compatibility - deleting this would be a breaking change
274
- // because existing components on a users experience will have the width value as fill
275
- // rather than 100%
276
- const transformFill = (value) => (value === 'fill' ? '100%' : value);
277
- const transformGridColumn = (span) => {
278
- if (!span) {
279
- return {};
280
- }
281
- return {
282
- gridColumn: `span ${span}`,
283
- };
284
- };
285
- const transformBorderStyle = (value) => {
286
- if (!value)
287
- return {};
288
- const parts = value.split(' ');
289
- // Just accept the passed value
290
- if (parts.length < 3)
291
- return { border: value };
292
- const [borderSize, borderStyle, ...borderColorParts] = parts;
293
- const borderColor = borderColorParts.join(' ');
294
- return {
295
- border: `${borderSize} ${borderStyle} ${borderColor}`,
296
- };
297
- };
298
- const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
299
- ? {
300
- alignItems: cfHorizontalAlignment,
301
- justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
302
- }
303
- : {
304
- alignItems: cfVerticalAlignment,
305
- justifyContent: cfHorizontalAlignment === 'center'
306
- ? `safe ${cfHorizontalAlignment}`
307
- : cfHorizontalAlignment,
308
- };
309
- const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
310
- const matchBackgroundSize = (scaling) => {
311
- if ('fill' === scaling)
312
- return 'cover';
313
- if ('fit' === scaling)
314
- return 'contain';
315
- };
316
- const matchBackgroundPosition = (alignment) => {
317
- if (!alignment || 'string' !== typeof alignment) {
318
- return;
319
- }
320
- let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
321
- // Special case for handling single values
322
- // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
323
- if (horizontalAlignment && !verticalAlignment) {
324
- const singleValue = horizontalAlignment;
325
- switch (singleValue) {
326
- case 'left':
327
- horizontalAlignment = 'left';
328
- verticalAlignment = 'center';
329
- break;
330
- case 'right':
331
- horizontalAlignment = 'right';
332
- verticalAlignment = 'center';
333
- break;
334
- case 'center':
335
- horizontalAlignment = 'center';
336
- verticalAlignment = 'center';
337
- break;
338
- case 'top':
339
- horizontalAlignment = 'center';
340
- verticalAlignment = 'top';
341
- break;
342
- case 'bottom':
343
- horizontalAlignment = 'center';
344
- verticalAlignment = 'bottom';
345
- break;
346
- // just fall down to the normal validation logic for horiz and vert
347
- }
348
- }
349
- const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
350
- const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
351
- horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
352
- verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
353
- return `${horizontalAlignment} ${verticalAlignment}`;
354
- };
355
- if (!cfBackgroundImageUrl) {
356
- return;
357
- }
358
- let backgroundImage;
359
- let backgroundImageSet;
360
- if (typeof cfBackgroundImageUrl === 'string') {
361
- backgroundImage = `url(${cfBackgroundImageUrl})`;
362
- }
363
- else {
364
- const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
365
- backgroundImage = `url(${cfBackgroundImageUrl.url})`;
366
- backgroundImageSet = `image-set(${imgSet})`;
367
- }
368
- return {
369
- backgroundImage,
370
- backgroundImage2: backgroundImageSet,
371
- backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
372
- backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
373
- backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
374
- };
375
- };
376
-
377
- const toCSSAttribute = (key) => {
378
- let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
379
- // Remove the number from the end of the key to allow for overrides on style properties
380
- val = val.replace(/\d+$/, '');
381
- return val;
382
- };
383
- /**
384
- * Turns a list of CSSProperties into a joined CSS string that can be
385
- * used for <style> tags. Per default it creates a minimized version.
386
- * For editor mode, use the `useWhitespaces` flag to create a more readable version.
387
- *
388
- * @param cssProperties list of CSS properties
389
- * @param useWhitespaces adds whitespaces and newlines between each rule
390
- * @returns a string of CSS rules
391
- */
392
- const stringifyCssProperties = (cssProperties, useWhitespaces = false) => {
393
- const rules = Object.entries(cssProperties)
394
- .filter(([, value]) => value !== undefined)
395
- .map(([key, value]) => useWhitespaces ? `${toCSSAttribute(key)}: ${value};` : `${toCSSAttribute(key)}:${value};`);
396
- return rules.join(useWhitespaces ? '\n' : '');
397
- };
398
- const buildStyleTag = ({ styles, nodeId }) => {
399
- const generatedStyles = stringifyCssProperties(styles, true);
400
- const className = `cfstyles-${nodeId ? nodeId : md5(generatedStyles)}`;
401
- const styleRule = `.${className}{ ${generatedStyles} }`;
402
- return [className, styleRule];
403
- };
404
- /**
405
- * Takes plain design values and transforms them into CSS properties. Undefined values will
406
- * be filtered out.
407
- *
408
- * **Example Input**
409
- * ```
410
- * values = {
411
- * cfVisibility: 'visible',
412
- * cfMargin: '10px',
413
- * cfFlexReverse: true,
414
- * cfImageOptions: { objectFit: 'cover' },
415
- * // ...
416
- * }
417
- * ```
418
- * **Example Output**
419
- * ```
420
- * cssProperties = {
421
- * margin: '10px',
422
- * flexDirection: 'row-reverse',
423
- * objectFit: 'cover',
424
- * // ...
425
- * }
426
- * ```
427
- */
428
- const buildCfStyles = (values) => {
429
- const cssProperties = {
430
- boxSizing: 'border-box',
431
- ...transformVisibility(values.cfVisibility),
432
- margin: values.cfMargin,
433
- padding: values.cfPadding,
434
- backgroundColor: values.cfBackgroundColor,
435
- width: transformFill(values.cfWidth || values.cfImageOptions?.width),
436
- height: transformFill(values.cfHeight || values.cfImageOptions?.height),
437
- maxWidth: values.cfMaxWidth,
438
- ...transformGridColumn(values.cfColumnSpan),
439
- ...transformBorderStyle(values.cfBorder),
440
- borderRadius: values.cfBorderRadius,
441
- gap: values.cfGap,
442
- ...transformAlignment(values.cfHorizontalAlignment, values.cfVerticalAlignment, values.cfFlexDirection),
443
- flexDirection: values.cfFlexReverse && values.cfFlexDirection
444
- ? `${values.cfFlexDirection}-reverse`
445
- : values.cfFlexDirection,
446
- flexWrap: values.cfFlexWrap,
447
- ...transformBackgroundImage(values.cfBackgroundImageUrl, values.cfBackgroundImageOptions),
448
- fontSize: values.cfFontSize,
449
- fontWeight: values.cfTextBold ? 'bold' : values.cfFontWeight,
450
- fontStyle: values.cfTextItalic ? 'italic' : undefined,
451
- textDecoration: values.cfTextUnderline ? 'underline' : undefined,
452
- lineHeight: values.cfLineHeight,
453
- letterSpacing: values.cfLetterSpacing,
454
- color: values.cfTextColor,
455
- textAlign: values.cfTextAlign,
456
- textTransform: values.cfTextTransform,
457
- objectFit: values.cfImageOptions?.objectFit,
458
- objectPosition: values.cfImageOptions?.objectPosition,
459
- };
460
- const cssPropertiesWithoutUndefined = Object.fromEntries(Object.entries(cssProperties).filter(([, value]) => value !== undefined));
461
- return cssPropertiesWithoutUndefined;
462
- };
463
- /**
464
- * **Only meant to be used in editor mode!**
465
- *
466
- * If the node is an empty structure component with a relative height (e.g. '100%'),
467
- * it might render with a zero height. In this case, add a min height of 80px to ensure
468
- * that child nodes can be added via drag & drop.
469
- */
470
- const addMinHeightForEmptyStructures = (cssProperties, node) => {
471
- if (!node.children.length &&
472
- isStructureWithRelativeHeight(node.definitionId, cssProperties.height)) {
473
- return {
474
- ...cssProperties,
475
- minHeight: EMPTY_CONTAINER_HEIGHT,
476
- };
477
- }
478
- return cssProperties;
479
- };
480
- /**
481
- * Container/section default behavior:
482
- * Default height => height: EMPTY_CONTAINER_HEIGHT
483
- * If a container component has children => height: 'fit-content'
484
- */
485
- const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
486
- if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
487
- return value;
488
- }
489
- if (children.length) {
490
- return '100%';
491
- }
492
- return EMPTY_CONTAINER_HEIGHT;
493
- };
494
-
495
- // These styles get added to every component, user custom or contentful provided
496
- const builtInStyles = {
497
- cfVerticalAlignment: {
303
+ const optionalBuiltInStyles = {
304
+ cfFontSize: {
305
+ displayName: 'Font Size',
306
+ type: 'Text',
307
+ group: 'style',
308
+ description: 'The font size of the element',
309
+ defaultValue: '16px',
310
+ },
311
+ cfFontWeight: {
498
312
  validations: {
499
313
  in: [
500
314
  {
501
- value: 'start',
502
- displayName: 'Align left',
315
+ value: '400',
316
+ displayName: 'Normal',
503
317
  },
504
318
  {
505
- value: 'center',
506
- displayName: 'Align center',
507
- },
508
- {
509
- value: 'end',
510
- displayName: 'Align right',
511
- },
512
- ],
513
- },
514
- type: 'Text',
515
- group: 'style',
516
- description: 'The vertical alignment of the section',
517
- defaultValue: 'center',
518
- displayName: 'Vertical alignment',
519
- },
520
- cfHorizontalAlignment: {
521
- validations: {
522
- in: [
523
- {
524
- value: 'start',
525
- displayName: 'Align top',
526
- },
527
- {
528
- value: 'center',
529
- displayName: 'Align center',
530
- },
531
- {
532
- value: 'end',
533
- displayName: 'Align bottom',
534
- },
535
- ],
536
- },
537
- type: 'Text',
538
- group: 'style',
539
- description: 'The horizontal alignment of the section',
540
- defaultValue: 'center',
541
- displayName: 'Horizontal alignment',
542
- },
543
- cfVisibility: {
544
- displayName: 'Visibility toggle',
545
- type: 'Boolean',
546
- group: 'style',
547
- defaultValue: true,
548
- description: 'The visibility of the component',
549
- },
550
- cfMargin: {
551
- displayName: 'Margin',
552
- type: 'Text',
553
- group: 'style',
554
- description: 'The margin of the section',
555
- defaultValue: '0 0 0 0',
556
- },
557
- cfPadding: {
558
- displayName: 'Padding',
559
- type: 'Text',
560
- group: 'style',
561
- description: 'The padding of the section',
562
- defaultValue: '0 0 0 0',
563
- },
564
- cfBackgroundColor: {
565
- displayName: 'Background color',
566
- type: 'Text',
567
- group: 'style',
568
- description: 'The background color of the section',
569
- defaultValue: 'rgba(0, 0, 0, 0)',
570
- },
571
- cfWidth: {
572
- displayName: 'Width',
573
- type: 'Text',
574
- group: 'style',
575
- description: 'The width of the section',
576
- defaultValue: '100%',
577
- },
578
- cfHeight: {
579
- displayName: 'Height',
580
- type: 'Text',
581
- group: 'style',
582
- description: 'The height of the section',
583
- defaultValue: 'fit-content',
584
- },
585
- cfMaxWidth: {
586
- displayName: 'Max width',
587
- type: 'Text',
588
- group: 'style',
589
- description: 'The max-width of the section',
590
- defaultValue: 'none',
591
- },
592
- cfFlexDirection: {
593
- displayName: 'Direction',
594
- type: 'Text',
595
- group: 'style',
596
- description: 'The orientation of the section',
597
- defaultValue: 'column',
598
- },
599
- cfFlexReverse: {
600
- displayName: 'Reverse Direction',
601
- type: 'Boolean',
602
- group: 'style',
603
- description: 'Toggle the flex direction to be reversed',
604
- defaultValue: false,
605
- },
606
- cfFlexWrap: {
607
- displayName: 'Wrap objects',
608
- type: 'Text',
609
- group: 'style',
610
- description: 'Wrap objects',
611
- defaultValue: 'nowrap',
612
- },
613
- cfBorder: {
614
- displayName: 'Border',
615
- type: 'Text',
616
- group: 'style',
617
- description: 'The border of the section',
618
- defaultValue: '0px solid rgba(0, 0, 0, 0)',
619
- },
620
- cfGap: {
621
- displayName: 'Gap',
622
- type: 'Text',
623
- group: 'style',
624
- description: 'The spacing between the elements of the section',
625
- defaultValue: '0px',
626
- },
627
- cfHyperlink: {
628
- displayName: 'URL',
629
- type: 'Hyperlink',
630
- defaultValue: '',
631
- validations: {
632
- format: 'URL',
633
- bindingSourceType: ['entry', 'experience', 'manual'],
634
- },
635
- description: 'hyperlink for section or container',
636
- },
637
- cfOpenInNewTab: {
638
- displayName: 'URL behaviour',
639
- type: 'Boolean',
640
- defaultValue: false,
641
- description: 'Open in new tab',
642
- },
643
- };
644
- const optionalBuiltInStyles = {
645
- cfFontSize: {
646
- displayName: 'Font Size',
647
- type: 'Text',
648
- group: 'style',
649
- description: 'The font size of the element',
650
- defaultValue: '16px',
651
- },
652
- cfFontWeight: {
653
- validations: {
654
- in: [
655
- {
656
- value: '400',
657
- displayName: 'Normal',
658
- },
659
- {
660
- value: '500',
661
- displayName: 'Medium',
319
+ value: '500',
320
+ displayName: 'Medium',
662
321
  },
663
322
  {
664
323
  value: '600',
@@ -1506,143 +1165,819 @@ z.object({
1506
1165
  })),
1507
1166
  });
1508
1167
 
1509
- var CodeNames;
1510
- (function (CodeNames) {
1511
- CodeNames["Type"] = "type";
1512
- CodeNames["Required"] = "required";
1513
- CodeNames["Unexpected"] = "unexpected";
1514
- CodeNames["Regex"] = "regex";
1515
- CodeNames["In"] = "in";
1516
- CodeNames["Size"] = "size";
1517
- CodeNames["Custom"] = "custom";
1518
- })(CodeNames || (CodeNames = {}));
1519
- const convertInvalidType = (issue) => {
1520
- const name = issue.received === 'undefined' ? CodeNames.Required : CodeNames.Type;
1521
- const details = issue.received === 'undefined'
1522
- ? `The property "${issue.path.slice(-1)}" is required here`
1523
- : `The type of "${issue.path.slice(-1)}" is incorrect, expected type: ${issue.expected}`;
1524
- return {
1525
- details: details,
1526
- name: name,
1527
- path: issue.path,
1528
- value: issue.received.toString(),
1168
+ var CodeNames;
1169
+ (function (CodeNames) {
1170
+ CodeNames["Type"] = "type";
1171
+ CodeNames["Required"] = "required";
1172
+ CodeNames["Unexpected"] = "unexpected";
1173
+ CodeNames["Regex"] = "regex";
1174
+ CodeNames["In"] = "in";
1175
+ CodeNames["Size"] = "size";
1176
+ CodeNames["Custom"] = "custom";
1177
+ })(CodeNames || (CodeNames = {}));
1178
+ const convertInvalidType = (issue) => {
1179
+ const name = issue.received === 'undefined' ? CodeNames.Required : CodeNames.Type;
1180
+ const details = issue.received === 'undefined'
1181
+ ? `The property "${issue.path.slice(-1)}" is required here`
1182
+ : `The type of "${issue.path.slice(-1)}" is incorrect, expected type: ${issue.expected}`;
1183
+ return {
1184
+ details: details,
1185
+ name: name,
1186
+ path: issue.path,
1187
+ value: issue.received.toString(),
1188
+ };
1189
+ };
1190
+ const convertUnrecognizedKeys = (issue) => {
1191
+ const missingProperties = issue.keys.map((k) => `"${k}"`).join(', ');
1192
+ return {
1193
+ details: issue.keys.length > 1
1194
+ ? `The properties ${missingProperties} are not expected`
1195
+ : `The property ${missingProperties} is not expected`,
1196
+ name: CodeNames.Unexpected,
1197
+ path: issue.path,
1198
+ };
1199
+ };
1200
+ const convertInvalidString = (issue) => {
1201
+ return {
1202
+ details: issue.message || 'Invalid string',
1203
+ name: issue.validation === 'regex' ? CodeNames.Regex : CodeNames.Unexpected,
1204
+ path: issue.path,
1205
+ };
1206
+ };
1207
+ const convertInvalidEnumValue = (issue) => {
1208
+ return {
1209
+ details: issue.message || 'Value must be one of expected values',
1210
+ name: CodeNames.In,
1211
+ path: issue.path,
1212
+ value: issue.received.toString(),
1213
+ expected: issue.options,
1214
+ };
1215
+ };
1216
+ const convertInvalidLiteral = (issue) => {
1217
+ return {
1218
+ details: issue.message || 'Value must be one of expected values',
1219
+ name: CodeNames.In,
1220
+ path: issue.path,
1221
+ value: issue.received,
1222
+ expected: [issue.expected],
1223
+ };
1224
+ };
1225
+ const convertTooBig = (issue) => {
1226
+ return {
1227
+ details: issue.message || `Size should be at most ${issue.maximum}`,
1228
+ name: CodeNames.Size,
1229
+ path: issue.path,
1230
+ max: issue.maximum,
1231
+ };
1232
+ };
1233
+ const convertTooSmall = (issue) => {
1234
+ return {
1235
+ details: issue.message || `Size should be at least ${issue.minimum}`,
1236
+ name: CodeNames.Size,
1237
+ path: issue.path,
1238
+ min: issue.minimum,
1239
+ };
1240
+ };
1241
+ const defaultConversion = (issue) => {
1242
+ return {
1243
+ details: issue.message || 'An unexpected error occurred',
1244
+ name: CodeNames.Custom,
1245
+ path: issue.path.map(String),
1246
+ };
1247
+ };
1248
+ const zodToContentfulError = (issue) => {
1249
+ switch (issue.code) {
1250
+ case ZodIssueCode.invalid_type:
1251
+ return convertInvalidType(issue);
1252
+ case ZodIssueCode.unrecognized_keys:
1253
+ return convertUnrecognizedKeys(issue);
1254
+ case ZodIssueCode.invalid_enum_value:
1255
+ return convertInvalidEnumValue(issue);
1256
+ case ZodIssueCode.invalid_string:
1257
+ return convertInvalidString(issue);
1258
+ case ZodIssueCode.too_small:
1259
+ return convertTooSmall(issue);
1260
+ case ZodIssueCode.too_big:
1261
+ return convertTooBig(issue);
1262
+ case ZodIssueCode.invalid_literal:
1263
+ return convertInvalidLiteral(issue);
1264
+ default:
1265
+ return defaultConversion(issue);
1266
+ }
1267
+ };
1268
+
1269
+ const validateBreakpointsDefinition = (breakpoints) => {
1270
+ const result = z
1271
+ .array(BreakpointSchema)
1272
+ .superRefine(breakpointsRefinement)
1273
+ .safeParse(breakpoints);
1274
+ if (!result.success) {
1275
+ return {
1276
+ success: false,
1277
+ errors: result.error.issues.map(zodToContentfulError),
1278
+ };
1279
+ }
1280
+ return { success: true };
1281
+ };
1282
+
1283
+ let breakpointsRegistry = [];
1284
+ /**
1285
+ * Register custom breakpoints
1286
+ * @param breakpoints - [{[key:string]: string}]
1287
+ * @returns void
1288
+ */
1289
+ const defineBreakpoints = (breakpoints) => {
1290
+ Object.assign(breakpointsRegistry, breakpoints);
1291
+ };
1292
+ const runBreakpointsValidation = () => {
1293
+ if (!breakpointsRegistry.length)
1294
+ return;
1295
+ const validation = validateBreakpointsDefinition(breakpointsRegistry);
1296
+ if (!validation.success) {
1297
+ throw new Error(`Invalid breakpoints definition. Failed with errors: \n${JSON.stringify(validation.errors, null, 2)}`);
1298
+ }
1299
+ };
1300
+ // Used in the tests to get a breakpoint registration
1301
+ const getBreakpointRegistration = (id) => breakpointsRegistry.find((breakpoint) => breakpoint.id === id);
1302
+ // Used in the tests to reset the registry
1303
+ const resetBreakpointsRegistry = () => {
1304
+ breakpointsRegistry = [];
1305
+ };
1306
+
1307
+ const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
1308
+ const toCSSMediaQuery = ({ query }) => {
1309
+ if (query === '*')
1310
+ return undefined;
1311
+ const match = query.match(MEDIA_QUERY_REGEXP);
1312
+ if (!match)
1313
+ return undefined;
1314
+ const [, operator, value, unit] = match;
1315
+ if (operator === '<') {
1316
+ const maxScreenWidth = Number(value) - 1;
1317
+ return `(max-width: ${maxScreenWidth}${unit})`;
1318
+ }
1319
+ else if (operator === '>') {
1320
+ const minScreenWidth = Number(value) + 1;
1321
+ return `(min-width: ${minScreenWidth}${unit})`;
1322
+ }
1323
+ return undefined;
1324
+ };
1325
+ // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
1326
+ const findLast = (array, predicate) => {
1327
+ return array.reverse().find(predicate);
1328
+ };
1329
+ // Initialise media query matchers. This won't include the always matching fallback breakpoint.
1330
+ const mediaQueryMatcher = (breakpoints) => {
1331
+ const mediaQueryMatches = {};
1332
+ const mediaQueryMatchers = breakpoints
1333
+ .map((breakpoint) => {
1334
+ const cssMediaQuery = toCSSMediaQuery(breakpoint);
1335
+ if (!cssMediaQuery)
1336
+ return undefined;
1337
+ if (typeof window === 'undefined')
1338
+ return undefined;
1339
+ const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
1340
+ mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
1341
+ return { id: breakpoint.id, signal: mediaQueryMatcher };
1342
+ })
1343
+ .filter((matcher) => !!matcher);
1344
+ return [mediaQueryMatchers, mediaQueryMatches];
1345
+ };
1346
+ const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
1347
+ // The breakpoints are ordered (desktop-first: descending by screen width)
1348
+ const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
1349
+ id,
1350
+ index,
1351
+ // The fallback breakpoint with wildcard query will always match
1352
+ isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
1353
+ }));
1354
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
1355
+ const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
1356
+ return mostSpecificIndex ?? fallbackBreakpointIndex;
1357
+ };
1358
+ const getFallbackBreakpointIndex = (breakpoints) => {
1359
+ // We assume that there will be a single breakpoint which uses the wildcard query.
1360
+ // If there is none, we just take the first one in the list.
1361
+ return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
1362
+ };
1363
+ const builtInStylesWithDesignTokens = [
1364
+ 'cfMargin',
1365
+ 'cfPadding',
1366
+ 'cfGap',
1367
+ 'cfWidth',
1368
+ 'cfHeight',
1369
+ 'cfBackgroundColor',
1370
+ 'cfBorder',
1371
+ 'cfBorderRadius',
1372
+ 'cfFontSize',
1373
+ 'cfLineHeight',
1374
+ 'cfLetterSpacing',
1375
+ 'cfTextColor',
1376
+ 'cfMaxWidth',
1377
+ ];
1378
+ const isValidBreakpointValue = (value) => {
1379
+ return value !== undefined && value !== null && value !== '';
1380
+ };
1381
+ const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, fallbackBreakpointIndex, variableName, resolveDesignTokens = true) => {
1382
+ const eventuallyResolveDesignTokens = (value) => {
1383
+ // For some built-in design properties, we support design tokens
1384
+ if (builtInStylesWithDesignTokens.includes(variableName)) {
1385
+ return getDesignTokenRegistration(value, variableName);
1386
+ }
1387
+ // For all other properties, we just return the breakpoint-specific value
1388
+ return value;
1389
+ };
1390
+ if (valuesByBreakpoint instanceof Object) {
1391
+ // Assume that the values are sorted by media query to apply the cascading CSS logic
1392
+ for (let index = activeBreakpointIndex; index >= 0; index--) {
1393
+ const breakpointId = breakpoints[index]?.id;
1394
+ if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
1395
+ // If the value is defined, we use it and stop the breakpoints cascade
1396
+ if (resolveDesignTokens) {
1397
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
1398
+ }
1399
+ return valuesByBreakpoint[breakpointId];
1400
+ }
1401
+ }
1402
+ const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
1403
+ if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
1404
+ if (resolveDesignTokens) {
1405
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
1406
+ }
1407
+ return valuesByBreakpoint[fallbackBreakpointId];
1408
+ }
1409
+ }
1410
+ else {
1411
+ // Old design properties did not support breakpoints, keep for backward compatibility
1412
+ return valuesByBreakpoint;
1413
+ }
1414
+ };
1415
+
1416
+ const CF_DEBUG_KEY = 'cf_debug';
1417
+ class DebugLogger {
1418
+ constructor() {
1419
+ // Public methods for logging
1420
+ this.error = this.logger('error');
1421
+ this.warn = this.logger('warn');
1422
+ this.log = this.logger('log');
1423
+ this.debug = this.logger('debug');
1424
+ if (typeof localStorage === 'undefined') {
1425
+ this.enabled = false;
1426
+ return;
1427
+ }
1428
+ // Default to checking localStorage for the debug mode on initialization if in browser
1429
+ this.enabled = localStorage.getItem(CF_DEBUG_KEY) === 'true';
1430
+ }
1431
+ static getInstance() {
1432
+ if (this.instance === null) {
1433
+ this.instance = new DebugLogger();
1434
+ }
1435
+ return this.instance;
1436
+ }
1437
+ getEnabled() {
1438
+ return this.enabled;
1439
+ }
1440
+ setEnabled(enabled) {
1441
+ this.enabled = enabled;
1442
+ if (typeof localStorage === 'undefined') {
1443
+ return;
1444
+ }
1445
+ if (enabled) {
1446
+ localStorage.setItem(CF_DEBUG_KEY, 'true');
1447
+ }
1448
+ else {
1449
+ localStorage.removeItem(CF_DEBUG_KEY);
1450
+ }
1451
+ }
1452
+ // Log method for different levels (error, warn, log)
1453
+ logger(level) {
1454
+ return (...args) => {
1455
+ if (this.enabled) {
1456
+ console[level]('[cf-experiences-sdk]', ...args);
1457
+ }
1458
+ };
1459
+ }
1460
+ }
1461
+ DebugLogger.instance = null;
1462
+ const debug = DebugLogger.getInstance();
1463
+ const enableDebug = () => {
1464
+ debug.setEnabled(true);
1465
+ console.log('Debug mode enabled');
1466
+ };
1467
+ const disableDebug = () => {
1468
+ debug.setEnabled(false);
1469
+ console.log('Debug mode disabled');
1470
+ };
1471
+
1472
+ const findOutermostCoordinates = (first, second) => {
1473
+ return {
1474
+ top: Math.min(first.top, second.top),
1475
+ right: Math.max(first.right, second.right),
1476
+ bottom: Math.max(first.bottom, second.bottom),
1477
+ left: Math.min(first.left, second.left),
1478
+ };
1479
+ };
1480
+ const getElementCoordinates = (element) => {
1481
+ const rect = element.getBoundingClientRect();
1482
+ /**
1483
+ * If element does not have children, or element has it's own width or height,
1484
+ * return the element's coordinates.
1485
+ */
1486
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
1487
+ return rect;
1488
+ }
1489
+ const rects = [];
1490
+ /**
1491
+ * If element has children, or element does not have it's own width and height,
1492
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
1493
+ * as the coordinate of the element.
1494
+ *
1495
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
1496
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
1497
+ */
1498
+ for (const child of element.children) {
1499
+ const childRect = getElementCoordinates(child);
1500
+ if (childRect.width !== 0 || childRect.height !== 0) {
1501
+ const { top, right, bottom, left } = childRect;
1502
+ rects.push({ top, right, bottom, left });
1503
+ }
1504
+ }
1505
+ if (rects.length === 0) {
1506
+ return rect;
1507
+ }
1508
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
1509
+ return DOMRect.fromRect({
1510
+ x: left,
1511
+ y: top,
1512
+ height: bottom - top,
1513
+ width: right - left,
1514
+ });
1515
+ };
1516
+
1517
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1518
+ const isLinkToAsset = (variable) => {
1519
+ if (!variable)
1520
+ return false;
1521
+ if (typeof variable !== 'object')
1522
+ return false;
1523
+ return (variable.sys?.linkType === 'Asset' &&
1524
+ typeof variable.sys?.id === 'string' &&
1525
+ !!variable.sys?.id &&
1526
+ variable.sys?.type === 'Link');
1527
+ };
1528
+
1529
+ const isLink = (maybeLink) => {
1530
+ if (maybeLink === null)
1531
+ return false;
1532
+ if (typeof maybeLink !== 'object')
1533
+ return false;
1534
+ const link = maybeLink;
1535
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link';
1536
+ };
1537
+
1538
+ /**
1539
+ * Localizes the provided entry or asset to match the regular format of CDA/CPA entities.
1540
+ * Note that this function does not apply a fallback to the default locale nor does it check
1541
+ * the content type for the localization setting of each field.
1542
+ * It will simply resolve each field to the requested locale. As using single and multiple
1543
+ * reference fields is still considered an experimental feature, this function does not handle
1544
+ * recursive localization of deeper referenced entities.
1545
+ *
1546
+ * If the entity is already localized, it will return the entity as is.
1547
+ *
1548
+ * Note that localization is later on determined by the existence of the `sys.locale` property (matching the API shape).
1549
+ *
1550
+ * @example
1551
+ * ```
1552
+ * const multiLocaleEntry = { fields: { title: { 'en-US': 'Hello' } } };
1553
+ * const localizedEntry = localizeEntity(multiLocaleEntry, 'en-US');
1554
+ * console.log(localizedEntry.fields.title); // 'Hello'
1555
+ * ```
1556
+ */
1557
+ function localizeEntity(entity, locale) {
1558
+ if (!entity || !entity.fields) {
1559
+ throw new Error('Invalid entity provided');
1560
+ }
1561
+ if (entity.sys.locale) {
1562
+ return entity;
1563
+ }
1564
+ const cloned = structuredClone(entity);
1565
+ // Set the requested locale as entry locale to follow the API shape:
1566
+ // https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes
1567
+ cloned.sys.locale = locale;
1568
+ for (const key in cloned.fields) {
1569
+ cloned.fields[key] = cloned.fields[key][locale];
1570
+ }
1571
+ return cloned;
1572
+ }
1573
+
1574
+ /**
1575
+ * This module encapsulates format of the path to a deep reference.
1576
+ */
1577
+ const parseDataSourcePathIntoFieldset = (path) => {
1578
+ const parsedPath = parseDeepPath(path);
1579
+ if (null === parsedPath) {
1580
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1581
+ }
1582
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
1583
+ };
1584
+ /**
1585
+ * Parse path into components, supports L1 references (one reference follow) atm.
1586
+ * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
1587
+ * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
1588
+ * @returns
1589
+ */
1590
+ const parseDataSourcePathWithL1DeepBindings = (path) => {
1591
+ const parsedPath = parseDeepPath(path);
1592
+ if (null === parsedPath) {
1593
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1594
+ }
1595
+ return {
1596
+ key: parsedPath.key,
1597
+ field: parsedPath.fields[0],
1598
+ referentField: parsedPath.fields[1],
1599
+ };
1600
+ };
1601
+ /**
1602
+ * Detects if paths is valid deep-path, like:
1603
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1604
+ * or regular, like:
1605
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1606
+ * @returns
1607
+ */
1608
+ const isDeepPath = (deepPathCandidate) => {
1609
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
1610
+ if (!deepPathParsed) {
1611
+ return false;
1612
+ }
1613
+ return deepPathParsed.fields.length > 1;
1614
+ };
1615
+ const parseDeepPath = (deepPathCandidate) => {
1616
+ // ALGORITHM:
1617
+ // We start with deep path in form:
1618
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1619
+ // First turn string into array of segments
1620
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1621
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1622
+ // [
1623
+ // [ "", "uuid123" ],
1624
+ // [ "fields", "mainStory", "~locale" ],
1625
+ // [ "fields", "cover", "~locale" ],
1626
+ // [ "fields", "title", "~locale" ]
1627
+ // ]
1628
+ // Then check "initial" chunk for corretness
1629
+ // Then check all "field-leading" chunks for correctness
1630
+ const isValidInitialChunk = (initialChunk) => {
1631
+ // must have start with '' and have at least 2 segments, second non-empty
1632
+ // eg. /-_432uuid123123
1633
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1634
+ };
1635
+ const isValidFieldChunk = (fieldChunk) => {
1636
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1637
+ // eg. fields/-32234mainStory/~locale
1638
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1639
+ };
1640
+ const deepPathSegments = deepPathCandidate.split('/');
1641
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1642
+ if (chunks.length <= 1) {
1643
+ return null; // malformed path, even regular paths have at least 2 chunks
1644
+ }
1645
+ else if (chunks.length === 2) {
1646
+ return null; // deep paths have at least 3 chunks
1647
+ }
1648
+ // With 3+ chunks we can now check for deep path correctness
1649
+ const [initialChunk, ...fieldChunks] = chunks;
1650
+ if (!isValidInitialChunk(initialChunk)) {
1651
+ return null;
1652
+ }
1653
+ if (!fieldChunks.every(isValidFieldChunk)) {
1654
+ return null;
1655
+ }
1656
+ return {
1657
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1658
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1659
+ };
1660
+ };
1661
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1662
+ const chunks = [];
1663
+ let currentChunk = [];
1664
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1665
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
1666
+ for (let i = 0; i < segments.length; i++) {
1667
+ const isInitialElement = i === 0;
1668
+ const segment = segments[i];
1669
+ if (isInitialElement) {
1670
+ currentChunk = [segment];
1671
+ }
1672
+ else if (isSegmentBeginningOfChunk(segment)) {
1673
+ chunks.push(currentChunk);
1674
+ currentChunk = [segment];
1675
+ }
1676
+ else {
1677
+ currentChunk.push(segment);
1678
+ }
1679
+ }
1680
+ chunks.push(currentChunk);
1681
+ return chunks.filter(excludeEmptyChunks);
1682
+ };
1683
+ const lastPathNamedSegmentEq = (path, expectedName) => {
1684
+ // `/key123/fields/featureImage/~locale/fields/file/~locale`
1685
+ // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
1686
+ const segments = path.split('/');
1687
+ if (segments.length < 2) {
1688
+ console.warn(`[experiences-sdk-react] Attempting to check whether last named segment of the path (${path}) equals to '${expectedName}', but the path doesn't have enough segments.`);
1689
+ return false;
1690
+ }
1691
+ const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
1692
+ return secondLast === expectedName;
1693
+ };
1694
+
1695
+ const resolveHyperlinkPattern = (pattern, entry, locale) => {
1696
+ if (!entry || !locale)
1697
+ return null;
1698
+ const variables = {
1699
+ entry,
1700
+ locale,
1529
1701
  };
1702
+ return buildTemplate({ template: pattern, context: variables });
1530
1703
  };
1531
- const convertUnrecognizedKeys = (issue) => {
1532
- const missingProperties = issue.keys.map((k) => `"${k}"`).join(', ');
1533
- return {
1534
- details: issue.keys.length > 1
1535
- ? `The properties ${missingProperties} are not expected`
1536
- : `The property ${missingProperties} is not expected`,
1537
- name: CodeNames.Unexpected,
1538
- path: issue.path,
1539
- };
1704
+ function getValue(obj, path) {
1705
+ return path
1706
+ .replace(/\[/g, '.')
1707
+ .replace(/\]/g, '')
1708
+ .split('.')
1709
+ .reduce((o, k) => (o || {})[k], obj);
1710
+ }
1711
+ function addLocale(str, locale) {
1712
+ const fieldsIndicator = 'fields';
1713
+ const fieldsIndex = str.indexOf(fieldsIndicator);
1714
+ if (fieldsIndex !== -1) {
1715
+ const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
1716
+ if (dotIndex !== -1) {
1717
+ return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
1718
+ }
1719
+ }
1720
+ return str;
1721
+ }
1722
+ function getTemplateValue(ctx, path) {
1723
+ const pathWithLocale = addLocale(path, ctx.locale);
1724
+ const retrievedValue = getValue(ctx, pathWithLocale);
1725
+ return typeof retrievedValue === 'object' && retrievedValue !== null
1726
+ ? retrievedValue[ctx.locale]
1727
+ : retrievedValue;
1728
+ }
1729
+ function buildTemplate({ template, context, }) {
1730
+ const localeVariable = /{\s*locale\s*}/g;
1731
+ // e.g. "{ page.sys.id }"
1732
+ const variables = /{\s*([\S]+?)\s*}/g;
1733
+ return (template
1734
+ // first replace the locale pattern
1735
+ .replace(localeVariable, context.locale)
1736
+ // then resolve the remaining variables
1737
+ .replace(variables, (_, path) => {
1738
+ const fallback = path + '_NOT_FOUND';
1739
+ const value = getTemplateValue(context, path) ?? fallback;
1740
+ // using _.result didn't gave proper results so we run our own version of it
1741
+ return String(typeof value === 'function' ? value() : value);
1742
+ }));
1743
+ }
1744
+
1745
+ const stylesToKeep = ['cfImageAsset'];
1746
+ const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
1747
+ const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
1748
+ const sanitizeNodeProps = (nodeProps) => {
1749
+ return omit(nodeProps, stylesToRemove, propsToRemove);
1540
1750
  };
1541
- const convertInvalidString = (issue) => {
1542
- return {
1543
- details: issue.message || 'Invalid string',
1544
- name: issue.validation === 'regex' ? CodeNames.Regex : CodeNames.Unexpected,
1545
- path: issue.path,
1546
- };
1751
+
1752
+ const transformVisibility = (value) => {
1753
+ if (value === false) {
1754
+ return {
1755
+ display: 'none !important',
1756
+ };
1757
+ }
1758
+ // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
1759
+ return {};
1547
1760
  };
1548
- const convertInvalidEnumValue = (issue) => {
1761
+ // Keep this for backwards compatibility - deleting this would be a breaking change
1762
+ // because existing components on a users experience will have the width value as fill
1763
+ // rather than 100%
1764
+ const transformFill = (value) => (value === 'fill' ? '100%' : value);
1765
+ const transformGridColumn = (span) => {
1766
+ if (!span) {
1767
+ return {};
1768
+ }
1549
1769
  return {
1550
- details: issue.message || 'Value must be one of expected values',
1551
- name: CodeNames.In,
1552
- path: issue.path,
1553
- value: issue.received.toString(),
1554
- expected: issue.options,
1770
+ gridColumn: `span ${span}`,
1555
1771
  };
1556
1772
  };
1557
- const convertInvalidLiteral = (issue) => {
1773
+ const transformBorderStyle = (value) => {
1774
+ if (!value)
1775
+ return {};
1776
+ const parts = value.split(' ');
1777
+ // Just accept the passed value
1778
+ if (parts.length < 3)
1779
+ return { border: value };
1780
+ const [borderSize, borderStyle, ...borderColorParts] = parts;
1781
+ const borderColor = borderColorParts.join(' ');
1558
1782
  return {
1559
- details: issue.message || 'Value must be one of expected values',
1560
- name: CodeNames.In,
1561
- path: issue.path,
1562
- value: issue.received,
1563
- expected: [issue.expected],
1783
+ border: `${borderSize} ${borderStyle} ${borderColor}`,
1564
1784
  };
1565
1785
  };
1566
- const convertTooBig = (issue) => {
1567
- return {
1568
- details: issue.message || `Size should be at most ${issue.maximum}`,
1569
- name: CodeNames.Size,
1570
- path: issue.path,
1571
- max: issue.maximum,
1786
+ const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
1787
+ ? {
1788
+ alignItems: cfHorizontalAlignment,
1789
+ justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
1790
+ }
1791
+ : {
1792
+ alignItems: cfVerticalAlignment,
1793
+ justifyContent: cfHorizontalAlignment === 'center'
1794
+ ? `safe ${cfHorizontalAlignment}`
1795
+ : cfHorizontalAlignment,
1572
1796
  };
1573
- };
1574
- const convertTooSmall = (issue) => {
1797
+ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
1798
+ const matchBackgroundSize = (scaling) => {
1799
+ if ('fill' === scaling)
1800
+ return 'cover';
1801
+ if ('fit' === scaling)
1802
+ return 'contain';
1803
+ };
1804
+ const matchBackgroundPosition = (alignment) => {
1805
+ if (!alignment || 'string' !== typeof alignment) {
1806
+ return;
1807
+ }
1808
+ let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
1809
+ // Special case for handling single values
1810
+ // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
1811
+ if (horizontalAlignment && !verticalAlignment) {
1812
+ const singleValue = horizontalAlignment;
1813
+ switch (singleValue) {
1814
+ case 'left':
1815
+ horizontalAlignment = 'left';
1816
+ verticalAlignment = 'center';
1817
+ break;
1818
+ case 'right':
1819
+ horizontalAlignment = 'right';
1820
+ verticalAlignment = 'center';
1821
+ break;
1822
+ case 'center':
1823
+ horizontalAlignment = 'center';
1824
+ verticalAlignment = 'center';
1825
+ break;
1826
+ case 'top':
1827
+ horizontalAlignment = 'center';
1828
+ verticalAlignment = 'top';
1829
+ break;
1830
+ case 'bottom':
1831
+ horizontalAlignment = 'center';
1832
+ verticalAlignment = 'bottom';
1833
+ break;
1834
+ // just fall down to the normal validation logic for horiz and vert
1835
+ }
1836
+ }
1837
+ const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
1838
+ const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
1839
+ horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
1840
+ verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
1841
+ return `${horizontalAlignment} ${verticalAlignment}`;
1842
+ };
1843
+ if (!cfBackgroundImageUrl) {
1844
+ return;
1845
+ }
1846
+ let backgroundImage;
1847
+ let backgroundImageSet;
1848
+ if (typeof cfBackgroundImageUrl === 'string') {
1849
+ backgroundImage = `url(${cfBackgroundImageUrl})`;
1850
+ }
1851
+ else {
1852
+ const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
1853
+ backgroundImage = `url(${cfBackgroundImageUrl.url})`;
1854
+ backgroundImageSet = `image-set(${imgSet})`;
1855
+ }
1575
1856
  return {
1576
- details: issue.message || `Size should be at least ${issue.minimum}`,
1577
- name: CodeNames.Size,
1578
- path: issue.path,
1579
- min: issue.minimum,
1857
+ backgroundImage,
1858
+ backgroundImage2: backgroundImageSet,
1859
+ backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
1860
+ backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
1861
+ backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
1580
1862
  };
1581
1863
  };
1582
- const defaultConversion = (issue) => {
1583
- return {
1584
- details: issue.message || 'An unexpected error occurred',
1585
- name: CodeNames.Custom,
1586
- path: issue.path.map(String),
1864
+
1865
+ const toCSSAttribute = (key) => {
1866
+ let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
1867
+ // Remove the number from the end of the key to allow for overrides on style properties
1868
+ val = val.replace(/\d+$/, '');
1869
+ return val;
1870
+ };
1871
+ /**
1872
+ * Turns a list of CSSProperties into a joined CSS string that can be
1873
+ * used for <style> tags. Per default it creates a minimized version.
1874
+ * For editor mode, use the `useWhitespaces` flag to create a more readable version.
1875
+ *
1876
+ * @param cssProperties list of CSS properties
1877
+ * @param useWhitespaces adds whitespaces and newlines between each rule
1878
+ * @returns a string of CSS rules
1879
+ */
1880
+ const stringifyCssProperties = (cssProperties, useWhitespaces = false) => {
1881
+ const rules = Object.entries(cssProperties)
1882
+ .filter(([, value]) => value !== undefined)
1883
+ .map(([key, value]) => useWhitespaces ? `${toCSSAttribute(key)}: ${value};` : `${toCSSAttribute(key)}:${value};`);
1884
+ return rules.join(useWhitespaces ? '\n' : '');
1885
+ };
1886
+ const buildStyleTag = ({ styles, nodeId }) => {
1887
+ const generatedStyles = stringifyCssProperties(styles, true);
1888
+ const className = `cfstyles-${nodeId ? nodeId : md5(generatedStyles)}`;
1889
+ const styleRule = `.${className}{ ${generatedStyles} }`;
1890
+ return [className, styleRule];
1891
+ };
1892
+ /**
1893
+ * Takes plain design values and transforms them into CSS properties. Undefined values will
1894
+ * be filtered out.
1895
+ *
1896
+ * **Example Input**
1897
+ * ```
1898
+ * values = {
1899
+ * cfVisibility: 'visible',
1900
+ * cfMargin: '10px',
1901
+ * cfFlexReverse: true,
1902
+ * cfImageOptions: { objectFit: 'cover' },
1903
+ * // ...
1904
+ * }
1905
+ * ```
1906
+ * **Example Output**
1907
+ * ```
1908
+ * cssProperties = {
1909
+ * margin: '10px',
1910
+ * flexDirection: 'row-reverse',
1911
+ * objectFit: 'cover',
1912
+ * // ...
1913
+ * }
1914
+ * ```
1915
+ */
1916
+ const buildCfStyles = (values) => {
1917
+ const cssProperties = {
1918
+ boxSizing: 'border-box',
1919
+ ...transformVisibility(values.cfVisibility),
1920
+ margin: values.cfMargin,
1921
+ padding: values.cfPadding,
1922
+ backgroundColor: values.cfBackgroundColor,
1923
+ width: transformFill(values.cfWidth || values.cfImageOptions?.width),
1924
+ height: transformFill(values.cfHeight || values.cfImageOptions?.height),
1925
+ maxWidth: values.cfMaxWidth,
1926
+ ...transformGridColumn(values.cfColumnSpan),
1927
+ ...transformBorderStyle(values.cfBorder),
1928
+ borderRadius: values.cfBorderRadius,
1929
+ gap: values.cfGap,
1930
+ ...transformAlignment(values.cfHorizontalAlignment, values.cfVerticalAlignment, values.cfFlexDirection),
1931
+ flexDirection: values.cfFlexReverse && values.cfFlexDirection
1932
+ ? `${values.cfFlexDirection}-reverse`
1933
+ : values.cfFlexDirection,
1934
+ flexWrap: values.cfFlexWrap,
1935
+ ...transformBackgroundImage(values.cfBackgroundImageUrl, values.cfBackgroundImageOptions),
1936
+ fontSize: values.cfFontSize,
1937
+ fontWeight: values.cfTextBold ? 'bold' : values.cfFontWeight,
1938
+ fontStyle: values.cfTextItalic ? 'italic' : undefined,
1939
+ textDecoration: values.cfTextUnderline ? 'underline' : undefined,
1940
+ lineHeight: values.cfLineHeight,
1941
+ letterSpacing: values.cfLetterSpacing,
1942
+ color: values.cfTextColor,
1943
+ textAlign: values.cfTextAlign,
1944
+ textTransform: values.cfTextTransform,
1945
+ objectFit: values.cfImageOptions?.objectFit,
1946
+ objectPosition: values.cfImageOptions?.objectPosition,
1587
1947
  };
1948
+ const cssPropertiesWithoutUndefined = Object.fromEntries(Object.entries(cssProperties).filter(([, value]) => value !== undefined));
1949
+ return cssPropertiesWithoutUndefined;
1588
1950
  };
1589
- const zodToContentfulError = (issue) => {
1590
- switch (issue.code) {
1591
- case ZodIssueCode.invalid_type:
1592
- return convertInvalidType(issue);
1593
- case ZodIssueCode.unrecognized_keys:
1594
- return convertUnrecognizedKeys(issue);
1595
- case ZodIssueCode.invalid_enum_value:
1596
- return convertInvalidEnumValue(issue);
1597
- case ZodIssueCode.invalid_string:
1598
- return convertInvalidString(issue);
1599
- case ZodIssueCode.too_small:
1600
- return convertTooSmall(issue);
1601
- case ZodIssueCode.too_big:
1602
- return convertTooBig(issue);
1603
- case ZodIssueCode.invalid_literal:
1604
- return convertInvalidLiteral(issue);
1605
- default:
1606
- return defaultConversion(issue);
1607
- }
1608
- };
1609
-
1610
- const validateBreakpointsDefinition = (breakpoints) => {
1611
- const result = z
1612
- .array(BreakpointSchema)
1613
- .superRefine(breakpointsRefinement)
1614
- .safeParse(breakpoints);
1615
- if (!result.success) {
1951
+ /**
1952
+ * **Only meant to be used in editor mode!**
1953
+ *
1954
+ * If the node is an empty structure component with a relative height (e.g. '100%'),
1955
+ * it might render with a zero height. In this case, add a min height of 80px to ensure
1956
+ * that child nodes can be added via drag & drop.
1957
+ */
1958
+ const addMinHeightForEmptyStructures = (cssProperties, node) => {
1959
+ if (!node.children.length &&
1960
+ isStructureWithRelativeHeight(node.definitionId, cssProperties.height)) {
1616
1961
  return {
1617
- success: false,
1618
- errors: result.error.issues.map(zodToContentfulError),
1962
+ ...cssProperties,
1963
+ minHeight: EMPTY_CONTAINER_HEIGHT,
1619
1964
  };
1620
1965
  }
1621
- return { success: true };
1966
+ return cssProperties;
1622
1967
  };
1623
-
1624
- let breakpointsRegistry = [];
1625
1968
  /**
1626
- * Register custom breakpoints
1627
- * @param breakpoints - [{[key:string]: string}]
1628
- * @returns void
1969
+ * Container/section default behavior:
1970
+ * Default height => height: EMPTY_CONTAINER_HEIGHT
1971
+ * If a container component has children => height: 'fit-content'
1629
1972
  */
1630
- const defineBreakpoints = (breakpoints) => {
1631
- Object.assign(breakpointsRegistry, breakpoints);
1632
- };
1633
- const runBreakpointsValidation = () => {
1634
- if (!breakpointsRegistry.length)
1635
- return;
1636
- const validation = validateBreakpointsDefinition(breakpointsRegistry);
1637
- if (!validation.success) {
1638
- throw new Error(`Invalid breakpoints definition. Failed with errors: \n${JSON.stringify(validation.errors, null, 2)}`);
1973
+ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1974
+ if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
1975
+ return value;
1639
1976
  }
1640
- };
1641
- // Used in the tests to get a breakpoint registration
1642
- const getBreakpointRegistration = (id) => breakpointsRegistry.find((breakpoint) => breakpoint.id === id);
1643
- // Used in the tests to reset the registry
1644
- const resetBreakpointsRegistry = () => {
1645
- breakpointsRegistry = [];
1977
+ if (children.length) {
1978
+ return '100%';
1979
+ }
1980
+ return EMPTY_CONTAINER_HEIGHT;
1646
1981
  };
1647
1982
 
1648
1983
  function getOptimizedImageUrl(url, width, quality, format) {
@@ -2499,138 +2834,30 @@ function getArrayValue(entryOrAsset, path, entityStore) {
2499
2834
  return undefined;
2500
2835
  }
2501
2836
  });
2502
- return result;
2503
- }
2504
-
2505
- const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableType, path) => {
2506
- const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
2507
- if (!entityOrAsset)
2508
- return;
2509
- switch (variableType) {
2510
- case 'Media':
2511
- // If we bound a normal entry field to the media variable we just return the bound value
2512
- if (entityOrAsset.sys.type === 'Entry') {
2513
- return getBoundValue(entityOrAsset, path);
2514
- }
2515
- return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
2516
- case 'RichText':
2517
- return transformRichText(entityOrAsset, entityStore, path);
2518
- case 'Array':
2519
- return getArrayValue(entityOrAsset, path, entityStore);
2520
- case 'Link':
2521
- return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
2522
- default:
2523
- return getBoundValue(entityOrAsset, path);
2524
- }
2525
- };
2526
-
2527
- const getDataFromTree = (tree) => {
2528
- let dataSource = {};
2529
- let unboundValues = {};
2530
- const queue = [...tree.root.children];
2531
- while (queue.length) {
2532
- const node = queue.shift();
2533
- if (!node) {
2534
- continue;
2535
- }
2536
- dataSource = { ...dataSource, ...node.data.dataSource };
2537
- unboundValues = { ...unboundValues, ...node.data.unboundValues };
2538
- if (node.children.length) {
2539
- queue.push(...node.children);
2540
- }
2541
- }
2542
- return {
2543
- dataSource,
2544
- unboundValues,
2545
- };
2546
- };
2547
- /**
2548
- * Gets calculates the index to drop the dragged component based on the mouse position
2549
- * @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
2550
- */
2551
- const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
2552
- const APPEND_INSIDE = dropReceiverNode.children.length;
2553
- const PREPEND_INSIDE = 0;
2554
- if (isMouseAtTopBorder || isMouseAtBottomBorder) {
2555
- const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2556
- const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2557
- const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2558
- return {
2559
- // when the mouse is around the border we want to drop the new component as a new section onto the root node
2560
- node: dropReceiverParentNode,
2561
- index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2562
- };
2563
- }
2564
- // if over one of the section indicators
2565
- if (isOverTopIndicator || isOverBottomIndicator) {
2566
- const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2567
- const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2568
- const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2569
- return {
2570
- // when the mouse is around the border we want to drop the new component as a new section onto the root node
2571
- node: dropReceiverParentNode,
2572
- index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2573
- };
2574
- }
2575
- if (flexDirection === undefined || flexDirection === 'row') {
2576
- return {
2577
- node: dropReceiverNode,
2578
- index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2579
- };
2580
- }
2581
- else {
2582
- return {
2583
- node: dropReceiverNode,
2584
- index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2585
- };
2586
- }
2587
- };
2588
- const generateRandomId = (letterCount) => {
2589
- const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
2590
- const NUMS = '0123456789';
2591
- const ALNUM = NUMS + LETTERS;
2592
- const times = (n, callback) => Array.from({ length: n }, callback);
2593
- const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
2594
- return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
2595
- };
2596
- const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
2597
- if (!usedComponents?.length)
2598
- return false;
2599
- return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
2600
- };
2601
- /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
2602
- const checkIsAssembly = checkIsAssemblyNode;
2603
- /**
2604
- * This check assumes that the entry is already ensured to be an experience, i.e. the
2605
- * content type of the entry is an experience type with the necessary annotations.
2606
- **/
2607
- const checkIsAssemblyEntry = (entry) => {
2608
- return Boolean(entry.fields?.componentSettings);
2609
- };
2610
- const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
2611
- function parseCSSValue(input) {
2612
- const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
2613
- const match = input.match(regex);
2614
- if (match) {
2615
- return {
2616
- value: parseFloat(match[1]),
2617
- unit: match[3],
2618
- };
2619
- }
2620
- return null;
2621
- }
2622
- function getTargetValueInPixels(targetWidthObject) {
2623
- switch (targetWidthObject.unit) {
2624
- case 'px':
2625
- return targetWidthObject.value;
2626
- case 'em':
2627
- return targetWidthObject.value * 16;
2628
- case 'rem':
2629
- return targetWidthObject.value * 16;
2837
+ return result;
2838
+ }
2839
+
2840
+ const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableType, path) => {
2841
+ const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
2842
+ if (!entityOrAsset)
2843
+ return;
2844
+ switch (variableType) {
2845
+ case 'Media':
2846
+ // If we bound a normal entry field to the media variable we just return the bound value
2847
+ if (entityOrAsset.sys.type === 'Entry') {
2848
+ return getBoundValue(entityOrAsset, path);
2849
+ }
2850
+ return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
2851
+ case 'RichText':
2852
+ return transformRichText(entityOrAsset, entityStore, path);
2853
+ case 'Array':
2854
+ return getArrayValue(entityOrAsset, path, entityStore);
2855
+ case 'Link':
2856
+ return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
2630
2857
  default:
2631
- return targetWidthObject.value;
2858
+ return getBoundValue(entityOrAsset, path);
2632
2859
  }
2633
- }
2860
+ };
2634
2861
 
2635
2862
  const isExperienceEntry = (entry) => {
2636
2863
  return (entry?.sys?.type === 'Entry' &&
@@ -2642,368 +2869,177 @@ const isExperienceEntry = (entry) => {
2642
2869
  typeof entry.fields.componentTree.schemaVersion === 'string');
2643
2870
  };
2644
2871
 
2645
- const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
2646
- const toCSSMediaQuery = ({ query }) => {
2647
- if (query === '*')
2648
- return undefined;
2649
- const match = query.match(MEDIA_QUERY_REGEXP);
2650
- if (!match)
2651
- return undefined;
2652
- const [, operator, value, unit] = match;
2653
- if (operator === '<') {
2654
- const maxScreenWidth = Number(value) - 1;
2655
- return `(max-width: ${maxScreenWidth}${unit})`;
2656
- }
2657
- else if (operator === '>') {
2658
- const minScreenWidth = Number(value) + 1;
2659
- return `(min-width: ${minScreenWidth}${unit})`;
2660
- }
2661
- return undefined;
2662
- };
2663
- // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
2664
- const findLast = (array, predicate) => {
2665
- return array.reverse().find(predicate);
2666
- };
2667
- // Initialise media query matchers. This won't include the always matching fallback breakpoint.
2668
- const mediaQueryMatcher = (breakpoints) => {
2669
- const mediaQueryMatches = {};
2670
- const mediaQueryMatchers = breakpoints
2671
- .map((breakpoint) => {
2672
- const cssMediaQuery = toCSSMediaQuery(breakpoint);
2673
- if (!cssMediaQuery)
2674
- return undefined;
2675
- if (typeof window === 'undefined')
2676
- return undefined;
2677
- const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
2678
- mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
2679
- return { id: breakpoint.id, signal: mediaQueryMatcher };
2680
- })
2681
- .filter((matcher) => !!matcher);
2682
- return [mediaQueryMatchers, mediaQueryMatches];
2683
- };
2684
- const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
2685
- // The breakpoints are ordered (desktop-first: descending by screen width)
2686
- const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
2687
- id,
2688
- index,
2689
- // The fallback breakpoint with wildcard query will always match
2690
- isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
2691
- }));
2692
- // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
2693
- const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
2694
- return mostSpecificIndex ?? fallbackBreakpointIndex;
2695
- };
2696
- const getFallbackBreakpointIndex = (breakpoints) => {
2697
- // We assume that there will be a single breakpoint which uses the wildcard query.
2698
- // If there is none, we just take the first one in the list.
2699
- return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
2700
- };
2701
- const builtInStylesWithDesignTokens = [
2702
- 'cfMargin',
2703
- 'cfPadding',
2704
- 'cfGap',
2705
- 'cfWidth',
2706
- 'cfHeight',
2707
- 'cfBackgroundColor',
2708
- 'cfBorder',
2709
- 'cfBorderRadius',
2710
- 'cfFontSize',
2711
- 'cfLineHeight',
2712
- 'cfLetterSpacing',
2713
- 'cfTextColor',
2714
- 'cfMaxWidth',
2715
- ];
2716
- const isValidBreakpointValue = (value) => {
2717
- return value !== undefined && value !== null && value !== '';
2718
- };
2719
- const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, fallbackBreakpointIndex, variableName, resolveDesignTokens = true) => {
2720
- const eventuallyResolveDesignTokens = (value) => {
2721
- // For some built-in design properties, we support design tokens
2722
- if (builtInStylesWithDesignTokens.includes(variableName)) {
2723
- return getDesignTokenRegistration(value, variableName);
2724
- }
2725
- // For all other properties, we just return the breakpoint-specific value
2726
- return value;
2727
- };
2728
- if (valuesByBreakpoint instanceof Object) {
2729
- // Assume that the values are sorted by media query to apply the cascading CSS logic
2730
- for (let index = activeBreakpointIndex; index >= 0; index--) {
2731
- const breakpointId = breakpoints[index]?.id;
2732
- if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
2733
- // If the value is defined, we use it and stop the breakpoints cascade
2734
- if (resolveDesignTokens) {
2735
- return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
2736
- }
2737
- return valuesByBreakpoint[breakpointId];
2738
- }
2872
+ const getDataFromTree = (tree) => {
2873
+ let dataSource = {};
2874
+ let unboundValues = {};
2875
+ const queue = [...tree.root.children];
2876
+ while (queue.length) {
2877
+ const node = queue.shift();
2878
+ if (!node) {
2879
+ continue;
2739
2880
  }
2740
- const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
2741
- if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
2742
- if (resolveDesignTokens) {
2743
- return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
2744
- }
2745
- return valuesByBreakpoint[fallbackBreakpointId];
2881
+ dataSource = { ...dataSource, ...node.data.dataSource };
2882
+ unboundValues = { ...unboundValues, ...node.data.unboundValues };
2883
+ if (node.children.length) {
2884
+ queue.push(...node.children);
2746
2885
  }
2747
2886
  }
2748
- else {
2749
- // Old design properties did not support breakpoints, keep for backward compatibility
2750
- return valuesByBreakpoint;
2751
- }
2752
- };
2753
-
2754
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2755
- const isLinkToAsset = (variable) => {
2756
- if (!variable)
2757
- return false;
2758
- if (typeof variable !== 'object')
2759
- return false;
2760
- return (variable.sys?.linkType === 'Asset' &&
2761
- typeof variable.sys?.id === 'string' &&
2762
- !!variable.sys?.id &&
2763
- variable.sys?.type === 'Link');
2764
- };
2765
-
2766
- const isLink = (maybeLink) => {
2767
- if (maybeLink === null)
2768
- return false;
2769
- if (typeof maybeLink !== 'object')
2770
- return false;
2771
- const link = maybeLink;
2772
- return Boolean(link.sys?.id) && link.sys?.type === 'Link';
2773
- };
2774
-
2775
- /**
2776
- * This module encapsulates format of the path to a deep reference.
2777
- */
2778
- const parseDataSourcePathIntoFieldset = (path) => {
2779
- const parsedPath = parseDeepPath(path);
2780
- if (null === parsedPath) {
2781
- throw new Error(`Cannot parse path '${path}' as deep path`);
2782
- }
2783
- return parsedPath.fields.map((field) => [null, field, '~locale']);
2784
- };
2785
- /**
2786
- * Parse path into components, supports L1 references (one reference follow) atm.
2787
- * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
2788
- * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
2789
- * @returns
2790
- */
2791
- const parseDataSourcePathWithL1DeepBindings = (path) => {
2792
- const parsedPath = parseDeepPath(path);
2793
- if (null === parsedPath) {
2794
- throw new Error(`Cannot parse path '${path}' as deep path`);
2795
- }
2796
2887
  return {
2797
- key: parsedPath.key,
2798
- field: parsedPath.fields[0],
2799
- referentField: parsedPath.fields[1],
2888
+ dataSource,
2889
+ unboundValues,
2800
2890
  };
2801
2891
  };
2802
2892
  /**
2803
- * Detects if paths is valid deep-path, like:
2804
- * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
2805
- * or regular, like:
2806
- * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
2807
- * @returns
2893
+ * Gets calculates the index to drop the dragged component based on the mouse position
2894
+ * @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
2808
2895
  */
2809
- const isDeepPath = (deepPathCandidate) => {
2810
- const deepPathParsed = parseDeepPath(deepPathCandidate);
2811
- if (!deepPathParsed) {
2812
- return false;
2813
- }
2814
- return deepPathParsed.fields.length > 1;
2815
- };
2816
- const parseDeepPath = (deepPathCandidate) => {
2817
- // ALGORITHM:
2818
- // We start with deep path in form:
2819
- // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
2820
- // First turn string into array of segments
2821
- // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
2822
- // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
2823
- // [
2824
- // [ "", "uuid123" ],
2825
- // [ "fields", "mainStory", "~locale" ],
2826
- // [ "fields", "cover", "~locale" ],
2827
- // [ "fields", "title", "~locale" ]
2828
- // ]
2829
- // Then check "initial" chunk for corretness
2830
- // Then check all "field-leading" chunks for correctness
2831
- const isValidInitialChunk = (initialChunk) => {
2832
- // must have start with '' and have at least 2 segments, second non-empty
2833
- // eg. /-_432uuid123123
2834
- return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
2835
- };
2836
- const isValidFieldChunk = (fieldChunk) => {
2837
- // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
2838
- // eg. fields/-32234mainStory/~locale
2839
- return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
2840
- };
2841
- const deepPathSegments = deepPathCandidate.split('/');
2842
- const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
2843
- if (chunks.length <= 1) {
2844
- return null; // malformed path, even regular paths have at least 2 chunks
2896
+ const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
2897
+ const APPEND_INSIDE = dropReceiverNode.children.length;
2898
+ const PREPEND_INSIDE = 0;
2899
+ if (isMouseAtTopBorder || isMouseAtBottomBorder) {
2900
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2901
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2902
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2903
+ return {
2904
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
2905
+ node: dropReceiverParentNode,
2906
+ index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2907
+ };
2845
2908
  }
2846
- else if (chunks.length === 2) {
2847
- return null; // deep paths have at least 3 chunks
2909
+ // if over one of the section indicators
2910
+ if (isOverTopIndicator || isOverBottomIndicator) {
2911
+ const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
2912
+ const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
2913
+ const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
2914
+ return {
2915
+ // when the mouse is around the border we want to drop the new component as a new section onto the root node
2916
+ node: dropReceiverParentNode,
2917
+ index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
2918
+ };
2848
2919
  }
2849
- // With 3+ chunks we can now check for deep path correctness
2850
- const [initialChunk, ...fieldChunks] = chunks;
2851
- if (!isValidInitialChunk(initialChunk)) {
2852
- return null;
2920
+ if (flexDirection === undefined || flexDirection === 'row') {
2921
+ return {
2922
+ node: dropReceiverNode,
2923
+ index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2924
+ };
2853
2925
  }
2854
- if (!fieldChunks.every(isValidFieldChunk)) {
2855
- return null;
2926
+ else {
2927
+ return {
2928
+ node: dropReceiverNode,
2929
+ index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
2930
+ };
2856
2931
  }
2857
- return {
2858
- key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
2859
- fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
2860
- };
2861
2932
  };
2862
- const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
2863
- const chunks = [];
2864
- let currentChunk = [];
2865
- const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
2866
- const excludeEmptyChunks = (chunk) => chunk.length > 0;
2867
- for (let i = 0; i < segments.length; i++) {
2868
- const isInitialElement = i === 0;
2869
- const segment = segments[i];
2870
- if (isInitialElement) {
2871
- currentChunk = [segment];
2872
- }
2873
- else if (isSegmentBeginningOfChunk(segment)) {
2874
- chunks.push(currentChunk);
2875
- currentChunk = [segment];
2876
- }
2877
- else {
2878
- currentChunk.push(segment);
2879
- }
2880
- }
2881
- chunks.push(currentChunk);
2882
- return chunks.filter(excludeEmptyChunks);
2933
+ const generateRandomId = (letterCount) => {
2934
+ const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
2935
+ const NUMS = '0123456789';
2936
+ const ALNUM = NUMS + LETTERS;
2937
+ const times = (n, callback) => Array.from({ length: n }, callback);
2938
+ const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
2939
+ return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
2883
2940
  };
2884
- const lastPathNamedSegmentEq = (path, expectedName) => {
2885
- // `/key123/fields/featureImage/~locale/fields/file/~locale`
2886
- // ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
2887
- const segments = path.split('/');
2888
- if (segments.length < 2) {
2889
- console.warn(`[experiences-sdk-react] Attempting to check whether last named segment of the path (${path}) equals to '${expectedName}', but the path doesn't have enough segments.`);
2941
+ const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
2942
+ if (!usedComponents?.length)
2890
2943
  return false;
2891
- }
2892
- const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
2893
- return secondLast === expectedName;
2944
+ return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
2894
2945
  };
2895
-
2896
- const resolveHyperlinkPattern = (pattern, entry, locale) => {
2897
- if (!entry || !locale)
2898
- return null;
2899
- const variables = {
2900
- entry,
2901
- locale,
2902
- };
2903
- return buildTemplate({ template: pattern, context: variables });
2946
+ /** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
2947
+ const checkIsAssembly = checkIsAssemblyNode;
2948
+ /**
2949
+ * This check assumes that the entry is already ensured to be an experience, i.e. the
2950
+ * content type of the entry is an experience type with the necessary annotations.
2951
+ **/
2952
+ const checkIsAssemblyEntry = (entry) => {
2953
+ return Boolean(entry.fields?.componentSettings);
2904
2954
  };
2905
- function getValue(obj, path) {
2906
- return path
2907
- .replace(/\[/g, '.')
2908
- .replace(/\]/g, '')
2909
- .split('.')
2910
- .reduce((o, k) => (o || {})[k], obj);
2911
- }
2912
- function addLocale(str, locale) {
2913
- const fieldsIndicator = 'fields';
2914
- const fieldsIndex = str.indexOf(fieldsIndicator);
2915
- if (fieldsIndex !== -1) {
2916
- const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
2917
- if (dotIndex !== -1) {
2918
- return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
2919
- }
2955
+ const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
2956
+ function parseCSSValue(input) {
2957
+ const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
2958
+ const match = input.match(regex);
2959
+ if (match) {
2960
+ return {
2961
+ value: parseFloat(match[1]),
2962
+ unit: match[3],
2963
+ };
2920
2964
  }
2921
- return str;
2922
- }
2923
- function getTemplateValue(ctx, path) {
2924
- const pathWithLocale = addLocale(path, ctx.locale);
2925
- const retrievedValue = getValue(ctx, pathWithLocale);
2926
- return typeof retrievedValue === 'object' && retrievedValue !== null
2927
- ? retrievedValue[ctx.locale]
2928
- : retrievedValue;
2965
+ return null;
2929
2966
  }
2930
- function buildTemplate({ template, context, }) {
2931
- const localeVariable = /{\s*locale\s*}/g;
2932
- // e.g. "{ page.sys.id }"
2933
- const variables = /{\s*([\S]+?)\s*}/g;
2934
- return (template
2935
- // first replace the locale pattern
2936
- .replace(localeVariable, context.locale)
2937
- // then resolve the remaining variables
2938
- .replace(variables, (_, path) => {
2939
- const fallback = path + '_NOT_FOUND';
2940
- const value = getTemplateValue(context, path) ?? fallback;
2941
- // using _.result didn't gave proper results so we run our own version of it
2942
- return String(typeof value === 'function' ? value() : value);
2943
- }));
2967
+ function getTargetValueInPixels(targetWidthObject) {
2968
+ switch (targetWidthObject.unit) {
2969
+ case 'px':
2970
+ return targetWidthObject.value;
2971
+ case 'em':
2972
+ return targetWidthObject.value * 16;
2973
+ case 'rem':
2974
+ return targetWidthObject.value * 16;
2975
+ default:
2976
+ return targetWidthObject.value;
2977
+ }
2944
2978
  }
2945
2979
 
2946
- const stylesToKeep = ['cfImageAsset'];
2947
- const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
2948
- const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
2949
- const sanitizeNodeProps = (nodeProps) => {
2950
- return omit(nodeProps, stylesToRemove, propsToRemove);
2951
- };
2952
-
2953
- const CF_DEBUG_KEY = 'cf_debug';
2954
- class DebugLogger {
2955
- constructor() {
2956
- // Public methods for logging
2957
- this.error = this.logger('error');
2958
- this.warn = this.logger('warn');
2959
- this.log = this.logger('log');
2960
- this.debug = this.logger('debug');
2961
- if (typeof localStorage === 'undefined') {
2962
- this.enabled = false;
2963
- return;
2964
- }
2965
- // Default to checking localStorage for the debug mode on initialization if in browser
2966
- this.enabled = localStorage.getItem(CF_DEBUG_KEY) === 'true';
2980
+ class ParseError extends Error {
2981
+ constructor(message) {
2982
+ super(message);
2967
2983
  }
2968
- static getInstance() {
2969
- if (this.instance === null) {
2970
- this.instance = new DebugLogger();
2984
+ }
2985
+ const isValidJsonObject = (s) => {
2986
+ try {
2987
+ const result = JSON.parse(s);
2988
+ if ('object' !== typeof result) {
2989
+ return false;
2971
2990
  }
2972
- return this.instance;
2991
+ return true;
2973
2992
  }
2974
- getEnabled() {
2975
- return this.enabled;
2993
+ catch (e) {
2994
+ return false;
2976
2995
  }
2977
- setEnabled(enabled) {
2978
- this.enabled = enabled;
2979
- if (typeof localStorage === 'undefined') {
2980
- return;
2981
- }
2982
- if (enabled) {
2983
- localStorage.setItem(CF_DEBUG_KEY, 'true');
2984
- }
2985
- else {
2986
- localStorage.removeItem(CF_DEBUG_KEY);
2996
+ };
2997
+ const doesMismatchMessageSchema = (event) => {
2998
+ try {
2999
+ tryParseMessage(event);
3000
+ return false;
3001
+ }
3002
+ catch (e) {
3003
+ if (e instanceof ParseError) {
3004
+ return e.message;
2987
3005
  }
3006
+ throw e;
2988
3007
  }
2989
- // Log method for different levels (error, warn, log)
2990
- logger(level) {
2991
- return (...args) => {
2992
- if (this.enabled) {
2993
- console[level]('[cf-experiences-sdk]', ...args);
2994
- }
2995
- };
3008
+ };
3009
+ const tryParseMessage = (event) => {
3010
+ if (!event.data) {
3011
+ throw new ParseError('Field event.data is missing');
2996
3012
  }
2997
- }
2998
- DebugLogger.instance = null;
2999
- const debug = DebugLogger.getInstance();
3000
- const enableDebug = () => {
3001
- debug.setEnabled(true);
3002
- console.log('Debug mode enabled');
3013
+ if ('string' !== typeof event.data) {
3014
+ throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
3015
+ }
3016
+ if (!isValidJsonObject(event.data)) {
3017
+ throw new ParseError('Field event.data must be a valid JSON object serialized as string');
3018
+ }
3019
+ const eventData = JSON.parse(event.data);
3020
+ if (!eventData.source) {
3021
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
3022
+ }
3023
+ if ('composability-app' !== eventData.source) {
3024
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
3025
+ }
3026
+ // check eventData.eventType
3027
+ const supportedEventTypes = Object.values(INCOMING_EVENTS);
3028
+ if (!supportedEventTypes.includes(eventData.eventType)) {
3029
+ // Expected message: This message is handled in the EntityStore to store fetched entities
3030
+ if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
3031
+ throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
3032
+ }
3033
+ }
3034
+ return eventData;
3003
3035
  };
3004
- const disableDebug = () => {
3005
- debug.setEnabled(false);
3006
- console.log('Debug mode disabled');
3036
+ const validateExperienceBuilderConfig = ({ locale, mode, }) => {
3037
+ if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
3038
+ return;
3039
+ }
3040
+ if (!locale) {
3041
+ throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
3042
+ }
3007
3043
  };
3008
3044
 
3009
3045
  const sendMessage = (eventType, data) => {
@@ -3443,7 +3479,7 @@ class EntityStore extends EntityStoreBase {
3443
3479
  else {
3444
3480
  const { experienceEntry, entities, locale } = options;
3445
3481
  if (!isExperienceEntry(experienceEntry)) {
3446
- throw new Error('Provided entry is not experience entry');
3482
+ throw new Error('Provided entry is not an experience entry');
3447
3483
  }
3448
3484
  super({ entities, locale });
3449
3485
  this._experienceEntryFields = experienceEntry.fields;
@@ -3525,8 +3561,11 @@ function createExperience(options) {
3525
3561
  }
3526
3562
  else {
3527
3563
  const { experienceEntry, referencedAssets, referencedEntries, locale } = options;
3564
+ if ([experienceEntry, ...referencedAssets, ...referencedEntries].some(isNotLocalized)) {
3565
+ throw new Error('Some of the provided content is not localized. Please localize every entity before passing it to this function. Note that this is solely determined by `sys.locale` being set respectively.');
3566
+ }
3528
3567
  if (!isExperienceEntry(experienceEntry)) {
3529
- throw new Error('Provided entry is not experience entry');
3568
+ throw new Error('Provided entry is not an experience entry');
3530
3569
  }
3531
3570
  const entityStore = new EntityStore({
3532
3571
  experienceEntry,
@@ -3538,21 +3577,33 @@ function createExperience(options) {
3538
3577
  };
3539
3578
  }
3540
3579
  }
3580
+ // Following the API shape, we check the `sys.locale` property as we can't rely on the shape of
3581
+ // fields to determine whether it's localized or not.
3582
+ // See CDA documentation mentioning it here: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes
3583
+ const isNotLocalized = (entity) => {
3584
+ return !entity.sys.locale;
3585
+ };
3541
3586
 
3587
+ /**
3588
+ * Fetches an experience entry by its slug or id. Throws an error if there are multiple
3589
+ * entries with the same slug. Additionally, it resolves all nested pattern entries inside `fields.usedComponents`.
3590
+ * @param options.client - Instantiated client from the Contentful SDK. If this is using the `withAllLocales` modifier, you may not provide a specific locale.
3591
+ * @param options.locale - Provide a locale if your experience contains custom localized fields. Otherwise, it will fallback to the default locale.
3592
+ * @param options.experienceTypeId - id of the content type associated with the experience
3593
+ * @param options.identifier - identifying condition to find the correct experience entry
3594
+ *
3595
+ */
3542
3596
  const fetchExperienceEntry = async ({ client, experienceTypeId, locale, identifier, }) => {
3543
3597
  if (!client) {
3544
3598
  throw new Error('Failed to fetch experience entities. Required "client" parameter was not provided');
3545
3599
  }
3546
- if (!locale) {
3547
- throw new Error('Failed to fetch experience entities. Required "locale" parameter was not provided');
3548
- }
3549
3600
  if (!experienceTypeId) {
3550
3601
  throw new Error('Failed to fetch experience entities. Required "experienceTypeId" parameter was not provided');
3551
3602
  }
3552
- if (!identifier.slug && !identifier.id) {
3603
+ if (!('slug' in identifier) && !('id' in identifier)) {
3553
3604
  throw new Error(`Failed to fetch experience entities. At least one identifier must be provided. Received: ${JSON.stringify(identifier)}`);
3554
3605
  }
3555
- const filter = identifier.slug ? { 'fields.slug': identifier.slug } : { 'sys.id': identifier.id };
3606
+ const filter = 'slug' in identifier ? { 'fields.slug': identifier.slug } : { 'sys.id': identifier.id };
3556
3607
  const entries = await client.getEntries({
3557
3608
  content_type: experienceTypeId,
3558
3609
  locale,
@@ -3817,13 +3868,18 @@ const fetchAllAssets = async ({ client, ids, locale, skip = 0, limit = 100, resp
3817
3868
  }
3818
3869
  };
3819
3870
 
3871
+ /**
3872
+ * Fetches all entries and assets from the `dataSource` of the given experience entry. This will
3873
+ * also consider deep references that are not listed explicitly but linked through deep binding paths.
3874
+ * @param options.client - Instantiated client from the Contentful SDK. If this is using the `withAllLocales` modifier, you may not provide a specific locale.
3875
+ * @param options.experienceEntry - Localized experience entry. To localize a multi locale entry, use the `localizeEntity` function.
3876
+ * @param options.locale - Retrieve a specific localized version of the referenced entities. Otherwise, it will fallback to the default locale.
3877
+ * @returns object with a list of `entries` and a list of `assets`
3878
+ */
3820
3879
  const fetchReferencedEntities = async ({ client, experienceEntry, locale, }) => {
3821
3880
  if (!client) {
3822
3881
  throw new Error('Failed to fetch experience entities. Required "client" parameter was not provided');
3823
3882
  }
3824
- if (!locale) {
3825
- throw new Error('Failed to fetch experience entities. Required "locale" parameter was not provided');
3826
- }
3827
3883
  if (!isExperienceEntry(experienceEntry)) {
3828
3884
  throw new Error('Failed to fetch experience entities. Provided "experienceEntry" does not match experience entry schema');
3829
3885
  }
@@ -3935,7 +3991,8 @@ const handleError$1 = (generalMessage, error) => {
3935
3991
  throw Error(message);
3936
3992
  };
3937
3993
  /**
3938
- * Fetches an experience object by its slug
3994
+ * Fetches an experience entry by its slug and additionally fetches all its references to return
3995
+ * an initilized experience instance.
3939
3996
  * @param {FetchBySlugParams} options - options to fetch the experience
3940
3997
  */
3941
3998
  async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEditorMode, }) {
@@ -3943,6 +4000,9 @@ async function fetchBySlug({ client, experienceTypeId, slug, localeCode, isEdito
3943
4000
  if (isEditorMode)
3944
4001
  return;
3945
4002
  let experienceEntry = undefined;
4003
+ if (!localeCode) {
4004
+ throw new Error('Failed to fetch by slug. Required "localeCode" parameter was not provided');
4005
+ }
3946
4006
  try {
3947
4007
  experienceEntry = await fetchExperienceEntry({
3948
4008
  client,
@@ -3989,7 +4049,8 @@ const handleError = (generalMessage, error) => {
3989
4049
  throw Error(message);
3990
4050
  };
3991
4051
  /**
3992
- * Fetches an experience object by its id
4052
+ * Fetches an experience entry by its id and additionally fetches all its references to return
4053
+ * an initilized experience instance.
3993
4054
  * @param {FetchByIdParams} options - options to fetch the experience
3994
4055
  */
3995
4056
  async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMode, }) {
@@ -3997,6 +4058,9 @@ async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMod
3997
4058
  if (isEditorMode)
3998
4059
  return;
3999
4060
  let experienceEntry = undefined;
4061
+ if (!localeCode) {
4062
+ throw new Error('Failed to fetch by id. Required "localeCode" parameter was not provided');
4063
+ }
4000
4064
  try {
4001
4065
  experienceEntry = await fetchExperienceEntry({
4002
4066
  client,
@@ -4034,5 +4098,5 @@ async function fetchById({ client, experienceTypeId, id, localeCode, isEditorMod
4034
4098
  }
4035
4099
  }
4036
4100
 
4037
- export { DebugLogger, DeepReference, EditorModeEntityStore, EntityStore, EntityStoreBase, MEDIA_QUERY_REGEXP, VisualEditorMode, addLocale, addMinHeightForEmptyStructures, breakpointsRegistry, buildCfStyles, buildStyleTag, buildTemplate, builtInStyles, calculateNodeDefaultHeight, checkIsAssembly, checkIsAssemblyDefinition, checkIsAssemblyEntry, checkIsAssemblyNode, columnsBuiltInStyles, containerBuiltInStyles, createExperience, debug, defineBreakpoints, defineDesignTokens, designTokensRegistry, detachExperienceStyles, disableDebug, dividerBuiltInStyles, doesMismatchMessageSchema, enableDebug, fetchAllAssets, fetchAllEntries, fetchById, fetchBySlug, findOutermostCoordinates, flattenDesignTokenRegistry, gatherDeepReferencesFromExperienceEntry, gatherDeepReferencesFromTree, generateRandomId, getActiveBreakpointIndex, getBreakpointRegistration, getDataFromTree, getDesignTokenRegistration, getElementCoordinates, getFallbackBreakpointIndex, getInsertionData, getTargetValueInPixels, getTemplateValue, getValueForBreakpoint, indexByBreakpoint, isCfStyleAttribute, isComponentAllowedOnRoot, isContentfulComponent, isContentfulStructureComponent, isDeepPath, isExperienceEntry, isLink, isLinkToAsset, isPatternComponent, isStructureWithRelativeHeight, isValidBreakpointValue, lastPathNamedSegmentEq, maybePopulateDesignTokenValue, mediaQueryMatcher, optionalBuiltInStyles, parseCSSValue, parseDataSourcePathIntoFieldset, parseDataSourcePathWithL1DeepBindings, resetBreakpointsRegistry, resetDesignTokenRegistry, resolveBackgroundImageBinding, resolveHyperlinkPattern, runBreakpointsValidation, sanitizeNodeProps, sectionBuiltInStyles, sendMessage, singleColumnBuiltInStyles, stringifyCssProperties, toCSSAttribute, toMediaQuery, transformBoundContentValue, tryParseMessage, validateExperienceBuilderConfig };
4101
+ export { DebugLogger, DeepReference, EditorModeEntityStore, EntityStore, EntityStoreBase, MEDIA_QUERY_REGEXP, VisualEditorMode, addLocale, addMinHeightForEmptyStructures, breakpointsRegistry, buildCfStyles, buildStyleTag, buildTemplate, builtInStyles, calculateNodeDefaultHeight, checkIsAssembly, checkIsAssemblyDefinition, checkIsAssemblyEntry, checkIsAssemblyNode, columnsBuiltInStyles, containerBuiltInStyles, createExperience, debug, defineBreakpoints, defineDesignTokens, designTokensRegistry, detachExperienceStyles, disableDebug, dividerBuiltInStyles, doesMismatchMessageSchema, enableDebug, fetchAllAssets, fetchAllEntries, fetchById, fetchBySlug, fetchExperienceEntry, fetchReferencedEntities, findOutermostCoordinates, flattenDesignTokenRegistry, gatherDeepReferencesFromExperienceEntry, gatherDeepReferencesFromTree, generateRandomId, getActiveBreakpointIndex, getBreakpointRegistration, getDataFromTree, getDesignTokenRegistration, getElementCoordinates, getFallbackBreakpointIndex, getInsertionData, getTargetValueInPixels, getTemplateValue, getValueForBreakpoint, indexByBreakpoint, isCfStyleAttribute, isComponentAllowedOnRoot, isContentfulComponent, isContentfulStructureComponent, isDeepPath, isExperienceEntry, isLink, isLinkToAsset, isPatternComponent, isStructureWithRelativeHeight, isValidBreakpointValue, lastPathNamedSegmentEq, localizeEntity, maybePopulateDesignTokenValue, mediaQueryMatcher, optionalBuiltInStyles, parseCSSValue, parseDataSourcePathIntoFieldset, parseDataSourcePathWithL1DeepBindings, resetBreakpointsRegistry, resetDesignTokenRegistry, resolveBackgroundImageBinding, resolveHyperlinkPattern, runBreakpointsValidation, sanitizeNodeProps, sectionBuiltInStyles, sendMessage, singleColumnBuiltInStyles, stringifyCssProperties, toCSSAttribute, toMediaQuery, transformBoundContentValue, tryParseMessage, validateExperienceBuilderConfig };
4038
4102
  //# sourceMappingURL=index.js.map