@contentful/experiences-sdk-react 0.0.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/VisualEditorInjectScript-DPer6DO3.js +38 -0
  4. package/dist/VisualEditorInjectScript-DPer6DO3.js.map +1 -0
  5. package/dist/VisualEditorLoader-CY2fhqS5.js +28 -0
  6. package/dist/VisualEditorLoader-CY2fhqS5.js.map +1 -0
  7. package/dist/index.d.ts +122 -0
  8. package/dist/index.js +859 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/src/ExperienceRoot.d.ts +14 -0
  11. package/dist/src/blocks/editor/VisualEditorInjectScript.d.ts +4 -0
  12. package/dist/src/blocks/editor/VisualEditorLoader.d.ts +7 -0
  13. package/dist/src/blocks/editor/VisualEditorRoot.d.ts +9 -0
  14. package/dist/src/blocks/preview/CompositionBlock.d.ts +10 -0
  15. package/dist/src/blocks/preview/CompositionBlock.spec.d.ts +1 -0
  16. package/dist/src/blocks/preview/DeprecatedPreviewDeliveryRoot.d.ts +12 -0
  17. package/dist/src/blocks/preview/PreviewDeliveryRoot.d.ts +8 -0
  18. package/dist/src/blocks/preview/PreviewDeliveryRoot.test.d.ts +1 -0
  19. package/dist/src/components/Assembly.d.ts +3 -0
  20. package/dist/src/components/ErrorBoundary.d.ts +25 -0
  21. package/dist/src/components/Flex.d.ts +64 -0
  22. package/dist/src/constants.d.ts +3 -0
  23. package/dist/src/core/componentRegistry.d.ts +23 -0
  24. package/dist/src/core/componentRegistry.test.d.ts +1 -0
  25. package/dist/src/core/index.d.ts +1 -0
  26. package/dist/src/core/preview/assemblyUtils.d.ts +10 -0
  27. package/dist/src/core/preview/assemblyUtils.spec.d.ts +1 -0
  28. package/dist/src/hooks/index.d.ts +5 -0
  29. package/dist/src/hooks/useBreakpoints.d.ts +4 -0
  30. package/dist/src/hooks/useBreakpoints.spec.d.ts +1 -0
  31. package/dist/src/hooks/useDetectEditorMode.d.ts +7 -0
  32. package/dist/src/hooks/useExperienceBuilder.d.ts +41 -0
  33. package/dist/src/hooks/useExperienceBuilder.test.d.ts +1 -0
  34. package/dist/src/hooks/useFetchByBase.d.ts +8 -0
  35. package/dist/src/hooks/useFetchById.d.ts +16 -0
  36. package/dist/src/hooks/useFetchById.spec.d.ts +1 -0
  37. package/dist/src/hooks/useFetchBySlug.d.ts +16 -0
  38. package/dist/src/hooks/useFetchBySlug.spec.d.ts +1 -0
  39. package/dist/src/hooks/useFetchExperience.d.ts +27 -0
  40. package/dist/src/hooks/useFetchExperience.test.d.ts +1 -0
  41. package/dist/src/hooks/useInitializeVisualEditor.d.ts +7 -0
  42. package/dist/src/hooks/usePrevious.d.ts +6 -0
  43. package/dist/src/hooks/useStyleTag.d.ts +16 -0
  44. package/dist/src/index.d.ts +19 -0
  45. package/dist/src/sdkVersion.d.ts +1 -0
  46. package/dist/src/utils/withComponentWrapper.d.ts +36 -0
  47. package/dist/src/utils/withComponentWrapper.spec.d.ts +1 -0
  48. package/dist/test/__fixtures__/assembly.d.ts +96 -0
  49. package/dist/test/__fixtures__/composition.d.ts +7 -0
  50. package/dist/test/__fixtures__/entities.d.ts +9 -0
  51. package/dist/test/components/Test.d.ts +1 -0
  52. package/dist/test/components/Test.spec.d.ts +1 -0
  53. package/dist/vite.config.d.ts +2 -0
  54. package/package.json +92 -0
package/dist/index.js ADDED
@@ -0,0 +1,859 @@
1
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
2
+ import { containerDefinition, sectionDefinition, columnsDefinition, singleColumnDefinition, builtInStyles, optionalBuiltInStyles, sendMessage, designTokensRegistry, buildStyleTag, checkIsAssemblyNode, isDeepPath, transformContentValue, buildCfStyles, isEmptyStructureWithRelativeHeight, mediaQueryMatcher, getFallbackBreakpointIndex, getActiveBreakpointIndex, getValueForBreakpoint, fetchBySlug, fetchById, doesMismatchMessageSchema, tryParseMessage, validateExperienceBuilderConfig, isDeprecatedExperience, VisualEditorMode } from '@contentful/experiences-core';
3
+ export { EntityStore, VisualEditorMode, calculateNodeDefaultHeight, checkIsAssembly, checkIsAssemblyEntry, checkIsAssemblyNode, createExperience, defineDesignTokens, fetchById, fetchBySlug, supportedModes } from '@contentful/experiences-core';
4
+ import React, { useState, useEffect, useMemo, useCallback, useRef, Suspense } from 'react';
5
+ import { omit } from 'lodash-es';
6
+ import { INTERNAL_EVENTS, CONTENTFUL_COMPONENTS, OUTGOING_EVENTS, ASSEMBLY_DEFAULT_CATEGORY, EMPTY_CONTAINER_HEIGHT, CF_STYLE_ATTRIBUTES, INCOMING_EVENTS, VISUAL_EDITOR_EVENTS } from '@contentful/experiences-core/constants';
7
+ export { ASSEMBLY_BLOCK_NODE_TYPE, ASSEMBLY_NODE_TYPE, ASSEMBLY_NODE_TYPES, CF_STYLE_ATTRIBUTES, CONTENTFUL_COMPONENTS, CONTENTFUL_COMPONENT_CATEGORY, CONTENTFUL_CONTAINER_ID, CONTENTFUL_SECTION_ID, DESIGN_COMPONENT_BLOCK_NODE_TYPE, DESIGN_COMPONENT_NODE_TYPE, DESIGN_COMPONENT_NODE_TYPES, INCOMING_EVENTS, LATEST_SCHEMA_VERSION, OUTGOING_EVENTS, SCROLL_STATES } from '@contentful/experiences-core/constants';
8
+ import * as Components from '@contentful/experiences-components-react';
9
+ import { ContentfulContainer, Columns, SingleColumn } from '@contentful/experiences-components-react';
10
+ import styleInject from 'style-inject';
11
+
12
+ const SDK_VERSION = '0.0.1-alpha.1';
13
+
14
+ /**
15
+ * Sets up a component to be consumed by Experience Builder. This function can be used to wrap a component with a container component, or to pass props to the component directly.
16
+ * @param Component Component to be used by Experience Builder.
17
+ * @param options Options for the `withComponentWrapper` function.
18
+ * @default { wrapComponent: true, wrapContainerTag: 'div' }
19
+ * @returns A component that can be passed to `defineComponents`.
20
+ */
21
+ function withComponentWrapper(Component, options = {
22
+ wrapComponent: true,
23
+ wrapContainerTag: 'div',
24
+ }) {
25
+ const Wrapped = ({ classes = '', className = '', 'data-cf-node-id': dataCfNodeId, 'data-cf-node-block-id': dataCfNodeBlockId, 'data-cf-node-block-type': dataCfNodeBlockType, onClick, onMouseDown, onMouseUp, ...props }) => {
26
+ const Tag = options.wrapContainerTag || 'div';
27
+ const cfProps = {
28
+ 'data-cf-node-id': dataCfNodeId,
29
+ 'data-cf-node-block-id': dataCfNodeBlockId,
30
+ 'data-cf-node-block-type': dataCfNodeBlockType,
31
+ onClick,
32
+ onMouseDown,
33
+ onMouseUp,
34
+ };
35
+ const component = options.wrapComponent ? (jsx(Tag, { className: className, ...cfProps, children: typeof Component === 'string' ? (React.createElement(Component, { className: classes, ...props })) : (jsx(Component, { className: classes, ...props })) })) : (React.createElement(Component, {
36
+ className: classes + className ? classes + ' ' + className : undefined,
37
+ ...cfProps,
38
+ ...props,
39
+ }));
40
+ return component;
41
+ };
42
+ return Wrapped;
43
+ }
44
+
45
+ // this is the array of version which currently LATEST_SCHEMA_VERSION is compatible with
46
+ const compatibleVersions = ['2023-08-23', '2023-09-28'];
47
+
48
+ const cloneObject = (targetObject) => {
49
+ if (typeof structuredClone !== 'undefined') {
50
+ return structuredClone(targetObject);
51
+ }
52
+ return JSON.parse(JSON.stringify(targetObject));
53
+ };
54
+ const applyComponentDefinitionFallbacks = (componentDefinition) => {
55
+ const clone = cloneObject(componentDefinition);
56
+ for (const variable of Object.values(clone.variables)) {
57
+ variable.group = variable.group ?? 'content';
58
+ }
59
+ return clone;
60
+ };
61
+ const applyBuiltInStyleDefinitions = (componentDefinition) => {
62
+ if ([CONTENTFUL_COMPONENTS.container.id].includes(componentDefinition.id)) {
63
+ return componentDefinition;
64
+ }
65
+ const clone = cloneObject(componentDefinition);
66
+ // set margin built-in style by default
67
+ if (!clone.builtInStyles) {
68
+ clone.builtInStyles = ['cfMargin'];
69
+ }
70
+ for (const style of Object.values(clone.builtInStyles || [])) {
71
+ if (builtInStyles[style]) {
72
+ clone.variables[style] = builtInStyles[style];
73
+ }
74
+ if (optionalBuiltInStyles[style]) {
75
+ clone.variables[style] = optionalBuiltInStyles[style];
76
+ }
77
+ }
78
+ return clone;
79
+ };
80
+ const enrichComponentDefinition = ({ component, definition, options, }) => {
81
+ const definitionWithFallbacks = applyComponentDefinitionFallbacks(definition);
82
+ const definitionWithBuiltInStyles = applyBuiltInStyleDefinitions(definitionWithFallbacks);
83
+ return {
84
+ component: withComponentWrapper(component, options),
85
+ definition: definitionWithBuiltInStyles,
86
+ };
87
+ };
88
+ const DEFAULT_COMPONENT_REGISTRATIONS = {
89
+ container: {
90
+ component: Components.ContentfulContainer,
91
+ definition: containerDefinition,
92
+ },
93
+ section: {
94
+ component: Components.ContentfulContainer,
95
+ definition: sectionDefinition,
96
+ },
97
+ columns: {
98
+ component: Components.Columns,
99
+ definition: columnsDefinition,
100
+ },
101
+ singleColumn: {
102
+ component: Components.SingleColumn,
103
+ definition: singleColumnDefinition,
104
+ },
105
+ button: enrichComponentDefinition({
106
+ component: Components.Button,
107
+ definition: Components.ButtonComponentDefinition,
108
+ options: {
109
+ wrapComponent: false,
110
+ },
111
+ }),
112
+ heading: enrichComponentDefinition({
113
+ component: Components.Heading,
114
+ definition: Components.HeadingComponentDefinition,
115
+ options: {
116
+ wrapComponent: false,
117
+ },
118
+ }),
119
+ image: enrichComponentDefinition({
120
+ component: Components.Image,
121
+ definition: Components.ImageComponentDefinition,
122
+ }),
123
+ richText: enrichComponentDefinition({
124
+ component: Components.RichText,
125
+ definition: Components.RichTextComponentDefinition,
126
+ options: {
127
+ wrapComponent: false,
128
+ },
129
+ }),
130
+ text: enrichComponentDefinition({
131
+ component: Components.Text,
132
+ definition: Components.TextComponentDefinition,
133
+ options: {
134
+ wrapComponent: false,
135
+ },
136
+ }),
137
+ };
138
+ // pre-filling with the default component registrations
139
+ const componentRegistry = new Map([
140
+ [DEFAULT_COMPONENT_REGISTRATIONS.section.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.section],
141
+ [
142
+ DEFAULT_COMPONENT_REGISTRATIONS.container.definition.id,
143
+ DEFAULT_COMPONENT_REGISTRATIONS.container,
144
+ ],
145
+ [
146
+ DEFAULT_COMPONENT_REGISTRATIONS.singleColumn.definition.id,
147
+ DEFAULT_COMPONENT_REGISTRATIONS.singleColumn,
148
+ ],
149
+ [DEFAULT_COMPONENT_REGISTRATIONS.columns.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.columns],
150
+ [DEFAULT_COMPONENT_REGISTRATIONS.button.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.button],
151
+ [DEFAULT_COMPONENT_REGISTRATIONS.heading.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.heading],
152
+ [DEFAULT_COMPONENT_REGISTRATIONS.image.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.image],
153
+ [
154
+ DEFAULT_COMPONENT_REGISTRATIONS.richText.definition.id,
155
+ DEFAULT_COMPONENT_REGISTRATIONS.richText,
156
+ ],
157
+ [DEFAULT_COMPONENT_REGISTRATIONS.text.definition.id, DEFAULT_COMPONENT_REGISTRATIONS.text],
158
+ ]);
159
+ const optionalBuiltInComponents = [
160
+ DEFAULT_COMPONENT_REGISTRATIONS.button.definition.id,
161
+ DEFAULT_COMPONENT_REGISTRATIONS.heading.definition.id,
162
+ DEFAULT_COMPONENT_REGISTRATIONS.image.definition.id,
163
+ DEFAULT_COMPONENT_REGISTRATIONS.richText.definition.id,
164
+ DEFAULT_COMPONENT_REGISTRATIONS.text.definition.id,
165
+ ];
166
+ const sendRegisteredComponentsMessage = () => {
167
+ // Send the definitions (without components) via the connection message to the experience builder
168
+ const registeredDefinitions = Array.from(componentRegistry.values());
169
+ sendMessage(OUTGOING_EVENTS.RegisteredComponents, {
170
+ definitions: registeredDefinitions,
171
+ });
172
+ };
173
+ const sendConnectedEventWithRegisteredComponents = () => {
174
+ // Send the definitions (without components) via the connection message to the experience builder
175
+ const registeredDefinitions = Array.from(componentRegistry.values()).map(({ definition }) => definition);
176
+ sendMessage(OUTGOING_EVENTS.Connected, {
177
+ sdkVersion: SDK_VERSION,
178
+ definitions: registeredDefinitions,
179
+ });
180
+ sendMessage(OUTGOING_EVENTS.DesignTokens, {
181
+ designTokens: designTokensRegistry,
182
+ });
183
+ };
184
+ /**
185
+ * Registers multiple components and their component definitions at once
186
+ * @param componentRegistrations - ComponentRegistration[]
187
+ * @returns void
188
+ */
189
+ const defineComponents = (componentRegistrations, options) => {
190
+ if (options?.enabledBuiltInComponents) {
191
+ for (const id of optionalBuiltInComponents) {
192
+ if (!options.enabledBuiltInComponents.includes(id)) {
193
+ componentRegistry.delete(id);
194
+ }
195
+ }
196
+ }
197
+ for (const registration of componentRegistrations) {
198
+ // Fill definitions with fallbacks values
199
+ const enrichedComponentRegistration = enrichComponentDefinition(registration);
200
+ componentRegistry.set(enrichedComponentRegistration.definition.id, enrichedComponentRegistration);
201
+ }
202
+ if (typeof window !== 'undefined') {
203
+ window.dispatchEvent(new CustomEvent(INTERNAL_EVENTS.ComponentsRegistered));
204
+ }
205
+ };
206
+ const getComponentRegistration = (id) => componentRegistry.get(id);
207
+ const addComponentRegistration = (componentRegistration) => {
208
+ componentRegistry.set(componentRegistration.definition.id, componentRegistration);
209
+ };
210
+ const createAssemblyRegistration = ({ definitionId, definitionName, component, }) => {
211
+ const componentRegistration = componentRegistry.get(definitionId);
212
+ if (componentRegistration) {
213
+ return componentRegistration;
214
+ }
215
+ const definition = {
216
+ id: definitionId,
217
+ name: definitionName || 'Component',
218
+ variables: {},
219
+ children: true,
220
+ category: ASSEMBLY_DEFAULT_CATEGORY,
221
+ };
222
+ addComponentRegistration({ component, definition });
223
+ return componentRegistry.get(definitionId);
224
+ };
225
+
226
+ /**
227
+ *
228
+ * @param styles: the list of styles to apply
229
+ * @param nodeId: [Optional] the id of node that these styles will be applied to
230
+ * @returns className: the className that was used
231
+ * Builds and adds a style tag in the document. Returns the className to be attached to the element.
232
+ * In editor mode the nodeId is used as the identifier in order to avoid creating endless tags as the styles are tweeked
233
+ * In preview/delivery mode the styles don't change oftem so we're using the md5 hash of the content of the tag
234
+ */
235
+ const useStyleTag = ({ styles, nodeId }) => {
236
+ const [className, setClassName] = useState('');
237
+ useEffect(() => {
238
+ if (Object.keys(styles).length === 0) {
239
+ return;
240
+ }
241
+ const [className, styleRule] = buildStyleTag({ styles, nodeId });
242
+ setClassName(className);
243
+ const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
244
+ if (existingTag) {
245
+ // editor mode - update existing
246
+ if (nodeId) {
247
+ existingTag.innerHTML = styleRule;
248
+ }
249
+ // preview/delivery mode - here we don't need to update the existing tag because
250
+ // the className is based on the md5 hash of the content so it hasn't changed
251
+ return;
252
+ }
253
+ const styleTag = document.createElement('style');
254
+ styleTag.dataset['cfStyles'] = className;
255
+ document.head.appendChild(styleTag).innerHTML = styleRule;
256
+ }, [styles, nodeId]);
257
+ return { className };
258
+ };
259
+
260
+ const deserializeAssemblyNode = ({ node, componentInstanceVariables, }) => {
261
+ const variables = {};
262
+ for (const [variableName, variable] of Object.entries(node.variables)) {
263
+ variables[variableName] = variable;
264
+ if (variable.type === 'ComponentValue') {
265
+ const componentValueKey = variable.key;
266
+ const instanceProperty = componentInstanceVariables[componentValueKey];
267
+ // For assembly, we look up the variable in the assembly instance and
268
+ // replace the componentValue with that one.
269
+ if (instanceProperty?.type === 'UnboundValue') {
270
+ variables[variableName] = {
271
+ type: 'UnboundValue',
272
+ key: instanceProperty.key,
273
+ };
274
+ }
275
+ else if (instanceProperty?.type === 'BoundValue') {
276
+ variables[variableName] = {
277
+ type: 'BoundValue',
278
+ path: instanceProperty.path,
279
+ };
280
+ }
281
+ }
282
+ }
283
+ const children = node.children.map((child) => deserializeAssemblyNode({
284
+ node: child,
285
+ componentInstanceVariables,
286
+ }));
287
+ return {
288
+ definitionId: node.definitionId,
289
+ variables,
290
+ children,
291
+ };
292
+ };
293
+ const resolveAssembly = ({ node, entityStore, }) => {
294
+ const isAssembly = checkIsAssemblyNode({
295
+ componentId: node.definitionId,
296
+ usedComponents: entityStore.usedComponents,
297
+ });
298
+ if (!isAssembly) {
299
+ return node;
300
+ }
301
+ const componentId = node.definitionId;
302
+ const assembly = entityStore.experienceEntryFields?.usedComponents?.find((component) => component.sys.id === componentId);
303
+ if (!assembly || !('fields' in assembly)) {
304
+ return node;
305
+ }
306
+ const componentFields = assembly.fields;
307
+ const deserializedNode = deserializeAssemblyNode({
308
+ node: {
309
+ definitionId: node.definitionId,
310
+ variables: {},
311
+ children: componentFields.componentTree.children,
312
+ },
313
+ componentInstanceVariables: node.variables,
314
+ });
315
+ entityStore.addAssemblyUnboundValues(componentFields.unboundValues);
316
+ return deserializedNode;
317
+ };
318
+
319
+ const assemblyStyle = { display: 'contents' };
320
+ // Feel free to do any magic as regards variable definitions for assemblies
321
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
322
+ const Assembly = ({ ...props }) => {
323
+ // Using a display contents so assembly content/children
324
+ // can appear as if they are direct children of the div wrapper's parent
325
+ return jsx("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
326
+ };
327
+
328
+ const CompositionBlock = ({ node: rawNode, locale, entityStore, resolveDesignValue, }) => {
329
+ const isAssembly = useMemo(() => checkIsAssemblyNode({
330
+ componentId: rawNode.definitionId,
331
+ usedComponents: entityStore.usedComponents,
332
+ }), [entityStore.usedComponents, rawNode.definitionId]);
333
+ const node = useMemo(() => {
334
+ return isAssembly
335
+ ? resolveAssembly({
336
+ node: rawNode,
337
+ entityStore,
338
+ })
339
+ : rawNode;
340
+ }, [entityStore, isAssembly, rawNode]);
341
+ const componentRegistration = useMemo(() => {
342
+ const registration = getComponentRegistration(node.definitionId);
343
+ if (isAssembly && !registration) {
344
+ return createAssemblyRegistration({
345
+ definitionId: node.definitionId,
346
+ component: Assembly,
347
+ });
348
+ }
349
+ return registration;
350
+ }, [isAssembly, node.definitionId]);
351
+ const nodeProps = useMemo(() => {
352
+ // Don't enrich the assembly wrapper node with props
353
+ if (!componentRegistration || isAssembly) {
354
+ return {};
355
+ }
356
+ const propMap = {};
357
+ return Object.entries(node.variables).reduce((acc, [variableName, variable]) => {
358
+ switch (variable.type) {
359
+ case 'DesignValue':
360
+ acc[variableName] = resolveDesignValue(variable.valuesByBreakpoint, variableName);
361
+ break;
362
+ case 'BoundValue': {
363
+ const variableDefinition = componentRegistration.definition.variables[variableName];
364
+ if (isDeepPath(variable.path)) {
365
+ const [, uuid] = variable.path.split('/');
366
+ const link = entityStore.dataSource[uuid];
367
+ const boundValue = entityStore.getValueDeep(link, variable.path);
368
+ const value = boundValue || variableDefinition.defaultValue;
369
+ acc[variableName] = transformContentValue(value, variableDefinition);
370
+ break;
371
+ }
372
+ const [, uuid, ...path] = variable.path.split('/');
373
+ const binding = entityStore.dataSource[uuid];
374
+ let value = entityStore.getValue(binding, path.slice(0, -1));
375
+ if (!value) {
376
+ const foundAssetValue = entityStore.getValue(binding, [
377
+ ...path.slice(0, -2),
378
+ 'fields',
379
+ 'file',
380
+ ]);
381
+ if (foundAssetValue) {
382
+ value = foundAssetValue;
383
+ }
384
+ }
385
+ acc[variableName] = transformContentValue(value, variableDefinition);
386
+ break;
387
+ }
388
+ case 'UnboundValue': {
389
+ const uuid = variable.key;
390
+ acc[variableName] = entityStore.unboundValues[uuid]?.value;
391
+ break;
392
+ }
393
+ }
394
+ return acc;
395
+ }, propMap);
396
+ }, [componentRegistration, isAssembly, node.variables, resolveDesignValue, entityStore]);
397
+ const cfStyles = buildCfStyles(nodeProps);
398
+ if (isEmptyStructureWithRelativeHeight(node.children.length, node.definitionId, cfStyles.height)) {
399
+ cfStyles.minHeight = EMPTY_CONTAINER_HEIGHT;
400
+ }
401
+ const { className } = useStyleTag({ styles: cfStyles });
402
+ if (!componentRegistration) {
403
+ return null;
404
+ }
405
+ const { component } = componentRegistration;
406
+ const children = componentRegistration.definition.children === true
407
+ ? node.children.map((childNode, index) => {
408
+ return (jsx(CompositionBlock, { node: childNode, locale: locale, entityStore: entityStore, resolveDesignValue: resolveDesignValue }, index));
409
+ })
410
+ : null;
411
+ if ([CONTENTFUL_COMPONENTS.container.id, CONTENTFUL_COMPONENTS.section.id].includes(node.definitionId)) {
412
+ return (jsx(ContentfulContainer, { editorMode: false, cfHyperlink: nodeProps.cfHyperlink, cfOpenInNewTab: nodeProps.cfOpenInNewTab, className: className, children: children }));
413
+ }
414
+ if (node.definitionId === CONTENTFUL_COMPONENTS.columns.id) {
415
+ return (jsx(Columns, { editorMode: false, className: className, children: children }));
416
+ }
417
+ if (node.definitionId === CONTENTFUL_COMPONENTS.singleColumn.id) {
418
+ return (jsx(SingleColumn, { editorMode: false, className: className, children: children }));
419
+ }
420
+ return React.createElement(component, {
421
+ ...omit(nodeProps, CF_STYLE_ATTRIBUTES, ['cfHyperlink', 'cfOpenInNewTab']),
422
+ className,
423
+ }, children);
424
+ };
425
+
426
+ /**
427
+ * @deprecated This hook is deprecated. Use fetchBySlug or fetchById instead
428
+ */
429
+ const useExperienceBuilder = ({ experienceTypeId, client, mode = 'delivery', }) => {
430
+ const experience = useMemo(() => ({
431
+ client,
432
+ experienceTypeId,
433
+ mode,
434
+ }), [mode, client, experienceTypeId]);
435
+ return {
436
+ /**
437
+ * @deprecated please fetch the experience using `useFetchExperience` hook or fetch the data manually using `fetchers` or `client` and create experience with `createExperience` function
438
+ *
439
+ * @example
440
+ *
441
+ * import { useFetchExperience } from '@contentful/experiences-sdk-react'
442
+ *
443
+ * const { fetchBySlug, fetchById, experience, isFetching } = useFetchExperience({ client, mode })
444
+ */
445
+ experience,
446
+ /**
447
+ * @deprecated please import the function from the library
448
+ *
449
+ * @example
450
+ *
451
+ * import { defineComponents } from '@contentful/experiences-sdk-react'
452
+ */
453
+ defineComponents,
454
+ };
455
+ };
456
+
457
+ // TODO: In order to support integrations without React, we should extract this heavy logic into simple
458
+ // functions that we can reuse in other frameworks.
459
+ /*
460
+ * Registers media query change listeners for each breakpoint (except for "*").
461
+ * It will always assume the last matching media query in the list. It therefore,
462
+ * assumes that the breakpoints are sorted beginning with the default value (query: "*")
463
+ * and then decending by screen width. For mobile-first designs, the order would be ascending
464
+ */
465
+ const useBreakpoints = (breakpoints) => {
466
+ const [mediaQueryMatchers, initialMediaQueryMatches] = mediaQueryMatcher(breakpoints);
467
+ const [mediaQueryMatches, setMediaQueryMatches] = useState(initialMediaQueryMatches);
468
+ const fallbackBreakpointIndex = getFallbackBreakpointIndex(breakpoints);
469
+ // Register event listeners to update the media query states
470
+ useEffect(() => {
471
+ const eventListeners = mediaQueryMatchers.map(({ id, signal }) => {
472
+ const onChange = () => setMediaQueryMatches((prev) => ({
473
+ ...prev,
474
+ [id]: signal.matches,
475
+ }));
476
+ signal.addEventListener('change', onChange);
477
+ return onChange;
478
+ });
479
+ return () => {
480
+ eventListeners.forEach((eventListener, index) => {
481
+ mediaQueryMatchers[index].signal.removeEventListener('change', eventListener);
482
+ });
483
+ };
484
+ }, [mediaQueryMatchers]);
485
+ const activeBreakpointIndex = getActiveBreakpointIndex(breakpoints, mediaQueryMatches, fallbackBreakpointIndex);
486
+ const resolveDesignValue = useCallback((valuesByBreakpoint, variableName) => {
487
+ return getValueForBreakpoint(valuesByBreakpoint, breakpoints, activeBreakpointIndex, variableName);
488
+ }, [activeBreakpointIndex, breakpoints]);
489
+ return { resolveDesignValue };
490
+ };
491
+
492
+ /**
493
+ * @deprecated please use `useFetchBySlug` or `useFetchById` hooks instead
494
+ */
495
+ const useFetchExperience = ({ client }) => {
496
+ const [experience, setExperience] = useState(undefined);
497
+ const [isFetching, setIsFetching] = useState(false);
498
+ const [error, setError] = useState();
499
+ /**
500
+ * Fetch experience entry using slug as the identifier
501
+ * @param {string} experienceTypeId - id of the content type associated with the experience
502
+ * @param {string} slug - slug of the experience (defined in entry settings)
503
+ * @param {string} localeCode - locale code to fetch the experience. Falls back to the currently active locale in the state
504
+ */
505
+ const fetchBySlug$1 = useCallback(async ({ experienceTypeId, slug, localeCode, }) => {
506
+ setIsFetching(true);
507
+ setError(undefined);
508
+ try {
509
+ const experience = await fetchBySlug({
510
+ client,
511
+ experienceTypeId,
512
+ localeCode,
513
+ slug,
514
+ });
515
+ setExperience(experience);
516
+ return experience;
517
+ }
518
+ catch (error) {
519
+ setError(error);
520
+ }
521
+ finally {
522
+ setIsFetching(false);
523
+ }
524
+ }, [client]);
525
+ /**
526
+ * Fetch experience entry using id as the identifier
527
+ * @param {string} experienceTypeId - id of the content type associated with the experience
528
+ * @param {string} id - id of the experience (defined in entry settings)
529
+ * @param {string} localeCode - locale code to fetch the experience. Falls back to the currently active locale in the state
530
+ */
531
+ const fetchById$1 = useCallback(async ({ experienceTypeId, id, localeCode, }) => {
532
+ setIsFetching(true);
533
+ setError(undefined);
534
+ try {
535
+ const experience = await fetchById({
536
+ client,
537
+ experienceTypeId,
538
+ localeCode,
539
+ id,
540
+ });
541
+ setExperience(experience);
542
+ return experience;
543
+ }
544
+ catch (error) {
545
+ setError(error);
546
+ }
547
+ finally {
548
+ setIsFetching(false);
549
+ }
550
+ }, [client]);
551
+ return {
552
+ fetchBySlug: fetchBySlug$1,
553
+ fetchById: fetchById$1,
554
+ error,
555
+ experience,
556
+ isFetching,
557
+ };
558
+ };
559
+
560
+ const useFetchByBase = (fetchMethod, isEditorMode) => {
561
+ const [experience, setExperience] = useState();
562
+ const [isLoading, setIsLoading] = useState(false);
563
+ const [error, setError] = useState();
564
+ useEffect(() => {
565
+ (async () => {
566
+ // if we are in editor mode, we don't want to fetch the experience here
567
+ // it is passed via postMessage instead
568
+ if (isEditorMode) {
569
+ return;
570
+ }
571
+ setIsLoading(true);
572
+ setError(undefined);
573
+ try {
574
+ const exp = await fetchMethod();
575
+ setExperience(exp);
576
+ }
577
+ catch (error) {
578
+ setError(error);
579
+ }
580
+ finally {
581
+ setIsLoading(false);
582
+ }
583
+ })();
584
+ }, [fetchMethod, isEditorMode]);
585
+ return {
586
+ error,
587
+ experience,
588
+ isLoading,
589
+ isEditorMode,
590
+ };
591
+ };
592
+
593
+ const useDetectEditorMode = ({ isClientSide = false } = {}) => {
594
+ const [mounted, setMounted] = useState(false);
595
+ const [isEditorMode, setIsEditorMode] = useState(isClientSide ? inIframe() : false);
596
+ const receivedMessage = useRef(false);
597
+ useEffect(() => {
598
+ const onMessage = (event) => {
599
+ if (doesMismatchMessageSchema(event)) {
600
+ return;
601
+ }
602
+ const eventData = tryParseMessage(event);
603
+ if (eventData.eventType === INCOMING_EVENTS.RequestEditorMode) {
604
+ setIsEditorMode(true);
605
+ receivedMessage.current = true;
606
+ if (typeof window !== 'undefined') {
607
+ //Once we definitely know that we are in editor mode, we set this flag so future postMessage connect calls are not made
608
+ window.__EB__.isEditorMode = true;
609
+ window.removeEventListener('message', onMessage);
610
+ }
611
+ }
612
+ };
613
+ //Only run check after component is mounted on the client to avoid hydration ssr issues
614
+ if (mounted) {
615
+ setIsEditorMode(inIframe());
616
+ //Double check if we are in editor mode by listening to postMessage events
617
+ if (typeof window !== 'undefined' && !window.__EB__?.isEditorMode) {
618
+ window.addEventListener('message', onMessage);
619
+ sendMessage(OUTGOING_EVENTS.Connected);
620
+ setTimeout(() => {
621
+ if (!receivedMessage.current) {
622
+ // if message is not received back in time, set editorMode back to false
623
+ setIsEditorMode(false);
624
+ }
625
+ }, 100);
626
+ }
627
+ }
628
+ else {
629
+ setMounted(true);
630
+ }
631
+ return () => window.removeEventListener('message', onMessage);
632
+ }, [mounted]);
633
+ return isEditorMode;
634
+ };
635
+ function inIframe() {
636
+ try {
637
+ return window.self !== window.top;
638
+ }
639
+ catch (e) {
640
+ return false;
641
+ }
642
+ }
643
+
644
+ const useFetchById = ({ id, localeCode, client, experienceTypeId }) => {
645
+ const isEditorMode = useDetectEditorMode({ isClientSide: typeof window !== 'undefined' });
646
+ const fetchMethod = useCallback(() => {
647
+ return fetchById({ id, localeCode, client, experienceTypeId });
648
+ }, [id, localeCode, client, experienceTypeId]);
649
+ return useFetchByBase(fetchMethod, isEditorMode);
650
+ };
651
+
652
+ const useFetchBySlug = ({ slug, localeCode, client, experienceTypeId, }) => {
653
+ const isEditorMode = useDetectEditorMode({ isClientSide: typeof window !== 'undefined' });
654
+ const fetchMethod = useCallback(() => {
655
+ return fetchBySlug({ slug, localeCode, client, experienceTypeId });
656
+ }, [slug, localeCode, client, experienceTypeId]);
657
+ return useFetchByBase(fetchMethod, isEditorMode);
658
+ };
659
+
660
+ /**
661
+ * Returns the value of the argument from the previous render
662
+ * @param {T} value
663
+ * @returns {T | undefined} previous value
664
+ */
665
+ function usePrevious(value) {
666
+ const ref = useRef();
667
+ useEffect(() => {
668
+ ref.current = value;
669
+ }, [value]);
670
+ return ref.current;
671
+ }
672
+
673
+ /**
674
+ * @deprecated Remove after the BETA release
675
+ * @returns
676
+ */
677
+ const DeprecatedPreviewDeliveryRoot = ({ locale, slug, deprecatedExperience, }) => {
678
+ const attemptedToFetch = useRef(false);
679
+ const previousLocale = usePrevious(locale);
680
+ const { experienceTypeId, client } = deprecatedExperience;
681
+ const { fetchBySlug, experience, isFetching } = useFetchExperience({
682
+ client,
683
+ });
684
+ const entityStore = experience?.entityStore;
685
+ useEffect(() => {
686
+ // TODO: Test it, it is crucial
687
+ // will make it fetch on each locale change as well as if experience entry hasn't been fetched yet at least once
688
+ const shouldFetch = (client && !entityStore && !attemptedToFetch.current) || previousLocale !== locale;
689
+ // this useEffect is meant to trigger fetching for the first time if it hasn't been done earlier
690
+ // if not yet fetched and not fetchin at the moment
691
+ if (shouldFetch && !isFetching && slug) {
692
+ attemptedToFetch.current = true;
693
+ fetchBySlug({
694
+ experienceTypeId,
695
+ localeCode: locale,
696
+ slug,
697
+ }).catch(() => {
698
+ // noop
699
+ });
700
+ }
701
+ }, [
702
+ experienceTypeId,
703
+ entityStore,
704
+ isFetching,
705
+ fetchBySlug,
706
+ client,
707
+ slug,
708
+ locale,
709
+ previousLocale,
710
+ ]);
711
+ const { resolveDesignValue } = useBreakpoints(entityStore?.breakpoints ?? []);
712
+ if (!entityStore?.experienceEntryFields || !entityStore?.schemaVersion) {
713
+ return null;
714
+ }
715
+ if (!compatibleVersions.includes(entityStore.schemaVersion)) {
716
+ console.warn(`[experiences-sdk-react] Contenful composition schema version: ${entityStore.schemaVersion} does not match the compatible schema versions: ${compatibleVersions}. Aborting.`);
717
+ return null;
718
+ }
719
+ return (jsx(Fragment, { children: entityStore.experienceEntryFields.componentTree.children.map((childNode, index) => (jsx(CompositionBlock, { node: childNode, locale: locale, entityStore: entityStore, resolveDesignValue: resolveDesignValue }, index))) }));
720
+ };
721
+
722
+ const PreviewDeliveryRoot = ({ locale, experience }) => {
723
+ const { entityStore } = experience;
724
+ const { resolveDesignValue } = useBreakpoints(entityStore?.breakpoints ?? []);
725
+ if (!entityStore?.experienceEntryFields || !entityStore?.schemaVersion) {
726
+ return null;
727
+ }
728
+ if (!compatibleVersions.includes(entityStore.schemaVersion)) {
729
+ console.warn(`[experiences-sdk-react] Contentful composition schema version: ${entityStore.schemaVersion} does not match the compatible schema versions: ${compatibleVersions}. Aborting.`);
730
+ return null;
731
+ }
732
+ return (jsx(Fragment, { children: entityStore.experienceEntryFields.componentTree.children.map((childNode, index) => (jsx(CompositionBlock, { node: childNode, locale: locale, entityStore: entityStore, resolveDesignValue: resolveDesignValue }, index))) }));
733
+ };
734
+
735
+ var css_248z = ".cf-error-message {\n margin: 24px;\n font-size: var(--exp-builder-font-size-m);\n font-family: var(--exp-builder-font-stack-primary);\n color: var(--exp-builder-red800);\n padding: 16px;\n background-color: var(--exp-builder-red200);\n}\n\n.cf-error-message .title {\n margin-top: 0;\n font-size: var(--exp-builder-font-size-l);\n}\n\n.cf-error-message .more-details {\n cursor: pointer;\n color: var(--exp-builder-blue700);\n}\n";
736
+ styleInject(css_248z);
737
+
738
+ class ImportedComponentError extends Error {
739
+ }
740
+ class ErrorBoundary extends React.Component {
741
+ constructor(props) {
742
+ super(props);
743
+ this.state = { hasError: false, error: null, errorInfo: null, showErrorDetails: false };
744
+ }
745
+ static getDerivedStateFromError() {
746
+ return { hasError: true };
747
+ }
748
+ componentDidCatch(error, errorInfo) {
749
+ this.setState({ error, errorInfo });
750
+ if (!(error instanceof ImportedComponentError)) {
751
+ sendMessage(OUTGOING_EVENTS.CanvasError, error);
752
+ }
753
+ else {
754
+ throw error;
755
+ }
756
+ }
757
+ render() {
758
+ if (this.state.hasError) {
759
+ return (jsxs("div", { className: "cf-error-message", children: [jsx("h2", { className: "title", children: `Something went wrong while rendering the experience` }), jsxs("div", { children: ["The Experience Builder SDK has encountered an error. It may be that the SDK has not been set up properly or an imported component has thrown this error. Try to refresh the page and find more guidance in our", ' ', jsx("a", { href: "https://www.contentful.com/developers/docs/tutorials/general/experience-builder/", rel: "noreferrer", target: "_blank", children: "documentation" }), "."] }), jsx("br", {}), jsxs("span", { className: "more-details", onClick: () => this.setState((prevState) => ({
760
+ showErrorDetails: !prevState.showErrorDetails,
761
+ })), children: [this.state.showErrorDetails ? 'Hide' : 'See', " details"] }), this.state.showErrorDetails && (jsx("code", { children: this.state.error?.stack?.split('\n').map((i, key) => {
762
+ return jsx("div", { children: i }, key);
763
+ }) }))] }));
764
+ }
765
+ return this.props.children;
766
+ }
767
+ }
768
+ class ImportedComponentErrorBoundary extends React.Component {
769
+ componentDidCatch(error, _errorInfo) {
770
+ const err = new ImportedComponentError(error.message);
771
+ err.stack = error.stack;
772
+ throw err;
773
+ }
774
+ render() {
775
+ return this.props.children;
776
+ }
777
+ }
778
+
779
+ const useInitializeVisualEditor = (params) => {
780
+ const { initialLocale, initialEntities } = params;
781
+ const [locale, setLocale] = useState(initialLocale);
782
+ const hasConnectEventBeenSent = useRef(false);
783
+ // sends component definitions to the web app
784
+ // InternalEvents.COMPONENTS_REGISTERED is triggered by defineComponents function
785
+ useEffect(() => {
786
+ if (!hasConnectEventBeenSent.current) {
787
+ // sending CONNECT but with the registered components now
788
+ sendConnectedEventWithRegisteredComponents();
789
+ hasConnectEventBeenSent.current = true;
790
+ }
791
+ const onComponentsRegistered = () => {
792
+ sendRegisteredComponentsMessage();
793
+ };
794
+ if (typeof window !== 'undefined') {
795
+ window.addEventListener(INTERNAL_EVENTS.ComponentsRegistered, onComponentsRegistered);
796
+ }
797
+ return () => {
798
+ if (typeof window !== 'undefined') {
799
+ window.removeEventListener(INTERNAL_EVENTS.ComponentsRegistered, onComponentsRegistered);
800
+ }
801
+ };
802
+ }, []);
803
+ useEffect(() => {
804
+ setLocale(initialLocale);
805
+ }, [initialLocale]);
806
+ useEffect(() => {
807
+ const onVisualEditorReady = () => {
808
+ window.dispatchEvent(new CustomEvent(INTERNAL_EVENTS.VisualEditorInitialize, {
809
+ detail: {
810
+ componentRegistry,
811
+ designTokens: designTokensRegistry,
812
+ locale,
813
+ entities: initialEntities ?? [],
814
+ },
815
+ }));
816
+ };
817
+ window.addEventListener(VISUAL_EDITOR_EVENTS.Ready, onVisualEditorReady);
818
+ return () => {
819
+ window.removeEventListener(VISUAL_EDITOR_EVENTS.Ready, onVisualEditorReady);
820
+ };
821
+ }, [locale, initialEntities]);
822
+ };
823
+
824
+ const VisualEditorLoader = React.lazy(() => import('./VisualEditorLoader-CY2fhqS5.js'));
825
+ const VisualEditorRoot = ({ visualEditorMode, initialEntities, initialLocale, }) => {
826
+ useInitializeVisualEditor({
827
+ initialLocale,
828
+ initialEntities,
829
+ });
830
+ return (jsx(ErrorBoundary, { children: jsx(Suspense, { fallback: jsx("div", { children: "Loading..." }), children: jsx(VisualEditorLoader, { visualEditorMode: visualEditorMode }) }) }));
831
+ };
832
+
833
+ const ExperienceRoot = ({ locale, experience, slug, visualEditorMode = VisualEditorMode.LazyLoad, }) => {
834
+ const isEditorMode = useDetectEditorMode();
835
+ validateExperienceBuilderConfig({
836
+ locale,
837
+ isEditorMode,
838
+ });
839
+ if (isEditorMode) {
840
+ const entityStore = experience && !isDeprecatedExperience(experience) ? experience.entityStore : undefined;
841
+ return (jsx(VisualEditorRoot, { visualEditorMode: visualEditorMode, initialEntities: entityStore?.entities || [], initialLocale: locale }));
842
+ }
843
+ if (!experience)
844
+ return null;
845
+ if (isDeprecatedExperience(experience)) {
846
+ return (jsx(DeprecatedPreviewDeliveryRoot, { deprecatedExperience: experience, locale: locale, slug: slug }));
847
+ }
848
+ return jsx(PreviewDeliveryRoot, { locale: locale, experience: experience });
849
+ };
850
+
851
+ // Simple state store to store a few things that are needed across the SDK
852
+ if (typeof window !== 'undefined') {
853
+ window.__EB__ = {
854
+ sdkVersion: SDK_VERSION,
855
+ };
856
+ }
857
+
858
+ export { ExperienceRoot, defineComponents, useExperienceBuilder, useFetchById, useFetchBySlug, useFetchExperience };
859
+ //# sourceMappingURL=index.js.map