@contentful/experiences-visual-editor-react 0.0.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4200 @@
1
+ import styleInject from 'style-inject';
2
+ import React, { useRef, useMemo, useEffect, useState, forwardRef, useCallback } from 'react';
3
+ import md5 from 'md5';
4
+ import { BLOCKS } from '@contentful/rich-text-types';
5
+ import { Draggable, Droppable, DragDropContext } from '@hello-pangea/dnd';
6
+ import classNames from 'classnames';
7
+ import { create } from 'zustand';
8
+ import { isEqual, get as get$1, omit } from 'lodash-es';
9
+ import '@contentful/rich-text-react-renderer';
10
+ import { produce } from 'immer';
11
+ import { createPortal } from 'react-dom';
12
+ import { v4 } from 'uuid';
13
+
14
+ var css_248z$7 = "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\n\n/*\n * All of these variables are tokens from Forma-36 and should not be adjusted as these\n * are global variables that may affect multiple places.\n * As our customers may use other design libraries, we try to avoid overlapping global\n * variables by always using the prefix `--exp-builder-` inside this SDK.\n */\n\n\n:root {\n /* Color tokens from Forma 36: https://f36.contentful.com/tokens/color-system */\n --exp-builder-blue100: #e8f5ff;\n --exp-builder-blue200: #ceecff;\n --exp-builder-blue300: #98cbff;\n --exp-builder-blue400: #40a0ff;\n --exp-builder-blue500: #036fe3;\n --exp-builder-blue600: #0059c8;\n --exp-builder-blue700: #0041ab;\n --exp-builder-blue800: #003298;\n --exp-builder-blue900: #002a8e;\n --exp-builder-gray100: #f7f9fa;\n --exp-builder-gray200: #e7ebee;\n --exp-builder-gray300: #cfd9e0;\n --exp-builder-gray400: #aec1cc;\n --exp-builder-gray500: #67728a;\n --exp-builder-gray600: #5a657c;\n --exp-builder-gray700: #414d63;\n --exp-builder-gray800: #1b273a;\n --exp-builder-gray900: #111b2b;\n --exp-builder-purple600: #6c3ecf;\n --exp-builder-red200: #ffe0e0;\n --exp-builder-red800: #7f0010;\n --exp-builder-color-white: #ffffff;\n --exp-builder-glow-primary: 0px 0px 0px 3px #e8f5ff;\n\n /* RGB colors for applying opacity */\n --exp-builder-blue100-rgb: 232, 245, 255;\n --exp-builder-blue300-rgb: 152, 203, 255;\n\n /* Spacing tokens from Forma 36: https://f36.contentful.com/tokens/spacing */\n --exp-builder-spacing-s: 0.75rem;\n --exp-builder-spacing-2xs: 0.25rem;\n\n /* Typography tokens from Forma 36: https://f36.contentful.com/tokens/typography */\n --exp-builder-font-size-l: 1rem;\n --exp-builder-font-size-m: 0.875rem;\n --exp-builder-font-stack-primary: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n --exp-builder-line-height-condensed: 1.25;\n}\n";
15
+ styleInject(css_248z$7);
16
+
17
+ const INCOMING_EVENTS$1 = {
18
+ RequestEditorMode: 'requestEditorMode',
19
+ CompositionUpdated: 'componentTreeUpdated',
20
+ ComponentDraggingChanged: 'componentDraggingChanged',
21
+ ComponentDragCanceled: 'componentDragCanceled',
22
+ ComponentDragStarted: 'componentDragStarted',
23
+ ComponentDragEnded: 'componentDragEnded',
24
+ CanvasResized: 'canvasResized',
25
+ SelectComponent: 'selectComponent',
26
+ HoverComponent: 'hoverComponent',
27
+ UpdatedEntity: 'updatedEntity',
28
+ /**
29
+ * @deprecated use `AssembliesAdded` instead. This will be removed in version 5.
30
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
31
+ */
32
+ DesignComponentsAdded: 'designComponentsAdded',
33
+ /**
34
+ * @deprecated use `AssembliesRegistered` instead. This will be removed in version 5.
35
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
36
+ */
37
+ DesignComponentsRegistered: 'designComponentsRegistered',
38
+ AssembliesAdded: 'assembliesAdded',
39
+ AssembliesRegistered: 'assembliesRegistered',
40
+ InitEditor: 'initEditor',
41
+ };
42
+ const CONTENTFUL_COMPONENTS$1 = {
43
+ section: {
44
+ id: 'contentful-section',
45
+ name: 'Section',
46
+ },
47
+ container: {
48
+ id: 'contentful-container',
49
+ name: 'Container',
50
+ },
51
+ columns: {
52
+ id: 'contentful-columns',
53
+ name: 'Columns',
54
+ },
55
+ singleColumn: {
56
+ id: 'contentful-single-column',
57
+ name: 'Column',
58
+ },
59
+ button: {
60
+ id: 'button',
61
+ name: 'Button',
62
+ },
63
+ heading: {
64
+ id: 'heading',
65
+ name: 'Heading',
66
+ },
67
+ image: {
68
+ id: 'image',
69
+ name: 'Image',
70
+ },
71
+ richText: {
72
+ id: 'richText',
73
+ name: 'Rich Text',
74
+ },
75
+ text: {
76
+ id: 'text',
77
+ name: 'Text',
78
+ },
79
+ };
80
+ const EMPTY_CONTAINER_HEIGHT$1 = '80px';
81
+ var PostMessageMethods$2;
82
+ (function (PostMessageMethods) {
83
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
84
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
85
+ })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
86
+
87
+ const structureComponents = new Set([
88
+ CONTENTFUL_COMPONENTS$1.section.id,
89
+ CONTENTFUL_COMPONENTS$1.columns.id,
90
+ CONTENTFUL_COMPONENTS$1.container.id,
91
+ CONTENTFUL_COMPONENTS$1.singleColumn.id,
92
+ ]);
93
+ const isContentfulStructureComponent = (componentId) => structureComponents.has(componentId ?? '');
94
+ const isEmptyStructureWithRelativeHeight = (children, componentId, height) => {
95
+ return (children === 0 &&
96
+ isContentfulStructureComponent(componentId) &&
97
+ !height?.toString().endsWith('px'));
98
+ };
99
+
100
+ const findOutermostCoordinates = (first, second) => {
101
+ return {
102
+ top: Math.min(first.top, second.top),
103
+ right: Math.max(first.right, second.right),
104
+ bottom: Math.max(first.bottom, second.bottom),
105
+ left: Math.min(first.left, second.left),
106
+ };
107
+ };
108
+ const getElementCoordinates = (element) => {
109
+ const rect = element.getBoundingClientRect();
110
+ /**
111
+ * If element does not have children, or element has it's own width or height,
112
+ * return the element's coordinates.
113
+ */
114
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
115
+ return rect;
116
+ }
117
+ const rects = [];
118
+ /**
119
+ * If element has children, or element does not have it's own width and height,
120
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
121
+ * as the coordinate of the element.
122
+ *
123
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
124
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
125
+ */
126
+ for (const child of element.children) {
127
+ const childRect = getElementCoordinates(child);
128
+ if (childRect.width !== 0 || childRect.height !== 0) {
129
+ const { top, right, bottom, left } = childRect;
130
+ rects.push({ top, right, bottom, left });
131
+ }
132
+ }
133
+ if (rects.length === 0) {
134
+ return rect;
135
+ }
136
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
137
+ return DOMRect.fromRect({
138
+ x: left,
139
+ y: top,
140
+ height: bottom - top,
141
+ width: right - left,
142
+ });
143
+ };
144
+
145
+ class ParseError extends Error {
146
+ constructor(message) {
147
+ super(message);
148
+ }
149
+ }
150
+ const isValidJsonObject = (s) => {
151
+ try {
152
+ const result = JSON.parse(s);
153
+ if ('object' !== typeof result) {
154
+ return false;
155
+ }
156
+ return true;
157
+ }
158
+ catch (e) {
159
+ return false;
160
+ }
161
+ };
162
+ const doesMismatchMessageSchema = (event) => {
163
+ try {
164
+ tryParseMessage(event);
165
+ return false;
166
+ }
167
+ catch (e) {
168
+ if (e instanceof ParseError) {
169
+ return e.message;
170
+ }
171
+ throw e;
172
+ }
173
+ };
174
+ const tryParseMessage = (event) => {
175
+ if (!event.data) {
176
+ throw new ParseError('Field event.data is missing');
177
+ }
178
+ if ('string' !== typeof event.data) {
179
+ throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
180
+ }
181
+ if (!isValidJsonObject(event.data)) {
182
+ throw new ParseError('Field event.data must be a valid JSON object serialized as string');
183
+ }
184
+ const eventData = JSON.parse(event.data);
185
+ if (!eventData.source) {
186
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
187
+ }
188
+ if ('composability-app' !== eventData.source) {
189
+ throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
190
+ }
191
+ // check eventData.eventType
192
+ const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
193
+ if (!supportedEventTypes.includes(eventData.eventType)) {
194
+ // Expected message: This message is handled in the EntityStore to store fetched entities
195
+ if (eventData.eventType !== PostMessageMethods$2.REQUESTED_ENTITIES) {
196
+ throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
197
+ }
198
+ }
199
+ return eventData;
200
+ };
201
+
202
+ const transformFill = (value) => (value === 'fill' ? '100%' : value);
203
+ const transformGridColumn = (span) => {
204
+ if (!span) {
205
+ return {};
206
+ }
207
+ return {
208
+ gridColumn: `span ${span}`,
209
+ };
210
+ };
211
+ const transformBorderStyle = (value) => {
212
+ if (!value)
213
+ return {};
214
+ const parts = value.split(' ');
215
+ // Just accept the passed value
216
+ if (parts.length < 3)
217
+ return { border: value };
218
+ // Replace the second part always with `solid` and set the box sizing accordingly
219
+ const [borderSize, borderStyle, ...borderColorParts] = parts;
220
+ const borderColor = borderColorParts.join(' ');
221
+ return {
222
+ border: `${borderSize} ${borderStyle} ${borderColor}`,
223
+ };
224
+ };
225
+ const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
226
+ ? {
227
+ alignItems: cfHorizontalAlignment,
228
+ justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
229
+ }
230
+ : {
231
+ alignItems: cfVerticalAlignment,
232
+ justifyContent: cfHorizontalAlignment === 'center'
233
+ ? `safe ${cfHorizontalAlignment}`
234
+ : cfHorizontalAlignment,
235
+ };
236
+ const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageScaling, cfBackgroundImageAlignment) => {
237
+ const matchBackgroundSize = (backgroundImageScaling) => {
238
+ if ('fill' === backgroundImageScaling)
239
+ return 'cover';
240
+ if ('fit' === backgroundImageScaling)
241
+ return 'contain';
242
+ return undefined;
243
+ };
244
+ const matchBackgroundPosition = (cfBackgroundImageAlignment) => {
245
+ if (!cfBackgroundImageAlignment) {
246
+ return undefined;
247
+ }
248
+ if ('string' !== typeof cfBackgroundImageAlignment) {
249
+ return undefined;
250
+ }
251
+ let [horizontalAlignment, verticalAlignment] = cfBackgroundImageAlignment
252
+ .trim()
253
+ .split(/\s+/, 2);
254
+ // Special case for handling single values
255
+ // for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
256
+ if (horizontalAlignment && !verticalAlignment) {
257
+ const singleValue = horizontalAlignment;
258
+ switch (singleValue) {
259
+ case 'left':
260
+ horizontalAlignment = 'left';
261
+ verticalAlignment = 'center';
262
+ break;
263
+ case 'right':
264
+ horizontalAlignment = 'right';
265
+ verticalAlignment = 'center';
266
+ break;
267
+ case 'center':
268
+ horizontalAlignment = 'center';
269
+ verticalAlignment = 'center';
270
+ break;
271
+ case 'top':
272
+ horizontalAlignment = 'center';
273
+ verticalAlignment = 'top';
274
+ break;
275
+ case 'bottom':
276
+ horizontalAlignment = 'center';
277
+ verticalAlignment = 'bottom';
278
+ break;
279
+ // just fall down to the normal validation logic for horiz and vert
280
+ }
281
+ }
282
+ const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
283
+ const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
284
+ horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
285
+ verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
286
+ return `${horizontalAlignment} ${verticalAlignment}`;
287
+ };
288
+ if (!cfBackgroundImageUrl) {
289
+ return undefined;
290
+ }
291
+ return {
292
+ backgroundImage: `url(${cfBackgroundImageUrl})`,
293
+ backgroundRepeat: cfBackgroundImageScaling === 'tile' ? 'repeat' : 'no-repeat',
294
+ backgroundPosition: matchBackgroundPosition(cfBackgroundImageAlignment),
295
+ backgroundSize: matchBackgroundSize(cfBackgroundImageScaling),
296
+ };
297
+ };
298
+ const transformContentValue = (value, variableDefinition) => {
299
+ if (variableDefinition.type === 'RichText') {
300
+ return transformRichText(value);
301
+ }
302
+ return value;
303
+ };
304
+ const transformRichText = (value) => {
305
+ if (typeof value === 'string') {
306
+ return {
307
+ data: {},
308
+ content: [
309
+ {
310
+ nodeType: BLOCKS.PARAGRAPH,
311
+ data: {},
312
+ content: [
313
+ {
314
+ data: {},
315
+ nodeType: 'text',
316
+ value: value,
317
+ marks: [],
318
+ },
319
+ ],
320
+ },
321
+ ],
322
+ nodeType: BLOCKS.DOCUMENT,
323
+ };
324
+ }
325
+ if (typeof value === 'object' && value.nodeType === BLOCKS.DOCUMENT) {
326
+ return value;
327
+ }
328
+ return undefined;
329
+ };
330
+ const transformWidthSizing = ({ value, cfMargin, }) => {
331
+ if (!value || !cfMargin)
332
+ return undefined;
333
+ const transformedValue = transformFill(value);
334
+ const marginValues = cfMargin.split(' ');
335
+ const rightMargin = marginValues[1] || '0px';
336
+ const leftMargin = marginValues[3] || '0px';
337
+ const calcValue = `calc(${transformedValue} - ${leftMargin} - ${rightMargin})`;
338
+ /**
339
+ * We want to check if the calculated value is valid CSS. If this fails,
340
+ * this means the `transformedValue` is not a calculable value (not a px, rem, or %).
341
+ * The value may instead be a string such as `min-content` or `max-content`. In
342
+ * that case we don't want to use calc and instead return the raw value.
343
+ */
344
+ if (typeof window !== 'undefined' && CSS.supports('width', calcValue)) {
345
+ return calcValue;
346
+ }
347
+ return transformedValue;
348
+ };
349
+
350
+ const toCSSAttribute = (key) => key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
351
+ const buildStyleTag = ({ styles, nodeId }) => {
352
+ const stylesStr = Object.entries(styles)
353
+ .filter(([, value]) => value !== undefined)
354
+ .reduce((acc, [key, value]) => `${acc}
355
+ ${toCSSAttribute(key)}: ${value};`, '');
356
+ const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
357
+ const styleRule = `.${className}{ ${stylesStr} }`;
358
+ return [className, styleRule];
359
+ };
360
+ const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfGap, cfBackgroundImageUrl, cfBackgroundImageAlignment, cfBackgroundImageScaling, cfFontSize, cfFontWeight, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, }) => {
361
+ return {
362
+ margin: cfMargin,
363
+ padding: cfPadding,
364
+ backgroundColor: cfBackgroundColor,
365
+ width: transformWidthSizing({ value: cfWidth, cfMargin }),
366
+ height: transformFill(cfHeight),
367
+ maxWidth: cfMaxWidth,
368
+ ...transformGridColumn(cfColumnSpan),
369
+ ...transformBorderStyle(cfBorder),
370
+ gap: cfGap,
371
+ ...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
372
+ flexDirection: cfFlexDirection,
373
+ flexWrap: cfFlexWrap,
374
+ ...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageScaling, cfBackgroundImageAlignment),
375
+ fontSize: cfFontSize,
376
+ fontWeight: cfTextBold ? 'bold' : cfFontWeight,
377
+ fontStyle: cfTextItalic ? 'italic' : 'normal',
378
+ lineHeight: cfLineHeight,
379
+ letterSpacing: cfLetterSpacing,
380
+ color: cfTextColor,
381
+ textAlign: cfTextAlign,
382
+ textTransform: cfTextTransform,
383
+ textDecoration: cfTextUnderline ? 'underline' : 'none',
384
+ boxSizing: 'border-box',
385
+ };
386
+ };
387
+ /**
388
+ * Container/section default behaviour:
389
+ * Default height => height: EMPTY_CONTAINER_HEIGHT (120px)
390
+ * If a container component has children => height: 'fit-content'
391
+ */
392
+ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
393
+ if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
394
+ return value;
395
+ }
396
+ if (children.length) {
397
+ return '100%';
398
+ }
399
+ return EMPTY_CONTAINER_HEIGHT$1;
400
+ };
401
+
402
+ const getDataFromTree = (tree) => {
403
+ let dataSource = {};
404
+ let unboundValues = {};
405
+ const queue = [...tree.root.children];
406
+ while (queue.length) {
407
+ const node = queue.shift();
408
+ if (!node) {
409
+ continue;
410
+ }
411
+ dataSource = { ...dataSource, ...node.data.dataSource };
412
+ unboundValues = { ...unboundValues, ...node.data.unboundValues };
413
+ if (node.children.length) {
414
+ queue.push(...node.children);
415
+ }
416
+ }
417
+ return {
418
+ dataSource,
419
+ unboundValues,
420
+ };
421
+ };
422
+
423
+ const builtInStyles = {
424
+ cfVerticalAlignment: {
425
+ validations: {
426
+ in: [
427
+ {
428
+ value: 'start',
429
+ displayName: 'Align left',
430
+ },
431
+ {
432
+ value: 'center',
433
+ displayName: 'Align center',
434
+ },
435
+ {
436
+ value: 'end',
437
+ displayName: 'Align right',
438
+ },
439
+ ],
440
+ },
441
+ type: 'Text',
442
+ group: 'style',
443
+ description: 'The horizontal alignment of the section',
444
+ defaultValue: 'center',
445
+ displayName: 'Vertical alignment',
446
+ },
447
+ cfHorizontalAlignment: {
448
+ validations: {
449
+ in: [
450
+ {
451
+ value: 'start',
452
+ displayName: 'Align top',
453
+ },
454
+ {
455
+ value: 'center',
456
+ displayName: 'Align center',
457
+ },
458
+ {
459
+ value: 'end',
460
+ displayName: 'Align bottom',
461
+ },
462
+ ],
463
+ },
464
+ type: 'Text',
465
+ group: 'style',
466
+ description: 'The horizontal alignment of the section',
467
+ defaultValue: 'center',
468
+ displayName: 'Horizontal alignment',
469
+ },
470
+ cfMargin: {
471
+ displayName: 'Margin',
472
+ type: 'Text',
473
+ group: 'style',
474
+ description: 'The margin of the section',
475
+ defaultValue: '0 0 0 0',
476
+ },
477
+ cfPadding: {
478
+ displayName: 'Padding',
479
+ type: 'Text',
480
+ group: 'style',
481
+ description: 'The padding of the section',
482
+ defaultValue: '0 0 0 0',
483
+ },
484
+ cfBackgroundColor: {
485
+ displayName: 'Background color',
486
+ type: 'Text',
487
+ group: 'style',
488
+ description: 'The background color of the section',
489
+ defaultValue: 'rgba(255, 255, 255, 0)',
490
+ },
491
+ cfWidth: {
492
+ displayName: 'Width',
493
+ type: 'Text',
494
+ group: 'style',
495
+ description: 'The width of the section',
496
+ defaultValue: 'fill',
497
+ },
498
+ cfHeight: {
499
+ displayName: 'Height',
500
+ type: 'Text',
501
+ group: 'style',
502
+ description: 'The height of the section',
503
+ defaultValue: 'fit-content',
504
+ },
505
+ cfMaxWidth: {
506
+ displayName: 'Max width',
507
+ type: 'Text',
508
+ group: 'style',
509
+ description: 'The max-width of the section',
510
+ defaultValue: 'none',
511
+ },
512
+ cfFlexDirection: {
513
+ displayName: 'Direction',
514
+ type: 'Text',
515
+ group: 'style',
516
+ description: 'The orientation of the section',
517
+ defaultValue: 'column',
518
+ },
519
+ cfFlexWrap: {
520
+ displayName: 'Wrap objects',
521
+ type: 'Text',
522
+ group: 'style',
523
+ description: 'Wrap objects',
524
+ defaultValue: 'nowrap',
525
+ },
526
+ cfBorder: {
527
+ displayName: 'Border',
528
+ type: 'Text',
529
+ group: 'style',
530
+ description: 'The border of the section',
531
+ defaultValue: '1px solid rgba(0, 0, 0, 0)',
532
+ },
533
+ cfGap: {
534
+ displayName: 'Gap',
535
+ type: 'Text',
536
+ group: 'style',
537
+ description: 'The spacing between the elements of the section',
538
+ defaultValue: '0px',
539
+ },
540
+ cfBackgroundImageUrl: {
541
+ displayName: 'Background image',
542
+ type: 'Text',
543
+ defaultValue: '',
544
+ description: 'Background image for section or container',
545
+ },
546
+ cfBackgroundImageScaling: {
547
+ displayName: 'Image scaling',
548
+ type: 'Text',
549
+ group: 'style',
550
+ description: 'Adjust background image to fit, fill or tile the container',
551
+ defaultValue: 'fit',
552
+ validations: {
553
+ in: [
554
+ {
555
+ value: 'fill',
556
+ displayName: 'Fill',
557
+ },
558
+ {
559
+ value: 'fit',
560
+ displayName: 'Fit',
561
+ },
562
+ {
563
+ value: 'tile',
564
+ displayName: 'Tile',
565
+ },
566
+ ],
567
+ },
568
+ },
569
+ cfBackgroundImageAlignment: {
570
+ displayName: 'Image alignment',
571
+ type: 'Text',
572
+ group: 'style',
573
+ description: 'Align background image to the edges of the container',
574
+ defaultValue: 'left top',
575
+ },
576
+ cfHyperlink: {
577
+ displayName: 'Hyperlink',
578
+ type: 'Text',
579
+ defaultValue: '',
580
+ validations: {
581
+ format: 'URL',
582
+ },
583
+ description: 'hyperlink for section or container',
584
+ },
585
+ cfOpenInNewTab: {
586
+ displayName: 'Hyperlink behaviour',
587
+ type: 'Boolean',
588
+ defaultValue: false,
589
+ description: 'To open hyperlink in new Tab or not',
590
+ },
591
+ };
592
+ const optionalBuiltInStyles = {
593
+ cfFontSize: {
594
+ displayName: 'Font Size',
595
+ type: 'Text',
596
+ group: 'style',
597
+ description: 'The font size of the element',
598
+ defaultValue: '16px',
599
+ },
600
+ cfFontWeight: {
601
+ validations: {
602
+ in: [
603
+ {
604
+ value: '400',
605
+ displayName: 'Normal',
606
+ },
607
+ {
608
+ value: '500',
609
+ displayName: 'Medium',
610
+ },
611
+ {
612
+ value: '600',
613
+ displayName: 'Semi Bold',
614
+ },
615
+ ],
616
+ },
617
+ displayName: 'Font Weight',
618
+ type: 'Text',
619
+ group: 'style',
620
+ description: 'The font weight of the element',
621
+ defaultValue: '400',
622
+ },
623
+ cfLineHeight: {
624
+ displayName: 'Line Height',
625
+ type: 'Text',
626
+ group: 'style',
627
+ description: 'The line height of the element',
628
+ defaultValue: '20px',
629
+ },
630
+ cfLetterSpacing: {
631
+ displayName: 'Letter Spacing',
632
+ type: 'Text',
633
+ group: 'style',
634
+ description: 'The letter spacing of the element',
635
+ defaultValue: '0px',
636
+ },
637
+ cfTextColor: {
638
+ displayName: 'Text Color',
639
+ type: 'Text',
640
+ group: 'style',
641
+ description: 'The text color of the element',
642
+ defaultValue: 'rgba(0, 0, 0, 1)',
643
+ },
644
+ cfTextAlign: {
645
+ validations: {
646
+ in: [
647
+ {
648
+ value: 'left',
649
+ displayName: 'Align left',
650
+ },
651
+ {
652
+ value: 'center',
653
+ displayName: 'Align center',
654
+ },
655
+ {
656
+ value: 'right',
657
+ displayName: 'Align right',
658
+ },
659
+ ],
660
+ },
661
+ displayName: 'Text Align',
662
+ type: 'Text',
663
+ group: 'style',
664
+ description: 'The text alignment of the element',
665
+ defaultValue: 'left',
666
+ },
667
+ cfTextTransform: {
668
+ validations: {
669
+ in: [
670
+ {
671
+ value: 'none',
672
+ displayName: 'Normal',
673
+ },
674
+ {
675
+ value: 'capitalize',
676
+ displayName: 'Capitalize',
677
+ },
678
+ {
679
+ value: 'uppercase',
680
+ displayName: 'Uppercase',
681
+ },
682
+ {
683
+ value: 'lowercase',
684
+ displayName: 'Lowercase',
685
+ },
686
+ ],
687
+ },
688
+ displayName: 'Text Transform',
689
+ type: 'Text',
690
+ group: 'style',
691
+ description: 'The text transform of the element',
692
+ defaultValue: 'none',
693
+ },
694
+ cfTextBold: {
695
+ displayName: 'Bold',
696
+ type: 'Boolean',
697
+ group: 'style',
698
+ description: 'The text bold of the element',
699
+ defaultValue: false,
700
+ },
701
+ cfTextItalic: {
702
+ displayName: 'Italic',
703
+ type: 'Boolean',
704
+ group: 'style',
705
+ description: 'The text italic of the element',
706
+ defaultValue: false,
707
+ },
708
+ cfTextUnderline: {
709
+ displayName: 'Underline',
710
+ type: 'Boolean',
711
+ group: 'style',
712
+ description: 'The text underline of the element',
713
+ defaultValue: false,
714
+ },
715
+ };
716
+
717
+ const designTokensRegistry = {};
718
+ /**
719
+ * Register design tokens styling
720
+ * @param designTokenDefinition - {[key:string]: Record<string, string>}
721
+ * @returns void
722
+ */
723
+ const defineDesignTokens = (designTokenDefinition) => {
724
+ Object.assign(designTokensRegistry, designTokenDefinition);
725
+ };
726
+ const templateStringRegex = /\${(.+?)}/g;
727
+ const getDesignTokenRegistration = (breakpointValue, variableName) => {
728
+ if (!breakpointValue)
729
+ return breakpointValue;
730
+ let resolvedValue = '';
731
+ for (const part of breakpointValue.split(' ')) {
732
+ const tokenValue = templateStringRegex.test(part)
733
+ ? resolveSimpleDesignToken(part, variableName)
734
+ : part;
735
+ resolvedValue += `${tokenValue} `;
736
+ }
737
+ // Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
738
+ return resolvedValue.trim();
739
+ };
740
+ const resolveSimpleDesignToken = (templateString, variableName) => {
741
+ const nonTemplateValue = templateString.replace(templateStringRegex, '$1');
742
+ const [tokenCategory, tokenName] = nonTemplateValue.split('.');
743
+ const tokenValues = designTokensRegistry[tokenCategory];
744
+ if (tokenValues && tokenValues[tokenName]) {
745
+ if (variableName === 'cfBorder') {
746
+ const { width, style, color } = tokenValues[tokenName];
747
+ return `${width} ${style} ${color}`;
748
+ }
749
+ return tokenValues[tokenName];
750
+ }
751
+ if (builtInStyles[variableName]) {
752
+ return builtInStyles[variableName].defaultValue;
753
+ }
754
+ if (optionalBuiltInStyles[variableName]) {
755
+ return optionalBuiltInStyles[variableName].defaultValue;
756
+ }
757
+ return '0px';
758
+ };
759
+
760
+ const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
761
+ const toCSSMediaQuery = ({ query }) => {
762
+ if (query === '*')
763
+ return undefined;
764
+ const match = query.match(MEDIA_QUERY_REGEXP);
765
+ if (!match)
766
+ return undefined;
767
+ const [, operator, value, unit] = match;
768
+ if (operator === '<') {
769
+ const maxScreenWidth = Number(value) - 1;
770
+ return `(max-width: ${maxScreenWidth}${unit})`;
771
+ }
772
+ else if (operator === '>') {
773
+ const minScreenWidth = Number(value) + 1;
774
+ return `(min-width: ${minScreenWidth}${unit})`;
775
+ }
776
+ return undefined;
777
+ };
778
+ // Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
779
+ const findLast = (array, predicate) => {
780
+ return array.reverse().find(predicate);
781
+ };
782
+ // Initialise media query matchers. This won't include the always matching fallback breakpoint.
783
+ const mediaQueryMatcher = (breakpoints) => {
784
+ const mediaQueryMatches = {};
785
+ const mediaQueryMatchers = breakpoints
786
+ .map((breakpoint) => {
787
+ const cssMediaQuery = toCSSMediaQuery(breakpoint);
788
+ if (!cssMediaQuery)
789
+ return undefined;
790
+ if (typeof window === 'undefined')
791
+ return undefined;
792
+ const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
793
+ mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
794
+ return { id: breakpoint.id, signal: mediaQueryMatcher };
795
+ })
796
+ .filter((matcher) => !!matcher);
797
+ return [mediaQueryMatchers, mediaQueryMatches];
798
+ };
799
+ const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
800
+ // The breakpoints are ordered (desktop-first: descending by screen width)
801
+ const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
802
+ id,
803
+ index,
804
+ // The fallback breakpoint with wildcard query will always match
805
+ isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
806
+ }));
807
+ // Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
808
+ const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
809
+ return mostSpecificIndex ?? fallbackBreakpointIndex;
810
+ };
811
+ const getFallbackBreakpointIndex = (breakpoints) => {
812
+ // We assume that there will be a single breakpoint which uses the wildcard query.
813
+ // If there is none, we just take the first one in the list.
814
+ return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
815
+ };
816
+ const builtInStylesWithDesignTokens = [
817
+ 'cfMargin',
818
+ 'cfPadding',
819
+ 'cfGap',
820
+ 'cfWidth',
821
+ 'cfHeight',
822
+ 'cfBackgroundColor',
823
+ 'cfBorder',
824
+ 'cfFontSize',
825
+ 'cfLineHeight',
826
+ 'cfLetterSpacing',
827
+ 'cfTextColor',
828
+ ];
829
+ const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName) => {
830
+ const eventuallyResolveDesignTokens = (value) => {
831
+ // For some built-in design propertier, we support design tokens
832
+ if (builtInStylesWithDesignTokens.includes(variableName)) {
833
+ return getDesignTokenRegistration(value, variableName);
834
+ }
835
+ // For all other properties, we just return the breakpoint-specific value
836
+ return value;
837
+ };
838
+ if (valuesByBreakpoint instanceof Object) {
839
+ // Assume that the values are sorted by media query to apply the cascading CSS logic
840
+ for (let index = activeBreakpointIndex; index >= 0; index--) {
841
+ const breakpointId = breakpoints[index].id;
842
+ if (valuesByBreakpoint[breakpointId]) {
843
+ // If the value is defined, we use it and stop the breakpoints cascade
844
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
845
+ }
846
+ }
847
+ // If no breakpoint matched, we search and apply the fallback breakpoint
848
+ const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
849
+ const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex].id;
850
+ return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
851
+ }
852
+ else {
853
+ // Old design properties did not support breakpoints, keep for backward compatibility
854
+ return valuesByBreakpoint;
855
+ }
856
+ };
857
+
858
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
859
+ const isLinkToAsset = (variable) => {
860
+ if (!variable)
861
+ return false;
862
+ if (typeof variable !== 'object')
863
+ return false;
864
+ return (variable.sys?.linkType === 'Asset' &&
865
+ typeof variable.sys?.id === 'string' &&
866
+ !!variable.sys?.id &&
867
+ variable.sys?.type === 'Link');
868
+ };
869
+
870
+ const isLink = (maybeLink) => {
871
+ if (maybeLink === null)
872
+ return false;
873
+ if (typeof maybeLink !== 'object')
874
+ return false;
875
+ const link = maybeLink;
876
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link';
877
+ };
878
+
879
+ /**
880
+ * This module encapsulates format of the path to a deep reference.
881
+ */
882
+ const parseDataSourcePathIntoFieldset = (path) => {
883
+ const parsedPath = parseDeepPath(path);
884
+ if (null === parsedPath) {
885
+ throw new Error(`Cannot parse path '${path}' as deep path`);
886
+ }
887
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
888
+ };
889
+ /**
890
+ * Parse path into components, supports L1 references (one reference follow) atm.
891
+ * @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
892
+ * eg. `/uuid123/fields/file/~locale/fields/title/~locale`
893
+ * @returns
894
+ */
895
+ const parseDataSourcePathWithL1DeepBindings = (path) => {
896
+ const parsedPath = parseDeepPath(path);
897
+ if (null === parsedPath) {
898
+ throw new Error(`Cannot parse path '${path}' as deep path`);
899
+ }
900
+ return {
901
+ key: parsedPath.key,
902
+ field: parsedPath.fields[0],
903
+ referentField: parsedPath.fields[1],
904
+ };
905
+ };
906
+ /**
907
+ * Detects if paths is valid deep-path, like:
908
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
909
+ * or regular, like:
910
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
911
+ * @returns
912
+ */
913
+ const isDeepPath = (deepPathCandidate) => {
914
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
915
+ if (!deepPathParsed) {
916
+ return false;
917
+ }
918
+ return deepPathParsed.fields.length > 1;
919
+ };
920
+ const parseDeepPath = (deepPathCandidate) => {
921
+ // ALGORITHM:
922
+ // We start with deep path in form:
923
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
924
+ // First turn string into array of segments
925
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
926
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
927
+ // [
928
+ // [ "", "uuid123" ],
929
+ // [ "fields", "mainStory", "~locale" ],
930
+ // [ "fields", "cover", "~locale" ],
931
+ // [ "fields", "title", "~locale" ]
932
+ // ]
933
+ // Then check "initial" chunk for corretness
934
+ // Then check all "field-leading" chunks for correctness
935
+ const isValidInitialChunk = (initialChunk) => {
936
+ // must have start with '' and have at least 2 segments, second non-empty
937
+ // eg. /-_432uuid123123
938
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
939
+ };
940
+ const isValidFieldChunk = (fieldChunk) => {
941
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
942
+ // eg. fields/-32234mainStory/~locale
943
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
944
+ };
945
+ const deepPathSegments = deepPathCandidate.split('/');
946
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
947
+ if (chunks.length <= 1) {
948
+ return null; // malformed path, even regular paths have at least 2 chunks
949
+ }
950
+ else if (chunks.length === 2) {
951
+ return null; // deep paths have at least 3 chunks
952
+ }
953
+ // With 3+ chunks we can now check for deep path correctness
954
+ const [initialChunk, ...fieldChunks] = chunks;
955
+ if (!isValidInitialChunk(initialChunk)) {
956
+ return null;
957
+ }
958
+ if (!fieldChunks.every(isValidFieldChunk)) {
959
+ return null;
960
+ }
961
+ return {
962
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
963
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
964
+ };
965
+ };
966
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
967
+ const chunks = [];
968
+ let currentChunk = [];
969
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
970
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
971
+ for (let i = 0; i < segments.length; i++) {
972
+ const isInitialElement = i === 0;
973
+ const segment = segments[i];
974
+ if (isInitialElement) {
975
+ currentChunk = [segment];
976
+ }
977
+ else if (isSegmentBeginningOfChunk(segment)) {
978
+ chunks.push(currentChunk);
979
+ currentChunk = [segment];
980
+ }
981
+ else {
982
+ currentChunk.push(segment);
983
+ }
984
+ }
985
+ chunks.push(currentChunk);
986
+ return chunks.filter(excludeEmptyChunks);
987
+ };
988
+
989
+ const sendMessage = (eventType, data) => {
990
+ if (typeof window === 'undefined') {
991
+ return;
992
+ }
993
+ console.debug(`[experiences-sdk-react::sendMessage] Sending message [${eventType}]`, {
994
+ source: 'customer-app',
995
+ eventType,
996
+ payload: data,
997
+ });
998
+ window.parent?.postMessage({
999
+ source: 'customer-app',
1000
+ eventType,
1001
+ payload: data,
1002
+ }, '*');
1003
+ };
1004
+
1005
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1006
+ function get(obj, path) {
1007
+ if (!path.length) {
1008
+ return obj;
1009
+ }
1010
+ try {
1011
+ const [currentPath, ...nextPath] = path;
1012
+ return get(obj[currentPath], nextPath);
1013
+ }
1014
+ catch (err) {
1015
+ return undefined;
1016
+ }
1017
+ }
1018
+
1019
+ function transformAssetFileToUrl(fieldValue) {
1020
+ return fieldValue && typeof fieldValue == 'object' && fieldValue.url
1021
+ ? fieldValue.url
1022
+ : fieldValue;
1023
+ }
1024
+
1025
+ /**
1026
+ * Base Store for entities
1027
+ * Can be extened for the different loading behaviours (editor, production, ..)
1028
+ */
1029
+ class EntityStoreBase {
1030
+ constructor({ entities, locale }) {
1031
+ this.entryMap = new Map();
1032
+ this.assetMap = new Map();
1033
+ this.locale = locale;
1034
+ for (const entity of entities) {
1035
+ this.addEntity(entity);
1036
+ }
1037
+ }
1038
+ get entities() {
1039
+ return [...this.entryMap.values(), ...this.assetMap.values()];
1040
+ }
1041
+ updateEntity(entity) {
1042
+ this.addEntity(entity);
1043
+ }
1044
+ getValueDeep(headLinkOrEntity, deepPath) {
1045
+ const resolveFieldset = (unresolvedFieldset, headEntity) => {
1046
+ const resolvedFieldset = [];
1047
+ let entityToResolveFieldsFrom = headEntity;
1048
+ for (let i = 0; i < unresolvedFieldset.length; i++) {
1049
+ const isLeaf = i === unresolvedFieldset.length - 1; // with last row, we are not expecting a link, but a value
1050
+ const row = unresolvedFieldset[i];
1051
+ const [, field, _localeQualifier] = row;
1052
+ if (!entityToResolveFieldsFrom) {
1053
+ throw new Error(`Logic Error: Cannot resolve field ${field} of a fieldset as there is no entity to resolve it from.`);
1054
+ }
1055
+ if (isLeaf) {
1056
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1057
+ break;
1058
+ }
1059
+ const fieldValue = get(entityToResolveFieldsFrom, ['fields', field]);
1060
+ if (undefined === fieldValue) {
1061
+ return {
1062
+ resolvedFieldset,
1063
+ isFullyResolved: false,
1064
+ reason: `Cannot resolve field Link<${entityToResolveFieldsFrom.sys.type}>(sys.id=${entityToResolveFieldsFrom.sys.id}).fields[${field}] as field value is not defined`,
1065
+ };
1066
+ }
1067
+ else if (isLink(fieldValue)) {
1068
+ const entity = this.getEntityFromLink(fieldValue);
1069
+ if (entity === undefined) {
1070
+ throw new Error(`Logic Error: Broken Precondition [by the time resolution of deep path happens all referents should be in EntityStore]: Cannot resolve field ${field} of a fieldset row [${JSON.stringify(row)}] as linked entity not found in the EntityStore. ${JSON.stringify({
1071
+ link: fieldValue,
1072
+ })}`);
1073
+ }
1074
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1075
+ entityToResolveFieldsFrom = entity; // we move up
1076
+ }
1077
+ else {
1078
+ // TODO: Eg. when someone changed the schema and the field is not a link anymore, what should we return then?
1079
+ throw new Error(`LogicError: Invalid value of a field we consider a reference field. Cannot resolve field ${field} of a fieldset as it is not a link, neither undefined.`);
1080
+ }
1081
+ }
1082
+ return {
1083
+ resolvedFieldset,
1084
+ isFullyResolved: true,
1085
+ };
1086
+ };
1087
+ const headEntity = isLink(headLinkOrEntity)
1088
+ ? this.getEntityFromLink(headLinkOrEntity)
1089
+ : headLinkOrEntity;
1090
+ if (undefined === headEntity) {
1091
+ return;
1092
+ }
1093
+ const unresolvedFieldset = parseDataSourcePathIntoFieldset(deepPath);
1094
+ // The purpose here is to take this intermediate representation of the deep-path
1095
+ // and to follow the links to the leaf-entity and field
1096
+ // in case we can't follow till the end, we should signal that there was null-reference in the path
1097
+ const { resolvedFieldset, isFullyResolved, reason } = resolveFieldset(unresolvedFieldset, headEntity);
1098
+ if (!isFullyResolved) {
1099
+ reason &&
1100
+ console.debug(`[experiences-sdk-react::EntityStoreBased::getValueDeep()] Deep path wasn't resolved till leaf node, falling back to undefined, because: ${reason}`);
1101
+ return undefined;
1102
+ }
1103
+ const [leafEntity, field /* localeQualifier */] = resolvedFieldset[resolvedFieldset.length - 1];
1104
+ const fieldValue = get(leafEntity, ['fields', field]); // is allowed to be undefined (when non-required field not set; or even when field does NOT exist on the type)
1105
+ return transformAssetFileToUrl(fieldValue);
1106
+ }
1107
+ /**
1108
+ * @deprecated in the base class this should be simply an abstract method
1109
+ * @param entityLink
1110
+ * @param path
1111
+ * @returns
1112
+ */
1113
+ getValue(entityLink, path) {
1114
+ const entity = this.getEntity(entityLink.sys.linkType, entityLink.sys.id);
1115
+ if (!entity) {
1116
+ // TODO: move to `debug` utils once it is extracted
1117
+ console.warn(`Unresolved entity reference: ${entityLink.sys.linkType} with ID ${entityLink.sys.id}`);
1118
+ return;
1119
+ }
1120
+ return get(entity, path);
1121
+ }
1122
+ getEntityFromLink(link) {
1123
+ const resolvedEntity = link.sys.linkType === 'Entry'
1124
+ ? this.entryMap.get(link.sys.id)
1125
+ : this.assetMap.get(link.sys.id);
1126
+ if (!resolvedEntity || resolvedEntity.sys.type !== link.sys.linkType) {
1127
+ console.warn(`Experience references unresolved entity: ${JSON.stringify(link)}`);
1128
+ return;
1129
+ }
1130
+ return resolvedEntity;
1131
+ }
1132
+ getEntitiesFromMap(type, ids) {
1133
+ const resolved = [];
1134
+ const missing = [];
1135
+ for (const id of ids) {
1136
+ const entity = this.getEntity(type, id);
1137
+ if (entity) {
1138
+ resolved.push(entity);
1139
+ }
1140
+ else {
1141
+ missing.push(id);
1142
+ }
1143
+ }
1144
+ return {
1145
+ resolved,
1146
+ missing,
1147
+ };
1148
+ }
1149
+ addEntity(entity) {
1150
+ if (this.isAsset(entity)) {
1151
+ this.assetMap.set(entity.sys.id, entity);
1152
+ }
1153
+ else {
1154
+ this.entryMap.set(entity.sys.id, entity);
1155
+ }
1156
+ }
1157
+ async fetchAsset(id) {
1158
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', [id]);
1159
+ if (missing.length) {
1160
+ // TODO: move to `debug` utils once it is extracted
1161
+ console.warn(`Asset "${id}" is not in the store`);
1162
+ return undefined;
1163
+ }
1164
+ return resolved[0];
1165
+ }
1166
+ async fetchAssets(ids) {
1167
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', ids);
1168
+ if (missing.length) {
1169
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1170
+ }
1171
+ return resolved;
1172
+ }
1173
+ async fetchEntry(id) {
1174
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', [id]);
1175
+ if (missing.length) {
1176
+ // TODO: move to `debug` utils once it is extracted
1177
+ console.warn(`Entry "${id}" is not in the store`);
1178
+ return undefined;
1179
+ }
1180
+ return resolved[0];
1181
+ }
1182
+ async fetchEntries(ids) {
1183
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', ids);
1184
+ if (missing.length) {
1185
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1186
+ }
1187
+ return resolved;
1188
+ }
1189
+ isAsset(entity) {
1190
+ return entity.sys.type === 'Asset';
1191
+ }
1192
+ getEntity(type, id) {
1193
+ if (type === 'Asset') {
1194
+ return this.assetMap.get(id);
1195
+ }
1196
+ return this.entryMap.get(id);
1197
+ }
1198
+ }
1199
+
1200
+ /**
1201
+ * EntityStore which resolves entries and assets from the editor
1202
+ * over the sendMessage and subscribe functions.
1203
+ */
1204
+ class EditorEntityStore extends EntityStoreBase {
1205
+ constructor({ entities, locale, sendMessage, subscribe, timeoutDuration = 3000, }) {
1206
+ super({ entities, locale });
1207
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1208
+ this.requestCache = new Map();
1209
+ this.cacheIdSeperator = ',';
1210
+ this.sendMessage = sendMessage;
1211
+ this.subscribe = subscribe;
1212
+ this.timeoutDuration = timeoutDuration;
1213
+ }
1214
+ cleanupPromise(referenceId) {
1215
+ setTimeout(() => {
1216
+ this.requestCache.delete(referenceId);
1217
+ }, 300);
1218
+ }
1219
+ getCacheId(id) {
1220
+ return id.length === 1 ? id[0] : id.join(this.cacheIdSeperator);
1221
+ }
1222
+ async fetchEntity(type, ids, skipCache = false) {
1223
+ let missing;
1224
+ if (!skipCache) {
1225
+ const { missing: missingFromCache, resolved } = this.getEntitiesFromMap(type, ids);
1226
+ if (missingFromCache.length === 0) {
1227
+ // everything is already in cache
1228
+ return resolved;
1229
+ }
1230
+ missing = missingFromCache;
1231
+ }
1232
+ else {
1233
+ missing = [...ids];
1234
+ }
1235
+ const cacheId = this.getCacheId(missing);
1236
+ const openRequest = this.requestCache.get(cacheId);
1237
+ if (openRequest) {
1238
+ return openRequest;
1239
+ }
1240
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1241
+ const newPromise = new Promise((resolve, reject) => {
1242
+ const unsubscribe = this.subscribe(PostMessageMethods$2.REQUESTED_ENTITIES, (message) => {
1243
+ const messageIds = [
1244
+ ...message.entities.map((entity) => entity.sys.id),
1245
+ ...(message.missingEntityIds ?? []),
1246
+ ];
1247
+ if (missing.every((id) => messageIds.find((entityId) => entityId === id))) {
1248
+ clearTimeout(timeout);
1249
+ resolve(message.entities);
1250
+ this.cleanupPromise(cacheId);
1251
+ ids.forEach((id) => this.cleanupPromise(id));
1252
+ unsubscribe();
1253
+ }
1254
+ else {
1255
+ console.warn('Unexpected entities received in REQUESTED_ENTITIES. Ignoring this response.');
1256
+ }
1257
+ });
1258
+ const timeout = setTimeout(() => {
1259
+ reject(new Error(`Request for entities timed out ${this.timeoutDuration}ms} for ${cacheId}`));
1260
+ this.cleanupPromise(cacheId);
1261
+ ids.forEach((id) => this.cleanupPromise(id));
1262
+ unsubscribe();
1263
+ }, this.timeoutDuration);
1264
+ this.sendMessage(PostMessageMethods$2.REQUEST_ENTITIES, {
1265
+ entityIds: missing,
1266
+ entityType: type,
1267
+ locale: this.locale,
1268
+ });
1269
+ });
1270
+ this.requestCache.set(cacheId, newPromise);
1271
+ ids.forEach((cid) => {
1272
+ this.requestCache.set(cid, newPromise);
1273
+ });
1274
+ const result = (await newPromise);
1275
+ result.forEach((value) => {
1276
+ this.addEntity(value);
1277
+ });
1278
+ return this.getEntitiesFromMap(type, ids).resolved;
1279
+ }
1280
+ async fetchAsset(id, skipCache = false) {
1281
+ try {
1282
+ return (await this.fetchAssets([id], skipCache))[0];
1283
+ }
1284
+ catch (err) {
1285
+ // TODO: move to debug utils once it is extracted
1286
+ console.warn(`Failed to request asset ${id}`);
1287
+ return undefined;
1288
+ }
1289
+ }
1290
+ fetchAssets(ids, skipCache = false) {
1291
+ return this.fetchEntity('Asset', ids, skipCache);
1292
+ }
1293
+ async fetchEntry(id, skipCache = false) {
1294
+ try {
1295
+ return (await this.fetchEntries([id], skipCache))[0];
1296
+ }
1297
+ catch (err) {
1298
+ // TODO: move to debug utils once it is extracted
1299
+ console.warn(`Failed to request entry ${id}`, err);
1300
+ return undefined;
1301
+ }
1302
+ }
1303
+ fetchEntries(ids, skipCache = false) {
1304
+ return this.fetchEntity('Entry', ids, skipCache);
1305
+ }
1306
+ }
1307
+
1308
+ // The default of 3s in the EditorEntityStore is sometimes timing out and
1309
+ // leads to not rendering bound content and assemblies.
1310
+ const REQUEST_TIMEOUT = 10000;
1311
+ class EditorModeEntityStore extends EditorEntityStore {
1312
+ constructor({ entities, locale }) {
1313
+ console.debug(`[experiences-sdk-react] Initializing editor entity store with ${entities.length} entities for locale ${locale}.`, { entities });
1314
+ const subscribe = (method, cb) => {
1315
+ const handleMessage = (event) => {
1316
+ const data = JSON.parse(event.data);
1317
+ if (typeof data !== 'object' || !data)
1318
+ return;
1319
+ if (data.source !== 'composability-app')
1320
+ return;
1321
+ if (data.eventType === method) {
1322
+ cb(data.payload);
1323
+ }
1324
+ };
1325
+ if (typeof window !== 'undefined') {
1326
+ window.addEventListener('message', handleMessage);
1327
+ }
1328
+ return () => {
1329
+ if (typeof window !== 'undefined') {
1330
+ window.removeEventListener('message', handleMessage);
1331
+ }
1332
+ };
1333
+ };
1334
+ super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
1335
+ this.locale = locale;
1336
+ }
1337
+ /**
1338
+ * This function collects and returns the list of requested entries and assets. Additionally, it checks
1339
+ * upfront whether any async fetching logic is actually happening. If not, it returns a plain `false` value, so we
1340
+ * can detect this early and avoid unnecessary re-renders.
1341
+ * @param entityLinks
1342
+ * @returns false if no async fetching is happening, otherwise a promise that resolves when all entities are fetched
1343
+ */
1344
+ async fetchEntities({ missingEntryIds, missingAssetIds, skipCache = false, }) {
1345
+ // Entries and assets will be stored in entryMap and assetMap
1346
+ await Promise.all([
1347
+ this.fetchEntries(missingEntryIds, skipCache),
1348
+ this.fetchAssets(missingAssetIds, skipCache),
1349
+ ]);
1350
+ }
1351
+ getMissingEntityIds(entityLinks) {
1352
+ const entryLinks = entityLinks.filter((link) => link.sys?.linkType === 'Entry');
1353
+ const assetLinks = entityLinks.filter((link) => link.sys?.linkType === 'Asset');
1354
+ const uniqueEntryIds = [...new Set(entryLinks.map((link) => link.sys.id))];
1355
+ const uniqueAssetIds = [...new Set(assetLinks.map((link) => link.sys.id))];
1356
+ const { missing: missingEntryIds } = this.getEntitiesFromMap('Entry', uniqueEntryIds);
1357
+ const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
1358
+ return { missingEntryIds, missingAssetIds };
1359
+ }
1360
+ getValue(entityLink, path) {
1361
+ if (!entityLink || !entityLink.sys)
1362
+ return;
1363
+ const fieldValue = super.getValue(entityLink, path);
1364
+ return transformAssetFileToUrl(fieldValue);
1365
+ }
1366
+ }
1367
+
1368
+ var VisualEditorMode;
1369
+ (function (VisualEditorMode) {
1370
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
1371
+ VisualEditorMode["InjectScript"] = "injectScript";
1372
+ })(VisualEditorMode || (VisualEditorMode = {}));
1373
+
1374
+ function treeVisit$1(initialNode, onNode) {
1375
+ // returns last used index
1376
+ const _treeVisit = (currentNode, currentIndex, currentDepth) => {
1377
+ // Copy children in case of onNode removing it as we pass the node by reference
1378
+ const children = [...currentNode.children];
1379
+ onNode(currentNode, currentIndex, currentDepth);
1380
+ let nextAvailableIndex = currentIndex + 1;
1381
+ const lastUsedIndex = currentIndex;
1382
+ for (const child of children) {
1383
+ const lastUsedIndex = _treeVisit(child, nextAvailableIndex, currentDepth + 1);
1384
+ nextAvailableIndex = lastUsedIndex + 1;
1385
+ }
1386
+ return lastUsedIndex;
1387
+ };
1388
+ _treeVisit(initialNode, 0, 0);
1389
+ }
1390
+
1391
+ class DeepReference {
1392
+ constructor({ path, dataSource }) {
1393
+ const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
1394
+ this.originalPath = path;
1395
+ this.entityId = dataSource[key].sys.id;
1396
+ this.entityLink = dataSource[key];
1397
+ this.field = field;
1398
+ this.referentField = referentField;
1399
+ }
1400
+ get headEntityId() {
1401
+ return this.entityId;
1402
+ }
1403
+ /**
1404
+ * Extracts referent from the path, using EntityStore as source of
1405
+ * entities during the resolution path.
1406
+ * TODO: should it be called `extractLeafReferent` ? or `followToLeafReferent`
1407
+ */
1408
+ extractReferent(entityStore) {
1409
+ const headEntity = entityStore.getEntityFromLink(this.entityLink);
1410
+ const maybeReferentLink = headEntity.fields[this.field];
1411
+ if (undefined === maybeReferentLink) {
1412
+ // field references nothing (or even field doesn't exist)
1413
+ return undefined;
1414
+ }
1415
+ if (!isLink(maybeReferentLink)) {
1416
+ // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
1417
+ // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
1418
+ // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
1419
+ return undefined;
1420
+ }
1421
+ return maybeReferentLink;
1422
+ }
1423
+ static from(opt) {
1424
+ return new DeepReference(opt);
1425
+ }
1426
+ }
1427
+ function gatherDeepReferencesFromTree(startingNode, dataSource) {
1428
+ const deepReferences = [];
1429
+ treeVisit$1(startingNode, (node) => {
1430
+ if (!node.data.props)
1431
+ return;
1432
+ for (const [, variableMapping] of Object.entries(node.data.props)) {
1433
+ if (variableMapping.type !== 'BoundValue')
1434
+ continue;
1435
+ if (!isDeepPath(variableMapping.path))
1436
+ continue;
1437
+ deepReferences.push(DeepReference.from({
1438
+ path: variableMapping.path,
1439
+ dataSource,
1440
+ }));
1441
+ }
1442
+ });
1443
+ return deepReferences;
1444
+ }
1445
+
1446
+ class DragState {
1447
+ constructor() {
1448
+ this.isDragStartedOnParent = false;
1449
+ this.isDraggingItem = false;
1450
+ }
1451
+ get isDragging() {
1452
+ return this.isDraggingItem;
1453
+ }
1454
+ get isDragStart() {
1455
+ return this.isDragStartedOnParent;
1456
+ }
1457
+ updateIsDragging(isDraggingItem) {
1458
+ this.isDraggingItem = isDraggingItem;
1459
+ }
1460
+ updateIsDragStartedOnParent(isDragStartedOnParent) {
1461
+ this.isDragStartedOnParent = isDragStartedOnParent;
1462
+ }
1463
+ reset() {
1464
+ this.isDraggingItem = false;
1465
+ this.isDragStartedOnParent = false;
1466
+ }
1467
+ }
1468
+ var dragState = new DragState();
1469
+
1470
+ var css_248z$6 = ".styles-module_DraggableComponent__m5-dA {\n pointer-events: all;\n position: relative;\n transition: outline 0.2s;\n cursor: grab;\n outline-offset: -2px;\n outline: 2px solid transparent;\n box-sizing: border-box;\n display: flex;\n}\n\n.styles-module_DraggableComponent__m5-dA > * {\n pointer-events: none;\n}\n\n.styles-module_isDragging__WHjPU {\n overflow: hidden;\n}\n\n.styles-module_isDragging__WHjPU * {\n pointer-events: none !important;\n}\n\n.styles-module_isSelected__BzICQ {\n outline: 2px solid transparent !important;\n}\n\n.styles-module_overlay__r4th9 {\n position: absolute;\n display: flex;\n align-items: center;\n min-width: max-content;\n height: 24px;\n z-index: 1;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 14px;\n font-weight: 500;\n background-color: var(--exp-builder-gray500);\n color: var(--exp-builder-color-white);\n border-radius: 0 0 2px 0;\n padding: 4px 12px 4px 12px;\n transition: opacity 0.2s;\n opacity: 0;\n text-wrap: nowrap;\n}\n\n.styles-module_overlayContainer__eiX-5 {\n opacity: 0;\n}\n\n.styles-module_overlayAssembly__tOzZU {\n background-color: var(--exp-builder-purple600);\n}\n\n/* .DraggableComponent:hover .overlay {\n opacity: 1;\n} */\n\n.styles-module_userIsDragging__lqbjG > .styles-module_overlay__r4th9,\n.styles-module_userIsDragging__lqbjG > .styles-module_overlayContainer__eiX-5 {\n opacity: 0 !important;\n}\n\n.styles-module_userIsDragging__lqbjG {\n outline: 2px solid transparent !important;\n}\n\n.styles-module_DraggableComponent__m5-dA:hover:not(:has(div[data-rfd-draggable-id]:hover)) > .styles-module_overlay__r4th9 {\n opacity: 1;\n}\n\n.styles-module_DraggableComponent__m5-dA:hover,\n.styles-module_DraggableComponent__m5-dA:hover div[data-rfd-draggable-id] {\n outline: 2px dashed var(--exp-builder-gray500);\n}\n\n.styles-module_DraggableComponent__m5-dA:hover:not(:has(div[data-rfd-draggable-id]:hover)) {\n outline: 2px solid var(--exp-builder-gray500);\n}\n\n.styles-module_isAssemblyBlock__Y3Avk:hover,\n.styles-module_isAssemblyBlock__Y3Avk:hover div[data-rfd-draggable-id],\n.styles-module_DraggableComponent__m5-dA:hover div[data-rfd-draggable-id][data-cf-node-block-type^='assembly'] {\n outline: 2px dashed var(--exp-builder-purple600) !important;\n}\n\n.styles-module_isAssemblyBlock__Y3Avk:hover:not(:has(div[data-rfd-draggable-id]:hover)) {\n outline: 2px solid var(--exp-builder-purple600) !important;\n}\n";
1471
+ var styles$3 = {"DraggableComponent":"styles-module_DraggableComponent__m5-dA","isDragging":"styles-module_isDragging__WHjPU","isSelected":"styles-module_isSelected__BzICQ","overlay":"styles-module_overlay__r4th9","overlayContainer":"styles-module_overlayContainer__eiX-5","overlayAssembly":"styles-module_overlayAssembly__tOzZU","userIsDragging":"styles-module_userIsDragging__lqbjG","isAssemblyBlock":"styles-module_isAssemblyBlock__Y3Avk"};
1472
+ styleInject(css_248z$6);
1473
+
1474
+ const SCROLL_STATES = {
1475
+ Start: 'scrollStart',
1476
+ IsScrolling: 'isScrolling',
1477
+ End: 'scrollEnd',
1478
+ };
1479
+ const OUTGOING_EVENTS = {
1480
+ Connected: 'connected',
1481
+ DesignTokens: 'registerDesignTokens',
1482
+ HoveredSection: 'hoveredSection',
1483
+ MouseMove: 'mouseMove',
1484
+ NewHoveredElement: 'newHoveredElement',
1485
+ ComponentSelected: 'componentSelected',
1486
+ RegisteredComponents: 'registeredComponents',
1487
+ RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
1488
+ ComponentDragCanceled: 'componentDragCanceled',
1489
+ ComponentDropped: 'componentDropped',
1490
+ ComponentMoved: 'componentMoved',
1491
+ CanvasReload: 'canvasReload',
1492
+ UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
1493
+ UpdateHoveredComponentCoordinates: 'updateHoveredComponentCoordinates',
1494
+ CanvasScroll: 'canvasScrolling',
1495
+ CanvasError: 'canvasError',
1496
+ OutsideCanvasClick: 'outsideCanvasClick',
1497
+ };
1498
+ const INCOMING_EVENTS = {
1499
+ RequestEditorMode: 'requestEditorMode',
1500
+ CompositionUpdated: 'componentTreeUpdated',
1501
+ ComponentDraggingChanged: 'componentDraggingChanged',
1502
+ ComponentDragCanceled: 'componentDragCanceled',
1503
+ ComponentDragStarted: 'componentDragStarted',
1504
+ ComponentDragEnded: 'componentDragEnded',
1505
+ CanvasResized: 'canvasResized',
1506
+ SelectComponent: 'selectComponent',
1507
+ HoverComponent: 'hoverComponent',
1508
+ UpdatedEntity: 'updatedEntity',
1509
+ /**
1510
+ * @deprecated use `AssembliesAdded` instead. This will be removed in version 5.
1511
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
1512
+ */
1513
+ DesignComponentsAdded: 'designComponentsAdded',
1514
+ /**
1515
+ * @deprecated use `AssembliesRegistered` instead. This will be removed in version 5.
1516
+ * In the meanwhile, the experience builder will send the old and the new event to support multiple SDK versions.
1517
+ */
1518
+ DesignComponentsRegistered: 'designComponentsRegistered',
1519
+ AssembliesAdded: 'assembliesAdded',
1520
+ AssembliesRegistered: 'assembliesRegistered',
1521
+ InitEditor: 'initEditor',
1522
+ };
1523
+ const INTERNAL_EVENTS = {
1524
+ ComponentsRegistered: 'cfComponentsRegistered',
1525
+ VisualEditorInitialize: 'cfVisualEditorInitialize',
1526
+ };
1527
+ const VISUAL_EDITOR_EVENTS = {
1528
+ Ready: 'cfVisualEditorReady',
1529
+ };
1530
+ const CONTENTFUL_COMPONENTS = {
1531
+ section: {
1532
+ id: 'contentful-section',
1533
+ name: 'Section',
1534
+ },
1535
+ container: {
1536
+ id: 'contentful-container',
1537
+ name: 'Container',
1538
+ },
1539
+ columns: {
1540
+ id: 'contentful-columns',
1541
+ name: 'Columns',
1542
+ },
1543
+ singleColumn: {
1544
+ id: 'contentful-single-column',
1545
+ name: 'Column',
1546
+ },
1547
+ button: {
1548
+ id: 'button',
1549
+ name: 'Button',
1550
+ },
1551
+ heading: {
1552
+ id: 'heading',
1553
+ name: 'Heading',
1554
+ },
1555
+ image: {
1556
+ id: 'image',
1557
+ name: 'Image',
1558
+ },
1559
+ richText: {
1560
+ id: 'richText',
1561
+ name: 'Rich Text',
1562
+ },
1563
+ text: {
1564
+ id: 'text',
1565
+ name: 'Text',
1566
+ },
1567
+ };
1568
+ const ASSEMBLY_NODE_TYPE = 'assembly';
1569
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
1570
+ const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
1571
+ const ASSEMBLY_NODE_TYPES = [ASSEMBLY_NODE_TYPE, ASSEMBLY_BLOCK_NODE_TYPE];
1572
+ /** @deprecated use `ASSEMBLY_NODE_TYPE` instead. This will be removed in version 5. */
1573
+ const DESIGN_COMPONENT_NODE_TYPE = 'designComponent';
1574
+ /** @deprecated use `ASSEMBLY_BLOCK_NODE_TYPE` instead. This will be removed in version 5. */
1575
+ const DESIGN_COMPONENT_BLOCK_NODE_TYPE = 'designComponentBlock';
1576
+ /** @deprecated use `ASSEMBLY_NODE_TYPES` instead. This will be removed in version 5. */
1577
+ const DESIGN_COMPONENT_NODE_TYPES = [
1578
+ DESIGN_COMPONENT_NODE_TYPE,
1579
+ DESIGN_COMPONENT_BLOCK_NODE_TYPE,
1580
+ ];
1581
+ const CF_STYLE_ATTRIBUTES = [
1582
+ 'cfHorizontalAlignment',
1583
+ 'cfVerticalAlignment',
1584
+ 'cfMargin',
1585
+ 'cfPadding',
1586
+ 'cfBackgroundColor',
1587
+ 'cfWidth',
1588
+ 'cfMaxWidth',
1589
+ 'cfHeight',
1590
+ 'cfFlexDirection',
1591
+ 'cfFlexWrap',
1592
+ 'cfBorder',
1593
+ 'cfGap',
1594
+ 'cfBackgroundImageUrl',
1595
+ 'cfBackgroundImageScaling',
1596
+ 'cfBackgroundImageAlignment',
1597
+ 'cfFontSize',
1598
+ 'cfFontWeight',
1599
+ 'cfLineHeight',
1600
+ 'cfLetterSpacing',
1601
+ 'cfTextColor',
1602
+ 'cfTextAlign',
1603
+ 'cfTextTransform',
1604
+ 'cfTextBold',
1605
+ 'cfTextItalic',
1606
+ 'cfTextUnderline',
1607
+ // For backwards compatibility
1608
+ // we need to keep those in this constant array
1609
+ // so that omit() in <VisualEditorBlock> and <CompositionBlock>
1610
+ // can filter them out and not pass as props
1611
+ 'cfBackgroundImageAlignmentVertical',
1612
+ 'cfBackgroundImageAlignmentHorizontal',
1613
+ ];
1614
+ const EMPTY_CONTAINER_HEIGHT = '80px';
1615
+ var PostMessageMethods$1;
1616
+ (function (PostMessageMethods) {
1617
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
1618
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
1619
+ })(PostMessageMethods$1 || (PostMessageMethods$1 = {}));
1620
+
1621
+ const isRelativePreviewSize = (width) => {
1622
+ // For now, we solely allow 100% as relative value
1623
+ return width === '100%';
1624
+ };
1625
+ const getTooltipPositions = ({ previewSize, tooltipRect, coordinates, }) => {
1626
+ if (!coordinates || !tooltipRect) {
1627
+ return { display: 'none' };
1628
+ }
1629
+ /**
1630
+ * By default, the tooltip floats to the left of the element
1631
+ */
1632
+ const newTooltipStyles = { display: 'flex' };
1633
+ // If the preview size is relative, we don't change the floating direction
1634
+ if (!isRelativePreviewSize(previewSize)) {
1635
+ const previewSizeMatch = previewSize.match(/(\d{1,})px/);
1636
+ if (!previewSizeMatch) {
1637
+ return { display: 'none' };
1638
+ }
1639
+ const previewSizePx = parseInt(previewSizeMatch[1]);
1640
+ /**
1641
+ * If the element is at the right edge of the canvas, and the element isn't wide enough to fit the tooltip width,
1642
+ * we float the tooltip to the right of the element.
1643
+ */
1644
+ if (tooltipRect.width > previewSizePx - coordinates.right &&
1645
+ tooltipRect.width > coordinates.width) {
1646
+ newTooltipStyles['float'] = 'right';
1647
+ }
1648
+ }
1649
+ const tooltipHeight = tooltipRect.height === 0 ? 32 : tooltipRect.height;
1650
+ /**
1651
+ * For elements with small heights, we don't want the tooltip covering the content in the element,
1652
+ * so we show the tooltip at the top or bottom.
1653
+ */
1654
+ if (tooltipHeight * 2 > coordinates.height) {
1655
+ /**
1656
+ * If there's enough space for the tooltip at the top of the element, we show the tooltip at the top of the element,
1657
+ * else we show the tooltip at the bottom.
1658
+ */
1659
+ if (tooltipHeight < coordinates.top) {
1660
+ newTooltipStyles['bottom'] = coordinates.height;
1661
+ }
1662
+ else {
1663
+ newTooltipStyles['top'] = coordinates.height;
1664
+ }
1665
+ }
1666
+ /**
1667
+ * If the component draws outside of the borders of the canvas to the left we move the tooltip to the right
1668
+ * so that it is fully visible.
1669
+ */
1670
+ if (coordinates.left < 0) {
1671
+ newTooltipStyles['left'] = -coordinates.left;
1672
+ }
1673
+ /**
1674
+ * If for any reason, the element's top is negative, we show the tooltip at the bottom
1675
+ */
1676
+ if (coordinates.top < 0) {
1677
+ newTooltipStyles['top'] = coordinates.height;
1678
+ }
1679
+ return newTooltipStyles;
1680
+ };
1681
+
1682
+ const Tooltip = ({ coordinates, id, label, isAssemblyBlock, isContainer }) => {
1683
+ const tooltipRef = useRef(null);
1684
+ const previewSize = '100%'; // This should be based on breakpoints and added to usememo dependency array
1685
+ const tooltipStyles = useMemo(() => {
1686
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect();
1687
+ const draggableRect = document
1688
+ .querySelector(`[data-ctfl-draggable-id="${id}"]`)
1689
+ ?.getBoundingClientRect();
1690
+ const newTooltipStyles = getTooltipPositions({
1691
+ previewSize,
1692
+ tooltipRect,
1693
+ coordinates: draggableRect,
1694
+ });
1695
+ return newTooltipStyles;
1696
+ // Ignore eslint because we intentionally want to trigger this whenever a user clicks on a container/component which is tracked by these coordinates of the component being clicked being changed
1697
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1698
+ }, [coordinates, id, tooltipRef.current]);
1699
+ return (React.createElement("div", { ref: tooltipRef, style: tooltipStyles, className: classNames(styles$3.overlay, {
1700
+ [styles$3.overlayContainer]: isContainer,
1701
+ [styles$3.overlayAssembly]: isAssemblyBlock,
1702
+ }) }, label));
1703
+ };
1704
+
1705
+ const useDraggedItemStore = create((set) => ({
1706
+ draggedItem: undefined,
1707
+ componentId: '',
1708
+ isDraggingOnCanvas: false,
1709
+ setComponentId(id) {
1710
+ set({ componentId: id });
1711
+ },
1712
+ updateItem: (item) => {
1713
+ set({ draggedItem: item });
1714
+ },
1715
+ setDraggingOnCanvas: (isDraggingOnCanvas) => {
1716
+ set({ isDraggingOnCanvas });
1717
+ },
1718
+ }));
1719
+
1720
+ const DRAGGABLE_HEIGHT = 30;
1721
+ const DRAGGABLE_WIDTH = 50;
1722
+ const DRAG_PADDING = 4;
1723
+ const ROOT_ID = 'root';
1724
+ const COMPONENT_LIST_ID = 'component-list';
1725
+ const CTFL_ZONE_ID = 'data-ctfl-zone-id';
1726
+ const HITBOX = {
1727
+ WIDTH: 80,
1728
+ HEIGHT: 20,
1729
+ INITIAL_OFFSET: 10,
1730
+ OFFSET_INCREMENT: 8,
1731
+ MIN_HEIGHT: 45,
1732
+ MIN_DEPTH_HEIGHT: 20,
1733
+ DEEP_ZONE: 5,
1734
+ };
1735
+ const builtInComponents = [
1736
+ CONTENTFUL_COMPONENTS.container.id,
1737
+ CONTENTFUL_COMPONENTS.section.id,
1738
+ CONTENTFUL_COMPONENTS.columns.id,
1739
+ CONTENTFUL_COMPONENTS.singleColumn.id,
1740
+ ];
1741
+ var TreeAction;
1742
+ (function (TreeAction) {
1743
+ TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
1744
+ TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
1745
+ TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
1746
+ TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
1747
+ TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
1748
+ TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
1749
+ })(TreeAction || (TreeAction = {}));
1750
+ var HitboxDirection;
1751
+ (function (HitboxDirection) {
1752
+ HitboxDirection[HitboxDirection["TOP"] = 0] = "TOP";
1753
+ HitboxDirection[HitboxDirection["LEFT"] = 1] = "LEFT";
1754
+ HitboxDirection[HitboxDirection["RIGHT"] = 2] = "RIGHT";
1755
+ HitboxDirection[HitboxDirection["BOTTOM"] = 3] = "BOTTOM";
1756
+ HitboxDirection[HitboxDirection["SELF_VERTICAL"] = 4] = "SELF_VERTICAL";
1757
+ HitboxDirection[HitboxDirection["SELF_HORIZONTAL"] = 5] = "SELF_HORIZONTAL";
1758
+ })(HitboxDirection || (HitboxDirection = {}));
1759
+
1760
+ /**
1761
+ * Calculate the size and position of the dropzone indicator
1762
+ * when dragging a new component onto the canvas
1763
+ */
1764
+ const calcNewComponentStyles = (params) => {
1765
+ const { destinationIndex, elementIndex, dropzoneElementId, id, direction, totalIndexes } = params;
1766
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
1767
+ const isHorizontal = direction === 'horizontal';
1768
+ const isRightAlign = isHorizontal && isEnd;
1769
+ const isBottomAlign = !isHorizontal && isEnd;
1770
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
1771
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
1772
+ if (!dropzone || !element) {
1773
+ return emptyStyles;
1774
+ }
1775
+ const elementSizes = element.getBoundingClientRect();
1776
+ const dropzoneSizes = dropzone.getBoundingClientRect();
1777
+ const width = isHorizontal ? DRAGGABLE_WIDTH : dropzoneSizes.width;
1778
+ const height = isHorizontal ? dropzoneSizes.height : DRAGGABLE_HEIGHT;
1779
+ const top = isHorizontal ? -(height - elementSizes.height) / 2 : -height;
1780
+ const left = isHorizontal ? -width : -(width - elementSizes.width) / 2;
1781
+ return {
1782
+ width,
1783
+ height,
1784
+ top: !isBottomAlign ? top : 'unset',
1785
+ right: isRightAlign ? -width : 'unset',
1786
+ bottom: isBottomAlign ? -height : 'unset',
1787
+ left: !isRightAlign ? left : 'unset',
1788
+ };
1789
+ };
1790
+ /**
1791
+ * Calculate the size and position of the dropzone indicator
1792
+ * when moving an existing component on the canvas
1793
+ */
1794
+ const calcMovementStyles = (params) => {
1795
+ const { destinationIndex, sourceIndex, destinationId, sourceId, elementIndex, dropzoneElementId, id, direction, totalIndexes, draggableId, } = params;
1796
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
1797
+ const isHorizontal = direction === 'horizontal';
1798
+ const isSameZone = destinationId === sourceId;
1799
+ const isBelowSourceIndex = destinationIndex > sourceIndex;
1800
+ const isRightAlign = isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
1801
+ const isBottomAlign = !isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
1802
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
1803
+ const draggable = document.querySelector(`[data-rfd-draggable-id="${draggableId}"]`);
1804
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
1805
+ if (!dropzone || !element || !draggable) {
1806
+ return emptyStyles;
1807
+ }
1808
+ const elementSizes = element.getBoundingClientRect();
1809
+ const dropzoneSizes = dropzone.getBoundingClientRect();
1810
+ const draggableSizes = draggable.getBoundingClientRect();
1811
+ const width = isHorizontal ? draggableSizes.width : dropzoneSizes.width;
1812
+ const height = isHorizontal ? dropzoneSizes.height : draggableSizes.height;
1813
+ const top = isHorizontal ? -(height - elementSizes.height) / 2 : -height;
1814
+ const left = isHorizontal ? -width : -(width - elementSizes.width) / 2;
1815
+ return {
1816
+ width,
1817
+ height,
1818
+ top: !isBottomAlign ? top : 'unset',
1819
+ right: isRightAlign ? -width : 'unset',
1820
+ bottom: isBottomAlign ? -height : 'unset',
1821
+ left: !isRightAlign ? left : 'unset',
1822
+ };
1823
+ };
1824
+ const emptyStyles = { width: 0, height: 0 };
1825
+ const calcPlaceholderStyles = (params) => {
1826
+ const { isDraggingOver, sourceId } = params;
1827
+ if (!isDraggingOver) {
1828
+ return emptyStyles;
1829
+ }
1830
+ if (sourceId === COMPONENT_LIST_ID) {
1831
+ return calcNewComponentStyles(params);
1832
+ }
1833
+ return calcMovementStyles(params);
1834
+ };
1835
+ const Placeholder = (props) => {
1836
+ const sourceIndex = useDraggedItemStore((state) => state.draggedItem?.source.index) ?? -1;
1837
+ const draggableId = useDraggedItemStore((state) => state.draggedItem?.draggableId) ?? '';
1838
+ const sourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId) ?? '';
1839
+ const destinationIndex = useDraggedItemStore((state) => state.draggedItem?.destination?.index) ?? -1;
1840
+ const destinationId = useDraggedItemStore((state) => state.draggedItem?.destination?.droppableId) ?? '';
1841
+ const { elementIndex, totalIndexes, isDraggingOver } = props;
1842
+ const isActive = destinationIndex === elementIndex;
1843
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
1844
+ const isVisible = isEnd || isActive;
1845
+ const isComponentList = destinationId === COMPONENT_LIST_ID;
1846
+ return (!isComponentList &&
1847
+ isDraggingOver &&
1848
+ isVisible && (React.createElement("div", { style: {
1849
+ ...calcPlaceholderStyles({
1850
+ ...props,
1851
+ sourceId,
1852
+ sourceIndex,
1853
+ destinationId,
1854
+ destinationIndex,
1855
+ draggableId,
1856
+ }),
1857
+ backgroundColor: 'rgba(var(--exp-builder-blue300-rgb), 0.5)',
1858
+ position: 'absolute',
1859
+ } })));
1860
+ };
1861
+
1862
+ function getStyle$2(style, snapshot) {
1863
+ if (!snapshot.isDropAnimating) {
1864
+ return style;
1865
+ }
1866
+ return {
1867
+ ...style,
1868
+ // cannot be 0, but make it super tiny
1869
+ transitionDuration: `0.001s`,
1870
+ };
1871
+ }
1872
+ const DraggableComponent = ({ children, id, index, isAssemblyBlock = false, isSelected = false, onClick = () => null, label, coordinates, userIsDragging, style, wrapperProps, isContainer, blockId, isDragDisabled = false, placeholder, ...rest }) => {
1873
+ return (React.createElement(Draggable, { key: id, draggableId: id, index: index, isDragDisabled: isDragDisabled }, (provided, snapshot) => (React.createElement("div", { "data-ctfl-draggable-id": id, "data-test-id": `draggable-${blockId ?? 'node'}`, ref: provided.innerRef, ...wrapperProps, ...provided.draggableProps, ...provided.dragHandleProps, ...rest, className: classNames(styles$3.DraggableComponent, wrapperProps.className, {
1874
+ [styles$3.isAssemblyBlock]: isAssemblyBlock,
1875
+ [styles$3.isDragging]: snapshot.isDragging,
1876
+ [styles$3.isSelected]: isSelected,
1877
+ [styles$3.userIsDragging]: userIsDragging,
1878
+ }), style: {
1879
+ ...style,
1880
+ ...getStyle$2(provided.draggableProps.style, snapshot),
1881
+ }, onClick: onClick },
1882
+ React.createElement(Tooltip, { id: id, coordinates: coordinates, isAssemblyBlock: isAssemblyBlock, isContainer: isContainer, label: label }),
1883
+ React.createElement(Placeholder, { ...placeholder, id: id }),
1884
+ children))));
1885
+ };
1886
+
1887
+ /**
1888
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
1889
+ * and sends the DOM Rect to the client app
1890
+ */
1891
+ const sendSelectedComponentCoordinates = (instanceId) => {
1892
+ if (!instanceId)
1893
+ return;
1894
+ let selectedElement = document.querySelector(`[data-cf-node-id="${instanceId}"]`);
1895
+ let selectedAssemblyChild = undefined;
1896
+ const [rootNodeId, nodeLocation] = instanceId.split('---');
1897
+ if (nodeLocation) {
1898
+ selectedAssemblyChild = selectedElement;
1899
+ selectedElement = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
1900
+ }
1901
+ // Finds the first parent that is a VisualEditorBlock
1902
+ let parent = selectedElement?.parentElement;
1903
+ while (parent) {
1904
+ if (parent?.dataset?.cfNodeId) {
1905
+ break;
1906
+ }
1907
+ parent = parent?.parentElement;
1908
+ }
1909
+ if (selectedElement) {
1910
+ sendMessage(OUTGOING_EVENTS.UpdateSelectedComponentCoordinates, {
1911
+ selectedNodeCoordinates: getElementCoordinates(selectedElement),
1912
+ selectedAssemblyChildCoordinates: selectedAssemblyChild
1913
+ ? getElementCoordinates(selectedAssemblyChild)
1914
+ : null,
1915
+ parentCoordinates: parent ? getElementCoordinates(parent) : null,
1916
+ });
1917
+ }
1918
+ };
1919
+
1920
+ // Note: During development, the hot reloading might empty this and it
1921
+ // stays empty leading to not rendering assemblies. Ideally, this is
1922
+ // integrated into the state machine to keep track of its state.
1923
+ const assembliesRegistry = new Map([]);
1924
+ const setAssemblies = (assemblies) => {
1925
+ for (const assembly of assemblies) {
1926
+ assembliesRegistry.set(assembly.sys.id, assembly);
1927
+ }
1928
+ };
1929
+ const componentRegistry = new Map();
1930
+ const addComponentRegistration = (componentRegistration) => {
1931
+ componentRegistry.set(componentRegistration.definition.id, componentRegistration);
1932
+ };
1933
+ const createAssemblyRegistration = ({ definitionId, definitionName, component, }) => {
1934
+ const componentRegistration = componentRegistry.get(definitionId);
1935
+ if (componentRegistration) {
1936
+ return componentRegistration;
1937
+ }
1938
+ const definition = {
1939
+ id: definitionId,
1940
+ name: definitionName || 'Component',
1941
+ variables: {},
1942
+ children: true,
1943
+ category: ASSEMBLY_DEFAULT_CATEGORY,
1944
+ };
1945
+ addComponentRegistration({ component, definition });
1946
+ return componentRegistry.get(definitionId);
1947
+ };
1948
+
1949
+ const useEditorStore = create((set, get) => ({
1950
+ dataSource: {},
1951
+ unboundValues: {},
1952
+ isDragging: false,
1953
+ dragItem: '',
1954
+ selectedNodeId: null,
1955
+ locale: null,
1956
+ setSelectedNodeId: (id) => {
1957
+ set({ selectedNodeId: id });
1958
+ },
1959
+ setDataSource(data) {
1960
+ const dataSource = get().dataSource;
1961
+ const newDataSource = { ...dataSource, ...data };
1962
+ if (isEqual(dataSource, newDataSource)) {
1963
+ return;
1964
+ }
1965
+ set({ dataSource: newDataSource });
1966
+ },
1967
+ setUnboundValues(values) {
1968
+ set({ unboundValues: values });
1969
+ },
1970
+ setLocale(locale) {
1971
+ const currentLocale = get().locale;
1972
+ if (locale === currentLocale) {
1973
+ return;
1974
+ }
1975
+ set({ locale });
1976
+ },
1977
+ initializeEditor({ componentRegistry: initialRegistry, designTokens, initialLocale }) {
1978
+ initialRegistry.forEach((registration) => {
1979
+ componentRegistry.set(registration.definition.id, registration);
1980
+ });
1981
+ // Re-register the design tokens with the Visual Editor's instance of the experiences-core package
1982
+ defineDesignTokens(designTokens);
1983
+ set({ locale: initialLocale });
1984
+ },
1985
+ }));
1986
+
1987
+ /**
1988
+ * This hook gets the element co-ordinates of a specified element in the DOM
1989
+ * and sends the DOM Rect to the client app
1990
+ */
1991
+ const useSelectedInstanceCoordinates = ({ node }) => {
1992
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
1993
+ useEffect(() => {
1994
+ if (selectedNodeId !== node.data.id) {
1995
+ return;
1996
+ }
1997
+ setTimeout(() => {
1998
+ sendSelectedComponentCoordinates(node.data.id);
1999
+ }, 200);
2000
+ }, [node, selectedNodeId]);
2001
+ const selectedElement = node.data.id
2002
+ ? document.querySelector(`[data-cf-node-id="${selectedNodeId}"]`)
2003
+ : undefined;
2004
+ return selectedElement ? getElementCoordinates(selectedElement) : null;
2005
+ };
2006
+
2007
+ /**
2008
+ *
2009
+ * @param styles: the list of styles to apply
2010
+ * @param nodeId: [Optional] the id of node that these styles will be applied to
2011
+ * @returns className: the className that was used
2012
+ * Builds and adds a style tag in the document. Returns the className to be attached to the element.
2013
+ * In editor mode the nodeId is used as the identifier in order to avoid creating endless tags as the styles are tweeked
2014
+ * In preview/delivery mode the styles don't change oftem so we're using the md5 hash of the content of the tag
2015
+ */
2016
+ const useStyleTag = ({ styles, nodeId }) => {
2017
+ const [className, setClassName] = useState('');
2018
+ useEffect(() => {
2019
+ if (Object.keys(styles).length === 0) {
2020
+ return;
2021
+ }
2022
+ const [className, styleRule] = buildStyleTag({ styles, nodeId });
2023
+ setClassName(className);
2024
+ const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
2025
+ if (existingTag) {
2026
+ // editor mode - update existing
2027
+ if (nodeId) {
2028
+ existingTag.innerHTML = styleRule;
2029
+ }
2030
+ // preview/delivery mode - here we don't need to update the existing tag because
2031
+ // the className is based on the md5 hash of the content so it hasn't changed
2032
+ return;
2033
+ }
2034
+ const styleTag = document.createElement('style');
2035
+ styleTag.dataset['cfStyles'] = className;
2036
+ document.head.appendChild(styleTag).innerHTML = styleRule;
2037
+ }, [styles, nodeId]);
2038
+ return { className };
2039
+ };
2040
+
2041
+ const getUnboundValues = ({ key, fallback, unboundValues, }) => {
2042
+ const lodashPath = `${key}.value`;
2043
+ return get$1(unboundValues, lodashPath, fallback);
2044
+ };
2045
+
2046
+ const useEntityStore = create((set) => ({
2047
+ entityStore: new EditorModeEntityStore({ locale: 'en-US', entities: [] }),
2048
+ areEntitiesFetched: false,
2049
+ setEntitiesFetched(fetched) {
2050
+ set({ areEntitiesFetched: fetched });
2051
+ },
2052
+ resetEntityStore(locale, entities = []) {
2053
+ console.debug(`[experiences-sdk-react] Resetting entity store because the locale changed to '${locale}'.`);
2054
+ set({
2055
+ entityStore: new EditorModeEntityStore({ locale, entities }),
2056
+ areEntitiesFetched: false,
2057
+ });
2058
+ },
2059
+ }));
2060
+
2061
+ const useComponentProps = ({ node, areEntitiesFetched, resolveDesignValue, renderDropzone, definition, userIsDragging, }) => {
2062
+ const unboundValues = useEditorStore((state) => state.unboundValues);
2063
+ const dataSource = useEditorStore((state) => state.dataSource);
2064
+ const newComponentId = useDraggedItemStore((state) => state.componentId);
2065
+ const isDraggingNewCompont = !!newComponentId;
2066
+ const entityStore = useEntityStore((state) => state.entityStore);
2067
+ const props = useMemo(() => {
2068
+ // Don't enrich the assembly wrapper node with props
2069
+ if (!definition ||
2070
+ node.type === DESIGN_COMPONENT_NODE_TYPE ||
2071
+ node.type === ASSEMBLY_NODE_TYPE) {
2072
+ return {};
2073
+ }
2074
+ return Object.entries(definition.variables).reduce((acc, [variableName, variableDefinition]) => {
2075
+ const variableMapping = node.data.props[variableName];
2076
+ if (!variableMapping) {
2077
+ return {
2078
+ ...acc,
2079
+ [variableName]: variableDefinition.defaultValue,
2080
+ };
2081
+ }
2082
+ if (variableMapping.type === 'DesignValue') {
2083
+ const valueByBreakpoint = resolveDesignValue(variableMapping.valuesByBreakpoint, variableName);
2084
+ const designValue = variableName === 'cfHeight'
2085
+ ? calculateNodeDefaultHeight({
2086
+ blockId: node.data.blockId,
2087
+ children: node.children,
2088
+ value: valueByBreakpoint,
2089
+ })
2090
+ : valueByBreakpoint;
2091
+ return {
2092
+ ...acc,
2093
+ [variableName]: designValue,
2094
+ };
2095
+ }
2096
+ else if (variableMapping.type === 'BoundValue') {
2097
+ if (!areEntitiesFetched) {
2098
+ console.debug(`[experiences-sdk-react::useComponentProps] Idle-cycle: as entities are not fetched(areEntitiesFetched=${areEntitiesFetched}), we cannot resolve bound values for ${variableName} so we just resolve them to default values.`);
2099
+ // Just forcing default value (if we're in idle-cycle, entities are missing)
2100
+ return {
2101
+ ...acc,
2102
+ [variableName]: transformContentValue(variableDefinition.defaultValue, variableDefinition),
2103
+ };
2104
+ }
2105
+ if (isDeepPath(variableMapping.path)) {
2106
+ const [, uuid] = variableMapping.path.split('/');
2107
+ const link = dataSource[uuid];
2108
+ const boundValue = entityStore?.getValueDeep(link, variableMapping.path);
2109
+ const value = boundValue || variableDefinition.defaultValue;
2110
+ return {
2111
+ ...acc,
2112
+ [variableName]: transformContentValue(value, variableDefinition),
2113
+ };
2114
+ }
2115
+ // // take value from the datasource for both bound and unbound value types
2116
+ const [, uuid, ...path] = variableMapping.path.split('/');
2117
+ const binding = dataSource[uuid];
2118
+ let boundValue = areEntitiesFetched
2119
+ ? entityStore.getValue(binding, path.slice(0, -1))
2120
+ : undefined;
2121
+ // In some cases, there may be an asset linked in the path, so we need to consider this scenario:
2122
+ // If no 'boundValue' is found, we also attempt to extract the value associated with the second-to-last item in the path.
2123
+ // If successful, it means we have identified the linked asset.
2124
+ if (!boundValue) {
2125
+ const maybeBoundAsset = areEntitiesFetched
2126
+ ? entityStore.getValue(binding, path.slice(0, -2))
2127
+ : undefined;
2128
+ if (isLinkToAsset(maybeBoundAsset)) {
2129
+ boundValue = maybeBoundAsset;
2130
+ }
2131
+ }
2132
+ if (typeof boundValue === 'object' && boundValue.sys?.linkType === 'Asset') {
2133
+ boundValue = entityStore?.getValue(boundValue, ['fields', 'file']);
2134
+ }
2135
+ const value = boundValue || variableDefinition.defaultValue;
2136
+ return {
2137
+ ...acc,
2138
+ [variableName]: transformContentValue(value, variableDefinition),
2139
+ };
2140
+ }
2141
+ else {
2142
+ const value = getUnboundValues({
2143
+ key: variableMapping.key,
2144
+ fallback: variableDefinition.defaultValue,
2145
+ unboundValues: unboundValues || {},
2146
+ });
2147
+ return {
2148
+ ...acc,
2149
+ [variableName]: value,
2150
+ };
2151
+ }
2152
+ }, {});
2153
+ }, [
2154
+ definition,
2155
+ node.data.props,
2156
+ node.children,
2157
+ node.data.blockId,
2158
+ resolveDesignValue,
2159
+ dataSource,
2160
+ areEntitiesFetched,
2161
+ unboundValues,
2162
+ node.type,
2163
+ entityStore,
2164
+ ]);
2165
+ const cfStyles = buildCfStyles(props);
2166
+ // Separate the component styles from the editor wrapper styles
2167
+ const { margin, height, width, maxWidth, ...componentStyles } = cfStyles;
2168
+ // Styles that will be applied to the editor wrapper (draggable) element
2169
+ const { className: wrapperClass } = useStyleTag({
2170
+ styles:
2171
+ // To ensure that assembly nodes are rendered like they are rendered in
2172
+ // the assembly editor, we need to use a normal block instead of a flex box.
2173
+ node.type === ASSEMBLY_NODE_TYPE
2174
+ ? {
2175
+ display: 'block !important',
2176
+ width: '100%',
2177
+ }
2178
+ : {
2179
+ margin,
2180
+ maxWidth,
2181
+ width,
2182
+ height,
2183
+ },
2184
+ nodeId: `editor-${node.data.id}`,
2185
+ });
2186
+ // Styles that will be applied to the component element
2187
+ const { className: componentClass } = useStyleTag({
2188
+ styles: {
2189
+ ...componentStyles,
2190
+ margin: 0,
2191
+ width: '100%',
2192
+ height: '100%',
2193
+ maxWidth: 'none',
2194
+ ...(isEmptyStructureWithRelativeHeight(node.children.length, node?.data.blockId, height) && {
2195
+ minHeight: EMPTY_CONTAINER_HEIGHT,
2196
+ }),
2197
+ ...(userIsDragging &&
2198
+ isDraggingNewCompont &&
2199
+ isContentfulStructureComponent(node?.data.blockId) &&
2200
+ node?.data.blockId !== CONTENTFUL_COMPONENTS.columns.id && {
2201
+ padding: addExtraDropzonePadding(componentStyles.padding?.toString() || '0 0 0 0'),
2202
+ }),
2203
+ },
2204
+ nodeId: node.data.id,
2205
+ });
2206
+ const wrapperProps = {
2207
+ className: wrapperClass,
2208
+ 'data-cf-node-id': node.data.id,
2209
+ 'data-cf-node-block-id': node.data.blockId,
2210
+ 'data-cf-node-block-type': node.type,
2211
+ };
2212
+ const componentProps = {
2213
+ className: componentClass,
2214
+ editorMode: true,
2215
+ node,
2216
+ renderDropzone,
2217
+ ...omit(props, CF_STYLE_ATTRIBUTES, ['cfHyperlink', 'cfOpenInNewTab']),
2218
+ ...(definition.children ? { children: renderDropzone(node) } : {}),
2219
+ };
2220
+ return { componentProps, wrapperProps };
2221
+ };
2222
+ const addExtraDropzonePadding = (padding) => padding
2223
+ .split(' ')
2224
+ .map((value) => {
2225
+ if (value.endsWith('px')) {
2226
+ const parsedValue = parseInt(value.replace(/px$/, ''), 10);
2227
+ return (parsedValue < DRAG_PADDING ? DRAG_PADDING : parsedValue) + 'px';
2228
+ }
2229
+ return `${DRAG_PADDING}px`;
2230
+ })
2231
+ .join(' ');
2232
+
2233
+ var PostMessageMethods;
2234
+ (function (PostMessageMethods) {
2235
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2236
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2237
+ })(PostMessageMethods || (PostMessageMethods = {}));
2238
+
2239
+ var css_248z$4 = ".cf-heading {\n white-space: pre-line;\n}\n";
2240
+ styleInject(css_248z$4);
2241
+
2242
+ var css_248z$3 = ".cf-richtext {\n white-space: pre-line;\n}\n";
2243
+ styleInject(css_248z$3);
2244
+
2245
+ var css_248z$2$1 = ".cf-text {\n white-space: pre-line;\n}\n";
2246
+ styleInject(css_248z$2$1);
2247
+
2248
+ var css_248z$1$1 = ".contentful-container {\n position: relative;\n display: flex;\n box-sizing: border-box;\n pointer-events: all;\n}\n\n.contentful-container::-webkit-scrollbar {\n display: none; /* Safari and Chrome */\n}\n\n.cf-single-column-wrapper {\n position: relative;\n}\n\n.cf-container-wrapper {\n position: relative;\n width: 100%;\n}\n\n.cf-container-label {\n position: absolute;\n pointer-events: none;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow-x: clip;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 10;\n}\n\n/* used by ContentfulSectionAsHyperlink.tsx */\n\n.contentful-container-link,\n.contentful-container-link:active,\n.contentful-container-link:visited,\n.contentful-container-link:hover,\n.contentful-container-link:read-write,\n.contentful-container-link:focus-visible {\n color: inherit;\n text-decoration: unset;\n outline: unset;\n}\n";
2249
+ styleInject(css_248z$1$1);
2250
+
2251
+ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave, onMouseDown, onClick, flex, flexBasis, flexShrink, flexDirection, gap, justifyContent, justifyItems, justifySelf, alignItems, alignSelf, alignContent, order, flexWrap, flexGrow, className, cssStyles, ...props }, ref) => {
2252
+ return (React.createElement("div", { id: id, ref: ref, style: {
2253
+ display: 'flex',
2254
+ flex,
2255
+ flexBasis,
2256
+ flexShrink,
2257
+ flexDirection,
2258
+ gap,
2259
+ justifyContent,
2260
+ justifyItems,
2261
+ justifySelf,
2262
+ alignItems,
2263
+ alignSelf,
2264
+ alignContent,
2265
+ order,
2266
+ flexWrap,
2267
+ flexGrow,
2268
+ ...cssStyles,
2269
+ }, className: className, onMouseEnter: onMouseEnter, onMouseUp: onMouseUp, onMouseDown: onMouseDown, onMouseLeave: onMouseLeave, onClick: onClick, ...props }, children));
2270
+ });
2271
+ Flex.displayName = 'Flex';
2272
+
2273
+ var css_248z$5 = ".Columns {\n display: flex;\n gap: 24px;\n grid-template-columns: repeat(12, 1fr);\n flex-direction: column;\n min-height: 0; /* NEW */\n min-width: 0; /* NEW; needed for Firefox */\n}\n\n@media (min-width: 768px) {\n .Columns {\n display: grid;\n }\n}\n\n.cf-single-column-wrapper {\n position: relative;\n}\n\n.cf-single-column {\n pointer-events: all;\n}\n\n.cf-single-column-label {\n pointer-events: none;\n position: absolute;\n z-index: -1;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 100;\n}\n";
2274
+ styleInject(css_248z$5);
2275
+
2276
+ const ColumnWrapper = forwardRef((props, ref) => {
2277
+ return (React.createElement("div", { ref: ref, ...props, style: {
2278
+ ...(props.style || {}),
2279
+ display: 'grid',
2280
+ gridTemplateColumns: 'repeat(12, [col-start] 1fr)',
2281
+ } }, props.children));
2282
+ });
2283
+ ColumnWrapper.displayName = 'ColumnWrapper';
2284
+
2285
+ const assemblyStyle = { display: 'contents' };
2286
+ // Feel free to do any magic as regards variable definitions for assemblies
2287
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
2288
+ const Assembly = (props) => {
2289
+ if (props.editorMode) {
2290
+ const { node } = props;
2291
+ return props.renderDropzone(node, {
2292
+ ['data-test-id']: 'contentful-assembly',
2293
+ className: props.className,
2294
+ style: assemblyStyle,
2295
+ });
2296
+ }
2297
+ // Using a display contents so assembly content/children
2298
+ // can appear as if they are direct children of the div wrapper's parent
2299
+ return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
2300
+ };
2301
+
2302
+ const deserializeAssemblyNode = ({ node, nodeId, nodeLocation, parentId, assemblyDataSource, assemblyId, assemblyComponentId, assemblyUnboundValues, componentInstanceProps, componentInstanceUnboundValues, componentInstanceDataSource, }) => {
2303
+ const childNodeVariable = {};
2304
+ const dataSource = {};
2305
+ const unboundValues = {};
2306
+ for (const [variableName, variable] of Object.entries(node.variables)) {
2307
+ childNodeVariable[variableName] = variable;
2308
+ if (variable.type === 'ComponentValue') {
2309
+ const componentValueKey = variable.key;
2310
+ const instanceProperty = componentInstanceProps[componentValueKey];
2311
+ // For assembly, we look up the value in the assembly instance and
2312
+ // replace the componentValue with that one.
2313
+ if (instanceProperty?.type === 'UnboundValue') {
2314
+ const componentInstanceValue = componentInstanceUnboundValues[instanceProperty.key];
2315
+ unboundValues[instanceProperty.key] = componentInstanceValue;
2316
+ childNodeVariable[variableName] = {
2317
+ type: 'UnboundValue',
2318
+ key: instanceProperty.key,
2319
+ };
2320
+ }
2321
+ else if (instanceProperty?.type === 'BoundValue') {
2322
+ const [, dataSourceKey] = instanceProperty.path.split('/');
2323
+ const componentInstanceValue = componentInstanceDataSource[dataSourceKey];
2324
+ dataSource[dataSourceKey] = componentInstanceValue;
2325
+ childNodeVariable[variableName] = {
2326
+ type: 'BoundValue',
2327
+ path: instanceProperty.path,
2328
+ };
2329
+ }
2330
+ }
2331
+ }
2332
+ const isAssembly = assembliesRegistry.has(node.definitionId);
2333
+ const children = node.children.map((child, childIndex) => {
2334
+ const newNodeLocation = nodeLocation === null ? `${childIndex}` : nodeLocation + '_' + childIndex;
2335
+ return deserializeAssemblyNode({
2336
+ node: child,
2337
+ nodeId: `${assemblyComponentId}---${newNodeLocation}`,
2338
+ parentId: nodeId,
2339
+ nodeLocation: newNodeLocation,
2340
+ assemblyId,
2341
+ assemblyDataSource,
2342
+ assemblyComponentId,
2343
+ assemblyUnboundValues,
2344
+ componentInstanceProps,
2345
+ componentInstanceUnboundValues,
2346
+ componentInstanceDataSource,
2347
+ });
2348
+ });
2349
+ return {
2350
+ // separate node type identifiers for assemblies and their blocks, so we can treat them differently in as much as we want
2351
+ type: isAssembly ? ASSEMBLY_NODE_TYPE : ASSEMBLY_BLOCK_NODE_TYPE,
2352
+ parentId,
2353
+ data: {
2354
+ id: nodeId,
2355
+ assembly: {
2356
+ id: assemblyId,
2357
+ componentId: assemblyComponentId,
2358
+ nodeLocation: nodeLocation || null,
2359
+ },
2360
+ blockId: node.definitionId,
2361
+ props: childNodeVariable,
2362
+ dataSource,
2363
+ unboundValues,
2364
+ breakpoints: [],
2365
+ },
2366
+ children,
2367
+ };
2368
+ };
2369
+ const resolveAssembly = ({ node, entityStore, }) => {
2370
+ if (node.type !== DESIGN_COMPONENT_NODE_TYPE && node.type !== ASSEMBLY_NODE_TYPE) {
2371
+ return node;
2372
+ }
2373
+ const componentId = node.data.blockId;
2374
+ const assembly = assembliesRegistry.get(componentId);
2375
+ if (!assembly) {
2376
+ console.warn(`Link to assembly with ID '${componentId}' not found`, {
2377
+ assembliesRegistry,
2378
+ });
2379
+ return node;
2380
+ }
2381
+ const componentFields = entityStore?.getValue(assembly, ['fields']);
2382
+ if (!componentFields) {
2383
+ console.warn(`Entry for assembly with ID '${componentId}' not found`, { entityStore });
2384
+ return node;
2385
+ }
2386
+ if (!componentFields.componentTree?.children) {
2387
+ console.warn(`Component tree for assembly with ID '${componentId}' not found`, {
2388
+ componentFields,
2389
+ });
2390
+ }
2391
+ const deserializedNode = deserializeAssemblyNode({
2392
+ node: {
2393
+ definitionId: node.data.blockId || '',
2394
+ variables: {},
2395
+ children: componentFields.componentTree?.children ?? [],
2396
+ },
2397
+ nodeLocation: null,
2398
+ nodeId: node.data.id,
2399
+ parentId: node.parentId,
2400
+ assemblyDataSource: {},
2401
+ assemblyId: assembly.sys.id,
2402
+ assemblyComponentId: node.data.id,
2403
+ assemblyUnboundValues: componentFields.unboundValues,
2404
+ componentInstanceProps: node.data.props,
2405
+ componentInstanceUnboundValues: node.data.unboundValues,
2406
+ componentInstanceDataSource: node.data.dataSource,
2407
+ });
2408
+ return deserializedNode;
2409
+ };
2410
+
2411
+ const useComponent = ({ node: rawNode, resolveDesignValue, renderDropzone, userIsDragging, }) => {
2412
+ const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
2413
+ const entityStore = useEntityStore((state) => state.entityStore);
2414
+ const node = useMemo(() => {
2415
+ if ((rawNode.type === DESIGN_COMPONENT_NODE_TYPE || rawNode.type === ASSEMBLY_NODE_TYPE) &&
2416
+ areEntitiesFetched) {
2417
+ return resolveAssembly({
2418
+ node: rawNode,
2419
+ entityStore,
2420
+ });
2421
+ }
2422
+ return rawNode;
2423
+ }, [areEntitiesFetched, rawNode, entityStore]);
2424
+ const componentRegistration = useMemo(() => {
2425
+ const registration = componentRegistry.get(node.data.blockId);
2426
+ if ((node.type === DESIGN_COMPONENT_NODE_TYPE || node.type === ASSEMBLY_NODE_TYPE) &&
2427
+ !registration) {
2428
+ return createAssemblyRegistration({
2429
+ definitionId: node.data.blockId,
2430
+ component: Assembly,
2431
+ });
2432
+ }
2433
+ else if (!registration) {
2434
+ console.warn(`[experiences-sdk-react] Component registration not found for ${node.data.blockId}`);
2435
+ }
2436
+ return registration;
2437
+ }, [node]);
2438
+ const componentId = node.data.id;
2439
+ const { componentProps, wrapperProps } = useComponentProps({
2440
+ node,
2441
+ areEntitiesFetched,
2442
+ resolveDesignValue,
2443
+ renderDropzone,
2444
+ definition: componentRegistration.definition,
2445
+ userIsDragging,
2446
+ });
2447
+ // Only pass editor props to built-in components
2448
+ const { editorMode, renderDropzone: _renderDropzone, ...otherComponentProps } = componentProps;
2449
+ const elementToRender = builtInComponents.includes(node.data.blockId || '')
2450
+ ? (dragProps) => React.createElement(componentRegistration.component, { ...dragProps, ...componentProps })
2451
+ : node.type === DESIGN_COMPONENT_NODE_TYPE || node.type === ASSEMBLY_NODE_TYPE
2452
+ ? // Assembly.tsx requires renderDropzone and editorMode as well
2453
+ () => React.createElement(componentRegistration.component, componentProps)
2454
+ : () => React.createElement(componentRegistration.component, otherComponentProps);
2455
+ return {
2456
+ node,
2457
+ componentId,
2458
+ elementToRender,
2459
+ wrapperProps,
2460
+ label: componentRegistration.definition.name,
2461
+ };
2462
+ };
2463
+
2464
+ /**
2465
+ * This component is meant to function the same as DraggableComponent except
2466
+ * with the difference that the draggable props are passed to the underlying
2467
+ * component. This removes an extra nexted `div` in editor mode that otherwise
2468
+ * is not visible in delivery mode.
2469
+ *
2470
+ * This is helpful for `flex` or `grid` layouts. Currently used by the SingleColumn
2471
+ * component.
2472
+ */
2473
+ const DraggableChildComponent = (props) => {
2474
+ const { elementToRender, id, index, isAssemblyBlock = false, isSelected = false, onClick = () => null, label, coordinates, userIsDragging, style, isContainer, blockId, isDragDisabled = false, wrapperProps, } = props;
2475
+ return (React.createElement(Draggable, { key: id, draggableId: id, index: index, isDragDisabled: isDragDisabled }, (provided, snapshot) => elementToRender({
2476
+ ['data-ctfl-draggable-id']: id,
2477
+ ['data-test-id']: `draggable-${blockId}`,
2478
+ innerRef: provided.innerRef,
2479
+ ...wrapperProps,
2480
+ draggableProps: provided.draggableProps,
2481
+ wrapperClassName: classNames(styles$3.DraggableComponent, wrapperProps.className, {
2482
+ [styles$3.isAssemblyBlock]: isAssemblyBlock,
2483
+ [styles$3.isDragging]: snapshot.isDragging,
2484
+ [styles$3.isSelected]: isSelected,
2485
+ [styles$3.userIsDragging]: userIsDragging,
2486
+ }),
2487
+ dragHandleProps: provided.dragHandleProps,
2488
+ style: {
2489
+ ...style,
2490
+ ...provided.draggableProps.style,
2491
+ },
2492
+ onClick,
2493
+ Tooltip: (React.createElement(Tooltip, { id: id, coordinates: coordinates, isAssemblyBlock: isAssemblyBlock, isContainer: isContainer, label: label })),
2494
+ })));
2495
+ };
2496
+
2497
+ var css_248z$2 = ".styles-module_container__te-1H {\n margin-left: auto;\n margin-right: auto;\n position: relative;\n height: 100%;\n outline-offset: -2px;\n outline: 2px solid transparent;\n width: 100%;\n transition: outline 0.2s;\n pointer-events: all;\n}\n\n.styles-module_container__te-1H:not(.styles-module_isRoot__5cn-i) {\n outline-offset: -1px;\n}\n\n.styles-module_isRoot__5cn-i,\n.styles-module_isEmptyCanvas__0XHZR {\n flex: 1;\n}\n\n.styles-module_isEmptyZone__zVpnZ {\n min-height: 80px;\n}\n\n.styles-module_isDragging__Gm8v5:not(.styles-module_isRoot__5cn-i) {\n outline: 2px dashed var(--exp-builder-blue500);\n}\n\n.styles-module_isDestination__5sCQx:not(.styles-module_isRoot__5cn-i) {\n outline: 2px dashed var(--exp-builder-blue500);\n background-color: rgba(var(--exp-builder-blue100-rgb), 0.5);\n}\n\n.styles-module_hitbox__YQ-1Z {\n position: fixed;\n pointer-events: all !important;\n}\n";
2498
+ var styles$2 = {"container":"styles-module_container__te-1H","isRoot":"styles-module_isRoot__5cn-i","isEmptyCanvas":"styles-module_isEmptyCanvas__0XHZR","isEmptyZone":"styles-module_isEmptyZone__zVpnZ","isDragging":"styles-module_isDragging__Gm8v5","isDestination":"styles-module_isDestination__5sCQx","hitbox":"styles-module_hitbox__YQ-1Z"};
2499
+ styleInject(css_248z$2);
2500
+
2501
+ function getItemFromTree(id, node) {
2502
+ // Check if the current node's id matches the search id
2503
+ if (node.data.id === id) {
2504
+ return node;
2505
+ }
2506
+ // Recursively search through each child
2507
+ for (const child of node.children) {
2508
+ const foundNode = getItemFromTree(id, child);
2509
+ if (foundNode) {
2510
+ // Node found in children
2511
+ return foundNode;
2512
+ }
2513
+ }
2514
+ // If the node is not found in this branch of the tree, return undefined
2515
+ return undefined;
2516
+ }
2517
+ function findDepthById(node, id, currentDepth = 1) {
2518
+ if (node.data.id === id) {
2519
+ return currentDepth;
2520
+ }
2521
+ // If the node has children, check each one
2522
+ for (const child of node.children) {
2523
+ const childDepth = findDepthById(child, id, currentDepth + 1);
2524
+ if (childDepth !== -1) {
2525
+ return childDepth; // Found the node in a child
2526
+ }
2527
+ }
2528
+ return -1; // Node not found in this branch
2529
+ }
2530
+ const getChildFromTree = (parentId, index, node) => {
2531
+ // Check if the current node's id matches the search id
2532
+ if (node.data.id === parentId) {
2533
+ return node.children[index];
2534
+ }
2535
+ // Recursively search through each child
2536
+ for (const child of node.children) {
2537
+ const foundNode = getChildFromTree(parentId, index, child);
2538
+ if (foundNode) {
2539
+ // Node found in children
2540
+ return foundNode;
2541
+ }
2542
+ }
2543
+ // If the node is not found in this branch of the tree, return undefined
2544
+ return undefined;
2545
+ };
2546
+ const getItem = (selector, tree) => {
2547
+ return getItemFromTree(selector.id, {
2548
+ type: 'block',
2549
+ data: {
2550
+ id: ROOT_ID,
2551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2552
+ },
2553
+ children: tree.root.children,
2554
+ });
2555
+ };
2556
+ const getItemDepthFromNode = (selector, node) => {
2557
+ return findDepthById(node, selector.id);
2558
+ };
2559
+
2560
+ function updateNode(nodeId, updatedNode, node) {
2561
+ if (node.data.id === nodeId) {
2562
+ node.data = updatedNode.data;
2563
+ return;
2564
+ }
2565
+ node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
2566
+ }
2567
+ function replaceNode(indexToReplace, updatedNode, node) {
2568
+ if (node.data.id === updatedNode.parentId) {
2569
+ node.children = [
2570
+ ...node.children.slice(0, indexToReplace),
2571
+ updatedNode,
2572
+ ...node.children.slice(indexToReplace + 1),
2573
+ ];
2574
+ return;
2575
+ }
2576
+ node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
2577
+ }
2578
+ function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
2579
+ if (node.data.id === parentNodeId) {
2580
+ const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
2581
+ node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
2582
+ return;
2583
+ }
2584
+ node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
2585
+ }
2586
+ function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
2587
+ if (node.data.id === parentNodeId) {
2588
+ node.children = [
2589
+ ...node.children.slice(0, indexToAdd),
2590
+ nodeToAdd,
2591
+ ...node.children.slice(indexToAdd),
2592
+ ];
2593
+ return;
2594
+ }
2595
+ node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
2596
+ }
2597
+ function reorderChildNode(oldIndex, newIndex, parentNodeId, node) {
2598
+ if (node.data.id === parentNodeId) {
2599
+ // Remove the child from the old position
2600
+ const [childToMove] = node.children.splice(oldIndex, 1);
2601
+ // Insert the child at the new position
2602
+ node.children.splice(newIndex, 0, childToMove);
2603
+ return;
2604
+ }
2605
+ node.children.forEach((childNode) => reorderChildNode(oldIndex, newIndex, parentNodeId, childNode));
2606
+ }
2607
+ function reparentChildNode(oldIndex, newIndex, sourceNodeId, destinationNodeId, node) {
2608
+ const nodeToMove = getChildFromTree(sourceNodeId, oldIndex, node);
2609
+ if (!nodeToMove) {
2610
+ return;
2611
+ }
2612
+ removeChildNode(oldIndex, nodeToMove.data.id, sourceNodeId, node);
2613
+ addChildNode(newIndex, destinationNodeId, nodeToMove, node);
2614
+ }
2615
+
2616
+ function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
2617
+ if (nodeAdded) {
2618
+ return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
2619
+ }
2620
+ const item = getItem({ id: child.data.id }, tree);
2621
+ if (item) {
2622
+ const parentNode = getItem({ id: item.parentId }, tree);
2623
+ if (!parentNode) {
2624
+ return null;
2625
+ }
2626
+ const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
2627
+ return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
2628
+ }
2629
+ return {
2630
+ type: TreeAction.REPLACE_NODE,
2631
+ originalId: currentNode.children[index].data.id,
2632
+ indexToReplace: index,
2633
+ node: child,
2634
+ };
2635
+ }
2636
+ function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
2637
+ if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
2638
+ return {
2639
+ type: TreeAction.REORDER_NODE,
2640
+ sourceIndex: originalIndex,
2641
+ destinationIndex: index,
2642
+ parentNodeId,
2643
+ };
2644
+ }
2645
+ return null;
2646
+ }
2647
+ function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
2648
+ // In the end, this map contains the list of nodes that are not present
2649
+ // in the updated tree and must be removed
2650
+ const map = new Map();
2651
+ if (!currentNode || !updatedNode) {
2652
+ return differences;
2653
+ }
2654
+ // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
2655
+ const currentNodeCount = currentNode.children.length;
2656
+ const updatedNodeCount = updatedNode.children.length;
2657
+ const nodeRemoved = currentNodeCount > updatedNodeCount;
2658
+ const nodeAdded = currentNodeCount < updatedNodeCount;
2659
+ const parentNodeId = updatedNode.data.id;
2660
+ const isRoot = currentNode.data.id === ROOT_ID;
2661
+ /**
2662
+ * The data of the current node has changed, we need to update
2663
+ * this node to reflect the data change. (design, content, unbound values)
2664
+ */
2665
+ if (!isRoot && !isEqual(currentNode.data, updatedNode.data)) {
2666
+ differences.push({
2667
+ type: TreeAction.UPDATE_NODE,
2668
+ nodeId: currentNode.data.id,
2669
+ node: updatedNode,
2670
+ });
2671
+ }
2672
+ // Map children of the first tree by their ID
2673
+ currentNode.children.forEach((child, index) => map.set(child.data.id, index));
2674
+ // Compare with the second tree
2675
+ updatedNode.children.forEach((child, index) => {
2676
+ const childId = child.data.id;
2677
+ // The original tree does not have this node in the updated tree.
2678
+ if (!map.has(childId)) {
2679
+ const diff = missingNodeAction({
2680
+ index,
2681
+ child,
2682
+ nodeAdded,
2683
+ parentNodeId,
2684
+ tree: originalTree,
2685
+ currentNode,
2686
+ });
2687
+ if (diff?.type === TreeAction.REPLACE_NODE) {
2688
+ // Remove it from the deletion map to avoid adding another REMOVE_NODE action
2689
+ map.delete(diff.originalId);
2690
+ }
2691
+ return differences.push(diff);
2692
+ }
2693
+ const originalIndex = map.get(childId);
2694
+ const diff = matchingNodeAction({
2695
+ index,
2696
+ originalIndex,
2697
+ nodeAdded,
2698
+ nodeRemoved,
2699
+ parentNodeId,
2700
+ });
2701
+ differences.push(diff);
2702
+ map.delete(childId);
2703
+ compareNodes({
2704
+ currentNode: currentNode.children[originalIndex],
2705
+ updatedNode: child,
2706
+ originalTree,
2707
+ differences,
2708
+ });
2709
+ });
2710
+ map.forEach((index, key) => {
2711
+ // If the node count of the entire tree doesn't signify
2712
+ // a node was removed, don't add that as a diff
2713
+ if (!nodeRemoved) {
2714
+ return;
2715
+ }
2716
+ // Remaining nodes in the map are removed in the second tree
2717
+ differences.push({
2718
+ type: TreeAction.REMOVE_NODE,
2719
+ indexToRemove: index,
2720
+ parentNodeId,
2721
+ idToRemove: key,
2722
+ });
2723
+ });
2724
+ return differences;
2725
+ }
2726
+ function getTreeDiffs(tree1, tree2, originalTree) {
2727
+ const differences = [];
2728
+ compareNodes({
2729
+ currentNode: tree1,
2730
+ updatedNode: tree2,
2731
+ originalTree,
2732
+ differences,
2733
+ });
2734
+ return differences.filter((diff) => diff);
2735
+ }
2736
+
2737
+ /**
2738
+ * @deprecated in favor of one in 'core' package
2739
+ */
2740
+ function treeVisit(initialNode, onNode) {
2741
+ // returns last used index
2742
+ const _treeVisit = (currentNode, currentIndex, currentDepth) => {
2743
+ // Copy children in case of onNode removing it as we pass the node by reference
2744
+ const children = [...currentNode.children];
2745
+ onNode(currentNode, currentIndex, currentDepth);
2746
+ let nextAvailableIndex = currentIndex + 1;
2747
+ const lastUsedIndex = currentIndex;
2748
+ for (const child of children) {
2749
+ const lastUsedIndex = _treeVisit(child, nextAvailableIndex, currentDepth + 1);
2750
+ nextAvailableIndex = lastUsedIndex + 1;
2751
+ }
2752
+ return lastUsedIndex;
2753
+ };
2754
+ _treeVisit(initialNode, 0, 0);
2755
+ }
2756
+
2757
+ const isAssemblyNode = (node) => {
2758
+ return node.type === DESIGN_COMPONENT_NODE_TYPE || node.type === ASSEMBLY_NODE_TYPE;
2759
+ };
2760
+ const useTreeStore = create((set, get) => ({
2761
+ tree: {
2762
+ root: {
2763
+ children: [],
2764
+ type: 'root',
2765
+ data: {
2766
+ breakpoints: [],
2767
+ dataSource: {},
2768
+ id: ROOT_ID,
2769
+ props: {},
2770
+ unboundValues: {},
2771
+ },
2772
+ },
2773
+ },
2774
+ breakpoints: [],
2775
+ updateNodesByUpdatedEntity: (entityId) => {
2776
+ set(produce((draftState) => {
2777
+ treeVisit(draftState.tree.root, (node) => {
2778
+ if (isAssemblyNode(node) && node.data.blockId === entityId) {
2779
+ // Cannot use `structuredClone()` as node is probably a Proxy object with weird references
2780
+ updateNode(node.data.id, cloneDeepAsPOJO(node), draftState.tree.root);
2781
+ return;
2782
+ }
2783
+ const dataSourceIds = Object.values(node.data.dataSource).map((link) => link.sys.id);
2784
+ if (dataSourceIds.includes(entityId)) {
2785
+ // Cannot use `structuredClone()` as node is probably a Proxy object with weird references
2786
+ updateNode(node.data.id, cloneDeepAsPOJO(node), draftState.tree.root);
2787
+ }
2788
+ });
2789
+ }));
2790
+ },
2791
+ /**
2792
+ * NOTE: this is for debugging purposes only as it causes ugly canvas flash.
2793
+ *
2794
+ * Force updates entire tree. Usually shouldn't be used as updateTree()
2795
+ * uses smart update algorithm based on diffs. But for troubleshooting
2796
+ * you may want to force update the tree so leaving this in.
2797
+ */
2798
+ updateTreeForced: (tree) => {
2799
+ set({
2800
+ tree,
2801
+ // Breakpoints must be updated, as we receive completely new tree with possibly new breakpoints
2802
+ breakpoints: tree?.root?.data?.breakpoints || [],
2803
+ });
2804
+ },
2805
+ updateTree: (tree) => {
2806
+ const currentTree = get().tree;
2807
+ /**
2808
+ * If we simply update the tree as in:
2809
+ *
2810
+ * `state.tree = tree`
2811
+ *
2812
+ * we end up causing a lot of unnecesary rerenders which can lead to
2813
+ * flickering of the component layout. Instead, we use this function
2814
+ * to deteremine exactly which nodes in the tree changed and combined
2815
+ * with immer, we end up updating only the changed nodes instead of
2816
+ * rerendering the entire tree.
2817
+ */
2818
+ const treeDiff = getTreeDiffs({ ...currentTree.root }, { ...tree.root }, currentTree);
2819
+ // The current and updated tree are the same, no tree update required.
2820
+ if (!treeDiff.length) {
2821
+ console.debug(`[exp-builder.visual-editor::updateTree()]: During smart-diffing no diffs. Skipping tree update.`);
2822
+ return;
2823
+ }
2824
+ set(produce((state) => {
2825
+ treeDiff.map((diff) => {
2826
+ switch (diff.type) {
2827
+ case TreeAction.ADD_NODE:
2828
+ addChildNode(diff.indexToAdd, diff.parentNodeId, diff.nodeToAdd, state.tree.root);
2829
+ break;
2830
+ case TreeAction.REPLACE_NODE:
2831
+ replaceNode(diff.indexToReplace, diff.node, state.tree.root);
2832
+ break;
2833
+ case TreeAction.UPDATE_NODE:
2834
+ updateNode(diff.nodeId, diff.node, state.tree.root);
2835
+ break;
2836
+ case TreeAction.REMOVE_NODE:
2837
+ removeChildNode(diff.indexToRemove, diff.idToRemove, diff.parentNodeId, state.tree.root);
2838
+ break;
2839
+ case TreeAction.MOVE_NODE:
2840
+ case TreeAction.REORDER_NODE:
2841
+ state.tree = tree;
2842
+ break;
2843
+ }
2844
+ });
2845
+ state.breakpoints = tree?.root?.data?.breakpoints || [];
2846
+ }));
2847
+ },
2848
+ addChild: (index, parentId, node) => {
2849
+ set(produce((state) => {
2850
+ addChildNode(index, parentId, node, state.tree.root);
2851
+ }));
2852
+ },
2853
+ reorderChildren: (destinationIndex, destinationParentId, sourceIndex) => {
2854
+ set(produce((state) => {
2855
+ reorderChildNode(sourceIndex, destinationIndex, destinationParentId, state.tree.root);
2856
+ }));
2857
+ },
2858
+ reparentChild: (destinationIndex, destinationParentId, sourceIndex, sourceParentId) => {
2859
+ set(produce((state) => {
2860
+ reparentChildNode(sourceIndex, destinationIndex, sourceParentId, destinationParentId, state.tree.root);
2861
+ }));
2862
+ },
2863
+ }));
2864
+ // Serialize and deserialize an object again to remove all functions and references.
2865
+ // Some people refer to this as "Plain Old JavaScript Object" (POJO) as it solely contains plain data.
2866
+ function cloneDeepAsPOJO(obj) {
2867
+ return JSON.parse(JSON.stringify(obj));
2868
+ }
2869
+
2870
+ const useZoneStore = create()((set) => ({
2871
+ zones: {},
2872
+ hoveringZone: '',
2873
+ setHoveringZone(zoneId) {
2874
+ set({
2875
+ hoveringZone: zoneId,
2876
+ });
2877
+ },
2878
+ upsertZone(id, data) {
2879
+ set(produce((state) => {
2880
+ state.zones[id] = { ...(state.zones[id] || {}), ...data };
2881
+ }));
2882
+ },
2883
+ }));
2884
+
2885
+ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_HEIGHT, DEEP_ZONE } = HITBOX;
2886
+ const calcOffsetDepth = (depth) => {
2887
+ return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
2888
+ };
2889
+ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
2890
+ if (!domRect) {
2891
+ return {
2892
+ display: 'none',
2893
+ };
2894
+ }
2895
+ const { width, height, top, left, bottom, right } = domRect;
2896
+ const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
2897
+ const isDeepZone = zoneDepth > DEEP_ZONE;
2898
+ const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
2899
+ switch (direction) {
2900
+ case HitboxDirection.TOP:
2901
+ return {
2902
+ width,
2903
+ height: HEIGHT,
2904
+ top: top - calcOffsetDepth(zoneDepth),
2905
+ left,
2906
+ zIndex: 100 + zoneDepth,
2907
+ };
2908
+ case HitboxDirection.BOTTOM:
2909
+ return {
2910
+ width,
2911
+ height: HEIGHT,
2912
+ top: bottom + calcOffsetDepth(zoneDepth),
2913
+ left,
2914
+ zIndex: 100 + zoneDepth,
2915
+ };
2916
+ case HitboxDirection.LEFT:
2917
+ return {
2918
+ width: WIDTH,
2919
+ height: height - HEIGHT,
2920
+ left: left - calcOffsetDepth(zoneDepth) - WIDTH / 2,
2921
+ top: top + HEIGHT / 2,
2922
+ zIndex: 100 + zoneDepth,
2923
+ };
2924
+ case HitboxDirection.RIGHT:
2925
+ return {
2926
+ width: WIDTH,
2927
+ height: height - HEIGHT,
2928
+ left: right - calcOffsetDepth(zoneDepth) - WIDTH / 2,
2929
+ top: top + HEIGHT / 2,
2930
+ zIndex: 100 + zoneDepth,
2931
+ };
2932
+ case HitboxDirection.SELF_VERTICAL: {
2933
+ if (isAboveMaxHeight && !isDeepZone) {
2934
+ return { display: 'none' };
2935
+ }
2936
+ const selfHeight = isDeepZone ? MIN_DEPTH_HEIGHT : MIN_HEIGHT;
2937
+ return {
2938
+ width,
2939
+ height: selfHeight,
2940
+ left,
2941
+ top: top + height / 2 - selfHeight / 2,
2942
+ zIndex: 1000 + zoneDepth,
2943
+ };
2944
+ }
2945
+ case HitboxDirection.SELF_HORIZONTAL: {
2946
+ if (width > DRAGGABLE_WIDTH) {
2947
+ return { display: 'none' };
2948
+ }
2949
+ return {
2950
+ width: width - DRAGGABLE_WIDTH * 2,
2951
+ height,
2952
+ left: left + (DRAGGABLE_WIDTH * 2) / 2,
2953
+ top,
2954
+ zIndex: 1000 + zoneDepth,
2955
+ };
2956
+ }
2957
+ default:
2958
+ return {};
2959
+ }
2960
+ };
2961
+
2962
+ const Hitboxes = ({ zoneId, parentZoneId, enableRootHitboxes }) => {
2963
+ const tree = useTreeStore((state) => state.tree);
2964
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
2965
+ const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
2966
+ const [fetchDomRect, setFetchDomRect] = useState(Date.now());
2967
+ useEffect(() => {
2968
+ /**
2969
+ * A bit hacky but we need to wait a very small amount
2970
+ * of time to fetch the dom getBoundingClientRect once a
2971
+ * drag starts because we need pre-drag styles like padding
2972
+ * applied before we calculate positions of hitboxes
2973
+ */
2974
+ setTimeout(() => {
2975
+ setFetchDomRect(Date.now());
2976
+ }, 50);
2977
+ }, [isDraggingOnCanvas]);
2978
+ const hitboxContainer = useMemo(() => {
2979
+ return document.querySelector('[data-ctfl-hitboxes]');
2980
+ }, []);
2981
+ const domRect = useMemo(() => {
2982
+ return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
2983
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2984
+ }, [zoneId, fetchDomRect]);
2985
+ const zones = useZoneStore((state) => state.zones);
2986
+ const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
2987
+ const isVertical = zoneDirection === 'vertical';
2988
+ const isRoot = parentZoneId === ROOT_ID;
2989
+ const showRootHitboxes = isRoot && enableRootHitboxes;
2990
+ const getStyles = useCallback((direction) => getHitboxStyles({ direction, zoneDepth, domRect }), [zoneDepth, domRect]);
2991
+ const ActiveHitboxes = (React.createElement(React.Fragment, null,
2992
+ React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
2993
+ showRootHitboxes ? (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) })) : (React.createElement(React.Fragment, null,
2994
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
2995
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })))));
2996
+ if (!hitboxContainer) {
2997
+ return null;
2998
+ }
2999
+ return createPortal(ActiveHitboxes, hitboxContainer);
3000
+ };
3001
+
3002
+ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, draggingNewComponent, index, zoneId, userIsDragging, placeholder, }) => {
3003
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
3004
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3005
+ const { node, componentId, wrapperProps, label, elementToRender } = useComponent({
3006
+ node: rawNode,
3007
+ resolveDesignValue,
3008
+ renderDropzone,
3009
+ userIsDragging,
3010
+ });
3011
+ const coordinates = useSelectedInstanceCoordinates({ node });
3012
+ const isContainer = node.data.blockId === CONTENTFUL_COMPONENTS.container.id;
3013
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS.singleColumn.id;
3014
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
3015
+ const isAssembly = node.type === ASSEMBLY_NODE_TYPE;
3016
+ const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
3017
+ const isRootComponent = zoneId === ROOT_ID;
3018
+ const enableRootHitboxes = isRootComponent && !draggingNewComponent;
3019
+ const onClick = (e) => {
3020
+ e.stopPropagation();
3021
+ if (!userIsDragging) {
3022
+ setSelectedNodeId(node.data.id);
3023
+ // if it is the assembly directly we just want to select it as a normal component
3024
+ if (isAssembly) {
3025
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
3026
+ nodeId: node.data.id,
3027
+ });
3028
+ return;
3029
+ }
3030
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
3031
+ assembly: node.data.assembly,
3032
+ nodeId: node.data.id,
3033
+ });
3034
+ }
3035
+ };
3036
+ if (node.data.blockId === CONTENTFUL_COMPONENTS.singleColumn.id) {
3037
+ return (React.createElement(React.Fragment, null,
3038
+ React.createElement(DraggableChildComponent, { elementToRender: elementToRender, label: label || 'No Label Specified', id: componentId, index: index, isAssemblyBlock: isAssemblyBlock, isDragDisabled: isSingleColumn, isSelected: selectedNodeId === componentId, userIsDragging: userIsDragging, isContainer: isContainer, blockId: node.data.blockId, coordinates: coordinates, wrapperProps: wrapperProps, onClick: onClick }),
3039
+ isStructureComponent && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, enableRootHitboxes: enableRootHitboxes }))));
3040
+ }
3041
+ return (React.createElement(DraggableComponent, { placeholder: placeholder, label: label || 'No Label Specified', id: componentId, index: index, isAssemblyBlock: isAssemblyBlock, isDragDisabled: isAssemblyBlock, isSelected: selectedNodeId === componentId, userIsDragging: userIsDragging, isContainer: isContainer, blockId: node.data.blockId, coordinates: coordinates, wrapperProps: wrapperProps, onClick: onClick },
3042
+ elementToRender(),
3043
+ isStructureComponent && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, enableRootHitboxes: enableRootHitboxes }))));
3044
+ };
3045
+
3046
+ var css_248z$1 = ".EmptyContainer-module_container__XPH5b {\n height: 200px;\n display: flex;\n width: 100%;\n position: absolute;\n align-items: center;\n justify-content: center;\n flex-direction: row;\n transition: all 0.2s;\n color: var(--exp-builder-gray400);\n font-size: var(--exp-builder-font-size-l);\n font-family: var(--exp-builder-font-stack-primary);\n outline: 2px dashed var(--exp-builder-gray400);\n outline-offset: -2px;\n}\n\n.EmptyContainer-module_highlight__lcICy:hover {\n outline: 2px dashed var(--exp-builder-blue500);\n background-color: rgba(var(--exp-builder-blue100-rgb), 0.5);\n cursor: grabbing;\n}\n\n.EmptyContainer-module_icon__82-2O rect {\n fill: var(--exp-builder-gray400);\n}\n\n.EmptyContainer-module_label__4TxRa {\n margin-left: var(--exp-builder-spacing-s);\n}\n";
3047
+ var styles$1 = {"container":"EmptyContainer-module_container__XPH5b","highlight":"EmptyContainer-module_highlight__lcICy","icon":"EmptyContainer-module_icon__82-2O","label":"EmptyContainer-module_label__4TxRa"};
3048
+ styleInject(css_248z$1);
3049
+
3050
+ const EmptyContainer = ({ isDragging }) => {
3051
+ return (React.createElement("div", { className: classNames(styles$1.container, {
3052
+ [styles$1.highlight]: isDragging,
3053
+ }), "data-type": "empty-container" },
3054
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles$1.icon },
3055
+ React.createElement("rect", { width: "11.676", height: "11.676", x: "18.512", y: ".153", rx: "1.621", transform: "rotate(45 18.512 .153)" }),
3056
+ React.createElement("rect", { width: "11.676", height: "11.676", x: "9.254", y: "9.139", rx: "1.621", transform: "rotate(45 9.254 9.139)" }),
3057
+ React.createElement("rect", { width: "11.676", height: "11.676", x: "18.011", y: "18.625", rx: "1.621", transform: "rotate(45 18.01 18.625)" }),
3058
+ React.createElement("rect", { width: "11.676", height: "11.676", x: "30.557", y: "10.131", rx: "1.621", transform: "rotate(60 30.557 10.13)" }),
3059
+ React.createElement("path", { fill: "#fff", stroke: "#fff", strokeWidth: ".243", d: "M31.113 17.038a.463.463 0 0 0-.683-.517l-1.763 1.032-1.033-1.763a.464.464 0 0 0-.8.469l1.034 1.763-1.763 1.033a.463.463 0 1 0 .468.8l1.763-1.033 1.033 1.763a.463.463 0 1 0 .8-.469l-1.033-1.763 1.763-1.033a.463.463 0 0 0 .214-.282Z" })),
3060
+ React.createElement("span", { className: styles$1.label }, "Add elements to begin")));
3061
+ };
3062
+
3063
+ const getZoneParents = (zoneId) => {
3064
+ const element = document.querySelector(`[data-rfd-droppable-id='${zoneId}']`);
3065
+ if (!element) {
3066
+ return [];
3067
+ }
3068
+ function getZonesToRoot(element, parentIds = []) {
3069
+ if (!element) {
3070
+ return parentIds;
3071
+ }
3072
+ const attribute = element.getAttribute('data-rfd-droppable-id');
3073
+ if (attribute === ROOT_ID) {
3074
+ return parentIds;
3075
+ }
3076
+ if (attribute) {
3077
+ parentIds.push(attribute);
3078
+ }
3079
+ return getZonesToRoot(element.parentElement, parentIds);
3080
+ }
3081
+ return getZonesToRoot(element);
3082
+ };
3083
+
3084
+ const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
3085
+ const zone = useZoneStore((state) => state.zones);
3086
+ const upsertZone = useZoneStore((state) => state.upsertZone);
3087
+ useEffect(() => {
3088
+ function getDirection() {
3089
+ if (!node || !node.data.blockId) {
3090
+ return 'vertical';
3091
+ }
3092
+ if (!isContentfulStructureComponent(node.data.blockId)) {
3093
+ return 'vertical';
3094
+ }
3095
+ if (node.data.blockId === CONTENTFUL_COMPONENTS.columns.id) {
3096
+ return 'horizontal';
3097
+ }
3098
+ const designValues = node.data.props['cfFlexDirection'];
3099
+ if (!designValues || !resolveDesignValue || designValues.type !== 'DesignValue') {
3100
+ return 'vertical';
3101
+ }
3102
+ const direction = resolveDesignValue(designValues.valuesByBreakpoint, 'cfFlexDirection');
3103
+ if (direction === 'row') {
3104
+ return 'horizontal';
3105
+ }
3106
+ return 'vertical';
3107
+ }
3108
+ upsertZone(zoneId, { direction: getDirection() });
3109
+ }, [node, resolveDesignValue, zoneId, upsertZone]);
3110
+ return zone[zoneId]?.direction || 'vertical';
3111
+ };
3112
+
3113
+ function getStyle$1(style = {}, snapshot) {
3114
+ if (!snapshot?.isDropAnimating) {
3115
+ return style;
3116
+ }
3117
+ return {
3118
+ ...style,
3119
+ // cannot be 0, but make it super tiny
3120
+ transitionDuration: `0.001s`,
3121
+ };
3122
+ }
3123
+ const EditorBlockClone = ({ node: rawNode, resolveDesignValue, snapshot, provided, renderDropzone, }) => {
3124
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
3125
+ const { node, wrapperProps, elementToRender } = useComponent({
3126
+ node: rawNode,
3127
+ resolveDesignValue,
3128
+ renderDropzone,
3129
+ userIsDragging,
3130
+ });
3131
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
3132
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS.singleColumn.id;
3133
+ if (isSingleColumn) {
3134
+ return elementToRender();
3135
+ }
3136
+ return (React.createElement("div", { ref: provided?.innerRef, ...wrapperProps, ...provided?.draggableProps, ...provided?.dragHandleProps, className: classNames(styles$3.DraggableComponent, wrapperProps.className, {
3137
+ [styles$3.isAssemblyBlock]: isAssemblyBlock,
3138
+ [styles$3.isDragging]: snapshot?.isDragging,
3139
+ }), style: getStyle$1(provided?.draggableProps.style, snapshot) }, elementToRender()));
3140
+ };
3141
+
3142
+ function DropzoneClone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', renderDropzone, ...rest }) {
3143
+ const tree = useTreeStore((state) => state.tree);
3144
+ const content = node?.children || tree.root?.children || [];
3145
+ const isRootZone = zoneId === ROOT_ID;
3146
+ if (!resolveDesignValue) {
3147
+ return null;
3148
+ }
3149
+ return (React.createElement(WrapperComponent, { className: classNames(styles$2.container, {
3150
+ [styles$2.isRoot]: isRootZone,
3151
+ [styles$2.isEmptyZone]: !content.length,
3152
+ }, className), node: node, ...rest }, content.map((item) => {
3153
+ const componentId = item.data.id;
3154
+ return (React.createElement(EditorBlockClone, { key: componentId, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone }));
3155
+ })));
3156
+ }
3157
+
3158
+ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', ...rest }) {
3159
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
3160
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
3161
+ const newComponentId = useDraggedItemStore((state) => state.componentId);
3162
+ const hoveringZone = useZoneStore((state) => state.hoveringZone);
3163
+ const tree = useTreeStore((state) => state.tree);
3164
+ const content = node?.children || tree.root?.children || [];
3165
+ const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
3166
+ const draggedSourceId = draggedItem && draggedItem.source.droppableId;
3167
+ const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
3168
+ const isDraggingNewComponent = !!newComponentId;
3169
+ const isHoveringZone = hoveringZone === zoneId;
3170
+ const isRootZone = zoneId === ROOT_ID;
3171
+ const isDestination = draggedDestinationId === zoneId;
3172
+ const isEmptyCanvas = isRootZone && !content.length;
3173
+ const isAssembly = DESIGN_COMPONENT_NODE_TYPES.includes(node?.type || '') ||
3174
+ ASSEMBLY_NODE_TYPES.includes(node?.type || '');
3175
+ // To avoid a circular dependency, we create the recursive rendering function here and trickle it down
3176
+ const renderDropzone = useCallback((node, props) => {
3177
+ return (React.createElement(Dropzone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, ...props }));
3178
+ }, [resolveDesignValue]);
3179
+ const renderClonedDropzone = useCallback((node, props) => {
3180
+ return (React.createElement(DropzoneClone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, ...props }));
3181
+ }, [resolveDesignValue]);
3182
+ if (!resolveDesignValue) {
3183
+ return null;
3184
+ }
3185
+ // Don't trigger the dropzone when it's the root because then the only hit boxes that show up will be root level zones
3186
+ // Exception 1: If it comes from the component list (because we want the component list components to work for all zones
3187
+ // Exception 2: If it's a child of a root level zone (because we want to be able to re-order root level containers)
3188
+ const isDropzoneEnabled = () => {
3189
+ if (node?.data.blockId === CONTENTFUL_COMPONENTS.columns.id) {
3190
+ return false;
3191
+ }
3192
+ if (isDraggingNewComponent) {
3193
+ return isHoveringZone;
3194
+ }
3195
+ const draggingParentIds = getZoneParents(draggedSourceId || '');
3196
+ if (!draggingParentIds.length) {
3197
+ return isRootZone;
3198
+ }
3199
+ return isHoveringZone && !isRootZone;
3200
+ };
3201
+ return (React.createElement(Droppable, { droppableId: zoneId, direction: direction, isDropDisabled: !isDropzoneEnabled(), renderClone: (provided, snapshot, rubic) => (React.createElement(EditorBlockClone, { node: content[rubic.source.index], resolveDesignValue: resolveDesignValue, provided: provided, snapshot: snapshot, renderDropzone: renderClonedDropzone })) }, (provided, snapshot) => {
3202
+ return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ref: provided?.innerRef, id: zoneId, "data-ctfl-zone-id": zoneId, className: classNames(styles$2.container, {
3203
+ [styles$2.isEmptyCanvas]: isEmptyCanvas,
3204
+ [styles$2.isDragging]: userIsDragging && !isAssembly,
3205
+ [styles$2.isDestination]: isDestination && !isAssembly,
3206
+ [styles$2.isRoot]: isRootZone,
3207
+ [styles$2.isEmptyZone]: !content.length,
3208
+ }, className), node: node, ...rest },
3209
+ isEmptyCanvas ? (React.createElement(EmptyContainer, { isDragging: isRootZone && userIsDragging })) : (content.map((item, i) => {
3210
+ const componentId = item.data.id;
3211
+ return (React.createElement(EditorBlock, { placeholder: {
3212
+ isDraggingOver: snapshot?.isDraggingOver,
3213
+ totalIndexes: content.length,
3214
+ elementIndex: i,
3215
+ dropzoneElementId: zoneId,
3216
+ direction,
3217
+ }, index: i, zoneId: zoneId, key: componentId, userIsDragging: userIsDragging, draggingNewComponent: isDraggingNewComponent, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone }));
3218
+ })),
3219
+ provided?.placeholder));
3220
+ }));
3221
+ }
3222
+
3223
+ function getStyle(style, snapshot) {
3224
+ if (!snapshot.isDropAnimating) {
3225
+ return style;
3226
+ }
3227
+ return {
3228
+ ...style,
3229
+ // cannot be 0, but make it super tiny
3230
+ transitionDuration: `0.001s`,
3231
+ };
3232
+ }
3233
+ const DraggableContainer = ({ id }) => {
3234
+ return (React.createElement("div", { id: COMPONENT_LIST_ID, style: {
3235
+ position: 'absolute',
3236
+ top: 0,
3237
+ left: 0,
3238
+ pointerEvents: 'none',
3239
+ zIndex: -1,
3240
+ } },
3241
+ React.createElement(Droppable, { droppableId: COMPONENT_LIST_ID, isDropDisabled: true }, (provided) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef },
3242
+ React.createElement(Draggable, { draggableId: id, key: id, index: 0 }, (provided, snapshot) => (React.createElement("div", { id: "item", ref: provided.innerRef, ...provided.draggableProps, ...provided.dragHandleProps, style: {
3243
+ ...getStyle(provided.draggableProps.style, snapshot),
3244
+ width: DRAGGABLE_WIDTH,
3245
+ height: DRAGGABLE_HEIGHT,
3246
+ pointerEvents: 'none',
3247
+ } }))),
3248
+ provided.placeholder)))));
3249
+ };
3250
+
3251
+ var css_248z = ".render-module_hitbox__l4ysJ {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 10px;\n z-index: 1000000;\n}\n\n.render-module_hitboxLower__tgsA1 {\n position: absolute;\n bottom: -10px;\n left: 0;\n width: 100%;\n height: 10px;\n z-index: 1000000;\n}\n\n.render-module_container__-C3d7 {\n position: relative;\n display: flex;\n flex-direction: column;\n}\n\nbody {\n margin: 0;\n}\n\nhtml {\n -ms-overflow-style: none; /* Internet Explorer 10+ */\n scrollbar-width: none; /* Firefox */\n}\n\nhtml::-webkit-scrollbar {\n display: none;\n}\n";
3252
+ var styles = {"hitbox":"render-module_hitbox__l4ysJ","hitboxLower":"render-module_hitboxLower__tgsA1","container":"render-module_container__-C3d7"};
3253
+ styleInject(css_248z);
3254
+
3255
+ // TODO: In order to support integrations without React, we should extract this heavy logic into simple
3256
+ // functions that we can reuse in other frameworks.
3257
+ /*
3258
+ * Registers media query change listeners for each breakpoint (except for "*").
3259
+ * It will always assume the last matching media query in the list. It therefore,
3260
+ * assumes that the breakpoints are sorted beginning with the default value (query: "*")
3261
+ * and then decending by screen width. For mobile-first designs, the order would be ascending
3262
+ */
3263
+ const useBreakpoints = (breakpoints) => {
3264
+ const [mediaQueryMatches, setMediaQueryMatches] = useState({});
3265
+ const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
3266
+ // Register event listeners to update the media query states
3267
+ useEffect(() => {
3268
+ const [mediaQueryMatchers, initialMediaQueryMatches] = mediaQueryMatcher(breakpoints);
3269
+ // Store the media query state in the beginning to initialise the state
3270
+ setMediaQueryMatches(initialMediaQueryMatches);
3271
+ const eventListeners = mediaQueryMatchers.map(({ id, signal }) => {
3272
+ const onChange = () => setMediaQueryMatches((prev) => ({
3273
+ ...prev,
3274
+ [id]: signal.matches,
3275
+ }));
3276
+ signal.addEventListener('change', onChange);
3277
+ return onChange;
3278
+ });
3279
+ return () => {
3280
+ eventListeners.forEach((eventListener, index) => {
3281
+ mediaQueryMatchers[index].signal.removeEventListener('change', eventListener);
3282
+ });
3283
+ };
3284
+ // Only re-setup all listeners when the breakpoint definition changed
3285
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3286
+ }, [breakpoints]);
3287
+ const activeBreakpointIndex = getActiveBreakpointIndex(breakpoints, mediaQueryMatches, fallbackBreakpointIndex);
3288
+ const resolveDesignValue = useCallback((valuesByBreakpoint, variableName) => {
3289
+ return getValueForBreakpoint(valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName);
3290
+ }, [activeBreakpointIndex, breakpoints]);
3291
+ return { resolveDesignValue };
3292
+ };
3293
+
3294
+ class MouseOverHandler {
3295
+ constructor() {
3296
+ this.currentHoveredElementId = null;
3297
+ this.getMargins = (element) => {
3298
+ if (typeof window === 'undefined')
3299
+ return undefined;
3300
+ const styles = window.getComputedStyle(element);
3301
+ const top = parseInt(styles.marginTop);
3302
+ const bottom = parseInt(styles.marginBottom);
3303
+ const left = parseInt(styles.marginLeft);
3304
+ const right = parseInt(styles.marginRight);
3305
+ return { top, bottom, left, right };
3306
+ };
3307
+ this.getFullCoordinates = (element) => {
3308
+ const validChildren = Array.from(element.children).filter((child) => child instanceof HTMLElement && child.dataset.cfNodeBlockType === 'block');
3309
+ const { left, top, width, height } = this.getBoundingClientRect(element);
3310
+ const margins = this.getMargins(element);
3311
+ const childrenCoordinates = validChildren.map((child) => {
3312
+ const { left, top, width, height } = this.getBoundingClientRect(child);
3313
+ return { left, top, width, height, margins };
3314
+ });
3315
+ return {
3316
+ left,
3317
+ top,
3318
+ width,
3319
+ height,
3320
+ margins,
3321
+ childrenCoordinates,
3322
+ };
3323
+ };
3324
+ this.getClosestComponentInformation = (element) => {
3325
+ let target = element;
3326
+ // If the target is outside on the root or anywhere else on the iframes body
3327
+ if (target?.id === 'VisualEditorRoot' || target?.tagName === 'BODY') {
3328
+ const rootElement = document.getElementById('VisualEditorRoot');
3329
+ const hoveredRootElement = {
3330
+ nodeId: 'root',
3331
+ blockType: 'root',
3332
+ blockId: 'root',
3333
+ };
3334
+ return [rootElement, hoveredRootElement];
3335
+ }
3336
+ // Find the closest contentful container or direct parent that is a contentful container
3337
+ while (target) {
3338
+ if (
3339
+ // is itself a section?
3340
+ target.dataset.cfNodeId ||
3341
+ // Or a direct child of a section
3342
+ (target.parentElement && target.parentElement.dataset.cfNodeBlockId === 'ContentfulSection')) {
3343
+ const sectionId = target.dataset.cfNodeId;
3344
+ const sectionBlockId = target.dataset.cfNodeBlockId;
3345
+ const sectionBlockType = target.dataset.cfNodeBlockType;
3346
+ const hoveredElement = {
3347
+ nodeId: sectionId,
3348
+ blockId: sectionBlockId,
3349
+ blockType: sectionBlockType,
3350
+ };
3351
+ return [target, hoveredElement];
3352
+ }
3353
+ target = target.parentElement;
3354
+ }
3355
+ };
3356
+ this.getNewlyHoveredElement = (element) => {
3357
+ let parentElement = null;
3358
+ let parentSectionIndex = -1;
3359
+ const [hoveredElement, hoveredInfo] = this.getClosestComponentInformation(element) || [
3360
+ null,
3361
+ null,
3362
+ ];
3363
+ if (!hoveredElement)
3364
+ return;
3365
+ // if hovered element is already hovered and the information is already sent
3366
+ // ignore the rest and don't proceed.
3367
+ if (hoveredInfo.nodeId === this.currentHoveredElementId)
3368
+ return;
3369
+ let parentHTMLElement = hoveredElement?.parentElement || null;
3370
+ while (parentHTMLElement) {
3371
+ const parentIsRoot = parentHTMLElement.id === 'VisualEditorRoot';
3372
+ if (parentHTMLElement.dataset.cfNodeId || parentIsRoot) {
3373
+ parentElement = {
3374
+ nodeId: parentIsRoot ? 'root' : parentHTMLElement.dataset.cfNodeId,
3375
+ blockType: parentHTMLElement.dataset.cfNodeBlockType,
3376
+ blockId: parentHTMLElement.dataset.cfNodeBlockId,
3377
+ };
3378
+ const parentChildrenElements = parentHTMLElement.children;
3379
+ parentSectionIndex = Array.from(parentChildrenElements).findIndex((child) => child === hoveredElement);
3380
+ break;
3381
+ }
3382
+ parentHTMLElement = parentHTMLElement.parentElement;
3383
+ }
3384
+ const coordinates = this.getFullCoordinates(hoveredElement);
3385
+ return { coordinates, hoveredElement: hoveredInfo, parentElement, parentSectionIndex };
3386
+ };
3387
+ this.handleMouseMove = (target) => {
3388
+ const hoveredElementInfo = this.getNewlyHoveredElement(target);
3389
+ if (!hoveredElementInfo) {
3390
+ return;
3391
+ }
3392
+ const { coordinates, hoveredElement, parentElement, parentSectionIndex } = hoveredElementInfo;
3393
+ this.currentHoveredElementId = hoveredElementInfo.hoveredElement.nodeId || null;
3394
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {
3395
+ hoveredElement,
3396
+ parentElement,
3397
+ parentSectionIndex,
3398
+ coordinates,
3399
+ });
3400
+ };
3401
+ this.onMouseMove = (event) => {
3402
+ const target = event.target;
3403
+ this.handleMouseMove(target);
3404
+ };
3405
+ this.onMouseLeave = () => {
3406
+ this.currentHoveredElementId = null;
3407
+ };
3408
+ }
3409
+ getBoundingClientRect(element) {
3410
+ const isAssembly = element.getAttribute('data-cf-node-block-type') === DESIGN_COMPONENT_NODE_TYPE ||
3411
+ element.getAttribute('data-cf-node-block-type') === ASSEMBLY_NODE_TYPE;
3412
+ if (!isAssembly) {
3413
+ return element.getBoundingClientRect();
3414
+ }
3415
+ else {
3416
+ // As we use `display: contents` for assemblies, there is no real "block"
3417
+ // in the DOM and thus the browser fails to calculate the bounding rect.
3418
+ // Instead, we calculate it for each child and add it up:
3419
+ if (!element.firstElementChild) {
3420
+ return { left: 0, top: 0, width: 0, height: 0 };
3421
+ }
3422
+ const firstChildRect = element.firstElementChild.getBoundingClientRect();
3423
+ let fullHeight = firstChildRect.height;
3424
+ let nextChild = element.firstElementChild.nextElementSibling;
3425
+ while (nextChild) {
3426
+ const nextChildRect = nextChild.getBoundingClientRect();
3427
+ fullHeight += nextChildRect.height;
3428
+ nextChild = nextChild.nextElementSibling;
3429
+ }
3430
+ // The root of a assembly positions its first level containers vertically.
3431
+ // So we just need to add up the height and use the remaining properties from the first child.
3432
+ return {
3433
+ left: firstChildRect.left,
3434
+ top: firstChildRect.top,
3435
+ width: firstChildRect.width,
3436
+ height: fullHeight,
3437
+ };
3438
+ }
3439
+ }
3440
+ attachEvent() {
3441
+ document.addEventListener('mousemove', this.onMouseMove);
3442
+ document.addEventListener('mouseout', this.onMouseLeave);
3443
+ }
3444
+ detachEvent() {
3445
+ document.removeEventListener('mousemove', this.onMouseMove);
3446
+ document.removeEventListener('mouseout', this.onMouseLeave);
3447
+ }
3448
+ }
3449
+
3450
+ /**
3451
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
3452
+ * and sends the DOM Rect to the client app
3453
+ */
3454
+ const sendHoveredComponentCoordinates = (instanceId) => {
3455
+ const selectedElement = instanceId
3456
+ ? document.querySelector(`[data-cf-node-id="${instanceId}"]`)
3457
+ : undefined;
3458
+ const mouseOverHandler = new MouseOverHandler();
3459
+ mouseOverHandler.handleMouseMove(selectedElement || null);
3460
+ };
3461
+
3462
+ function updateDraggableElement(x, y) {
3463
+ const container = document.querySelector('#component-list');
3464
+ if (!container) {
3465
+ return;
3466
+ }
3467
+ container.style.setProperty('top', `${y}px`);
3468
+ container.style.setProperty('left', `${x}px`);
3469
+ }
3470
+ function simulateMouseEvent(coordX, coordY, eventName = 'mousemove') {
3471
+ const element = document.querySelector('#item');
3472
+ if (!dragState.isDragStart) {
3473
+ return;
3474
+ }
3475
+ if (!dragState.isDragging) {
3476
+ updateDraggableElement(coordX, coordY);
3477
+ eventName = 'mousedown';
3478
+ dragState.updateIsDragging(true);
3479
+ }
3480
+ const options = {
3481
+ bubbles: true,
3482
+ cancelable: true,
3483
+ view: window,
3484
+ pageX: 0,
3485
+ pageY: 0,
3486
+ clientX: coordX - DRAGGABLE_WIDTH / 2,
3487
+ clientY: coordY - DRAGGABLE_HEIGHT / 2 - window.scrollY,
3488
+ };
3489
+ if (!element) {
3490
+ return;
3491
+ }
3492
+ const event = new MouseEvent(eventName, options);
3493
+ element.dispatchEvent(event);
3494
+ }
3495
+
3496
+ function useEditorSubscriber() {
3497
+ const entityStore = useEntityStore((state) => state.entityStore);
3498
+ const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
3499
+ const setEntitiesFetched = useEntityStore((state) => state.setEntitiesFetched);
3500
+ const { updateTree, updateNodesByUpdatedEntity } = useTreeStore((state) => ({
3501
+ updateTree: state.updateTree,
3502
+ updateNodesByUpdatedEntity: state.updateNodesByUpdatedEntity,
3503
+ }));
3504
+ const unboundValues = useEditorStore((state) => state.unboundValues);
3505
+ const dataSource = useEditorStore((state) => state.dataSource);
3506
+ const setLocale = useEditorStore((state) => state.setLocale);
3507
+ const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
3508
+ const setDataSource = useEditorStore((state) => state.setDataSource);
3509
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
3510
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3511
+ const setComponentId = useDraggedItemStore((state) => state.setComponentId);
3512
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
3513
+ // TODO: As we have disabled the useEffect, we can remove these states
3514
+ const [, /* isFetchingEntities */ setFetchingEntities] = useState(false);
3515
+ const reloadApp = () => {
3516
+ sendMessage(OUTGOING_EVENTS.CanvasReload, {});
3517
+ // Wait a moment to ensure that the message was sent
3518
+ setTimeout(() => {
3519
+ // Received a hot reload message from webpack dev server -> reload the canvas
3520
+ window.location.reload();
3521
+ }, 50);
3522
+ };
3523
+ useEffect(() => {
3524
+ sendMessage(OUTGOING_EVENTS.RequestComponentTreeUpdate);
3525
+ }, []);
3526
+ /**
3527
+ * Fills up entityStore with entities from newDataSource and from the tree.
3528
+ * Also manages "entity status" variables (areEntitiesFetched, isFetchingEntities)
3529
+ */
3530
+ const fetchMissingEntities = useCallback(async (newDataSource, tree) => {
3531
+ // if we realize that there's nothing missing and nothing to fill-fetch before we do any async call,
3532
+ // then we can simply return and not lock the EntityStore at all.
3533
+ const startFetching = () => {
3534
+ setEntitiesFetched(false);
3535
+ setFetchingEntities(true);
3536
+ };
3537
+ const endFetching = () => {
3538
+ setEntitiesFetched(true);
3539
+ setFetchingEntities(false);
3540
+ };
3541
+ // Prepare L1 entities and deepReferences
3542
+ const entityLinksL1 = [
3543
+ ...Object.values(newDataSource),
3544
+ ...assembliesRegistry.values(), // we count assemblies here as "L1 entities", for convenience. Even though they're not headEntities.
3545
+ ];
3546
+ const deepReferences = gatherDeepReferencesFromTree(tree.root, newDataSource);
3547
+ /**
3548
+ * Checks only for _missing_ L1 entities
3549
+ * WARNING: Does NOT check for entity staleness/versions. If an entity is stale, it will NOT be considered missing.
3550
+ * If ExperienceBuilder wants to update stale entities, it should post `▼UPDATED_ENTITY` message to SDK.
3551
+ */
3552
+ const isMissingL1Entities = (entityLinks) => {
3553
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinks);
3554
+ return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
3555
+ };
3556
+ /**
3557
+ * PRECONDITION: all L1 entities are fetched
3558
+ */
3559
+ const isMissingL2Entities = (deepReferences) => {
3560
+ const referentLinks = deepReferences
3561
+ .map((deepReference) => deepReference.extractReferent(entityStore))
3562
+ .filter(isLink);
3563
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
3564
+ return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
3565
+ };
3566
+ /**
3567
+ * POST_CONDITION: entityStore is has all L1 entities (aka headEntities)
3568
+ */
3569
+ const fillupL1 = async ({ entityLinksL1, }) => {
3570
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinksL1);
3571
+ await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
3572
+ };
3573
+ /**
3574
+ * PRECONDITION: all L1 entites are fetched
3575
+ */
3576
+ const fillupL2 = async ({ deepReferences }) => {
3577
+ const referentLinks = deepReferences
3578
+ .map((deepReference) => deepReference.extractReferent(entityStore))
3579
+ .filter(isLink);
3580
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
3581
+ await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
3582
+ };
3583
+ try {
3584
+ if (isMissingL1Entities(entityLinksL1)) {
3585
+ startFetching();
3586
+ await fillupL1({ entityLinksL1 });
3587
+ }
3588
+ if (isMissingL2Entities(deepReferences)) {
3589
+ startFetching();
3590
+ await fillupL2({ deepReferences });
3591
+ }
3592
+ }
3593
+ catch (error) {
3594
+ console.error('[experiences-sdk-react] Failed fetching entities');
3595
+ console.error(error);
3596
+ throw error; // TODO: The original catch didn't let's rethrow; for the moment throw to see if we have any errors
3597
+ }
3598
+ finally {
3599
+ endFetching();
3600
+ }
3601
+ }, [
3602
+ /* dataSource, */ entityStore,
3603
+ setEntitiesFetched /* setFetchingEntities, assembliesRegistry */,
3604
+ ]);
3605
+ useEffect(() => {
3606
+ const onMessage = async (event) => {
3607
+ let reason;
3608
+ if ((reason = doesMismatchMessageSchema(event))) {
3609
+ if (event.origin.startsWith('http://localhost') &&
3610
+ `${event.data}`.includes('webpackHotUpdate')) {
3611
+ reloadApp();
3612
+ }
3613
+ else {
3614
+ console.warn(`[experiences-sdk-react::onMessage] Ignoring alien incoming message from origin [${event.origin}], due to: [${reason}]`, event);
3615
+ }
3616
+ return;
3617
+ }
3618
+ const eventData = tryParseMessage(event);
3619
+ if (eventData.eventType === PostMessageMethods$1.REQUESTED_ENTITIES) {
3620
+ // Expected message: This message is handled in the EntityStore to store fetched entities
3621
+ return;
3622
+ }
3623
+ console.debug(`[experiences-sdk-react::onMessage] Received message [${eventData.eventType}]`, eventData);
3624
+ const { payload } = eventData;
3625
+ switch (eventData.eventType) {
3626
+ case INCOMING_EVENTS.CompositionUpdated: {
3627
+ const { tree, locale, changedNode, changedValueType, assemblies, } = payload;
3628
+ // Make sure to first store the assemblies before setting the tree and thus triggering a rerender
3629
+ if (assemblies) {
3630
+ setAssemblies(assemblies);
3631
+ // If the assemblyEntry is not yet fetched, this will be done below by
3632
+ // the imperative calls to fetchMissingEntities.
3633
+ }
3634
+ // Below are mutually exclusive cases
3635
+ if (changedNode) {
3636
+ /**
3637
+ * On single node updates, we want to skip the process of getting the data (datasource and unbound values)
3638
+ * from tree. Since we know the updated node, we can skip that recursion everytime the tree updates and
3639
+ * just update the relevant data we need from the relevant node.
3640
+ *
3641
+ * We still update the tree here so we don't have a stale "tree"
3642
+ */
3643
+ if (changedValueType === 'BoundValue') {
3644
+ const newDataSource = { ...dataSource, ...changedNode.data.dataSource };
3645
+ setDataSource(newDataSource);
3646
+ await fetchMissingEntities(newDataSource, tree);
3647
+ }
3648
+ else if (changedValueType === 'UnboundValue') {
3649
+ setUnboundValues({
3650
+ ...unboundValues,
3651
+ ...changedNode.data.unboundValues,
3652
+ });
3653
+ }
3654
+ // Update the tree when all necessary data is fetched and ready for rendering.
3655
+ updateTree(tree);
3656
+ setLocale(locale);
3657
+ }
3658
+ else {
3659
+ const { dataSource, unboundValues } = getDataFromTree(tree);
3660
+ setDataSource(dataSource);
3661
+ setUnboundValues(unboundValues);
3662
+ await fetchMissingEntities(dataSource, tree);
3663
+ // Update the tree when all necessary data is fetched and ready for rendering.
3664
+ updateTree(tree);
3665
+ setLocale(locale);
3666
+ }
3667
+ break;
3668
+ }
3669
+ case INCOMING_EVENTS.DesignComponentsRegistered:
3670
+ // Event was deprecated and support will be discontinued with version 5
3671
+ break;
3672
+ case INCOMING_EVENTS.AssembliesRegistered: {
3673
+ const { assemblies } = payload;
3674
+ assemblies.forEach((definition) => {
3675
+ addComponentRegistration({
3676
+ component: Assembly,
3677
+ definition,
3678
+ });
3679
+ });
3680
+ break;
3681
+ }
3682
+ case INCOMING_EVENTS.DesignComponentsAdded:
3683
+ // Event was deprecated and support will be discontinued with version 5
3684
+ break;
3685
+ case INCOMING_EVENTS.AssembliesAdded: {
3686
+ const { assembly, assemblyDefinition, } = payload;
3687
+ entityStore.updateEntity(assembly);
3688
+ // Using a Map here to avoid setting state and rerending all existing assemblies when a new assembly is added
3689
+ // TODO: Figure out if we can extend this love to data source and unbound values. Maybe that'll solve the blink
3690
+ // of all bound and unbound values when new values are added
3691
+ assembliesRegistry.set(assembly.sys.id, {
3692
+ sys: { id: assembly.sys.id, linkType: 'Entry', type: 'Link' },
3693
+ });
3694
+ if (assemblyDefinition) {
3695
+ addComponentRegistration({
3696
+ component: Assembly,
3697
+ definition: assemblyDefinition,
3698
+ });
3699
+ }
3700
+ break;
3701
+ }
3702
+ case INCOMING_EVENTS.CanvasResized: {
3703
+ break;
3704
+ }
3705
+ case INCOMING_EVENTS.HoverComponent: {
3706
+ const { hoveredNodeId } = payload;
3707
+ sendHoveredComponentCoordinates(hoveredNodeId);
3708
+ break;
3709
+ }
3710
+ case INCOMING_EVENTS.ComponentDraggingChanged: {
3711
+ const { isDragging } = payload;
3712
+ if (!isDragging) {
3713
+ setComponentId('');
3714
+ setDraggingOnCanvas(false);
3715
+ dragState.reset();
3716
+ }
3717
+ break;
3718
+ }
3719
+ case INCOMING_EVENTS.UpdatedEntity: {
3720
+ const { entity: updatedEntity, shouldRerender } = payload;
3721
+ if (updatedEntity) {
3722
+ const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
3723
+ const didEntityChange = storedEntity?.sys.version !== updatedEntity.sys.version;
3724
+ entityStore.updateEntity(updatedEntity);
3725
+ // We traverse the whole tree, so this is a opt-in feature to only use it when required.
3726
+ if (shouldRerender && didEntityChange) {
3727
+ updateNodesByUpdatedEntity(updatedEntity.sys.id);
3728
+ }
3729
+ }
3730
+ break;
3731
+ }
3732
+ case INCOMING_EVENTS.RequestEditorMode: {
3733
+ break;
3734
+ }
3735
+ case INCOMING_EVENTS.ComponentDragCanceled: {
3736
+ if (dragState.isDragging) {
3737
+ //simulate a mouseup event to cancel the drag
3738
+ simulateMouseEvent(0, 0, 'mouseup');
3739
+ }
3740
+ break;
3741
+ }
3742
+ case INCOMING_EVENTS.ComponentDragStarted: {
3743
+ dragState.updateIsDragStartedOnParent(true);
3744
+ setDraggingOnCanvas(true);
3745
+ setComponentId(payload.id || '');
3746
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
3747
+ nodeId: '',
3748
+ });
3749
+ break;
3750
+ }
3751
+ case INCOMING_EVENTS.ComponentDragEnded: {
3752
+ dragState.reset();
3753
+ setComponentId('');
3754
+ setDraggingOnCanvas(false);
3755
+ break;
3756
+ }
3757
+ case INCOMING_EVENTS.SelectComponent: {
3758
+ const { selectedNodeId: nodeId } = payload;
3759
+ setSelectedNodeId(nodeId);
3760
+ sendSelectedComponentCoordinates(nodeId);
3761
+ break;
3762
+ }
3763
+ default:
3764
+ console.error(`[experiences-sdk-react::onMessage] Logic error, unsupported eventType: [${eventData.eventType}]`);
3765
+ }
3766
+ };
3767
+ window.addEventListener('message', onMessage);
3768
+ return () => {
3769
+ window.removeEventListener('message', onMessage);
3770
+ };
3771
+ }, [
3772
+ entityStore,
3773
+ setComponentId,
3774
+ setDraggingOnCanvas,
3775
+ setDataSource,
3776
+ setLocale,
3777
+ setSelectedNodeId,
3778
+ dataSource,
3779
+ areEntitiesFetched,
3780
+ fetchMissingEntities,
3781
+ setUnboundValues,
3782
+ unboundValues,
3783
+ updateTree,
3784
+ updateNodesByUpdatedEntity,
3785
+ ]);
3786
+ /*
3787
+ * Handles on scroll business
3788
+ */
3789
+ useEffect(() => {
3790
+ let timeoutId = 0;
3791
+ let isScrolling = false;
3792
+ const onScroll = () => {
3793
+ if (isScrolling === false) {
3794
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.Start);
3795
+ }
3796
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.IsScrolling);
3797
+ isScrolling = true;
3798
+ clearTimeout(timeoutId);
3799
+ timeoutId = window.setTimeout(() => {
3800
+ if (isScrolling === false) {
3801
+ return;
3802
+ }
3803
+ isScrolling = false;
3804
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.End);
3805
+ /**
3806
+ * On scroll end, send new co-ordinates of selected node
3807
+ */
3808
+ if (selectedNodeId) {
3809
+ sendSelectedComponentCoordinates(selectedNodeId);
3810
+ }
3811
+ }, 150);
3812
+ };
3813
+ window.addEventListener('scroll', onScroll, { capture: true, passive: true });
3814
+ return () => {
3815
+ window.removeEventListener('scroll', onScroll, { capture: true });
3816
+ clearTimeout(timeoutId);
3817
+ };
3818
+ }, [selectedNodeId]);
3819
+ }
3820
+
3821
+ const onComponentMoved = (options) => {
3822
+ sendMessage(OUTGOING_EVENTS.ComponentMoved, options);
3823
+ };
3824
+
3825
+ const generateId = (type) => `${type}-${v4()}`;
3826
+
3827
+ const createTreeNode = ({ blockId, parentId }) => {
3828
+ const node = {
3829
+ type: 'block',
3830
+ data: {
3831
+ id: generateId(blockId),
3832
+ blockId,
3833
+ props: {},
3834
+ dataSource: {},
3835
+ breakpoints: [],
3836
+ unboundValues: {},
3837
+ },
3838
+ parentId,
3839
+ children: [],
3840
+ };
3841
+ return node;
3842
+ };
3843
+
3844
+ const onComponentDropped = ({ node, index, parentBlockId, parentType, parentId, }) => {
3845
+ sendMessage(OUTGOING_EVENTS.ComponentDropped, {
3846
+ node,
3847
+ index: index ?? node.children.length,
3848
+ parentNode: {
3849
+ type: parentType,
3850
+ data: {
3851
+ blockId: parentBlockId,
3852
+ id: parentId,
3853
+ },
3854
+ },
3855
+ });
3856
+ };
3857
+
3858
+ const onDrop = ({ destinationIndex, componentType, destinationZoneId, data, }) => {
3859
+ const parentId = destinationZoneId;
3860
+ const parentNode = getItem({ id: parentId }, data);
3861
+ const parentIsRoot = parentId === ROOT_ID;
3862
+ const emptyComponentData = {
3863
+ type: 'block',
3864
+ parentId,
3865
+ children: [],
3866
+ data: {
3867
+ blockId: componentType,
3868
+ id: generateId(componentType),
3869
+ breakpoints: [],
3870
+ dataSource: {},
3871
+ props: {},
3872
+ unboundValues: {},
3873
+ },
3874
+ };
3875
+ onComponentDropped({
3876
+ node: emptyComponentData,
3877
+ index: destinationIndex,
3878
+ parentType: parentIsRoot ? 'root' : parentNode?.type,
3879
+ parentBlockId: parentNode?.data.blockId,
3880
+ parentId: parentIsRoot ? 'root' : parentId,
3881
+ });
3882
+ };
3883
+
3884
+ function useCanvasInteractions() {
3885
+ const tree = useTreeStore((state) => state.tree);
3886
+ const reorderChildren = useTreeStore((state) => state.reorderChildren);
3887
+ const reparentChild = useTreeStore((state) => state.reparentChild);
3888
+ const addChild = useTreeStore((state) => state.addChild);
3889
+ const onAddComponent = (droppedItem) => {
3890
+ const { destination, draggableId } = droppedItem;
3891
+ if (!destination) {
3892
+ return;
3893
+ }
3894
+ const droppingOnRoot = destination.droppableId === ROOT_ID;
3895
+ const isValidRootComponent = draggableId === CONTENTFUL_COMPONENTS.container.id;
3896
+ let node = createTreeNode({ blockId: draggableId, parentId: destination.droppableId });
3897
+ if (droppingOnRoot && !isValidRootComponent) {
3898
+ const wrappingContainer = createTreeNode({
3899
+ blockId: CONTENTFUL_COMPONENTS.container.id,
3900
+ parentId: destination.droppableId,
3901
+ });
3902
+ const childNode = createTreeNode({
3903
+ blockId: draggableId,
3904
+ parentId: wrappingContainer.data.id,
3905
+ });
3906
+ node = wrappingContainer;
3907
+ node.children = [childNode];
3908
+ }
3909
+ addChild(destination.index, destination.droppableId, node);
3910
+ onDrop({
3911
+ data: tree,
3912
+ componentType: draggableId,
3913
+ destinationIndex: destination.index,
3914
+ destinationZoneId: destination.droppableId,
3915
+ });
3916
+ };
3917
+ const onMoveComponent = (droppedItem) => {
3918
+ const { destination, source, draggableId } = droppedItem;
3919
+ if (!destination || !source) {
3920
+ return;
3921
+ }
3922
+ if (destination.droppableId === source.droppableId) {
3923
+ reorderChildren(destination.index, destination.droppableId, source.index);
3924
+ }
3925
+ if (destination.droppableId !== source.droppableId) {
3926
+ reparentChild(destination.index, destination.droppableId, source.index, source.droppableId);
3927
+ }
3928
+ onComponentMoved({
3929
+ nodeId: draggableId,
3930
+ destinationIndex: destination.index,
3931
+ destinationParentId: destination.droppableId,
3932
+ sourceIndex: source.index,
3933
+ sourceParentId: source.droppableId,
3934
+ });
3935
+ };
3936
+ return { onAddComponent, onMoveComponent };
3937
+ }
3938
+
3939
+ const TestDNDContainer = ({ onDragEnd, onBeforeDragStart, onDragUpdate, children, }) => {
3940
+ const handleDragStart = (event) => {
3941
+ const draggedItem = event.nativeEvent;
3942
+ const start = {
3943
+ mode: draggedItem.mode,
3944
+ draggableId: draggedItem.draggableId,
3945
+ type: draggedItem.type,
3946
+ source: draggedItem.source,
3947
+ };
3948
+ onBeforeDragStart(start);
3949
+ };
3950
+ const handleDrag = (event) => {
3951
+ const draggedItem = event.nativeEvent;
3952
+ const update = {
3953
+ mode: draggedItem.mode,
3954
+ draggableId: draggedItem.draggableId,
3955
+ type: draggedItem.type,
3956
+ source: draggedItem.source,
3957
+ destination: draggedItem.destination,
3958
+ combine: draggedItem.combine,
3959
+ };
3960
+ onDragUpdate(update, {});
3961
+ };
3962
+ const handleDragEnd = (event) => {
3963
+ const draggedItem = event.nativeEvent;
3964
+ const result = {
3965
+ mode: draggedItem.mode,
3966
+ draggableId: draggedItem.draggableId,
3967
+ type: draggedItem.type,
3968
+ source: draggedItem.source,
3969
+ destination: draggedItem.destination,
3970
+ combine: draggedItem.combine,
3971
+ reason: draggedItem.reason,
3972
+ };
3973
+ onDragEnd(result, {});
3974
+ };
3975
+ return (React.createElement("div", { "data-test-id": "dnd-context-substitute", onDragStart: handleDragStart, onDrag: handleDrag, onDragEnd: handleDragEnd }, children));
3976
+ };
3977
+
3978
+ const DNDProvider = ({ children }) => {
3979
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
3980
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
3981
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
3982
+ const updateItem = useDraggedItemStore((state) => state.updateItem);
3983
+ const { onAddComponent, onMoveComponent } = useCanvasInteractions();
3984
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3985
+ const prevSelectedNodeId = useRef(null);
3986
+ const isTestRun = typeof window !== 'undefined' && Object.prototype.hasOwnProperty.call(window, 'Cypress');
3987
+ const dragStart = () => {
3988
+ prevSelectedNodeId.current = selectedNodeId;
3989
+ //Unselect the current node when dragging and remove the outline
3990
+ setSelectedNodeId('');
3991
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
3992
+ nodeId: '',
3993
+ });
3994
+ };
3995
+ const beforeCapture = () => {
3996
+ setDraggingOnCanvas(true);
3997
+ };
3998
+ const dragUpdate = (update) => {
3999
+ updateItem(update);
4000
+ };
4001
+ const dragEnd = (dropResult) => {
4002
+ setDraggingOnCanvas(false);
4003
+ updateItem(undefined);
4004
+ dragState.reset();
4005
+ if (!dropResult.destination) {
4006
+ if (!draggedItem?.destination) {
4007
+ // User cancel drag
4008
+ sendMessage(OUTGOING_EVENTS.ComponentDragCanceled);
4009
+ //select the previously selected node if drag was canceled
4010
+ if (prevSelectedNodeId.current) {
4011
+ setSelectedNodeId(prevSelectedNodeId.current);
4012
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4013
+ nodeId: prevSelectedNodeId.current,
4014
+ });
4015
+ prevSelectedNodeId.current = null;
4016
+ }
4017
+ return;
4018
+ }
4019
+ // Use the destination from the draggedItem (when clicking the canvas)
4020
+ dropResult.destination = draggedItem.destination;
4021
+ }
4022
+ // New component added to canvas
4023
+ if (dropResult.source.droppableId.startsWith('component-list')) {
4024
+ onAddComponent(dropResult);
4025
+ }
4026
+ else {
4027
+ onMoveComponent(dropResult);
4028
+ }
4029
+ // If a node was previously selected prior to dragging, re-select it
4030
+ setSelectedNodeId(dropResult.draggableId);
4031
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4032
+ nodeId: dropResult.draggableId,
4033
+ });
4034
+ };
4035
+ return (React.createElement(DragDropContext, { onBeforeCapture: beforeCapture, onDragUpdate: dragUpdate, onBeforeDragStart: dragStart, onDragEnd: dragEnd }, isTestRun ? (React.createElement(TestDNDContainer, { onDragEnd: dragEnd, onBeforeDragStart: dragStart, onDragUpdate: dragUpdate }, children)) : (children)));
4036
+ };
4037
+
4038
+ const RootRenderer = ({ onChange }) => {
4039
+ useEditorSubscriber();
4040
+ const dragItem = useDraggedItemStore((state) => state.componentId);
4041
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
4042
+ const breakpoints = useTreeStore((state) => state.breakpoints);
4043
+ const draggableSourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId);
4044
+ const draggingNewComponent = !!draggableSourceId?.startsWith(COMPONENT_LIST_ID);
4045
+ const containerRef = useRef(null);
4046
+ const { resolveDesignValue } = useBreakpoints(breakpoints);
4047
+ const [containerStyles, setContainerStyles] = useState({});
4048
+ const tree = useTreeStore((state) => state.tree);
4049
+ useEffect(() => {
4050
+ if (onChange)
4051
+ onChange(tree);
4052
+ }, [tree, onChange]);
4053
+ useEffect(() => {
4054
+ document.addEventListener('click', handleClickOutside);
4055
+ return () => {
4056
+ document.removeEventListener('click', handleClickOutside);
4057
+ };
4058
+ }, []);
4059
+ const handleClickOutside = () => {
4060
+ sendMessage(OUTGOING_EVENTS.OutsideCanvasClick, {
4061
+ outsideCanvasClick: true,
4062
+ });
4063
+ };
4064
+ const handleResizeCanvas = useCallback(() => {
4065
+ const parentElement = containerRef.current?.parentElement;
4066
+ if (!parentElement) {
4067
+ return;
4068
+ }
4069
+ let siblingHeight = 0;
4070
+ for (const child of parentElement.children) {
4071
+ if (!child.hasAttribute('data-ctfl-root')) {
4072
+ siblingHeight += child.getBoundingClientRect().height;
4073
+ }
4074
+ }
4075
+ if (!siblingHeight) {
4076
+ /**
4077
+ * DRAGGABLE_HEIGHT is subtracted here due to an uninteded scrolling effect
4078
+ * when dragging a new component onto the canvas
4079
+ *
4080
+ * The DRAGGABLE_HEIGHT is then added as margin bottom to offset this value
4081
+ * so that visually there is no difference to the user.
4082
+ */
4083
+ setContainerStyles({
4084
+ minHeight: `${window.innerHeight - DRAGGABLE_HEIGHT}px`,
4085
+ });
4086
+ return;
4087
+ }
4088
+ setContainerStyles({
4089
+ minHeight: `${window.innerHeight - siblingHeight}px`,
4090
+ });
4091
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4092
+ }, [containerRef.current]);
4093
+ useEffect(() => {
4094
+ handleResizeCanvas();
4095
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4096
+ }, [containerRef.current]);
4097
+ return (React.createElement(DNDProvider, null,
4098
+ dragItem && React.createElement(DraggableContainer, { id: dragItem }),
4099
+ React.createElement("div", { "data-ctfl-root": true, className: styles.container, ref: containerRef, style: containerStyles },
4100
+ userIsDragging && draggingNewComponent && (React.createElement("div", { className: styles.hitbox, "data-ctfl-zone-id": ROOT_ID })),
4101
+ React.createElement(Dropzone, { zoneId: ROOT_ID, resolveDesignValue: resolveDesignValue }),
4102
+ userIsDragging && draggingNewComponent && (React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles.hitboxLower }))),
4103
+ React.createElement("div", { "data-ctfl-hitboxes": true })));
4104
+ };
4105
+
4106
+ const useInitializeEditor = () => {
4107
+ const initializeEditor = useEditorStore((state) => state.initializeEditor);
4108
+ const [initialized, setInitialized] = useState(false);
4109
+ const resetEntityStore = useEntityStore((state) => state.resetEntityStore);
4110
+ useEffect(() => {
4111
+ const onVisualEditorInitialize = (event) => {
4112
+ if (!event.detail)
4113
+ return;
4114
+ const { componentRegistry, designTokens, locale: initialLocale,
4115
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
4116
+ entities, } = event.detail;
4117
+ initializeEditor({
4118
+ initialLocale,
4119
+ componentRegistry,
4120
+ designTokens,
4121
+ });
4122
+ // if entities is set to [], then everything will still work as EntityStore will
4123
+ // request entities on demand via ▲REQUEST_ENTITY
4124
+ resetEntityStore(initialLocale, entities);
4125
+ setInitialized(true);
4126
+ };
4127
+ // Listen for VisualEditorComponents internal event
4128
+ window.addEventListener(INTERNAL_EVENTS.VisualEditorInitialize, onVisualEditorInitialize);
4129
+ // Clean up the event listener
4130
+ return () => {
4131
+ window.removeEventListener(INTERNAL_EVENTS.VisualEditorInitialize, onVisualEditorInitialize);
4132
+ };
4133
+ }, [initializeEditor, resetEntityStore]);
4134
+ useEffect(() => {
4135
+ if (initialized) {
4136
+ return;
4137
+ }
4138
+ // Dispatch Visual Editor Ready event
4139
+ window.dispatchEvent(new CustomEvent(VISUAL_EDITOR_EVENTS.Ready));
4140
+ }, [initialized]);
4141
+ return initialized;
4142
+ };
4143
+
4144
+ const findNearestDropzone = (element) => {
4145
+ const zoneId = element.getAttribute(CTFL_ZONE_ID);
4146
+ if (!element.parentElement) {
4147
+ return null;
4148
+ }
4149
+ if (element.tagName === 'BODY') {
4150
+ return null;
4151
+ }
4152
+ return zoneId ?? findNearestDropzone(element.parentElement);
4153
+ };
4154
+ const VisualEditorRoot = () => {
4155
+ const initialized = useInitializeEditor();
4156
+ const locale = useEditorStore((state) => state.locale);
4157
+ const entityStore = useEntityStore((state) => state.entityStore);
4158
+ const setHoveringZone = useZoneStore((state) => state.setHoveringZone);
4159
+ const resetEntityStore = useEntityStore((state) => state.resetEntityStore);
4160
+ useEffect(() => {
4161
+ if (!locale) {
4162
+ return;
4163
+ }
4164
+ if (entityStore.locale === locale) {
4165
+ return;
4166
+ }
4167
+ resetEntityStore(locale);
4168
+ }, [locale, resetEntityStore, entityStore.locale]);
4169
+ useEffect(() => {
4170
+ const onMouseMove = (e) => {
4171
+ const target = e.target;
4172
+ const zoneId = findNearestDropzone(target);
4173
+ if (zoneId) {
4174
+ setHoveringZone(zoneId);
4175
+ }
4176
+ if (e.target?.id === 'item') {
4177
+ return;
4178
+ }
4179
+ if (!dragState.isDragStart) {
4180
+ return;
4181
+ }
4182
+ simulateMouseEvent(e.pageX, e.pageY);
4183
+ sendMessage(OUTGOING_EVENTS.MouseMove, {
4184
+ clientX: e.pageX,
4185
+ clientY: e.pageY - window.scrollY,
4186
+ });
4187
+ };
4188
+ document.addEventListener('mousemove', onMouseMove);
4189
+ return () => {
4190
+ document.removeEventListener('mousemove', onMouseMove);
4191
+ };
4192
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4193
+ }, []);
4194
+ if (!initialized)
4195
+ return null;
4196
+ return React.createElement(RootRenderer, null);
4197
+ };
4198
+
4199
+ export { VisualEditorRoot as default };
4200
+ //# sourceMappingURL=index.js.map