@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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4200 -0
- package/dist/index.js.map +1 -0
- package/dist/renderApp.js +53825 -0
- package/dist/renderApp.js.map +1 -0
- package/package.json +73 -0
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
|