@contentful/experiences-core 1.28.0-dev-20250115T1128-04df9c2.0 → 1.28.0-prerelease-20250115T2332-646080a.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fetchers/createExperience.d.ts +3 -4
- package/dist/fetchers/fetchById.d.ts +2 -3
- package/dist/fetchers/fetchBySlug.d.ts +2 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1112 -1110
- package/dist/index.js.map +1 -1
- package/dist/types/Experience.d.ts +6 -0
- package/dist/types.d.ts +1 -2
- package/dist/utils/styleUtils/ssrStyles.d.ts +2 -1
- package/dist/utils/transformers/transformBoundContentValue.d.ts +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import md5 from 'md5';
|
|
2
1
|
import { z, ZodIssueCode } from 'zod';
|
|
2
|
+
import { omit, isArray, uniqBy } from 'lodash-es';
|
|
3
|
+
import md5 from 'md5';
|
|
3
4
|
import { BLOCKS } from '@contentful/rich-text-types';
|
|
4
|
-
import { isArray, omit, uniqBy } from 'lodash-es';
|
|
5
5
|
|
|
6
6
|
const INCOMING_EVENTS = {
|
|
7
7
|
RequestEditorMode: 'requestEditorMode',
|
|
@@ -193,243 +193,14 @@ const getElementCoordinates = (element) => {
|
|
|
193
193
|
});
|
|
194
194
|
};
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if ('object' !== typeof result) {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
catch (e) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const doesMismatchMessageSchema = (event) => {
|
|
214
|
-
try {
|
|
215
|
-
tryParseMessage(event);
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
catch (e) {
|
|
219
|
-
if (e instanceof ParseError) {
|
|
220
|
-
return e.message;
|
|
221
|
-
}
|
|
222
|
-
throw e;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
const tryParseMessage = (event) => {
|
|
226
|
-
if (!event.data) {
|
|
227
|
-
throw new ParseError('Field event.data is missing');
|
|
228
|
-
}
|
|
229
|
-
if ('string' !== typeof event.data) {
|
|
230
|
-
throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
|
|
231
|
-
}
|
|
232
|
-
if (!isValidJsonObject(event.data)) {
|
|
233
|
-
throw new ParseError('Field event.data must be a valid JSON object serialized as string');
|
|
234
|
-
}
|
|
235
|
-
const eventData = JSON.parse(event.data);
|
|
236
|
-
if (!eventData.source) {
|
|
237
|
-
throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
|
|
238
|
-
}
|
|
239
|
-
if ('composability-app' !== eventData.source) {
|
|
240
|
-
throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
|
|
241
|
-
}
|
|
242
|
-
// check eventData.eventType
|
|
243
|
-
const supportedEventTypes = Object.values(INCOMING_EVENTS);
|
|
244
|
-
if (!supportedEventTypes.includes(eventData.eventType)) {
|
|
245
|
-
// Expected message: This message is handled in the EntityStore to store fetched entities
|
|
246
|
-
if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
|
|
247
|
-
throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return eventData;
|
|
251
|
-
};
|
|
252
|
-
const validateExperienceBuilderConfig = ({ locale, mode, }) => {
|
|
253
|
-
if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (!locale) {
|
|
257
|
-
throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const transformVisibility = (value) => {
|
|
262
|
-
if (value === false) {
|
|
263
|
-
return {
|
|
264
|
-
display: 'none',
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
// Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
|
|
268
|
-
return {};
|
|
269
|
-
};
|
|
270
|
-
// Keep this for backwards compatibility - deleting this would be a breaking change
|
|
271
|
-
// because existing components on a users experience will have the width value as fill
|
|
272
|
-
// rather than 100%
|
|
273
|
-
const transformFill = (value) => (value === 'fill' ? '100%' : value);
|
|
274
|
-
const transformGridColumn = (span) => {
|
|
275
|
-
if (!span) {
|
|
276
|
-
return {};
|
|
277
|
-
}
|
|
278
|
-
return {
|
|
279
|
-
gridColumn: `span ${span}`,
|
|
280
|
-
};
|
|
281
|
-
};
|
|
282
|
-
const transformBorderStyle = (value) => {
|
|
283
|
-
if (!value)
|
|
284
|
-
return {};
|
|
285
|
-
const parts = value.split(' ');
|
|
286
|
-
// Just accept the passed value
|
|
287
|
-
if (parts.length < 3)
|
|
288
|
-
return { border: value };
|
|
289
|
-
const [borderSize, borderStyle, ...borderColorParts] = parts;
|
|
290
|
-
const borderColor = borderColorParts.join(' ');
|
|
291
|
-
return {
|
|
292
|
-
border: `${borderSize} ${borderStyle} ${borderColor}`,
|
|
293
|
-
};
|
|
294
|
-
};
|
|
295
|
-
const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
|
|
296
|
-
? {
|
|
297
|
-
alignItems: cfHorizontalAlignment,
|
|
298
|
-
justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
|
|
299
|
-
}
|
|
300
|
-
: {
|
|
301
|
-
alignItems: cfVerticalAlignment,
|
|
302
|
-
justifyContent: cfHorizontalAlignment === 'center'
|
|
303
|
-
? `safe ${cfHorizontalAlignment}`
|
|
304
|
-
: cfHorizontalAlignment,
|
|
305
|
-
};
|
|
306
|
-
const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
|
|
307
|
-
const matchBackgroundSize = (scaling) => {
|
|
308
|
-
if ('fill' === scaling)
|
|
309
|
-
return 'cover';
|
|
310
|
-
if ('fit' === scaling)
|
|
311
|
-
return 'contain';
|
|
312
|
-
};
|
|
313
|
-
const matchBackgroundPosition = (alignment) => {
|
|
314
|
-
if (!alignment || 'string' !== typeof alignment) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
|
|
318
|
-
// Special case for handling single values
|
|
319
|
-
// for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
|
|
320
|
-
if (horizontalAlignment && !verticalAlignment) {
|
|
321
|
-
const singleValue = horizontalAlignment;
|
|
322
|
-
switch (singleValue) {
|
|
323
|
-
case 'left':
|
|
324
|
-
horizontalAlignment = 'left';
|
|
325
|
-
verticalAlignment = 'center';
|
|
326
|
-
break;
|
|
327
|
-
case 'right':
|
|
328
|
-
horizontalAlignment = 'right';
|
|
329
|
-
verticalAlignment = 'center';
|
|
330
|
-
break;
|
|
331
|
-
case 'center':
|
|
332
|
-
horizontalAlignment = 'center';
|
|
333
|
-
verticalAlignment = 'center';
|
|
334
|
-
break;
|
|
335
|
-
case 'top':
|
|
336
|
-
horizontalAlignment = 'center';
|
|
337
|
-
verticalAlignment = 'top';
|
|
338
|
-
break;
|
|
339
|
-
case 'bottom':
|
|
340
|
-
horizontalAlignment = 'center';
|
|
341
|
-
verticalAlignment = 'bottom';
|
|
342
|
-
break;
|
|
343
|
-
// just fall down to the normal validation logic for horiz and vert
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
|
|
347
|
-
const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
|
|
348
|
-
horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
|
|
349
|
-
verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
|
|
350
|
-
return `${horizontalAlignment} ${verticalAlignment}`;
|
|
351
|
-
};
|
|
352
|
-
if (!cfBackgroundImageUrl) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
let backgroundImage;
|
|
356
|
-
let backgroundImageSet;
|
|
357
|
-
if (typeof cfBackgroundImageUrl === 'string') {
|
|
358
|
-
backgroundImage = `url(${cfBackgroundImageUrl})`;
|
|
359
|
-
}
|
|
360
|
-
else {
|
|
361
|
-
const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
|
|
362
|
-
backgroundImage = `url(${cfBackgroundImageUrl.url})`;
|
|
363
|
-
backgroundImageSet = `image-set(${imgSet})`;
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
backgroundImage,
|
|
367
|
-
backgroundImage2: backgroundImageSet,
|
|
368
|
-
backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
|
|
369
|
-
backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
|
|
370
|
-
backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
|
|
371
|
-
};
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
const toCSSAttribute = (key) => {
|
|
375
|
-
let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
376
|
-
// Remove the number from the end of the key to allow for overrides on style properties
|
|
377
|
-
val = val.replace(/\d+$/, '');
|
|
378
|
-
return val;
|
|
379
|
-
};
|
|
380
|
-
const buildStyleTag = ({ styles, nodeId }) => {
|
|
381
|
-
const stylesStr = Object.entries(styles)
|
|
382
|
-
.filter(([, value]) => value !== undefined)
|
|
383
|
-
.reduce((acc, [key, value]) => `${acc}
|
|
384
|
-
${toCSSAttribute(key)}: ${value};`, '');
|
|
385
|
-
const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
|
|
386
|
-
const styleRule = `.${className}{ ${stylesStr} }`;
|
|
387
|
-
return [className, styleRule];
|
|
388
|
-
};
|
|
389
|
-
const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexReverse, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfBorderRadius, cfGap, cfBackgroundImageUrl, cfBackgroundImageOptions, cfFontSize, cfFontWeight, cfImageOptions, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, cfVisibility, }) => {
|
|
390
|
-
return {
|
|
391
|
-
boxSizing: 'border-box',
|
|
392
|
-
...transformVisibility(cfVisibility),
|
|
393
|
-
margin: cfMargin,
|
|
394
|
-
padding: cfPadding,
|
|
395
|
-
backgroundColor: cfBackgroundColor,
|
|
396
|
-
width: transformFill(cfWidth || cfImageOptions?.width),
|
|
397
|
-
height: transformFill(cfHeight || cfImageOptions?.height),
|
|
398
|
-
maxWidth: cfMaxWidth,
|
|
399
|
-
...transformGridColumn(cfColumnSpan),
|
|
400
|
-
...transformBorderStyle(cfBorder),
|
|
401
|
-
borderRadius: cfBorderRadius,
|
|
402
|
-
gap: cfGap,
|
|
403
|
-
...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
|
|
404
|
-
flexDirection: cfFlexReverse && cfFlexDirection ? `${cfFlexDirection}-reverse` : cfFlexDirection,
|
|
405
|
-
flexWrap: cfFlexWrap,
|
|
406
|
-
...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageOptions),
|
|
407
|
-
fontSize: cfFontSize,
|
|
408
|
-
fontWeight: cfTextBold ? 'bold' : cfFontWeight,
|
|
409
|
-
fontStyle: cfTextItalic ? 'italic' : undefined,
|
|
410
|
-
textDecoration: cfTextUnderline ? 'underline' : undefined,
|
|
411
|
-
lineHeight: cfLineHeight,
|
|
412
|
-
letterSpacing: cfLetterSpacing,
|
|
413
|
-
color: cfTextColor,
|
|
414
|
-
textAlign: cfTextAlign,
|
|
415
|
-
textTransform: cfTextTransform,
|
|
416
|
-
objectFit: cfImageOptions?.objectFit,
|
|
417
|
-
objectPosition: cfImageOptions?.objectPosition,
|
|
418
|
-
};
|
|
419
|
-
};
|
|
420
|
-
/**
|
|
421
|
-
* Container/section default behavior:
|
|
422
|
-
* Default height => height: EMPTY_CONTAINER_HEIGHT
|
|
423
|
-
* If a container component has children => height: 'fit-content'
|
|
424
|
-
*/
|
|
425
|
-
const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
|
|
426
|
-
if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
|
|
427
|
-
return value;
|
|
428
|
-
}
|
|
429
|
-
if (children.length) {
|
|
430
|
-
return '100%';
|
|
431
|
-
}
|
|
432
|
-
return EMPTY_CONTAINER_HEIGHT;
|
|
196
|
+
const isExperienceEntry = (entry) => {
|
|
197
|
+
return (entry?.sys?.type === 'Entry' &&
|
|
198
|
+
!!entry.fields?.title &&
|
|
199
|
+
!!entry.fields?.slug &&
|
|
200
|
+
!!entry.fields?.componentTree &&
|
|
201
|
+
Array.isArray(entry.fields.componentTree.breakpoints) &&
|
|
202
|
+
Array.isArray(entry.fields.componentTree.children) &&
|
|
203
|
+
typeof entry.fields.componentTree.schemaVersion === 'string');
|
|
433
204
|
};
|
|
434
205
|
|
|
435
206
|
// These styles get added to every component, user custom or contentful provided
|
|
@@ -1509,478 +1280,553 @@ const resetBreakpointsRegistry = () => {
|
|
|
1509
1280
|
breakpointsRegistry = [];
|
|
1510
1281
|
};
|
|
1511
1282
|
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
{
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1283
|
+
const MEDIA_QUERY_REGEXP = /(<|>)(\d{1,})(px|cm|mm|in|pt|pc)$/;
|
|
1284
|
+
const toCSSMediaQuery = ({ query }) => {
|
|
1285
|
+
if (query === '*')
|
|
1286
|
+
return undefined;
|
|
1287
|
+
const match = query.match(MEDIA_QUERY_REGEXP);
|
|
1288
|
+
if (!match)
|
|
1289
|
+
return undefined;
|
|
1290
|
+
const [, operator, value, unit] = match;
|
|
1291
|
+
if (operator === '<') {
|
|
1292
|
+
const maxScreenWidth = Number(value) - 1;
|
|
1293
|
+
return `(max-width: ${maxScreenWidth}${unit})`;
|
|
1294
|
+
}
|
|
1295
|
+
else if (operator === '>') {
|
|
1296
|
+
const minScreenWidth = Number(value) + 1;
|
|
1297
|
+
return `(min-width: ${minScreenWidth}${unit})`;
|
|
1298
|
+
}
|
|
1299
|
+
return undefined;
|
|
1300
|
+
};
|
|
1301
|
+
// Remove this helper when upgrading to TypeScript 5.0 - https://github.com/microsoft/TypeScript/issues/48829
|
|
1302
|
+
const findLast = (array, predicate) => {
|
|
1303
|
+
return array.reverse().find(predicate);
|
|
1304
|
+
};
|
|
1305
|
+
// Initialise media query matchers. This won't include the always matching fallback breakpoint.
|
|
1306
|
+
const mediaQueryMatcher = (breakpoints) => {
|
|
1307
|
+
const mediaQueryMatches = {};
|
|
1308
|
+
const mediaQueryMatchers = breakpoints
|
|
1309
|
+
.map((breakpoint) => {
|
|
1310
|
+
const cssMediaQuery = toCSSMediaQuery(breakpoint);
|
|
1311
|
+
if (!cssMediaQuery)
|
|
1312
|
+
return undefined;
|
|
1313
|
+
if (typeof window === 'undefined')
|
|
1314
|
+
return undefined;
|
|
1315
|
+
const mediaQueryMatcher = window.matchMedia(cssMediaQuery);
|
|
1316
|
+
mediaQueryMatches[breakpoint.id] = mediaQueryMatcher.matches;
|
|
1317
|
+
return { id: breakpoint.id, signal: mediaQueryMatcher };
|
|
1318
|
+
})
|
|
1319
|
+
.filter((matcher) => !!matcher);
|
|
1320
|
+
return [mediaQueryMatchers, mediaQueryMatches];
|
|
1321
|
+
};
|
|
1322
|
+
const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
|
|
1323
|
+
// The breakpoints are ordered (desktop-first: descending by screen width)
|
|
1324
|
+
const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
|
|
1325
|
+
id,
|
|
1326
|
+
index,
|
|
1327
|
+
// The fallback breakpoint with wildcard query will always match
|
|
1328
|
+
isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
|
|
1329
|
+
}));
|
|
1330
|
+
// Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
|
|
1331
|
+
const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
|
|
1332
|
+
return mostSpecificIndex ?? fallbackBreakpointIndex;
|
|
1333
|
+
};
|
|
1334
|
+
const getFallbackBreakpointIndex = (breakpoints) => {
|
|
1335
|
+
// We assume that there will be a single breakpoint which uses the wildcard query.
|
|
1336
|
+
// If there is none, we just take the first one in the list.
|
|
1337
|
+
return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
|
|
1338
|
+
};
|
|
1339
|
+
const builtInStylesWithDesignTokens = [
|
|
1340
|
+
'cfMargin',
|
|
1341
|
+
'cfPadding',
|
|
1342
|
+
'cfGap',
|
|
1343
|
+
'cfWidth',
|
|
1344
|
+
'cfHeight',
|
|
1345
|
+
'cfBackgroundColor',
|
|
1346
|
+
'cfBorder',
|
|
1347
|
+
'cfBorderRadius',
|
|
1348
|
+
'cfFontSize',
|
|
1349
|
+
'cfLineHeight',
|
|
1350
|
+
'cfLetterSpacing',
|
|
1351
|
+
'cfTextColor',
|
|
1352
|
+
'cfMaxWidth',
|
|
1353
|
+
];
|
|
1354
|
+
const isValidBreakpointValue = (value) => {
|
|
1355
|
+
return value !== undefined && value !== null && value !== '';
|
|
1356
|
+
};
|
|
1357
|
+
const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName, resolveDesignTokens = true) => {
|
|
1358
|
+
const eventuallyResolveDesignTokens = (value) => {
|
|
1359
|
+
// For some built-in design properties, we support design tokens
|
|
1360
|
+
if (builtInStylesWithDesignTokens.includes(variableName)) {
|
|
1361
|
+
return getDesignTokenRegistration(value, variableName);
|
|
1539
1362
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
[
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
};
|
|
1552
|
-
}, {});
|
|
1553
|
-
// getting the breakpoint ids
|
|
1554
|
-
const breakpointIds = Object.keys(mediaQueriesTemplate);
|
|
1555
|
-
const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
|
|
1556
|
-
// traversing the tree
|
|
1557
|
-
const queue = [];
|
|
1558
|
-
queue.push(...componentTree.children);
|
|
1559
|
-
let currentNode = undefined;
|
|
1560
|
-
// for each tree node
|
|
1561
|
-
while (queue.length) {
|
|
1562
|
-
currentNode = queue.shift();
|
|
1563
|
-
if (!currentNode) {
|
|
1564
|
-
break;
|
|
1565
|
-
}
|
|
1566
|
-
const usedComponents = experience.entityStore?.usedComponents ?? [];
|
|
1567
|
-
const isPatternNode = checkIsAssemblyNode({
|
|
1568
|
-
componentId: currentNode.definitionId,
|
|
1569
|
-
usedComponents,
|
|
1570
|
-
});
|
|
1571
|
-
if (isPatternNode) {
|
|
1572
|
-
const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
|
|
1573
|
-
if (!patternEntry || !('fields' in patternEntry)) {
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
const defaultPatternDivStyles = Object.fromEntries(Object.entries(buildCfStyles({}))
|
|
1577
|
-
.filter(([, value]) => value !== undefined)
|
|
1578
|
-
.map(([key, value]) => [toCSSAttribute(key), value]));
|
|
1579
|
-
// I create a hash of the object above because that would ensure hash stability
|
|
1580
|
-
const styleHash = md5(JSON.stringify(defaultPatternDivStyles));
|
|
1581
|
-
// and prefix the className to make sure the value can be processed
|
|
1582
|
-
const className = `cf-${styleHash}`;
|
|
1583
|
-
for (const breakpointId of breakpointIds) {
|
|
1584
|
-
if (!mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
|
|
1585
|
-
mediaQueriesTemplate[breakpointId].cssByClassName[className] =
|
|
1586
|
-
toCSSString(defaultPatternDivStyles);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
currentNode.variables.cfSsrClassName = {
|
|
1590
|
-
type: 'DesignValue',
|
|
1591
|
-
valuesByBreakpoint: {
|
|
1592
|
-
[breakpointIds[0]]: className,
|
|
1593
|
-
},
|
|
1594
|
-
};
|
|
1595
|
-
// the node of a used pattern contains only the definitionId (id of the patter entry)
|
|
1596
|
-
// as well as the variables overwrites
|
|
1597
|
-
// the layout of a pattern is stored in it's entry
|
|
1598
|
-
iterateOverTreeAndExtractStyles({
|
|
1599
|
-
// that is why we pass it here to iterate of the pattern tree
|
|
1600
|
-
componentTree: patternEntry.fields.componentTree,
|
|
1601
|
-
// but we pass the data source of the experience entry cause that's where the binding is stored
|
|
1602
|
-
dataSource,
|
|
1603
|
-
// unbound values of a pattern store the default values of pattern variables
|
|
1604
|
-
unboundValues: patternEntry.fields.unboundValues,
|
|
1605
|
-
// this is where we can map the pattern variable to it's default value
|
|
1606
|
-
componentSettings: patternEntry.fields.componentSettings,
|
|
1607
|
-
// and this is where the over-writes for the default values are stored
|
|
1608
|
-
// yes, I know, it's a bit confusing
|
|
1609
|
-
componentVariablesOverwrites: currentNode.variables,
|
|
1610
|
-
// pass top-level pattern node to store instance-specific child styles for rendering
|
|
1611
|
-
patternWrapper: currentNode,
|
|
1612
|
-
});
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
/** Variables value is stored in `valuesByBreakpoint` object
|
|
1616
|
-
* {
|
|
1617
|
-
cfVerticalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
|
|
1618
|
-
cfHorizontalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
|
|
1619
|
-
cfMargin: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
|
|
1620
|
-
cfPadding: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
|
|
1621
|
-
cfBackgroundColor: {
|
|
1622
|
-
type: 'DesignValue',
|
|
1623
|
-
valuesByBreakpoint: { desktop: 'rgba(246, 246, 246, 1)' }
|
|
1624
|
-
},
|
|
1625
|
-
cfWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'fill' } },
|
|
1626
|
-
cfHeight: {
|
|
1627
|
-
type: 'DesignValue',
|
|
1628
|
-
valuesByBreakpoint: { desktop: 'fit-content' }
|
|
1629
|
-
},
|
|
1630
|
-
cfMaxWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'none' } },
|
|
1631
|
-
cfFlexDirection: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'column' } },
|
|
1632
|
-
cfFlexWrap: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'nowrap' } },
|
|
1633
|
-
cfBorder: {
|
|
1634
|
-
type: 'DesignValue',
|
|
1635
|
-
valuesByBreakpoint: { desktop: '0px solid rgba(0, 0, 0, 0)' }
|
|
1636
|
-
},
|
|
1637
|
-
cfBorderRadius: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px' } },
|
|
1638
|
-
cfGap: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px 0px' } },
|
|
1639
|
-
cfHyperlink: { type: 'UnboundValue', key: 'VNc49Qyepd6IzN7rmKUyS' },
|
|
1640
|
-
cfOpenInNewTab: { type: 'UnboundValue', key: 'ZA5YqB2fmREQ4pTKqY5hX' },
|
|
1641
|
-
cfBackgroundImageUrl: { type: 'UnboundValue', key: 'FeskH0WbYD5_RQVXX-1T8' },
|
|
1642
|
-
cfBackgroundImageOptions: { type: 'DesignValue', valuesByBreakpoint: { desktop: [Object] } }
|
|
1643
|
-
}
|
|
1644
|
-
*/
|
|
1645
|
-
// so first, I convert it into a map to help me make it easier to access the values
|
|
1646
|
-
const propsByBreakpoint = indexByBreakpoint({
|
|
1647
|
-
variables: currentNode.variables,
|
|
1648
|
-
breakpointIds,
|
|
1649
|
-
unboundValues: unboundValues,
|
|
1650
|
-
dataSource: dataSource,
|
|
1651
|
-
componentSettings,
|
|
1652
|
-
componentVariablesOverwrites,
|
|
1653
|
-
getBoundEntityById: (id) => {
|
|
1654
|
-
return experience.entityStore?.entities.find((entity) => entity.sys.id === id);
|
|
1655
|
-
},
|
|
1656
|
-
});
|
|
1657
|
-
/**
|
|
1658
|
-
* propsByBreakpoint {
|
|
1659
|
-
desktop: {
|
|
1660
|
-
cfVerticalAlignment: 'center',
|
|
1661
|
-
cfHorizontalAlignment: 'center',
|
|
1662
|
-
cfMargin: '0 0 0 0',
|
|
1663
|
-
cfPadding: '0 0 0 0',
|
|
1664
|
-
cfBackgroundColor: 'rgba(246, 246, 246, 1)',
|
|
1665
|
-
cfWidth: 'fill',
|
|
1666
|
-
cfHeight: 'fit-content',
|
|
1667
|
-
cfMaxWidth: 'none',
|
|
1668
|
-
cfFlexDirection: 'column',
|
|
1669
|
-
cfFlexWrap: 'nowrap',
|
|
1670
|
-
cfBorder: '0px solid rgba(0, 0, 0, 0)',
|
|
1671
|
-
cfBorderRadius: '0px',
|
|
1672
|
-
cfGap: '0px 0px',
|
|
1673
|
-
cfBackgroundImageOptions: { scaling: 'fill', alignment: 'left top', targetSize: '2000px' }
|
|
1674
|
-
},
|
|
1675
|
-
tablet: {},
|
|
1676
|
-
mobile: {}
|
|
1677
|
-
}
|
|
1678
|
-
*/
|
|
1679
|
-
const currentNodeClassNames = [];
|
|
1680
|
-
// then for each breakpoint
|
|
1681
|
-
for (const breakpointId of breakpointIds) {
|
|
1682
|
-
const propsByBreakpointWithResolvedDesignTokens = Object.entries(propsByBreakpoint[breakpointId]).reduce((acc, [variableName, variableValue]) => {
|
|
1683
|
-
return {
|
|
1684
|
-
...acc,
|
|
1685
|
-
[variableName]: maybePopulateDesignTokenValue(variableName, variableValue, mapOfDesignVariableKeys),
|
|
1686
|
-
};
|
|
1687
|
-
}, {});
|
|
1688
|
-
// We convert cryptic prop keys to css variables
|
|
1689
|
-
// Eg: cfMargin to margin
|
|
1690
|
-
const stylesForBreakpoint = buildCfStyles(propsByBreakpointWithResolvedDesignTokens);
|
|
1691
|
-
const stylesForBreakpointWithoutUndefined = Object.fromEntries(Object.entries(stylesForBreakpoint)
|
|
1692
|
-
.filter(([, value]) => value !== undefined)
|
|
1693
|
-
.map(([key, value]) => [toCSSAttribute(key), value]));
|
|
1694
|
-
/**
|
|
1695
|
-
* stylesForBreakpoint {
|
|
1696
|
-
margin: '0 0 0 0',
|
|
1697
|
-
padding: '0 0 0 0',
|
|
1698
|
-
'background-color': 'rgba(246, 246, 246, 1)',
|
|
1699
|
-
width: '100%',
|
|
1700
|
-
height: 'fit-content',
|
|
1701
|
-
'max-width': 'none',
|
|
1702
|
-
border: '0px solid rgba(0, 0, 0, 0)',
|
|
1703
|
-
'border-radius': '0px',
|
|
1704
|
-
gap: '0px 0px',
|
|
1705
|
-
'align-items': 'center',
|
|
1706
|
-
'justify-content': 'safe center',
|
|
1707
|
-
'flex-direction': 'column',
|
|
1708
|
-
'flex-wrap': 'nowrap',
|
|
1709
|
-
'font-style': 'normal',
|
|
1710
|
-
'text-decoration': 'none',
|
|
1711
|
-
'box-sizing': 'border-box'
|
|
1712
|
-
}
|
|
1713
|
-
*/
|
|
1714
|
-
// I create a hash of the object above because that would ensure hash stability
|
|
1715
|
-
const styleHash = md5(JSON.stringify(stylesForBreakpointWithoutUndefined));
|
|
1716
|
-
// and prefix the className to make sure the value can be processed
|
|
1717
|
-
const className = `cf-${styleHash}`;
|
|
1718
|
-
// I save the generated hashes into an array to later save it in the tree node
|
|
1719
|
-
// as cfSsrClassName prop
|
|
1720
|
-
// making sure to avoid the duplicates in case styles for > 1 breakpoints are the same
|
|
1721
|
-
if (!currentNodeClassNames.includes(className)) {
|
|
1722
|
-
currentNodeClassNames.push(className);
|
|
1723
|
-
}
|
|
1724
|
-
// if there is already the similar hash - no need to over-write it
|
|
1725
|
-
if (mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
|
|
1726
|
-
continue;
|
|
1363
|
+
// For all other properties, we just return the breakpoint-specific value
|
|
1364
|
+
return value;
|
|
1365
|
+
};
|
|
1366
|
+
if (valuesByBreakpoint instanceof Object) {
|
|
1367
|
+
// Assume that the values are sorted by media query to apply the cascading CSS logic
|
|
1368
|
+
for (let index = activeBreakpointIndex; index >= 0; index--) {
|
|
1369
|
+
const breakpointId = breakpoints[index]?.id;
|
|
1370
|
+
if (isValidBreakpointValue(valuesByBreakpoint[breakpointId])) {
|
|
1371
|
+
// If the value is defined, we use it and stop the breakpoints cascade
|
|
1372
|
+
if (resolveDesignTokens) {
|
|
1373
|
+
return eventuallyResolveDesignTokens(valuesByBreakpoint[breakpointId]);
|
|
1727
1374
|
}
|
|
1728
|
-
|
|
1729
|
-
mediaQueriesTemplate[breakpointId].cssByClassName[className] = toCSSString(stylesForBreakpointWithoutUndefined);
|
|
1730
|
-
}
|
|
1731
|
-
// all generated classNames are saved in the tree node
|
|
1732
|
-
// to be handled by the sdk later
|
|
1733
|
-
// each node will get N classNames, where N is the number of breakpoints
|
|
1734
|
-
// browsers process classNames in the order they are defined
|
|
1735
|
-
// meaning that in case of className1 className2 className3
|
|
1736
|
-
// className3 will win over className2 and className1
|
|
1737
|
-
// making sure that we respect the order of breakpoints from
|
|
1738
|
-
// we can achieve "desktop first" or "mobile first" approach to style over-writes
|
|
1739
|
-
if (patternWrapper) {
|
|
1740
|
-
currentNode.id = currentNode.id ?? generateRandomId(15);
|
|
1741
|
-
// @ts-expect-error -- valueByBreakpoint is not explicitly defined, but it's already defined in the patternWrapper styles
|
|
1742
|
-
patternWrapper.variables.cfSsrClassName = {
|
|
1743
|
-
...(patternWrapper.variables.cfSsrClassName ?? {}),
|
|
1744
|
-
type: 'DesignValue',
|
|
1745
|
-
[currentNode.id]: {
|
|
1746
|
-
valuesByBreakpoint: {
|
|
1747
|
-
[breakpointIds[0]]: currentNodeClassNames.join(' '),
|
|
1748
|
-
},
|
|
1749
|
-
},
|
|
1750
|
-
};
|
|
1375
|
+
return valuesByBreakpoint[breakpointId];
|
|
1751
1376
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1377
|
+
}
|
|
1378
|
+
// If no breakpoint matched, we search and apply the fallback breakpoint
|
|
1379
|
+
const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
|
|
1380
|
+
const fallbackBreakpointId = breakpoints[fallbackBreakpointIndex]?.id;
|
|
1381
|
+
if (isValidBreakpointValue(valuesByBreakpoint[fallbackBreakpointId])) {
|
|
1382
|
+
if (resolveDesignTokens) {
|
|
1383
|
+
return eventuallyResolveDesignTokens(valuesByBreakpoint[fallbackBreakpointId]);
|
|
1759
1384
|
}
|
|
1760
|
-
|
|
1385
|
+
return valuesByBreakpoint[fallbackBreakpointId];
|
|
1761
1386
|
}
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
// Old design properties did not support breakpoints, keep for backward compatibility
|
|
1390
|
+
return valuesByBreakpoint;
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1395
|
+
const isLinkToAsset = (variable) => {
|
|
1396
|
+
if (!variable)
|
|
1397
|
+
return false;
|
|
1398
|
+
if (typeof variable !== 'object')
|
|
1399
|
+
return false;
|
|
1400
|
+
return (variable.sys?.linkType === 'Asset' &&
|
|
1401
|
+
typeof variable.sys?.id === 'string' &&
|
|
1402
|
+
!!variable.sys?.id &&
|
|
1403
|
+
variable.sys?.type === 'Link');
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
const isLink = (maybeLink) => {
|
|
1407
|
+
if (maybeLink === null)
|
|
1408
|
+
return false;
|
|
1409
|
+
if (typeof maybeLink !== 'object')
|
|
1410
|
+
return false;
|
|
1411
|
+
const link = maybeLink;
|
|
1412
|
+
return Boolean(link.sys?.id) && link.sys?.type === 'Link';
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* This module encapsulates format of the path to a deep reference.
|
|
1417
|
+
*/
|
|
1418
|
+
const parseDataSourcePathIntoFieldset = (path) => {
|
|
1419
|
+
const parsedPath = parseDeepPath(path);
|
|
1420
|
+
if (null === parsedPath) {
|
|
1421
|
+
throw new Error(`Cannot parse path '${path}' as deep path`);
|
|
1422
|
+
}
|
|
1423
|
+
return parsedPath.fields.map((field) => [null, field, '~locale']);
|
|
1424
|
+
};
|
|
1425
|
+
/**
|
|
1426
|
+
* Parse path into components, supports L1 references (one reference follow) atm.
|
|
1427
|
+
* @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
|
|
1428
|
+
* eg. `/uuid123/fields/file/~locale/fields/title/~locale`
|
|
1429
|
+
* @returns
|
|
1430
|
+
*/
|
|
1431
|
+
const parseDataSourcePathWithL1DeepBindings = (path) => {
|
|
1432
|
+
const parsedPath = parseDeepPath(path);
|
|
1433
|
+
if (null === parsedPath) {
|
|
1434
|
+
throw new Error(`Cannot parse path '${path}' as deep path`);
|
|
1435
|
+
}
|
|
1436
|
+
return {
|
|
1437
|
+
key: parsedPath.key,
|
|
1438
|
+
field: parsedPath.fields[0],
|
|
1439
|
+
referentField: parsedPath.fields[1],
|
|
1440
|
+
};
|
|
1441
|
+
};
|
|
1442
|
+
/**
|
|
1443
|
+
* Detects if paths is valid deep-path, like:
|
|
1444
|
+
* - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
|
|
1445
|
+
* or regular, like:
|
|
1446
|
+
* - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
|
|
1447
|
+
* @returns
|
|
1448
|
+
*/
|
|
1449
|
+
const isDeepPath = (deepPathCandidate) => {
|
|
1450
|
+
const deepPathParsed = parseDeepPath(deepPathCandidate);
|
|
1451
|
+
if (!deepPathParsed) {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
return deepPathParsed.fields.length > 1;
|
|
1455
|
+
};
|
|
1456
|
+
const parseDeepPath = (deepPathCandidate) => {
|
|
1457
|
+
// ALGORITHM:
|
|
1458
|
+
// We start with deep path in form:
|
|
1459
|
+
// /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
|
|
1460
|
+
// First turn string into array of segments
|
|
1461
|
+
// ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
|
|
1462
|
+
// Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
|
|
1463
|
+
// [
|
|
1464
|
+
// [ "", "uuid123" ],
|
|
1465
|
+
// [ "fields", "mainStory", "~locale" ],
|
|
1466
|
+
// [ "fields", "cover", "~locale" ],
|
|
1467
|
+
// [ "fields", "title", "~locale" ]
|
|
1468
|
+
// ]
|
|
1469
|
+
// Then check "initial" chunk for corretness
|
|
1470
|
+
// Then check all "field-leading" chunks for correctness
|
|
1471
|
+
const isValidInitialChunk = (initialChunk) => {
|
|
1472
|
+
// must have start with '' and have at least 2 segments, second non-empty
|
|
1473
|
+
// eg. /-_432uuid123123
|
|
1474
|
+
return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
|
|
1475
|
+
};
|
|
1476
|
+
const isValidFieldChunk = (fieldChunk) => {
|
|
1477
|
+
// must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
|
|
1478
|
+
// eg. fields/-32234mainStory/~locale
|
|
1479
|
+
return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
|
|
1762
1480
|
};
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
|
|
1768
|
-
});
|
|
1769
|
-
// once the whole tree was traversed, for each breakpoint, I aggregate the styles
|
|
1770
|
-
// for each generated className into one css string
|
|
1771
|
-
const styleSheet = Object.entries(mediaQueriesTemplate).reduce((acc, [, breakpointPayload]) => {
|
|
1772
|
-
return `${acc}${toMediaQuery(breakpointPayload)}`;
|
|
1773
|
-
}, '');
|
|
1774
|
-
return styleSheet;
|
|
1775
|
-
};
|
|
1776
|
-
const isCfStyleAttribute = (variableName) => {
|
|
1777
|
-
return CF_STYLE_ATTRIBUTES.includes(variableName);
|
|
1778
|
-
};
|
|
1779
|
-
const maybePopulateDesignTokenValue = (variableName, variableValue, mapOfDesignVariableKeys) => {
|
|
1780
|
-
// TODO: refactor to reuse fn from core package
|
|
1781
|
-
if (typeof variableValue !== 'string') {
|
|
1782
|
-
return variableValue;
|
|
1481
|
+
const deepPathSegments = deepPathCandidate.split('/');
|
|
1482
|
+
const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
|
|
1483
|
+
if (chunks.length <= 1) {
|
|
1484
|
+
return null; // malformed path, even regular paths have at least 2 chunks
|
|
1783
1485
|
}
|
|
1784
|
-
if (
|
|
1785
|
-
return
|
|
1486
|
+
else if (chunks.length === 2) {
|
|
1487
|
+
return null; // deep paths have at least 3 chunks
|
|
1786
1488
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1489
|
+
// With 3+ chunks we can now check for deep path correctness
|
|
1490
|
+
const [initialChunk, ...fieldChunks] = chunks;
|
|
1491
|
+
if (!isValidInitialChunk(initialChunk)) {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
if (!fieldChunks.every(isValidFieldChunk)) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
|
|
1499
|
+
fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
|
|
1500
|
+
};
|
|
1501
|
+
};
|
|
1502
|
+
const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
|
|
1503
|
+
const chunks = [];
|
|
1504
|
+
let currentChunk = [];
|
|
1505
|
+
const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
|
|
1506
|
+
const excludeEmptyChunks = (chunk) => chunk.length > 0;
|
|
1507
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1508
|
+
const isInitialElement = i === 0;
|
|
1509
|
+
const segment = segments[i];
|
|
1510
|
+
if (isInitialElement) {
|
|
1511
|
+
currentChunk = [segment];
|
|
1798
1512
|
}
|
|
1799
|
-
if (
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1513
|
+
else if (isSegmentBeginningOfChunk(segment)) {
|
|
1514
|
+
chunks.push(currentChunk);
|
|
1515
|
+
currentChunk = [segment];
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
currentChunk.push(segment);
|
|
1804
1519
|
}
|
|
1805
|
-
return tokenValue;
|
|
1806
|
-
};
|
|
1807
|
-
const templateStringRegex = /\${(.+?)}/g;
|
|
1808
|
-
const parts = variableValue.split(' ');
|
|
1809
|
-
let resolvedValue = '';
|
|
1810
|
-
for (const part of parts) {
|
|
1811
|
-
const tokenValue = templateStringRegex.test(part)
|
|
1812
|
-
? resolveSimpleDesignToken(variableName, part)
|
|
1813
|
-
: part;
|
|
1814
|
-
resolvedValue += `${tokenValue} `;
|
|
1815
1520
|
}
|
|
1816
|
-
|
|
1817
|
-
return
|
|
1521
|
+
chunks.push(currentChunk);
|
|
1522
|
+
return chunks.filter(excludeEmptyChunks);
|
|
1818
1523
|
};
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1524
|
+
const lastPathNamedSegmentEq = (path, expectedName) => {
|
|
1525
|
+
// `/key123/fields/featureImage/~locale/fields/file/~locale`
|
|
1526
|
+
// ['', 'key123', 'fields', 'featureImage', '~locale', 'fields', 'file', '~locale']
|
|
1527
|
+
const segments = path.split('/');
|
|
1528
|
+
if (segments.length < 2) {
|
|
1529
|
+
console.warn(`[experiences-sdk-react] Attempting to check whether last named segment of the path (${path}) equals to '${expectedName}', but the path doesn't have enough segments.`);
|
|
1530
|
+
return false;
|
|
1823
1531
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1532
|
+
const secondLast = segments[segments.length - 2]; // skipping trailing '~locale'
|
|
1533
|
+
return secondLast === expectedName;
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
const resolveHyperlinkPattern = (pattern, entry, locale) => {
|
|
1537
|
+
if (!entry || !locale)
|
|
1538
|
+
return null;
|
|
1539
|
+
const variables = {
|
|
1540
|
+
entry,
|
|
1541
|
+
locale,
|
|
1542
|
+
};
|
|
1543
|
+
return buildTemplate({ template: pattern, context: variables });
|
|
1544
|
+
};
|
|
1545
|
+
function getValue(obj, path) {
|
|
1546
|
+
return path
|
|
1547
|
+
.replace(/\[/g, '.')
|
|
1548
|
+
.replace(/\]/g, '')
|
|
1549
|
+
.split('.')
|
|
1550
|
+
.reduce((o, k) => (o || {})[k], obj);
|
|
1551
|
+
}
|
|
1552
|
+
function addLocale(str, locale) {
|
|
1553
|
+
const fieldsIndicator = 'fields';
|
|
1554
|
+
const fieldsIndex = str.indexOf(fieldsIndicator);
|
|
1555
|
+
if (fieldsIndex !== -1) {
|
|
1556
|
+
const dotIndex = str.indexOf('.', fieldsIndex + fieldsIndicator.length + 1); // +1 for '.'
|
|
1557
|
+
if (dotIndex !== -1) {
|
|
1558
|
+
return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
|
|
1834
1559
|
}
|
|
1835
|
-
// at this point userSetValue will either be type of 'DesignValue' or 'BoundValue'
|
|
1836
|
-
// so we recursively run resolution again to resolve it
|
|
1837
|
-
const resolvedValue = resolveBackgroundImageBinding({
|
|
1838
|
-
variableData: userSetValue,
|
|
1839
|
-
getBoundEntityById,
|
|
1840
|
-
dataSource,
|
|
1841
|
-
unboundValues,
|
|
1842
|
-
componentVariablesOverwrites,
|
|
1843
|
-
componentSettings,
|
|
1844
|
-
});
|
|
1845
|
-
return resolvedValue || defaultValue;
|
|
1846
1560
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1561
|
+
return str;
|
|
1562
|
+
}
|
|
1563
|
+
function getTemplateValue(ctx, path) {
|
|
1564
|
+
const pathWithLocale = addLocale(path, ctx.locale);
|
|
1565
|
+
const retrievedValue = getValue(ctx, pathWithLocale);
|
|
1566
|
+
return typeof retrievedValue === 'object' && retrievedValue !== null
|
|
1567
|
+
? retrievedValue[ctx.locale]
|
|
1568
|
+
: retrievedValue;
|
|
1569
|
+
}
|
|
1570
|
+
function buildTemplate({ template, context, }) {
|
|
1571
|
+
const localeVariable = /{\s*locale\s*}/g;
|
|
1572
|
+
// e.g. "{ page.sys.id }"
|
|
1573
|
+
const variables = /{\s*([\S]+?)\s*}/g;
|
|
1574
|
+
return (template
|
|
1575
|
+
// first replace the locale pattern
|
|
1576
|
+
.replace(localeVariable, context.locale)
|
|
1577
|
+
// then resolve the remaining variables
|
|
1578
|
+
.replace(variables, (_, path) => {
|
|
1579
|
+
const fallback = path + '_NOT_FOUND';
|
|
1580
|
+
const value = getTemplateValue(context, path) ?? fallback;
|
|
1581
|
+
// using _.result didn't gave proper results so we run our own version of it
|
|
1582
|
+
return String(typeof value === 'function' ? value() : value);
|
|
1583
|
+
}));
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const stylesToKeep = ['cfImageAsset'];
|
|
1587
|
+
const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
|
|
1588
|
+
const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
|
|
1589
|
+
const sanitizeNodeProps = (nodeProps) => {
|
|
1590
|
+
return omit(nodeProps, stylesToRemove, propsToRemove);
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
class ParseError extends Error {
|
|
1594
|
+
constructor(message) {
|
|
1595
|
+
super(message);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
const isValidJsonObject = (s) => {
|
|
1599
|
+
try {
|
|
1600
|
+
const result = JSON.parse(s);
|
|
1601
|
+
if ('object' !== typeof result) {
|
|
1602
|
+
return false;
|
|
1854
1603
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
catch (e) {
|
|
1607
|
+
return false;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const doesMismatchMessageSchema = (event) => {
|
|
1611
|
+
try {
|
|
1612
|
+
tryParseMessage(event);
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
catch (e) {
|
|
1616
|
+
if (e instanceof ParseError) {
|
|
1617
|
+
return e.message;
|
|
1857
1618
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1619
|
+
throw e;
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
const tryParseMessage = (event) => {
|
|
1623
|
+
if (!event.data) {
|
|
1624
|
+
throw new ParseError('Field event.data is missing');
|
|
1625
|
+
}
|
|
1626
|
+
if ('string' !== typeof event.data) {
|
|
1627
|
+
throw new ParseError(`Field event.data must be a string, instead of '${typeof event.data}'`);
|
|
1628
|
+
}
|
|
1629
|
+
if (!isValidJsonObject(event.data)) {
|
|
1630
|
+
throw new ParseError('Field event.data must be a valid JSON object serialized as string');
|
|
1631
|
+
}
|
|
1632
|
+
const eventData = JSON.parse(event.data);
|
|
1633
|
+
if (!eventData.source) {
|
|
1634
|
+
throw new ParseError(`Field eventData.source must be equal to 'composability-app'`);
|
|
1635
|
+
}
|
|
1636
|
+
if ('composability-app' !== eventData.source) {
|
|
1637
|
+
throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
|
|
1638
|
+
}
|
|
1639
|
+
// check eventData.eventType
|
|
1640
|
+
const supportedEventTypes = Object.values(INCOMING_EVENTS);
|
|
1641
|
+
if (!supportedEventTypes.includes(eventData.eventType)) {
|
|
1642
|
+
// Expected message: This message is handled in the EntityStore to store fetched entities
|
|
1643
|
+
if (eventData.eventType !== PostMessageMethods.REQUESTED_ENTITIES) {
|
|
1644
|
+
throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
|
|
1882
1645
|
}
|
|
1883
1646
|
}
|
|
1647
|
+
return eventData;
|
|
1884
1648
|
};
|
|
1885
|
-
const
|
|
1886
|
-
|
|
1649
|
+
const validateExperienceBuilderConfig = ({ locale, mode, }) => {
|
|
1650
|
+
if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (!locale) {
|
|
1654
|
+
throw new Error('Parameter "locale" is required for experience builder initialization outside of editor mode');
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1657
|
+
|
|
1658
|
+
const transformVisibility = (value) => {
|
|
1659
|
+
if (value === false) {
|
|
1887
1660
|
return {
|
|
1888
|
-
|
|
1889
|
-
[breakpointId]: {},
|
|
1661
|
+
display: 'none',
|
|
1890
1662
|
};
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1663
|
+
}
|
|
1664
|
+
// Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
|
|
1665
|
+
return {};
|
|
1666
|
+
};
|
|
1667
|
+
// Keep this for backwards compatibility - deleting this would be a breaking change
|
|
1668
|
+
// because existing components on a users experience will have the width value as fill
|
|
1669
|
+
// rather than 100%
|
|
1670
|
+
const transformFill = (value) => (value === 'fill' ? '100%' : value);
|
|
1671
|
+
const transformGridColumn = (span) => {
|
|
1672
|
+
if (!span) {
|
|
1673
|
+
return {};
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
gridColumn: `span ${span}`,
|
|
1677
|
+
};
|
|
1678
|
+
};
|
|
1679
|
+
const transformBorderStyle = (value) => {
|
|
1680
|
+
if (!value)
|
|
1681
|
+
return {};
|
|
1682
|
+
const parts = value.split(' ');
|
|
1683
|
+
// Just accept the passed value
|
|
1684
|
+
if (parts.length < 3)
|
|
1685
|
+
return { border: value };
|
|
1686
|
+
const [borderSize, borderStyle, ...borderColorParts] = parts;
|
|
1687
|
+
const borderColor = borderColorParts.join(' ');
|
|
1688
|
+
return {
|
|
1689
|
+
border: `${borderSize} ${borderStyle} ${borderColor}`,
|
|
1690
|
+
};
|
|
1691
|
+
};
|
|
1692
|
+
const transformAlignment = (cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection = 'column') => cfFlexDirection === 'row'
|
|
1693
|
+
? {
|
|
1694
|
+
alignItems: cfHorizontalAlignment,
|
|
1695
|
+
justifyContent: cfVerticalAlignment === 'center' ? `safe ${cfVerticalAlignment}` : cfVerticalAlignment,
|
|
1696
|
+
}
|
|
1697
|
+
: {
|
|
1698
|
+
alignItems: cfVerticalAlignment,
|
|
1699
|
+
justifyContent: cfHorizontalAlignment === 'center'
|
|
1700
|
+
? `safe ${cfHorizontalAlignment}`
|
|
1701
|
+
: cfHorizontalAlignment,
|
|
1702
|
+
};
|
|
1703
|
+
const transformBackgroundImage = (cfBackgroundImageUrl, cfBackgroundImageOptions) => {
|
|
1704
|
+
const matchBackgroundSize = (scaling) => {
|
|
1705
|
+
if ('fill' === scaling)
|
|
1706
|
+
return 'cover';
|
|
1707
|
+
if ('fit' === scaling)
|
|
1708
|
+
return 'contain';
|
|
1709
|
+
};
|
|
1710
|
+
const matchBackgroundPosition = (alignment) => {
|
|
1711
|
+
if (!alignment || 'string' !== typeof alignment) {
|
|
1712
|
+
return;
|
|
1923
1713
|
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1714
|
+
let [horizontalAlignment, verticalAlignment] = alignment.trim().split(/\s+/, 2);
|
|
1715
|
+
// Special case for handling single values
|
|
1716
|
+
// for backwards compatibility with single values 'right','left', 'center', 'top','bottom'
|
|
1717
|
+
if (horizontalAlignment && !verticalAlignment) {
|
|
1718
|
+
const singleValue = horizontalAlignment;
|
|
1719
|
+
switch (singleValue) {
|
|
1720
|
+
case 'left':
|
|
1721
|
+
horizontalAlignment = 'left';
|
|
1722
|
+
verticalAlignment = 'center';
|
|
1723
|
+
break;
|
|
1724
|
+
case 'right':
|
|
1725
|
+
horizontalAlignment = 'right';
|
|
1726
|
+
verticalAlignment = 'center';
|
|
1727
|
+
break;
|
|
1728
|
+
case 'center':
|
|
1729
|
+
horizontalAlignment = 'center';
|
|
1730
|
+
verticalAlignment = 'center';
|
|
1731
|
+
break;
|
|
1732
|
+
case 'top':
|
|
1733
|
+
horizontalAlignment = 'center';
|
|
1734
|
+
verticalAlignment = 'top';
|
|
1735
|
+
break;
|
|
1736
|
+
case 'bottom':
|
|
1737
|
+
horizontalAlignment = 'center';
|
|
1738
|
+
verticalAlignment = 'bottom';
|
|
1739
|
+
break;
|
|
1740
|
+
// just fall down to the normal validation logic for horiz and vert
|
|
1927
1741
|
}
|
|
1928
|
-
variableValuesByBreakpoints[breakpointId] = {
|
|
1929
|
-
...variableValuesByBreakpoints[breakpointId],
|
|
1930
|
-
[variableName]: variableValue,
|
|
1931
|
-
};
|
|
1932
1742
|
}
|
|
1743
|
+
const isHorizontalValid = ['left', 'right', 'center'].includes(horizontalAlignment);
|
|
1744
|
+
const isVerticalValid = ['top', 'bottom', 'center'].includes(verticalAlignment);
|
|
1745
|
+
horizontalAlignment = isHorizontalValid ? horizontalAlignment : 'left';
|
|
1746
|
+
verticalAlignment = isVerticalValid ? verticalAlignment : 'top';
|
|
1747
|
+
return `${horizontalAlignment} ${verticalAlignment}`;
|
|
1748
|
+
};
|
|
1749
|
+
if (!cfBackgroundImageUrl) {
|
|
1750
|
+
return;
|
|
1933
1751
|
}
|
|
1934
|
-
|
|
1752
|
+
let backgroundImage;
|
|
1753
|
+
let backgroundImageSet;
|
|
1754
|
+
if (typeof cfBackgroundImageUrl === 'string') {
|
|
1755
|
+
backgroundImage = `url(${cfBackgroundImageUrl})`;
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
const imgSet = cfBackgroundImageUrl.srcSet?.join(',');
|
|
1759
|
+
backgroundImage = `url(${cfBackgroundImageUrl.url})`;
|
|
1760
|
+
backgroundImageSet = `image-set(${imgSet})`;
|
|
1761
|
+
}
|
|
1762
|
+
return {
|
|
1763
|
+
backgroundImage,
|
|
1764
|
+
backgroundImage2: backgroundImageSet,
|
|
1765
|
+
backgroundRepeat: cfBackgroundImageOptions?.scaling === 'tile' ? 'repeat' : 'no-repeat',
|
|
1766
|
+
backgroundPosition: matchBackgroundPosition(cfBackgroundImageOptions?.alignment),
|
|
1767
|
+
backgroundSize: matchBackgroundSize(cfBackgroundImageOptions?.scaling),
|
|
1768
|
+
};
|
|
1935
1769
|
};
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
* }
|
|
1943
|
-
*
|
|
1944
|
-
* to
|
|
1945
|
-
*
|
|
1946
|
-
* {
|
|
1947
|
-
* 'color.key': [value]
|
|
1948
|
-
* }
|
|
1949
|
-
*/
|
|
1950
|
-
const flattenDesignTokenRegistry = (designTokenRegistry) => {
|
|
1951
|
-
return Object.entries(designTokenRegistry).reduce((acc, [categoryName, tokenCategory]) => {
|
|
1952
|
-
const tokensWithCategory = Object.entries(tokenCategory).reduce((acc, [tokenName, tokenValue]) => {
|
|
1953
|
-
return {
|
|
1954
|
-
...acc,
|
|
1955
|
-
[`${categoryName}.${tokenName}`]: tokenValue,
|
|
1956
|
-
};
|
|
1957
|
-
}, {});
|
|
1958
|
-
return {
|
|
1959
|
-
...acc,
|
|
1960
|
-
...tokensWithCategory,
|
|
1961
|
-
};
|
|
1962
|
-
}, {});
|
|
1770
|
+
|
|
1771
|
+
const toCSSAttribute = (key) => {
|
|
1772
|
+
let val = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
|
1773
|
+
// Remove the number from the end of the key to allow for overrides on style properties
|
|
1774
|
+
val = val.replace(/\d+$/, '');
|
|
1775
|
+
return val;
|
|
1963
1776
|
};
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1777
|
+
const buildStyleTag = ({ styles, nodeId }) => {
|
|
1778
|
+
const stylesStr = Object.entries(styles)
|
|
1779
|
+
.filter(([, value]) => value !== undefined)
|
|
1780
|
+
.reduce((acc, [key, value]) => `${acc}
|
|
1781
|
+
${toCSSAttribute(key)}: ${value};`, '');
|
|
1782
|
+
const className = `cfstyles-${nodeId ? nodeId : md5(stylesStr)}`;
|
|
1783
|
+
const styleRule = `.${className}{ ${stylesStr} }`;
|
|
1784
|
+
return [className, styleRule];
|
|
1970
1785
|
};
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1786
|
+
const buildCfStyles = ({ cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection, cfFlexReverse, cfFlexWrap, cfMargin, cfPadding, cfBackgroundColor, cfWidth, cfHeight, cfMaxWidth, cfBorder, cfBorderRadius, cfGap, cfBackgroundImageUrl, cfBackgroundImageOptions, cfFontSize, cfFontWeight, cfImageOptions, cfLineHeight, cfLetterSpacing, cfTextColor, cfTextAlign, cfTextTransform, cfTextBold, cfTextItalic, cfTextUnderline, cfColumnSpan, cfVisibility, }) => {
|
|
1787
|
+
return {
|
|
1788
|
+
boxSizing: 'border-box',
|
|
1789
|
+
...transformVisibility(cfVisibility),
|
|
1790
|
+
margin: cfMargin,
|
|
1791
|
+
padding: cfPadding,
|
|
1792
|
+
backgroundColor: cfBackgroundColor,
|
|
1793
|
+
width: transformFill(cfWidth || cfImageOptions?.width),
|
|
1794
|
+
height: transformFill(cfHeight || cfImageOptions?.height),
|
|
1795
|
+
maxWidth: cfMaxWidth,
|
|
1796
|
+
...transformGridColumn(cfColumnSpan),
|
|
1797
|
+
...transformBorderStyle(cfBorder),
|
|
1798
|
+
borderRadius: cfBorderRadius,
|
|
1799
|
+
gap: cfGap,
|
|
1800
|
+
...transformAlignment(cfHorizontalAlignment, cfVerticalAlignment, cfFlexDirection),
|
|
1801
|
+
flexDirection: cfFlexReverse && cfFlexDirection ? `${cfFlexDirection}-reverse` : cfFlexDirection,
|
|
1802
|
+
flexWrap: cfFlexWrap,
|
|
1803
|
+
...transformBackgroundImage(cfBackgroundImageUrl, cfBackgroundImageOptions),
|
|
1804
|
+
fontSize: cfFontSize,
|
|
1805
|
+
fontWeight: cfTextBold ? 'bold' : cfFontWeight,
|
|
1806
|
+
fontStyle: cfTextItalic ? 'italic' : undefined,
|
|
1807
|
+
textDecoration: cfTextUnderline ? 'underline' : undefined,
|
|
1808
|
+
lineHeight: cfLineHeight,
|
|
1809
|
+
letterSpacing: cfLetterSpacing,
|
|
1810
|
+
color: cfTextColor,
|
|
1811
|
+
textAlign: cfTextAlign,
|
|
1812
|
+
textTransform: cfTextTransform,
|
|
1813
|
+
objectFit: cfImageOptions?.objectFit,
|
|
1814
|
+
objectPosition: cfImageOptions?.objectPosition,
|
|
1815
|
+
};
|
|
1816
|
+
};
|
|
1817
|
+
/**
|
|
1818
|
+
* Container/section default behavior:
|
|
1819
|
+
* Default height => height: EMPTY_CONTAINER_HEIGHT
|
|
1820
|
+
* If a container component has children => height: 'fit-content'
|
|
1821
|
+
*/
|
|
1822
|
+
const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
|
|
1823
|
+
if (!blockId || !isContentfulStructureComponent(blockId) || value !== 'auto') {
|
|
1824
|
+
return value;
|
|
1977
1825
|
}
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
const mediaQueryRule = evaluation === '<' ? 'max-width' : 'min-width';
|
|
1983
|
-
return `@media(${mediaQueryRule}:${pixelValue}){${mediaQueryStyles}}`;
|
|
1826
|
+
if (children.length) {
|
|
1827
|
+
return '100%';
|
|
1828
|
+
}
|
|
1829
|
+
return EMPTY_CONTAINER_HEIGHT;
|
|
1984
1830
|
};
|
|
1985
1831
|
|
|
1986
1832
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -2142,6 +1988,114 @@ const getOptimizedImageAsset = ({ file, sizes, loading, quality = '100%', format
|
|
|
2142
1988
|
return optimizedImageAsset;
|
|
2143
1989
|
};
|
|
2144
1990
|
|
|
1991
|
+
const getDataFromTree = (tree) => {
|
|
1992
|
+
let dataSource = {};
|
|
1993
|
+
let unboundValues = {};
|
|
1994
|
+
const queue = [...tree.root.children];
|
|
1995
|
+
while (queue.length) {
|
|
1996
|
+
const node = queue.shift();
|
|
1997
|
+
if (!node) {
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
dataSource = { ...dataSource, ...node.data.dataSource };
|
|
2001
|
+
unboundValues = { ...unboundValues, ...node.data.unboundValues };
|
|
2002
|
+
if (node.children.length) {
|
|
2003
|
+
queue.push(...node.children);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return {
|
|
2007
|
+
dataSource,
|
|
2008
|
+
unboundValues,
|
|
2009
|
+
};
|
|
2010
|
+
};
|
|
2011
|
+
/**
|
|
2012
|
+
* Gets calculates the index to drop the dragged component based on the mouse position
|
|
2013
|
+
* @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
|
|
2014
|
+
*/
|
|
2015
|
+
const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
|
|
2016
|
+
const APPEND_INSIDE = dropReceiverNode.children.length;
|
|
2017
|
+
const PREPEND_INSIDE = 0;
|
|
2018
|
+
if (isMouseAtTopBorder || isMouseAtBottomBorder) {
|
|
2019
|
+
const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
|
|
2020
|
+
const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
|
|
2021
|
+
const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
|
|
2022
|
+
return {
|
|
2023
|
+
// when the mouse is around the border we want to drop the new component as a new section onto the root node
|
|
2024
|
+
node: dropReceiverParentNode,
|
|
2025
|
+
index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
// if over one of the section indicators
|
|
2029
|
+
if (isOverTopIndicator || isOverBottomIndicator) {
|
|
2030
|
+
const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
|
|
2031
|
+
const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
|
|
2032
|
+
const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
|
|
2033
|
+
return {
|
|
2034
|
+
// when the mouse is around the border we want to drop the new component as a new section onto the root node
|
|
2035
|
+
node: dropReceiverParentNode,
|
|
2036
|
+
index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
if (flexDirection === undefined || flexDirection === 'row') {
|
|
2040
|
+
return {
|
|
2041
|
+
node: dropReceiverNode,
|
|
2042
|
+
index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
else {
|
|
2046
|
+
return {
|
|
2047
|
+
node: dropReceiverNode,
|
|
2048
|
+
index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
const generateRandomId = (letterCount) => {
|
|
2053
|
+
const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
|
|
2054
|
+
const NUMS = '0123456789';
|
|
2055
|
+
const ALNUM = NUMS + LETTERS;
|
|
2056
|
+
const times = (n, callback) => Array.from({ length: n }, callback);
|
|
2057
|
+
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2058
|
+
return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
|
|
2059
|
+
};
|
|
2060
|
+
const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
|
|
2061
|
+
if (!usedComponents?.length)
|
|
2062
|
+
return false;
|
|
2063
|
+
return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
|
|
2064
|
+
};
|
|
2065
|
+
/** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
|
|
2066
|
+
const checkIsAssembly = checkIsAssemblyNode;
|
|
2067
|
+
/**
|
|
2068
|
+
* This check assumes that the entry is already ensured to be an experience, i.e. the
|
|
2069
|
+
* content type of the entry is an experience type with the necessary annotations.
|
|
2070
|
+
**/
|
|
2071
|
+
const checkIsAssemblyEntry = (entry) => {
|
|
2072
|
+
return Boolean(entry.fields?.componentSettings);
|
|
2073
|
+
};
|
|
2074
|
+
const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
|
|
2075
|
+
function parseCSSValue(input) {
|
|
2076
|
+
const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
|
|
2077
|
+
const match = input.match(regex);
|
|
2078
|
+
if (match) {
|
|
2079
|
+
return {
|
|
2080
|
+
value: parseFloat(match[1]),
|
|
2081
|
+
unit: match[3],
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
return null;
|
|
2085
|
+
}
|
|
2086
|
+
function getTargetValueInPixels(targetWidthObject) {
|
|
2087
|
+
switch (targetWidthObject.unit) {
|
|
2088
|
+
case 'px':
|
|
2089
|
+
return targetWidthObject.value;
|
|
2090
|
+
case 'em':
|
|
2091
|
+
return targetWidthObject.value * 16;
|
|
2092
|
+
case 'rem':
|
|
2093
|
+
return targetWidthObject.value * 16;
|
|
2094
|
+
default:
|
|
2095
|
+
return targetWidthObject.value;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2145
2099
|
const transformMedia = (asset, variables, resolveDesignValue, variableName, path) => {
|
|
2146
2100
|
let value;
|
|
2147
2101
|
// If it is not a deep path and not pointing to the file of the asset,
|
|
@@ -2273,462 +2227,508 @@ function getArrayValue(entryOrAsset, path, entityStore) {
|
|
|
2273
2227
|
});
|
|
2274
2228
|
return resolvedEntity;
|
|
2275
2229
|
}
|
|
2276
|
-
else {
|
|
2277
|
-
console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
|
|
2278
|
-
return undefined;
|
|
2279
|
-
}
|
|
2280
|
-
});
|
|
2281
|
-
return result;
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableDefinition, path) => {
|
|
2285
|
-
const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
|
|
2286
|
-
if (!entityOrAsset)
|
|
2287
|
-
return;
|
|
2288
|
-
switch (variableDefinition.type) {
|
|
2289
|
-
case 'Media':
|
|
2290
|
-
// If we bound a normal entry field to the media variable we just return the bound value
|
|
2291
|
-
if (entityOrAsset.sys.type === 'Entry') {
|
|
2292
|
-
return getBoundValue(entityOrAsset, path);
|
|
2293
|
-
}
|
|
2294
|
-
return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
|
|
2295
|
-
case 'RichText':
|
|
2296
|
-
return transformRichText(entityOrAsset, entityStore, path);
|
|
2297
|
-
case 'Array':
|
|
2298
|
-
return getArrayValue(entityOrAsset, path, entityStore);
|
|
2299
|
-
case 'Link':
|
|
2300
|
-
return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
|
|
2301
|
-
default:
|
|
2302
|
-
return getBoundValue(entityOrAsset, path);
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2305
|
-
|
|
2306
|
-
const getDataFromTree = (tree) => {
|
|
2307
|
-
let dataSource = {};
|
|
2308
|
-
let unboundValues = {};
|
|
2309
|
-
const queue = [...tree.root.children];
|
|
2310
|
-
while (queue.length) {
|
|
2311
|
-
const node = queue.shift();
|
|
2312
|
-
if (!node) {
|
|
2313
|
-
continue;
|
|
2314
|
-
}
|
|
2315
|
-
dataSource = { ...dataSource, ...node.data.dataSource };
|
|
2316
|
-
unboundValues = { ...unboundValues, ...node.data.unboundValues };
|
|
2317
|
-
if (node.children.length) {
|
|
2318
|
-
queue.push(...node.children);
|
|
2319
|
-
}
|
|
2320
|
-
}
|
|
2321
|
-
return {
|
|
2322
|
-
dataSource,
|
|
2323
|
-
unboundValues,
|
|
2324
|
-
};
|
|
2325
|
-
};
|
|
2326
|
-
/**
|
|
2327
|
-
* Gets calculates the index to drop the dragged component based on the mouse position
|
|
2328
|
-
* @returns {InsertionData} a object containing a node that will become a parent for dragged component and index at which it must be inserted
|
|
2329
|
-
*/
|
|
2330
|
-
const getInsertionData = ({ dropReceiverParentNode, dropReceiverNode, flexDirection, isMouseAtTopBorder, isMouseAtBottomBorder, isMouseInLeftHalf, isMouseInUpperHalf, isOverTopIndicator, isOverBottomIndicator, }) => {
|
|
2331
|
-
const APPEND_INSIDE = dropReceiverNode.children.length;
|
|
2332
|
-
const PREPEND_INSIDE = 0;
|
|
2333
|
-
if (isMouseAtTopBorder || isMouseAtBottomBorder) {
|
|
2334
|
-
const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
|
|
2335
|
-
const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
|
|
2336
|
-
const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
|
|
2337
|
-
return {
|
|
2338
|
-
// when the mouse is around the border we want to drop the new component as a new section onto the root node
|
|
2339
|
-
node: dropReceiverParentNode,
|
|
2340
|
-
index: isMouseAtBottomBorder ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
|
|
2341
|
-
};
|
|
2342
|
-
}
|
|
2343
|
-
// if over one of the section indicators
|
|
2344
|
-
if (isOverTopIndicator || isOverBottomIndicator) {
|
|
2345
|
-
const indexOfSectionInParentChildren = dropReceiverParentNode.children.findIndex((n) => n.data.id === dropReceiverNode.data.id);
|
|
2346
|
-
const APPEND_OUTSIDE = indexOfSectionInParentChildren + 1;
|
|
2347
|
-
const PREPEND_OUTSIDE = indexOfSectionInParentChildren;
|
|
2348
|
-
return {
|
|
2349
|
-
// when the mouse is around the border we want to drop the new component as a new section onto the root node
|
|
2350
|
-
node: dropReceiverParentNode,
|
|
2351
|
-
index: isOverBottomIndicator ? APPEND_OUTSIDE : PREPEND_OUTSIDE,
|
|
2352
|
-
};
|
|
2353
|
-
}
|
|
2354
|
-
if (flexDirection === undefined || flexDirection === 'row') {
|
|
2355
|
-
return {
|
|
2356
|
-
node: dropReceiverNode,
|
|
2357
|
-
index: isMouseInLeftHalf ? PREPEND_INSIDE : APPEND_INSIDE,
|
|
2358
|
-
};
|
|
2359
|
-
}
|
|
2360
|
-
else {
|
|
2361
|
-
return {
|
|
2362
|
-
node: dropReceiverNode,
|
|
2363
|
-
index: isMouseInUpperHalf ? PREPEND_INSIDE : APPEND_INSIDE,
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
};
|
|
2367
|
-
const generateRandomId = (letterCount) => {
|
|
2368
|
-
const LETTERS = 'abcdefghijklmnopqvwxyzABCDEFGHIJKLMNOPQVWXYZ';
|
|
2369
|
-
const NUMS = '0123456789';
|
|
2370
|
-
const ALNUM = NUMS + LETTERS;
|
|
2371
|
-
const times = (n, callback) => Array.from({ length: n }, callback);
|
|
2372
|
-
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
|
|
2373
|
-
return times(letterCount, () => ALNUM[random(0, ALNUM.length - 1)]).join('');
|
|
2374
|
-
};
|
|
2375
|
-
const checkIsAssemblyNode = ({ componentId, usedComponents, }) => {
|
|
2376
|
-
if (!usedComponents?.length)
|
|
2377
|
-
return false;
|
|
2378
|
-
return usedComponents.some((usedComponent) => usedComponent.sys.id === componentId);
|
|
2379
|
-
};
|
|
2380
|
-
/** @deprecated use `checkIsAssemblyNode` instead. Will be removed with SDK v5. */
|
|
2381
|
-
const checkIsAssembly = checkIsAssemblyNode;
|
|
2382
|
-
/**
|
|
2383
|
-
* This check assumes that the entry is already ensured to be an experience, i.e. the
|
|
2384
|
-
* content type of the entry is an experience type with the necessary annotations.
|
|
2385
|
-
**/
|
|
2386
|
-
const checkIsAssemblyEntry = (entry) => {
|
|
2387
|
-
return Boolean(entry.fields?.componentSettings);
|
|
2388
|
-
};
|
|
2389
|
-
const checkIsAssemblyDefinition = (component) => component?.category === ASSEMBLY_DEFAULT_CATEGORY;
|
|
2390
|
-
function parseCSSValue(input) {
|
|
2391
|
-
const regex = /^(\d+(\.\d+)?)(px|em|rem)$/;
|
|
2392
|
-
const match = input.match(regex);
|
|
2393
|
-
if (match) {
|
|
2394
|
-
return {
|
|
2395
|
-
value: parseFloat(match[1]),
|
|
2396
|
-
unit: match[3],
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
return null;
|
|
2400
|
-
}
|
|
2401
|
-
function getTargetValueInPixels(targetWidthObject) {
|
|
2402
|
-
switch (targetWidthObject.unit) {
|
|
2403
|
-
case 'px':
|
|
2404
|
-
return targetWidthObject.value;
|
|
2405
|
-
case 'em':
|
|
2406
|
-
return targetWidthObject.value * 16;
|
|
2407
|
-
case 'rem':
|
|
2408
|
-
return targetWidthObject.value * 16;
|
|
2409
|
-
default:
|
|
2410
|
-
return targetWidthObject.value;
|
|
2411
|
-
}
|
|
2230
|
+
else {
|
|
2231
|
+
console.warn(`Expected value to be a string or Link, but got: ${JSON.stringify(value)}`);
|
|
2232
|
+
return undefined;
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
return result;
|
|
2412
2236
|
}
|
|
2413
2237
|
|
|
2414
|
-
const
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2238
|
+
const transformBoundContentValue = (variables, entityStore, binding, resolveDesignValue, variableName, variableDefinition, path) => {
|
|
2239
|
+
const entityOrAsset = entityStore.getEntryOrAsset(binding, path);
|
|
2240
|
+
if (!entityOrAsset)
|
|
2241
|
+
return;
|
|
2242
|
+
switch (variableDefinition.type) {
|
|
2243
|
+
case 'Media':
|
|
2244
|
+
// If we bound a normal entry field to the media variable we just return the bound value
|
|
2245
|
+
if (entityOrAsset.sys.type === 'Entry') {
|
|
2246
|
+
return getBoundValue(entityOrAsset, path);
|
|
2247
|
+
}
|
|
2248
|
+
return transformMedia(entityOrAsset, variables, resolveDesignValue, variableName, path);
|
|
2249
|
+
case 'RichText':
|
|
2250
|
+
return transformRichText(entityOrAsset, entityStore, path);
|
|
2251
|
+
case 'Array':
|
|
2252
|
+
return getArrayValue(entityOrAsset, path, entityStore);
|
|
2253
|
+
case 'Link':
|
|
2254
|
+
return getResolvedEntryFromLink(entityOrAsset, path, entityStore);
|
|
2255
|
+
default:
|
|
2256
|
+
return getBoundValue(entityOrAsset, path);
|
|
2257
|
+
}
|
|
2422
2258
|
};
|
|
2423
2259
|
|
|
2424
|
-
const
|
|
2425
|
-
const
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
if (!match)
|
|
2430
|
-
return undefined;
|
|
2431
|
-
const [, operator, value, unit] = match;
|
|
2432
|
-
if (operator === '<') {
|
|
2433
|
-
const maxScreenWidth = Number(value) - 1;
|
|
2434
|
-
return `(max-width: ${maxScreenWidth}${unit})`;
|
|
2435
|
-
}
|
|
2436
|
-
else if (operator === '>') {
|
|
2437
|
-
const minScreenWidth = Number(value) + 1;
|
|
2438
|
-
return `(min-width: ${minScreenWidth}${unit})`;
|
|
2260
|
+
const detachExperienceStyles = (experience) => {
|
|
2261
|
+
const experienceTreeRoot = experience.entityStore?.experienceEntryFields
|
|
2262
|
+
?.componentTree;
|
|
2263
|
+
if (!experienceTreeRoot) {
|
|
2264
|
+
return;
|
|
2439
2265
|
}
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
return [mediaQueryMatchers, mediaQueryMatches];
|
|
2462
|
-
};
|
|
2463
|
-
const getActiveBreakpointIndex = (breakpoints, mediaQueryMatches, fallbackBreakpointIndex) => {
|
|
2464
|
-
// The breakpoints are ordered (desktop-first: descending by screen width)
|
|
2465
|
-
const breakpointsWithMatches = breakpoints.map(({ id }, index) => ({
|
|
2466
|
-
id,
|
|
2467
|
-
index,
|
|
2468
|
-
// The fallback breakpoint with wildcard query will always match
|
|
2469
|
-
isMatch: mediaQueryMatches[id] ?? index === fallbackBreakpointIndex,
|
|
2470
|
-
}));
|
|
2471
|
-
// Find the last breakpoint in the list that matches (desktop-first: the narrowest one)
|
|
2472
|
-
const mostSpecificIndex = findLast(breakpointsWithMatches, ({ isMatch }) => isMatch)?.index;
|
|
2473
|
-
return mostSpecificIndex ?? fallbackBreakpointIndex;
|
|
2474
|
-
};
|
|
2475
|
-
const getFallbackBreakpointIndex = (breakpoints) => {
|
|
2476
|
-
// We assume that there will be a single breakpoint which uses the wildcard query.
|
|
2477
|
-
// If there is none, we just take the first one in the list.
|
|
2478
|
-
return Math.max(breakpoints.findIndex(({ query }) => query === '*'), 0);
|
|
2479
|
-
};
|
|
2480
|
-
const builtInStylesWithDesignTokens = [
|
|
2481
|
-
'cfMargin',
|
|
2482
|
-
'cfPadding',
|
|
2483
|
-
'cfGap',
|
|
2484
|
-
'cfWidth',
|
|
2485
|
-
'cfHeight',
|
|
2486
|
-
'cfBackgroundColor',
|
|
2487
|
-
'cfBorder',
|
|
2488
|
-
'cfBorderRadius',
|
|
2489
|
-
'cfFontSize',
|
|
2490
|
-
'cfLineHeight',
|
|
2491
|
-
'cfLetterSpacing',
|
|
2492
|
-
'cfTextColor',
|
|
2493
|
-
'cfMaxWidth',
|
|
2494
|
-
];
|
|
2495
|
-
const isValidBreakpointValue = (value) => {
|
|
2496
|
-
return value !== undefined && value !== null && value !== '';
|
|
2497
|
-
};
|
|
2498
|
-
const getValueForBreakpoint = (valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName, resolveDesignTokens = true) => {
|
|
2499
|
-
const eventuallyResolveDesignTokens = (value) => {
|
|
2500
|
-
// For some built-in design properties, we support design tokens
|
|
2501
|
-
if (builtInStylesWithDesignTokens.includes(variableName)) {
|
|
2502
|
-
return getDesignTokenRegistration(value, variableName);
|
|
2266
|
+
const mapOfDesignVariableKeys = flattenDesignTokenRegistry(designTokensRegistry);
|
|
2267
|
+
// getting breakpoints from the entry componentTree field
|
|
2268
|
+
/**
|
|
2269
|
+
* breakpoints [
|
|
2270
|
+
{
|
|
2271
|
+
id: 'desktop',
|
|
2272
|
+
query: '*',
|
|
2273
|
+
displayName: 'All Sizes',
|
|
2274
|
+
previewSize: '100%'
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
id: 'tablet',
|
|
2278
|
+
query: '<992px',
|
|
2279
|
+
displayName: 'Tablet',
|
|
2280
|
+
previewSize: '820px'
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
id: 'mobile',
|
|
2284
|
+
query: '<576px',
|
|
2285
|
+
displayName: 'Mobile',
|
|
2286
|
+
previewSize: '390px'
|
|
2503
2287
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
};
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2288
|
+
]
|
|
2289
|
+
*/
|
|
2290
|
+
const { breakpoints } = experienceTreeRoot;
|
|
2291
|
+
// creating the structure which I thought would work best for aggregation
|
|
2292
|
+
const mediaQueriesTemplate = breakpoints.reduce((mediaQueryTemplate, breakpoint) => {
|
|
2293
|
+
return {
|
|
2294
|
+
...mediaQueryTemplate,
|
|
2295
|
+
[breakpoint.id]: {
|
|
2296
|
+
condition: breakpoint.query,
|
|
2297
|
+
cssByClassName: {},
|
|
2298
|
+
},
|
|
2299
|
+
};
|
|
2300
|
+
}, {});
|
|
2301
|
+
// getting the breakpoint ids
|
|
2302
|
+
const breakpointIds = Object.keys(mediaQueriesTemplate);
|
|
2303
|
+
const iterateOverTreeAndExtractStyles = ({ componentTree, dataSource, unboundValues, componentSettings, componentVariablesOverwrites, patternWrapper, }) => {
|
|
2304
|
+
// traversing the tree
|
|
2305
|
+
const queue = [];
|
|
2306
|
+
queue.push(...componentTree.children);
|
|
2307
|
+
let currentNode = undefined;
|
|
2308
|
+
// for each tree node
|
|
2309
|
+
while (queue.length) {
|
|
2310
|
+
currentNode = queue.shift();
|
|
2311
|
+
if (!currentNode) {
|
|
2312
|
+
break;
|
|
2313
|
+
}
|
|
2314
|
+
const usedComponents = experience.entityStore?.usedComponents ?? [];
|
|
2315
|
+
const isPatternNode = checkIsAssemblyNode({
|
|
2316
|
+
componentId: currentNode.definitionId,
|
|
2317
|
+
usedComponents,
|
|
2318
|
+
});
|
|
2319
|
+
if (isPatternNode) {
|
|
2320
|
+
const patternEntry = usedComponents.find((component) => component.sys.id === currentNode.definitionId);
|
|
2321
|
+
if (!patternEntry || !('fields' in patternEntry)) {
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
const defaultPatternDivStyles = Object.fromEntries(Object.entries(buildCfStyles({}))
|
|
2325
|
+
.filter(([, value]) => value !== undefined)
|
|
2326
|
+
.map(([key, value]) => [toCSSAttribute(key), value]));
|
|
2327
|
+
// I create a hash of the object above because that would ensure hash stability
|
|
2328
|
+
const styleHash = md5(JSON.stringify(defaultPatternDivStyles));
|
|
2329
|
+
// and prefix the className to make sure the value can be processed
|
|
2330
|
+
const className = `cf-${styleHash}`;
|
|
2331
|
+
for (const breakpointId of breakpointIds) {
|
|
2332
|
+
if (!mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
|
|
2333
|
+
mediaQueriesTemplate[breakpointId].cssByClassName[className] =
|
|
2334
|
+
toCSSString(defaultPatternDivStyles);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
currentNode.variables.cfSsrClassName = {
|
|
2338
|
+
type: 'DesignValue',
|
|
2339
|
+
valuesByBreakpoint: {
|
|
2340
|
+
[breakpointIds[0]]: className,
|
|
2341
|
+
},
|
|
2342
|
+
};
|
|
2343
|
+
// the node of a used pattern contains only the definitionId (id of the patter entry)
|
|
2344
|
+
// as well as the variables overwrites
|
|
2345
|
+
// the layout of a pattern is stored in it's entry
|
|
2346
|
+
iterateOverTreeAndExtractStyles({
|
|
2347
|
+
// that is why we pass it here to iterate of the pattern tree
|
|
2348
|
+
componentTree: patternEntry.fields.componentTree,
|
|
2349
|
+
// but we pass the data source of the experience entry cause that's where the binding is stored
|
|
2350
|
+
dataSource,
|
|
2351
|
+
// unbound values of a pattern store the default values of pattern variables
|
|
2352
|
+
unboundValues: patternEntry.fields.unboundValues,
|
|
2353
|
+
// this is where we can map the pattern variable to it's default value
|
|
2354
|
+
componentSettings: patternEntry.fields.componentSettings,
|
|
2355
|
+
// and this is where the over-writes for the default values are stored
|
|
2356
|
+
// yes, I know, it's a bit confusing
|
|
2357
|
+
componentVariablesOverwrites: currentNode.variables,
|
|
2358
|
+
// pass top-level pattern node to store instance-specific child styles for rendering
|
|
2359
|
+
patternWrapper: currentNode,
|
|
2360
|
+
});
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
/** Variables value is stored in `valuesByBreakpoint` object
|
|
2364
|
+
* {
|
|
2365
|
+
cfVerticalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
|
|
2366
|
+
cfHorizontalAlignment: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'center' } },
|
|
2367
|
+
cfMargin: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
|
|
2368
|
+
cfPadding: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0 0 0 0' } },
|
|
2369
|
+
cfBackgroundColor: {
|
|
2370
|
+
type: 'DesignValue',
|
|
2371
|
+
valuesByBreakpoint: { desktop: 'rgba(246, 246, 246, 1)' }
|
|
2372
|
+
},
|
|
2373
|
+
cfWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'fill' } },
|
|
2374
|
+
cfHeight: {
|
|
2375
|
+
type: 'DesignValue',
|
|
2376
|
+
valuesByBreakpoint: { desktop: 'fit-content' }
|
|
2377
|
+
},
|
|
2378
|
+
cfMaxWidth: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'none' } },
|
|
2379
|
+
cfFlexDirection: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'column' } },
|
|
2380
|
+
cfFlexWrap: { type: 'DesignValue', valuesByBreakpoint: { desktop: 'nowrap' } },
|
|
2381
|
+
cfBorder: {
|
|
2382
|
+
type: 'DesignValue',
|
|
2383
|
+
valuesByBreakpoint: { desktop: '0px solid rgba(0, 0, 0, 0)' }
|
|
2384
|
+
},
|
|
2385
|
+
cfBorderRadius: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px' } },
|
|
2386
|
+
cfGap: { type: 'DesignValue', valuesByBreakpoint: { desktop: '0px 0px' } },
|
|
2387
|
+
cfHyperlink: { type: 'UnboundValue', key: 'VNc49Qyepd6IzN7rmKUyS' },
|
|
2388
|
+
cfOpenInNewTab: { type: 'UnboundValue', key: 'ZA5YqB2fmREQ4pTKqY5hX' },
|
|
2389
|
+
cfBackgroundImageUrl: { type: 'UnboundValue', key: 'FeskH0WbYD5_RQVXX-1T8' },
|
|
2390
|
+
cfBackgroundImageOptions: { type: 'DesignValue', valuesByBreakpoint: { desktop: [Object] } }
|
|
2391
|
+
}
|
|
2392
|
+
*/
|
|
2393
|
+
// so first, I convert it into a map to help me make it easier to access the values
|
|
2394
|
+
const propsByBreakpoint = indexByBreakpoint({
|
|
2395
|
+
variables: currentNode.variables,
|
|
2396
|
+
breakpointIds,
|
|
2397
|
+
unboundValues: unboundValues,
|
|
2398
|
+
dataSource: dataSource,
|
|
2399
|
+
componentSettings,
|
|
2400
|
+
componentVariablesOverwrites,
|
|
2401
|
+
getBoundEntityById: (id) => {
|
|
2402
|
+
return experience.entityStore?.entities.find((entity) => entity.sys.id === id);
|
|
2403
|
+
},
|
|
2404
|
+
});
|
|
2405
|
+
/**
|
|
2406
|
+
* propsByBreakpoint {
|
|
2407
|
+
desktop: {
|
|
2408
|
+
cfVerticalAlignment: 'center',
|
|
2409
|
+
cfHorizontalAlignment: 'center',
|
|
2410
|
+
cfMargin: '0 0 0 0',
|
|
2411
|
+
cfPadding: '0 0 0 0',
|
|
2412
|
+
cfBackgroundColor: 'rgba(246, 246, 246, 1)',
|
|
2413
|
+
cfWidth: 'fill',
|
|
2414
|
+
cfHeight: 'fit-content',
|
|
2415
|
+
cfMaxWidth: 'none',
|
|
2416
|
+
cfFlexDirection: 'column',
|
|
2417
|
+
cfFlexWrap: 'nowrap',
|
|
2418
|
+
cfBorder: '0px solid rgba(0, 0, 0, 0)',
|
|
2419
|
+
cfBorderRadius: '0px',
|
|
2420
|
+
cfGap: '0px 0px',
|
|
2421
|
+
cfBackgroundImageOptions: { scaling: 'fill', alignment: 'left top', targetSize: '2000px' }
|
|
2422
|
+
},
|
|
2423
|
+
tablet: {},
|
|
2424
|
+
mobile: {}
|
|
2425
|
+
}
|
|
2426
|
+
*/
|
|
2427
|
+
const currentNodeClassNames = [];
|
|
2428
|
+
// then for each breakpoint
|
|
2429
|
+
for (const breakpointId of breakpointIds) {
|
|
2430
|
+
const propsByBreakpointWithResolvedDesignTokens = Object.entries(propsByBreakpoint[breakpointId]).reduce((acc, [variableName, variableValue]) => {
|
|
2431
|
+
return {
|
|
2432
|
+
...acc,
|
|
2433
|
+
[variableName]: maybePopulateDesignTokenValue(variableName, variableValue, mapOfDesignVariableKeys),
|
|
2434
|
+
};
|
|
2435
|
+
}, {});
|
|
2436
|
+
// We convert cryptic prop keys to css variables
|
|
2437
|
+
// Eg: cfMargin to margin
|
|
2438
|
+
const stylesForBreakpoint = buildCfStyles(propsByBreakpointWithResolvedDesignTokens);
|
|
2439
|
+
const stylesForBreakpointWithoutUndefined = Object.fromEntries(Object.entries(stylesForBreakpoint)
|
|
2440
|
+
.filter(([, value]) => value !== undefined)
|
|
2441
|
+
.map(([key, value]) => [toCSSAttribute(key), value]));
|
|
2442
|
+
/**
|
|
2443
|
+
* stylesForBreakpoint {
|
|
2444
|
+
margin: '0 0 0 0',
|
|
2445
|
+
padding: '0 0 0 0',
|
|
2446
|
+
'background-color': 'rgba(246, 246, 246, 1)',
|
|
2447
|
+
width: '100%',
|
|
2448
|
+
height: 'fit-content',
|
|
2449
|
+
'max-width': 'none',
|
|
2450
|
+
border: '0px solid rgba(0, 0, 0, 0)',
|
|
2451
|
+
'border-radius': '0px',
|
|
2452
|
+
gap: '0px 0px',
|
|
2453
|
+
'align-items': 'center',
|
|
2454
|
+
'justify-content': 'safe center',
|
|
2455
|
+
'flex-direction': 'column',
|
|
2456
|
+
'flex-wrap': 'nowrap',
|
|
2457
|
+
'font-style': 'normal',
|
|
2458
|
+
'text-decoration': 'none',
|
|
2459
|
+
'box-sizing': 'border-box'
|
|
2515
2460
|
}
|
|
2516
|
-
|
|
2461
|
+
*/
|
|
2462
|
+
// I create a hash of the object above because that would ensure hash stability
|
|
2463
|
+
const styleHash = md5(JSON.stringify(stylesForBreakpointWithoutUndefined));
|
|
2464
|
+
// and prefix the className to make sure the value can be processed
|
|
2465
|
+
const className = `cf-${styleHash}`;
|
|
2466
|
+
// I save the generated hashes into an array to later save it in the tree node
|
|
2467
|
+
// as cfSsrClassName prop
|
|
2468
|
+
// making sure to avoid the duplicates in case styles for > 1 breakpoints are the same
|
|
2469
|
+
if (!currentNodeClassNames.includes(className)) {
|
|
2470
|
+
currentNodeClassNames.push(className);
|
|
2471
|
+
}
|
|
2472
|
+
// if there is already the similar hash - no need to over-write it
|
|
2473
|
+
if (mediaQueriesTemplate[breakpointId].cssByClassName[className]) {
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
// otherwise, save it to the stylesheet
|
|
2477
|
+
mediaQueriesTemplate[breakpointId].cssByClassName[className] = toCSSString(stylesForBreakpointWithoutUndefined);
|
|
2517
2478
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2479
|
+
// all generated classNames are saved in the tree node
|
|
2480
|
+
// to be handled by the sdk later
|
|
2481
|
+
// each node will get N classNames, where N is the number of breakpoints
|
|
2482
|
+
// browsers process classNames in the order they are defined
|
|
2483
|
+
// meaning that in case of className1 className2 className3
|
|
2484
|
+
// className3 will win over className2 and className1
|
|
2485
|
+
// making sure that we respect the order of breakpoints from
|
|
2486
|
+
// we can achieve "desktop first" or "mobile first" approach to style over-writes
|
|
2487
|
+
if (patternWrapper) {
|
|
2488
|
+
currentNode.id = currentNode.id ?? generateRandomId(15);
|
|
2489
|
+
// @ts-expect-error -- valueByBreakpoint is not explicitly defined, but it's already defined in the patternWrapper styles
|
|
2490
|
+
patternWrapper.variables.cfSsrClassName = {
|
|
2491
|
+
...(patternWrapper.variables.cfSsrClassName ?? {}),
|
|
2492
|
+
type: 'DesignValue',
|
|
2493
|
+
[currentNode.id]: {
|
|
2494
|
+
valuesByBreakpoint: {
|
|
2495
|
+
[breakpointIds[0]]: currentNodeClassNames.join(' '),
|
|
2496
|
+
},
|
|
2497
|
+
},
|
|
2498
|
+
};
|
|
2525
2499
|
}
|
|
2526
|
-
|
|
2500
|
+
else {
|
|
2501
|
+
currentNode.variables.cfSsrClassName = {
|
|
2502
|
+
type: 'DesignValue',
|
|
2503
|
+
valuesByBreakpoint: {
|
|
2504
|
+
[breakpointIds[0]]: currentNodeClassNames.join(' '),
|
|
2505
|
+
},
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
queue.push(...currentNode.children);
|
|
2527
2509
|
}
|
|
2528
|
-
}
|
|
2529
|
-
else {
|
|
2530
|
-
// Old design properties did not support breakpoints, keep for backward compatibility
|
|
2531
|
-
return valuesByBreakpoint;
|
|
2532
|
-
}
|
|
2533
|
-
};
|
|
2534
|
-
|
|
2535
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2536
|
-
const isLinkToAsset = (variable) => {
|
|
2537
|
-
if (!variable)
|
|
2538
|
-
return false;
|
|
2539
|
-
if (typeof variable !== 'object')
|
|
2540
|
-
return false;
|
|
2541
|
-
return (variable.sys?.linkType === 'Asset' &&
|
|
2542
|
-
typeof variable.sys?.id === 'string' &&
|
|
2543
|
-
!!variable.sys?.id &&
|
|
2544
|
-
variable.sys?.type === 'Link');
|
|
2545
|
-
};
|
|
2546
|
-
|
|
2547
|
-
const isLink = (maybeLink) => {
|
|
2548
|
-
if (maybeLink === null)
|
|
2549
|
-
return false;
|
|
2550
|
-
if (typeof maybeLink !== 'object')
|
|
2551
|
-
return false;
|
|
2552
|
-
const link = maybeLink;
|
|
2553
|
-
return Boolean(link.sys?.id) && link.sys?.type === 'Link';
|
|
2554
|
-
};
|
|
2555
|
-
|
|
2556
|
-
/**
|
|
2557
|
-
* This module encapsulates format of the path to a deep reference.
|
|
2558
|
-
*/
|
|
2559
|
-
const parseDataSourcePathIntoFieldset = (path) => {
|
|
2560
|
-
const parsedPath = parseDeepPath(path);
|
|
2561
|
-
if (null === parsedPath) {
|
|
2562
|
-
throw new Error(`Cannot parse path '${path}' as deep path`);
|
|
2563
|
-
}
|
|
2564
|
-
return parsedPath.fields.map((field) => [null, field, '~locale']);
|
|
2565
|
-
};
|
|
2566
|
-
/**
|
|
2567
|
-
* Parse path into components, supports L1 references (one reference follow) atm.
|
|
2568
|
-
* @param path from data source. eg. `/uuid123/fields/image/~locale/fields/file/~locale`
|
|
2569
|
-
* eg. `/uuid123/fields/file/~locale/fields/title/~locale`
|
|
2570
|
-
* @returns
|
|
2571
|
-
*/
|
|
2572
|
-
const parseDataSourcePathWithL1DeepBindings = (path) => {
|
|
2573
|
-
const parsedPath = parseDeepPath(path);
|
|
2574
|
-
if (null === parsedPath) {
|
|
2575
|
-
throw new Error(`Cannot parse path '${path}' as deep path`);
|
|
2576
|
-
}
|
|
2577
|
-
return {
|
|
2578
|
-
key: parsedPath.key,
|
|
2579
|
-
field: parsedPath.fields[0],
|
|
2580
|
-
referentField: parsedPath.fields[1],
|
|
2581
2510
|
};
|
|
2511
|
+
iterateOverTreeAndExtractStyles({
|
|
2512
|
+
componentTree: experienceTreeRoot,
|
|
2513
|
+
dataSource: experience.entityStore?.dataSource ?? {},
|
|
2514
|
+
unboundValues: experience.entityStore?.unboundValues ?? {},
|
|
2515
|
+
componentSettings: experience.entityStore?.experienceEntryFields?.componentSettings,
|
|
2516
|
+
});
|
|
2517
|
+
// once the whole tree was traversed, for each breakpoint, I aggregate the styles
|
|
2518
|
+
// for each generated className into one css string
|
|
2519
|
+
const styleSheet = Object.entries(mediaQueriesTemplate).reduce((acc, [, breakpointPayload]) => {
|
|
2520
|
+
return `${acc}${toMediaQuery(breakpointPayload)}`;
|
|
2521
|
+
}, '');
|
|
2522
|
+
return styleSheet;
|
|
2582
2523
|
};
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
* - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
|
|
2586
|
-
* or regular, like:
|
|
2587
|
-
* - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
|
|
2588
|
-
* @returns
|
|
2589
|
-
*/
|
|
2590
|
-
const isDeepPath = (deepPathCandidate) => {
|
|
2591
|
-
const deepPathParsed = parseDeepPath(deepPathCandidate);
|
|
2592
|
-
if (!deepPathParsed) {
|
|
2593
|
-
return false;
|
|
2594
|
-
}
|
|
2595
|
-
return deepPathParsed.fields.length > 1;
|
|
2524
|
+
const isCfStyleAttribute = (variableName) => {
|
|
2525
|
+
return CF_STYLE_ATTRIBUTES.includes(variableName);
|
|
2596
2526
|
};
|
|
2597
|
-
const
|
|
2598
|
-
//
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
// First turn string into array of segments
|
|
2602
|
-
// ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
|
|
2603
|
-
// Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
|
|
2604
|
-
// [
|
|
2605
|
-
// [ "", "uuid123" ],
|
|
2606
|
-
// [ "fields", "mainStory", "~locale" ],
|
|
2607
|
-
// [ "fields", "cover", "~locale" ],
|
|
2608
|
-
// [ "fields", "title", "~locale" ]
|
|
2609
|
-
// ]
|
|
2610
|
-
// Then check "initial" chunk for corretness
|
|
2611
|
-
// Then check all "field-leading" chunks for correctness
|
|
2612
|
-
const isValidInitialChunk = (initialChunk) => {
|
|
2613
|
-
// must have start with '' and have at least 2 segments, second non-empty
|
|
2614
|
-
// eg. /-_432uuid123123
|
|
2615
|
-
return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
|
|
2616
|
-
};
|
|
2617
|
-
const isValidFieldChunk = (fieldChunk) => {
|
|
2618
|
-
// must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
|
|
2619
|
-
// eg. fields/-32234mainStory/~locale
|
|
2620
|
-
return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
|
|
2621
|
-
};
|
|
2622
|
-
const deepPathSegments = deepPathCandidate.split('/');
|
|
2623
|
-
const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
|
|
2624
|
-
if (chunks.length <= 1) {
|
|
2625
|
-
return null; // malformed path, even regular paths have at least 2 chunks
|
|
2527
|
+
const maybePopulateDesignTokenValue = (variableName, variableValue, mapOfDesignVariableKeys) => {
|
|
2528
|
+
// TODO: refactor to reuse fn from core package
|
|
2529
|
+
if (typeof variableValue !== 'string') {
|
|
2530
|
+
return variableValue;
|
|
2626
2531
|
}
|
|
2627
|
-
|
|
2628
|
-
return
|
|
2532
|
+
if (!isCfStyleAttribute(variableName)) {
|
|
2533
|
+
return variableValue;
|
|
2534
|
+
}
|
|
2535
|
+
const resolveSimpleDesignToken = (variableName, variableValue) => {
|
|
2536
|
+
const nonTemplateDesignTokenValue = variableValue.replace(templateStringRegex, '$1');
|
|
2537
|
+
const tokenValue = mapOfDesignVariableKeys[nonTemplateDesignTokenValue];
|
|
2538
|
+
if (!tokenValue) {
|
|
2539
|
+
if (builtInStyles[variableName]) {
|
|
2540
|
+
return builtInStyles[variableName].defaultValue;
|
|
2541
|
+
}
|
|
2542
|
+
if (optionalBuiltInStyles[variableName]) {
|
|
2543
|
+
return optionalBuiltInStyles[variableName].defaultValue;
|
|
2544
|
+
}
|
|
2545
|
+
return '0px';
|
|
2546
|
+
}
|
|
2547
|
+
if (variableName === 'cfBorder' || variableName.startsWith('cfBorder_')) {
|
|
2548
|
+
if (typeof tokenValue === 'object') {
|
|
2549
|
+
const { width, style, color } = tokenValue;
|
|
2550
|
+
return `${width} ${style} ${color}`;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return tokenValue;
|
|
2554
|
+
};
|
|
2555
|
+
const templateStringRegex = /\${(.+?)}/g;
|
|
2556
|
+
const parts = variableValue.split(' ');
|
|
2557
|
+
let resolvedValue = '';
|
|
2558
|
+
for (const part of parts) {
|
|
2559
|
+
const tokenValue = templateStringRegex.test(part)
|
|
2560
|
+
? resolveSimpleDesignToken(variableName, part)
|
|
2561
|
+
: part;
|
|
2562
|
+
resolvedValue += `${tokenValue} `;
|
|
2629
2563
|
}
|
|
2630
|
-
//
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2564
|
+
// Not trimming would end up with a trailing space that breaks the check in `calculateNodeDefaultHeight`
|
|
2565
|
+
return resolvedValue.trim();
|
|
2566
|
+
};
|
|
2567
|
+
const resolveBackgroundImageBinding = ({ variableData, getBoundEntityById, dataSource = {}, unboundValues = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
|
|
2568
|
+
if (variableData.type === 'UnboundValue') {
|
|
2569
|
+
const uuid = variableData.key;
|
|
2570
|
+
return unboundValues[uuid]?.value;
|
|
2634
2571
|
}
|
|
2635
|
-
if (
|
|
2636
|
-
|
|
2572
|
+
if (variableData.type === 'ComponentValue') {
|
|
2573
|
+
const variableDefinitionKey = variableData.key;
|
|
2574
|
+
const variableDefinition = componentSettings.variableDefinitions[variableDefinitionKey];
|
|
2575
|
+
// @ts-expect-error TODO: Types coming from validations erroneously assume that `defaultValue` can be a primitive value (e.g. string or number)
|
|
2576
|
+
const defaultValueKey = variableDefinition.defaultValue?.key;
|
|
2577
|
+
const defaultValue = unboundValues[defaultValueKey].value;
|
|
2578
|
+
const userSetValue = componentVariablesOverwrites?.[variableDefinitionKey];
|
|
2579
|
+
// userSetValue is a ComponentValue we can safely return the default value
|
|
2580
|
+
if (!userSetValue || userSetValue.type === 'ComponentValue') {
|
|
2581
|
+
return defaultValue;
|
|
2582
|
+
}
|
|
2583
|
+
// at this point userSetValue will either be type of 'DesignValue' or 'BoundValue'
|
|
2584
|
+
// so we recursively run resolution again to resolve it
|
|
2585
|
+
const resolvedValue = resolveBackgroundImageBinding({
|
|
2586
|
+
variableData: userSetValue,
|
|
2587
|
+
getBoundEntityById,
|
|
2588
|
+
dataSource,
|
|
2589
|
+
unboundValues,
|
|
2590
|
+
componentVariablesOverwrites,
|
|
2591
|
+
componentSettings,
|
|
2592
|
+
});
|
|
2593
|
+
return resolvedValue || defaultValue;
|
|
2637
2594
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
let currentChunk = [];
|
|
2646
|
-
const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
|
|
2647
|
-
const excludeEmptyChunks = (chunk) => chunk.length > 0;
|
|
2648
|
-
for (let i = 0; i < segments.length; i++) {
|
|
2649
|
-
const isInitialElement = i === 0;
|
|
2650
|
-
const segment = segments[i];
|
|
2651
|
-
if (isInitialElement) {
|
|
2652
|
-
currentChunk = [segment];
|
|
2595
|
+
if (variableData.type === 'BoundValue') {
|
|
2596
|
+
// '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
|
|
2597
|
+
const [, uuid] = variableData.path.split('/');
|
|
2598
|
+
const binding = dataSource[uuid];
|
|
2599
|
+
const boundEntity = getBoundEntityById(binding.sys.id);
|
|
2600
|
+
if (!boundEntity) {
|
|
2601
|
+
return;
|
|
2653
2602
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
currentChunk = [segment];
|
|
2603
|
+
if (boundEntity.sys.type === 'Asset') {
|
|
2604
|
+
return boundEntity.fields.file?.url;
|
|
2657
2605
|
}
|
|
2658
2606
|
else {
|
|
2659
|
-
|
|
2607
|
+
// '/lUERH7tX7nJTaPX6f0udB/fields/assetReference/~locale/fields/file/~locale'
|
|
2608
|
+
// becomes
|
|
2609
|
+
// '/fields/assetReference/~locale/fields/file/~locale'
|
|
2610
|
+
const pathWithoutUUID = variableData.path.split(uuid)[1];
|
|
2611
|
+
// '/fields/assetReference/~locale/fields/file/~locale'
|
|
2612
|
+
// becomes
|
|
2613
|
+
// '/fields/assetReference/'
|
|
2614
|
+
const pathToReferencedAsset = pathWithoutUUID.split('~locale')[0];
|
|
2615
|
+
// '/fields/assetReference/'
|
|
2616
|
+
// becomes
|
|
2617
|
+
// '[fields, assetReference]'
|
|
2618
|
+
const [, fieldName] = pathToReferencedAsset.substring(1).split('/') ?? undefined;
|
|
2619
|
+
const referenceToAsset = boundEntity.fields[fieldName];
|
|
2620
|
+
if (!referenceToAsset) {
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
if (referenceToAsset.sys?.linkType === 'Asset') {
|
|
2624
|
+
const referencedAsset = getBoundEntityById(referenceToAsset.sys.id);
|
|
2625
|
+
if (!referencedAsset) {
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
return referencedAsset.fields.file?.url;
|
|
2629
|
+
}
|
|
2660
2630
|
}
|
|
2661
2631
|
}
|
|
2662
|
-
chunks.push(currentChunk);
|
|
2663
|
-
return chunks.filter(excludeEmptyChunks);
|
|
2664
2632
|
};
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2633
|
+
const indexByBreakpoint = ({ variables, breakpointIds, getBoundEntityById, unboundValues = {}, dataSource = {}, componentVariablesOverwrites, componentSettings = { variableDefinitions: {} }, }) => {
|
|
2634
|
+
const variableValuesByBreakpoints = breakpointIds.reduce((acc, breakpointId) => {
|
|
2635
|
+
return {
|
|
2636
|
+
...acc,
|
|
2637
|
+
[breakpointId]: {},
|
|
2638
|
+
};
|
|
2639
|
+
}, {});
|
|
2640
|
+
const defaultBreakpoint = breakpointIds[0];
|
|
2641
|
+
for (const [variableName, variableData] of Object.entries(variables)) {
|
|
2642
|
+
// handling the special case - cfBackgroundImageUrl variable, which can be bound or unbound
|
|
2643
|
+
// so, we need to resolve it here and pass it down as a css property to be convereted into the CSS
|
|
2644
|
+
// I used .startsWith() cause it can be part of a pattern node
|
|
2645
|
+
if (variableName === 'cfBackgroundImageUrl' ||
|
|
2646
|
+
variableName.startsWith('cfBackgroundImageUrl_')) {
|
|
2647
|
+
const imageUrl = resolveBackgroundImageBinding({
|
|
2648
|
+
variableData,
|
|
2649
|
+
getBoundEntityById,
|
|
2650
|
+
unboundValues,
|
|
2651
|
+
dataSource,
|
|
2652
|
+
componentSettings,
|
|
2653
|
+
componentVariablesOverwrites,
|
|
2654
|
+
});
|
|
2655
|
+
if (imageUrl) {
|
|
2656
|
+
variableValuesByBreakpoints[defaultBreakpoint][variableName] = imageUrl;
|
|
2657
|
+
}
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
let resolvedVariableData = variableData;
|
|
2661
|
+
if (variableData.type === 'ComponentValue') {
|
|
2662
|
+
const variableDefinition = componentSettings?.variableDefinitions[variableData.key];
|
|
2663
|
+
if (variableDefinition.group === 'style' && variableDefinition.defaultValue !== undefined) {
|
|
2664
|
+
const overrideVariableData = componentVariablesOverwrites?.[variableData.key];
|
|
2665
|
+
resolvedVariableData =
|
|
2666
|
+
overrideVariableData || variableDefinition.defaultValue;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (resolvedVariableData.type !== 'DesignValue') {
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
for (const [breakpointId, variableValue] of Object.entries(resolvedVariableData.valuesByBreakpoint)) {
|
|
2673
|
+
if (!isValidBreakpointValue(variableValue)) {
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
variableValuesByBreakpoints[breakpointId] = {
|
|
2677
|
+
...variableValuesByBreakpoints[breakpointId],
|
|
2678
|
+
[variableName]: variableValue,
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2672
2681
|
}
|
|
2673
|
-
|
|
2674
|
-
return secondLast === expectedName;
|
|
2682
|
+
return variableValuesByBreakpoints;
|
|
2675
2683
|
};
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2684
|
+
/**
|
|
2685
|
+
* Flattens the object from
|
|
2686
|
+
* {
|
|
2687
|
+
* color: {
|
|
2688
|
+
* [key]: [value]
|
|
2689
|
+
* }
|
|
2690
|
+
* }
|
|
2691
|
+
*
|
|
2692
|
+
* to
|
|
2693
|
+
*
|
|
2694
|
+
* {
|
|
2695
|
+
* 'color.key': [value]
|
|
2696
|
+
* }
|
|
2697
|
+
*/
|
|
2698
|
+
const flattenDesignTokenRegistry = (designTokenRegistry) => {
|
|
2699
|
+
return Object.entries(designTokenRegistry).reduce((acc, [categoryName, tokenCategory]) => {
|
|
2700
|
+
const tokensWithCategory = Object.entries(tokenCategory).reduce((acc, [tokenName, tokenValue]) => {
|
|
2701
|
+
return {
|
|
2702
|
+
...acc,
|
|
2703
|
+
[`${categoryName}.${tokenName}`]: tokenValue,
|
|
2704
|
+
};
|
|
2705
|
+
}, {});
|
|
2706
|
+
return {
|
|
2707
|
+
...acc,
|
|
2708
|
+
...tokensWithCategory,
|
|
2709
|
+
};
|
|
2710
|
+
}, {});
|
|
2685
2711
|
};
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
.
|
|
2691
|
-
.
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
return str.slice(0, dotIndex + 1) + locale + '.' + str.slice(dotIndex + 1);
|
|
2700
|
-
}
|
|
2712
|
+
// Replaces camelCase with kebab-case
|
|
2713
|
+
// converts the <key, value> object into a css string
|
|
2714
|
+
const toCSSString = (breakpointStyles) => {
|
|
2715
|
+
return Object.entries(breakpointStyles)
|
|
2716
|
+
.map(([key, value]) => `${key}:${value};`)
|
|
2717
|
+
.join('');
|
|
2718
|
+
};
|
|
2719
|
+
const toMediaQuery = (breakpointPayload) => {
|
|
2720
|
+
const mediaQueryStyles = Object.entries(breakpointPayload.cssByClassName).reduce((acc, [className, css]) => {
|
|
2721
|
+
return `${acc}.${className}{${css}}`;
|
|
2722
|
+
}, ``);
|
|
2723
|
+
if (breakpointPayload.condition === '*') {
|
|
2724
|
+
return mediaQueryStyles;
|
|
2701
2725
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
const
|
|
2707
|
-
return
|
|
2708
|
-
? retrievedValue[ctx.locale]
|
|
2709
|
-
: retrievedValue;
|
|
2710
|
-
}
|
|
2711
|
-
function buildTemplate({ template, context, }) {
|
|
2712
|
-
const localeVariable = /{\s*locale\s*}/g;
|
|
2713
|
-
// e.g. "{ page.sys.id }"
|
|
2714
|
-
const variables = /{\s*([\S]+?)\s*}/g;
|
|
2715
|
-
return (template
|
|
2716
|
-
// first replace the locale pattern
|
|
2717
|
-
.replace(localeVariable, context.locale)
|
|
2718
|
-
// then resolve the remaining variables
|
|
2719
|
-
.replace(variables, (_, path) => {
|
|
2720
|
-
const fallback = path + '_NOT_FOUND';
|
|
2721
|
-
const value = getTemplateValue(context, path) ?? fallback;
|
|
2722
|
-
// using _.result didn't gave proper results so we run our own version of it
|
|
2723
|
-
return String(typeof value === 'function' ? value() : value);
|
|
2724
|
-
}));
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
const stylesToKeep = ['cfImageAsset'];
|
|
2728
|
-
const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
|
|
2729
|
-
const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
|
|
2730
|
-
const sanitizeNodeProps = (nodeProps) => {
|
|
2731
|
-
return omit(nodeProps, stylesToRemove, propsToRemove);
|
|
2726
|
+
const [evaluation, pixelValue] = [
|
|
2727
|
+
breakpointPayload.condition[0],
|
|
2728
|
+
breakpointPayload.condition.substring(1),
|
|
2729
|
+
];
|
|
2730
|
+
const mediaQueryRule = evaluation === '<' ? 'max-width' : 'min-width';
|
|
2731
|
+
return `@media(${mediaQueryRule}:${pixelValue}){${mediaQueryStyles}}`;
|
|
2732
2732
|
};
|
|
2733
2733
|
|
|
2734
2734
|
const sendMessage = (eventType, data) => {
|
|
@@ -3228,6 +3228,8 @@ var VisualEditorMode;
|
|
|
3228
3228
|
VisualEditorMode["InjectScript"] = "injectScript";
|
|
3229
3229
|
})(VisualEditorMode || (VisualEditorMode = {}));
|
|
3230
3230
|
|
|
3231
|
+
// export function createExperience(args: createExperienceArgs): Experience<EntityStore>;
|
|
3232
|
+
// export function createExperience(options: string | createExperienceArgs): Experience<EntityStore> {
|
|
3231
3233
|
function createExperience(options) {
|
|
3232
3234
|
if (typeof options === 'string') {
|
|
3233
3235
|
const entityStore = new EntityStore(options);
|