@contentful/experiences-visual-editor-react 1.39.0-alpha-20250528T1342-e28bc3d.0 → 1.39.0-dev-20250528T1423-593ccdb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,268 +1,38 @@
1
1
  import styleInject from 'style-inject';
2
- import React, { useState, useEffect, useCallback, forwardRef, useMemo, useLayoutEffect, useRef } from 'react';
3
- import { create } from 'zustand';
4
- import { produce } from 'immer';
5
- import { isEqual, omit, isArray, get as get$1 } from 'lodash-es';
2
+ import React, { useEffect, useRef, useState, useCallback, forwardRef, useLayoutEffect, useMemo } from 'react';
6
3
  import { z } from 'zod';
4
+ import { omit, isArray, isEqual, get as get$1 } from 'lodash-es';
7
5
  import md5 from 'md5';
8
6
  import { BLOCKS } from '@contentful/rich-text-types';
7
+ import { create } from 'zustand';
8
+ import { Droppable, Draggable, DragDropContext } from '@hello-pangea/dnd';
9
+ import { produce } from 'immer';
9
10
  import '@contentful/rich-text-react-renderer';
11
+ import { v4 } from 'uuid';
12
+ import { createPortal } from 'react-dom';
13
+ import classNames from 'classnames';
10
14
 
11
15
  var css_248z$b = "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\n/*\n * All of these variables are tokens from Forma-36 and should not be adjusted as these\n * are global variables that may affect multiple places.\n * As our customers may use other design libraries, we try to avoid overlapping global\n * variables by always using the prefix `--exp-builder-` inside this SDK.\n */\n\n:root {\n /* Color tokens from Forma 36: https://f36.contentful.com/tokens/color-system */\n --exp-builder-blue100: #e8f5ff;\n --exp-builder-blue200: #ceecff;\n --exp-builder-blue300: #98cbff;\n --exp-builder-blue400: #40a0ff;\n --exp-builder-blue500: #036fe3;\n --exp-builder-blue600: #0059c8;\n --exp-builder-blue700: #0041ab;\n --exp-builder-blue800: #003298;\n --exp-builder-blue900: #002a8e;\n --exp-builder-gray100: #f7f9fa;\n --exp-builder-gray200: #e7ebee;\n --exp-builder-gray300: #cfd9e0;\n --exp-builder-gray400: #aec1cc;\n --exp-builder-gray500: #67728a;\n --exp-builder-gray600: #5a657c;\n --exp-builder-gray700: #414d63;\n --exp-builder-gray800: #1b273a;\n --exp-builder-gray900: #111b2b;\n --exp-builder-purple600: #6c3ecf;\n --exp-builder-red200: #ffe0e0;\n --exp-builder-red800: #7f0010;\n --exp-builder-color-white: #ffffff;\n --exp-builder-glow-primary: 0px 0px 0px 3px #e8f5ff;\n\n /* RGB colors for applying opacity */\n --exp-builder-blue100-rgb: 232, 245, 255;\n --exp-builder-blue300-rgb: 152, 203, 255;\n\n /* Spacing tokens from Forma 36: https://f36.contentful.com/tokens/spacing */\n --exp-builder-spacing-s: 0.75rem;\n --exp-builder-spacing-2xs: 0.25rem;\n\n /* Typography tokens from Forma 36: https://f36.contentful.com/tokens/typography */\n --exp-builder-font-size-l: 1rem;\n --exp-builder-font-size-m: 0.875rem;\n --exp-builder-font-stack-primary: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n --exp-builder-line-height-condensed: 1.25;\n}\n";
12
16
  styleInject(css_248z$b);
13
17
 
14
- const ROOT_ID = 'root';
15
- var TreeAction;
16
- (function (TreeAction) {
17
- TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
18
- TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
19
- TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
20
- TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
21
- TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
22
- TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
23
- })(TreeAction || (TreeAction = {}));
24
-
25
- function updateNode(nodeId, updatedNode, node) {
26
- if (node.data.id === nodeId) {
27
- node.data = updatedNode.data;
28
- return;
29
- }
30
- node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
31
- }
32
- function replaceNode(indexToReplace, updatedNode, node) {
33
- if (node.data.id === updatedNode.parentId) {
34
- node.children = [
35
- ...node.children.slice(0, indexToReplace),
36
- updatedNode,
37
- ...node.children.slice(indexToReplace + 1),
38
- ];
39
- return;
40
- }
41
- node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
42
- }
43
- function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
44
- if (node.data.id === parentNodeId) {
45
- const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
46
- node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
47
- return;
48
- }
49
- node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
50
- }
51
- function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
52
- if (node.data.id === parentNodeId) {
53
- node.children = [
54
- ...node.children.slice(0, indexToAdd),
55
- nodeToAdd,
56
- ...node.children.slice(indexToAdd),
57
- ];
58
- return;
59
- }
60
- node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
61
- }
62
-
63
- function getItemFromTree(id, node) {
64
- // Check if the current node's id matches the search id
65
- if (node.data.id === id) {
66
- return node;
67
- }
68
- // Recursively search through each child
69
- for (const child of node.children) {
70
- const foundNode = getItemFromTree(id, child);
71
- if (foundNode) {
72
- // Node found in children
73
- return foundNode;
74
- }
75
- }
76
- // If the node is not found in this branch of the tree, return undefined
77
- return undefined;
78
- }
79
- const getItem = (selector, tree) => {
80
- return getItemFromTree(selector.id, {
81
- type: 'block',
82
- data: {
83
- id: ROOT_ID,
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- },
86
- children: tree.root.children,
87
- });
88
- };
89
-
90
- function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
91
- if (nodeAdded) {
92
- return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
93
- }
94
- const item = getItem({ id: child.data.id }, tree);
95
- if (item) {
96
- const parentNode = getItem({ id: item.parentId }, tree);
97
- if (!parentNode) {
98
- return null;
99
- }
100
- const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
101
- return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
102
- }
103
- return {
104
- type: TreeAction.REPLACE_NODE,
105
- originalId: currentNode.children[index].data.id,
106
- indexToReplace: index,
107
- node: child,
108
- };
109
- }
110
- function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
111
- if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
112
- return {
113
- type: TreeAction.REORDER_NODE,
114
- sourceIndex: originalIndex,
115
- destinationIndex: index,
116
- parentNodeId,
117
- };
118
- }
119
- return null;
120
- }
121
- function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
122
- // In the end, this map contains the list of nodes that are not present
123
- // in the updated tree and must be removed
124
- const map = new Map();
125
- if (!currentNode || !updatedNode) {
126
- return differences;
127
- }
128
- // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
129
- const currentNodeCount = currentNode.children.length;
130
- const updatedNodeCount = updatedNode.children.length;
131
- const nodeRemoved = currentNodeCount > updatedNodeCount;
132
- const nodeAdded = currentNodeCount < updatedNodeCount;
133
- const parentNodeId = updatedNode.data.id;
134
- /**
135
- * The data of the current node has changed, we need to update
136
- * this node to reflect the data change. (design, content, unbound values)
137
- */
138
- if (!isEqual(currentNode.data, updatedNode.data)) {
139
- differences.push({
140
- type: TreeAction.UPDATE_NODE,
141
- nodeId: currentNode.data.id,
142
- node: updatedNode,
143
- });
144
- }
145
- // Map children of the first tree by their ID
146
- currentNode.children.forEach((child, index) => map.set(child.data.id, index));
147
- // Compare with the second tree
148
- updatedNode.children.forEach((child, index) => {
149
- const childId = child.data.id;
150
- // The original tree does not have this node in the updated tree.
151
- if (!map.has(childId)) {
152
- const diff = missingNodeAction({
153
- index,
154
- child,
155
- nodeAdded,
156
- parentNodeId,
157
- tree: originalTree,
158
- currentNode,
159
- });
160
- if (diff?.type === TreeAction.REPLACE_NODE) {
161
- // Remove it from the deletion map to avoid adding another REMOVE_NODE action
162
- map.delete(diff.originalId);
163
- }
164
- return differences.push(diff);
165
- }
166
- const originalIndex = map.get(childId);
167
- const diff = matchingNodeAction({
168
- index,
169
- originalIndex,
170
- nodeAdded,
171
- nodeRemoved,
172
- parentNodeId,
173
- });
174
- differences.push(diff);
175
- map.delete(childId);
176
- compareNodes({
177
- currentNode: currentNode.children[originalIndex],
178
- updatedNode: child,
179
- originalTree,
180
- differences,
181
- });
182
- });
183
- map.forEach((index, key) => {
184
- // If the node count of the entire tree doesn't signify
185
- // a node was removed, don't add that as a diff
186
- if (!nodeRemoved) {
187
- return;
188
- }
189
- // Remaining nodes in the map are removed in the second tree
190
- differences.push({
191
- type: TreeAction.REMOVE_NODE,
192
- indexToRemove: index,
193
- parentNodeId,
194
- idToRemove: key,
195
- });
196
- });
197
- return differences;
198
- }
199
- function getTreeDiffs(tree1, tree2, originalTree) {
200
- const differences = [];
201
- compareNodes({
202
- currentNode: tree1,
203
- updatedNode: tree2,
204
- originalTree,
205
- differences,
206
- });
207
- return differences.filter((diff) => diff);
208
- }
209
-
210
- /** @deprecated will be removed when dropping backward compatibility for old DND */
211
- const OUTGOING_EVENTS = {
212
- Connected: 'connected',
213
- DesignTokens: 'registerDesignTokens',
214
- RegisteredBreakpoints: 'registeredBreakpoints',
215
- /** @deprecated will be removed when dropping backward compatibility for old DND */
216
- MouseMove: 'mouseMove',
217
- /** @deprecated will be removed when dropping backward compatibility for old DND */
218
- ComponentSelected: 'componentSelected',
219
- RegisteredComponents: 'registeredComponents',
220
- RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
221
- CanvasReload: 'canvasReload',
222
- /** @deprecated will be removed when dropping backward compatibility for old DND */
223
- UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
224
- /** @deprecated will be removed when dropping backward compatibility for old DND */
225
- CanvasScroll: 'canvasScrolling',
226
- CanvasError: 'canvasError',
227
- /** @deprecated will be removed when dropping backward compatibility for old DND */
228
- OutsideCanvasClick: 'outsideCanvasClick',
229
- SDKFeatures: 'sdkFeatures',
230
- RequestEntities: 'REQUEST_ENTITIES',
231
- };
232
18
  const INCOMING_EVENTS$1 = {
233
19
  RequestEditorMode: 'requestEditorMode',
234
20
  RequestReadOnlyMode: 'requestReadOnlyMode',
235
21
  ExperienceUpdated: 'componentTreeUpdated',
236
- /** @deprecated will be removed when dropping backward compatibility for old DND */
237
22
  ComponentDraggingChanged: 'componentDraggingChanged',
238
- /** @deprecated will be removed when dropping backward compatibility for old DND */
239
23
  ComponentDragCanceled: 'componentDragCanceled',
240
- /** @deprecated will be removed when dropping backward compatibility for old DND */
241
24
  ComponentDragStarted: 'componentDragStarted',
242
- /** @deprecated will be removed when dropping backward compatibility for old DND */
243
25
  ComponentDragEnded: 'componentDragEnded',
244
- /** @deprecated will be removed when dropping backward compatibility for old DND */
245
26
  ComponentMoveEnded: 'componentMoveEnded',
246
- /** @deprecated will be removed when dropping backward compatibility for old DND */
247
27
  CanvasResized: 'canvasResized',
248
- /** @deprecated will be removed when dropping backward compatibility for old DND */
249
28
  SelectComponent: 'selectComponent',
250
- /** @deprecated will be removed when dropping backward compatibility for old DND */
251
29
  HoverComponent: 'hoverComponent',
252
30
  UpdatedEntity: 'updatedEntity',
253
31
  AssembliesAdded: 'assembliesAdded',
254
32
  AssembliesRegistered: 'assembliesRegistered',
255
- /** @deprecated will be removed when dropping backward compatibility for old DND */
256
33
  MouseMove: 'mouseMove',
257
34
  RequestedEntities: 'REQUESTED_ENTITIES',
258
35
  };
259
- const INTERNAL_EVENTS = {
260
- ComponentsRegistered: 'cfComponentsRegistered',
261
- VisualEditorInitialize: 'cfVisualEditorInitialize',
262
- };
263
- const VISUAL_EDITOR_EVENTS = {
264
- Ready: 'cfVisualEditorReady',
265
- };
266
36
  /**
267
37
  * These modes are ONLY intended to be internally used within the context of
268
38
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -274,56 +44,7 @@ var StudioCanvasMode$3;
274
44
  StudioCanvasMode["EDITOR"] = "editorMode";
275
45
  StudioCanvasMode["NONE"] = "none";
276
46
  })(StudioCanvasMode$3 || (StudioCanvasMode$3 = {}));
277
- const ASSEMBLY_NODE_TYPE = 'assembly';
278
- const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
279
- const EMPTY_CONTAINER_HEIGHT$1 = '80px';
280
- const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
281
- var PostMessageMethods$3;
282
- (function (PostMessageMethods) {
283
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
284
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
285
- })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
286
-
287
- /** @deprecated will be removed when dropping backward compatibility for old DND */
288
- const INCOMING_EVENTS = {
289
- RequestEditorMode: 'requestEditorMode',
290
- RequestReadOnlyMode: 'requestReadOnlyMode',
291
- ExperienceUpdated: 'componentTreeUpdated',
292
- /** @deprecated will be removed when dropping backward compatibility for old DND */
293
- ComponentDraggingChanged: 'componentDraggingChanged',
294
- /** @deprecated will be removed when dropping backward compatibility for old DND */
295
- ComponentDragCanceled: 'componentDragCanceled',
296
- /** @deprecated will be removed when dropping backward compatibility for old DND */
297
- ComponentDragStarted: 'componentDragStarted',
298
- /** @deprecated will be removed when dropping backward compatibility for old DND */
299
- ComponentDragEnded: 'componentDragEnded',
300
- /** @deprecated will be removed when dropping backward compatibility for old DND */
301
- ComponentMoveEnded: 'componentMoveEnded',
302
- /** @deprecated will be removed when dropping backward compatibility for old DND */
303
- CanvasResized: 'canvasResized',
304
- /** @deprecated will be removed when dropping backward compatibility for old DND */
305
- SelectComponent: 'selectComponent',
306
- /** @deprecated will be removed when dropping backward compatibility for old DND */
307
- HoverComponent: 'hoverComponent',
308
- UpdatedEntity: 'updatedEntity',
309
- AssembliesAdded: 'assembliesAdded',
310
- AssembliesRegistered: 'assembliesRegistered',
311
- /** @deprecated will be removed when dropping backward compatibility for old DND */
312
- MouseMove: 'mouseMove',
313
- RequestedEntities: 'REQUESTED_ENTITIES',
314
- };
315
- /**
316
- * These modes are ONLY intended to be internally used within the context of
317
- * editing an experience inside of Contentful Studio. i.e. these modes
318
- * intentionally do not include preview/delivery modes.
319
- */
320
- var StudioCanvasMode$2;
321
- (function (StudioCanvasMode) {
322
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
323
- StudioCanvasMode["EDITOR"] = "editorMode";
324
- StudioCanvasMode["NONE"] = "none";
325
- })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
326
- const CONTENTFUL_COMPONENTS$1 = {
47
+ const CONTENTFUL_COMPONENTS$2 = {
327
48
  section: {
328
49
  id: 'contentful-section',
329
50
  name: 'Section',
@@ -369,6 +90,9 @@ const CONTENTFUL_COMPONENTS$1 = {
369
90
  name: 'Carousel',
370
91
  },
371
92
  };
93
+ const ASSEMBLY_NODE_TYPE$1 = 'assembly';
94
+ const ASSEMBLY_DEFAULT_CATEGORY$1 = 'Assemblies';
95
+ const ASSEMBLY_BLOCK_NODE_TYPE$1 = 'assemblyBlock';
372
96
  const CF_STYLE_ATTRIBUTES = [
373
97
  'cfVisibility',
374
98
  'cfHorizontalAlignment',
@@ -411,24 +135,30 @@ const CF_STYLE_ATTRIBUTES = [
411
135
  'cfBackgroundImageAlignmentVertical',
412
136
  'cfBackgroundImageAlignmentHorizontal',
413
137
  ];
414
- const EMPTY_CONTAINER_HEIGHT = '80px';
138
+ const EMPTY_CONTAINER_HEIGHT$1 = '80px';
415
139
  const DEFAULT_IMAGE_WIDTH = '500px';
416
- var PostMessageMethods$2;
140
+ var PostMessageMethods$3;
417
141
  (function (PostMessageMethods) {
418
142
  PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
419
143
  PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
420
- })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
144
+ })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
421
145
  const SUPPORTED_IMAGE_FORMATS = ['jpg', 'png', 'webp', 'gif', 'avif'];
422
146
 
423
147
  const structureComponentIds = new Set([
424
- CONTENTFUL_COMPONENTS$1.section.id,
425
- CONTENTFUL_COMPONENTS$1.columns.id,
426
- CONTENTFUL_COMPONENTS$1.container.id,
427
- CONTENTFUL_COMPONENTS$1.singleColumn.id,
148
+ CONTENTFUL_COMPONENTS$2.section.id,
149
+ CONTENTFUL_COMPONENTS$2.columns.id,
150
+ CONTENTFUL_COMPONENTS$2.container.id,
151
+ CONTENTFUL_COMPONENTS$2.singleColumn.id,
428
152
  ]);
429
- const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$1).map((component) => component.id));
153
+ const patternTypes = new Set([ASSEMBLY_NODE_TYPE$1, ASSEMBLY_BLOCK_NODE_TYPE$1]);
154
+ const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$2).map((component) => component.id));
155
+ const isPatternComponent = (type) => patternTypes.has(type ?? '');
430
156
  const isContentfulStructureComponent = (componentId) => structureComponentIds.has((componentId ?? ''));
431
157
  const isContentfulComponent = (componentId) => allContentfulComponentIds.has((componentId ?? ''));
158
+ const isComponentAllowedOnRoot = ({ type, category, componentId }) => isPatternComponent(type) ||
159
+ category === ASSEMBLY_DEFAULT_CATEGORY$1 ||
160
+ isContentfulStructureComponent(componentId) ||
161
+ componentId === CONTENTFUL_COMPONENTS$2.divider.id;
432
162
  const isStructureWithRelativeHeight = (componentId, height) => {
433
163
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
434
164
  };
@@ -867,17 +597,16 @@ const PrimitiveValueSchema$1 = z.union([
867
597
  z.record(z.any(), z.any()),
868
598
  z.undefined(),
869
599
  ]);
600
+ const UsedComponentsSchema$1 = z.array(z.object({
601
+ sys: z.object({
602
+ type: z.literal('Link'),
603
+ id: z.string(),
604
+ linkType: z.literal('Entry'),
605
+ }),
606
+ }));
870
607
  const uuidKeySchema$1 = z
871
608
  .string()
872
609
  .regex(/^[a-zA-Z0-9-_]{1,21}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,21}$/' });
873
- /**
874
- * Property keys for imported components have a limit of 32 characters (to be implemented) while
875
- * property keys for patterns have a limit of 54 characters (<32-char-variabl-name>_<21-char-nanoid-id>).
876
- * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
877
- */
878
- const propertyKeySchema$1 = z
879
- .string()
880
- .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
881
610
  const DataSourceSchema$1 = z.record(uuidKeySchema$1, z.object({
882
611
  sys: z.object({
883
612
  type: z.literal('Link'),
@@ -885,7 +614,62 @@ const DataSourceSchema$1 = z.record(uuidKeySchema$1, z.object({
885
614
  linkType: z.enum(['Entry', 'Asset']),
886
615
  }),
887
616
  }));
617
+ const UnboundValuesSchema$1 = z.record(uuidKeySchema$1, z.object({
618
+ value: PrimitiveValueSchema$1,
619
+ }));
620
+ /**
621
+ * Property keys for imported components have a limit of 32 characters (to be implemented) while
622
+ * property keys for patterns have a limit of 54 characters (<32-char-variable-name>_<21-char-nanoid-id>).
623
+ * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
624
+ */
625
+ const propertyKeySchema$1 = z
626
+ .string()
627
+ .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
628
+ const ComponentTreeNodeIdSchema$1 = z
629
+ .string()
630
+ .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
631
+ const breakpointsRefinement$1 = (value, ctx) => {
632
+ if (!value.length || value[0].query !== '*') {
633
+ ctx.addIssue({
634
+ code: z.ZodIssueCode.custom,
635
+ message: `The first breakpoint should include the following attributes: { "query": "*" }`,
636
+ });
637
+ }
638
+ const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
639
+ // check if the current breakpoint id is found in the rest of the array
640
+ const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
641
+ return breakpointIndex !== currentBreakpointIndex;
642
+ });
643
+ if (hasDuplicateIds) {
644
+ ctx.addIssue({
645
+ code: z.ZodIssueCode.custom,
646
+ message: `Breakpoint IDs must be unique`,
647
+ });
648
+ }
649
+ // Extract the queries boundary by removing the special characters around it
650
+ const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
651
+ // sort updates queries array in place so we need to create a copy
652
+ const originalQueries = [...queries];
653
+ queries.sort((q1, q2) => {
654
+ if (q1 === '*') {
655
+ return -1;
656
+ }
657
+ if (q2 === '*') {
658
+ return 1;
659
+ }
660
+ return q1 > q2 ? -1 : 1;
661
+ });
662
+ if (originalQueries.join('') !== queries.join('')) {
663
+ ctx.addIssue({
664
+ code: z.ZodIssueCode.custom,
665
+ message: `Breakpoints should be ordered from largest to smallest pixel value`,
666
+ });
667
+ }
668
+ };
888
669
  const ValuesByBreakpointSchema$1 = z.record(z.lazy(() => PrimitiveValueSchema$1));
670
+ const BindingSourceTypeEnumSchema$1 = z
671
+ .array(z.enum(['entry', 'asset', 'manual', 'experience']))
672
+ .nonempty();
889
673
  const DesignValueSchema$1 = z
890
674
  .object({
891
675
  type: z.literal('DesignValue'),
@@ -918,8 +702,6 @@ const ComponentValueSchema$1 = z
918
702
  key: z.string(),
919
703
  })
920
704
  .strict();
921
- // TODO: finalize schema structure before release
922
- // https://contentful.atlassian.net/browse/LUMOS-523
923
705
  const NoValueSchema$1 = z.object({ type: z.literal('NoValue') }).strict();
924
706
  const ComponentPropertyValueSchema$1 = z.discriminatedUnion('type', [
925
707
  DesignValueSchema$1,
@@ -931,41 +713,12 @@ const ComponentPropertyValueSchema$1 = z.discriminatedUnion('type', [
931
713
  ]);
932
714
  // TODO: finalize schema structure before release
933
715
  // https://contentful.atlassian.net/browse/LUMOS-523
934
- const VariableMappingSchema$1 = z.object({
935
- patternPropertyDefinitionId: propertyKeySchema$1,
936
- type: z.literal('ContentTypeMapping'),
937
- pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
938
- });
939
- const VariableMappingsSchema$1 = z.record(propertyKeySchema$1, VariableMappingSchema$1);
940
- // TODO: finalize schema structure before release
941
- // https://contentful.atlassian.net/browse/LUMOS-523
942
- const PatternPropertyDefinitionSchema$1 = z.object({
943
- defaultValue: z
944
- .record(z.string(), z.object({
945
- sys: z.object({
946
- type: z.literal('Link'),
947
- id: z.string(),
948
- linkType: z.enum(['Entry']),
949
- }),
950
- }))
951
- .optional(),
952
- contentTypes: z.record(z.string(), z.object({
953
- sys: z.object({
954
- type: z.literal('Link'),
955
- id: z.string(),
956
- linkType: z.enum(['ContentType']),
957
- }),
958
- })),
959
- });
960
- const PatternPropertyDefinitionsSchema$1 = z.record(propertyKeySchema$1, PatternPropertyDefinitionSchema$1);
961
- // TODO: finalize schema structure before release
962
- // https://contentful.atlassian.net/browse/LUMOS-523
963
716
  const PatternPropertySchema$1 = z.object({
964
717
  type: z.literal('BoundValue'),
965
718
  path: z.string(),
966
719
  contentType: z.string(),
967
720
  });
968
- const PatternPropertysSchema$1 = z.record(propertyKeySchema$1, PatternPropertySchema$1);
721
+ const PatternPropertiesSchema$1 = z.record(propertyKeySchema$1, PatternPropertySchema$1);
969
722
  const BreakpointSchema$1 = z
970
723
  .object({
971
724
  id: propertyKeySchema$1,
@@ -975,12 +728,6 @@ const BreakpointSchema$1 = z
975
728
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
976
729
  })
977
730
  .strict();
978
- const UnboundValuesSchema$1 = z.record(uuidKeySchema$1, z.object({
979
- value: PrimitiveValueSchema$1,
980
- }));
981
- const ComponentTreeNodeIdSchema$1 = z
982
- .string()
983
- .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
984
731
  // Use helper schema to define a recursive schema with its type correctly below
985
732
  const BaseComponentTreeNodeSchema$1 = z.object({
986
733
  id: ComponentTreeNodeIdSchema$1.optional(),
@@ -988,14 +735,8 @@ const BaseComponentTreeNodeSchema$1 = z.object({
988
735
  displayName: z.string().optional(),
989
736
  slotId: z.string().optional(),
990
737
  variables: z.record(propertyKeySchema$1, ComponentPropertyValueSchema$1),
991
- patternProperties: PatternPropertysSchema$1.optional(),
992
- });
993
- const ComponentTreeNodeSchema$1 = BaseComponentTreeNodeSchema$1.extend({
994
- children: z.lazy(() => ComponentTreeNodeSchema$1.array()),
738
+ patternProperties: PatternPropertiesSchema$1.optional(),
995
739
  });
996
- const BindingSourceTypeEnumSchema$1 = z
997
- .array(z.enum(['entry', 'asset', 'manual', 'experience']))
998
- .nonempty();
999
740
  const ComponentVariableSchema$1 = z.object({
1000
741
  displayName: z.string().optional(),
1001
742
  type: DefinitionPropertyTypeSchema$1,
@@ -1016,8 +757,25 @@ const ComponentVariableSchema$1 = z.object({
1016
757
  })
1017
758
  .optional(),
1018
759
  });
1019
- const ComponentVariablesSchema$1 = z.record(z.string().regex(/^[a-zA-Z0-9-_]{1,54}$/), // Here the key is <variableName>_<nanoidId> so we need to allow for a longer length
1020
- ComponentVariableSchema$1);
760
+ const ComponentTreeNodeSchema$1 = BaseComponentTreeNodeSchema$1.extend({
761
+ children: z.lazy(() => ComponentTreeNodeSchema$1.array()),
762
+ });
763
+ const ComponentTreeSchema$1 = z
764
+ .object({
765
+ breakpoints: z.array(BreakpointSchema$1).superRefine(breakpointsRefinement$1),
766
+ children: z.array(ComponentTreeNodeSchema$1),
767
+ schemaVersion: SchemaVersions$1,
768
+ })
769
+ .strict();
770
+ const localeWrapper$1 = (fieldSchema) => z.record(z.string(), fieldSchema);
771
+
772
+ z.object({
773
+ componentTree: localeWrapper$1(ComponentTreeSchema$1),
774
+ dataSource: localeWrapper$1(DataSourceSchema$1),
775
+ unboundValues: localeWrapper$1(UnboundValuesSchema$1),
776
+ usedComponents: localeWrapper$1(UsedComponentsSchema$1).optional(),
777
+ });
778
+
1021
779
  const THUMBNAIL_IDS$1 = [
1022
780
  'columns',
1023
781
  'columnsPlusRight',
@@ -1043,6 +801,37 @@ const THUMBNAIL_IDS$1 = [
1043
801
  'textColumns',
1044
802
  'duplex',
1045
803
  ];
804
+ // TODO: finalize schema structure before release
805
+ // https://contentful.atlassian.net/browse/LUMOS-523
806
+ const VariableMappingSchema$1 = z.object({
807
+ patternPropertyDefinitionId: propertyKeySchema$1,
808
+ type: z.literal('ContentTypeMapping'),
809
+ pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
810
+ });
811
+ // TODO: finalize schema structure before release
812
+ // https://contentful.atlassian.net/browse/LUMOS-523
813
+ const PatternPropertyDefinitionSchema$1 = z.object({
814
+ defaultValue: z
815
+ .record(z.string(), z.object({
816
+ sys: z.object({
817
+ type: z.literal('Link'),
818
+ id: z.string(),
819
+ linkType: z.enum(['Entry']),
820
+ }),
821
+ }))
822
+ .optional(),
823
+ contentTypes: z.record(z.string(), z.object({
824
+ sys: z.object({
825
+ type: z.literal('Link'),
826
+ id: z.string(),
827
+ linkType: z.enum(['ContentType']),
828
+ }),
829
+ })),
830
+ });
831
+ const PatternPropertyDefinitionsSchema$1 = z.record(propertyKeySchema$1, PatternPropertyDefinitionSchema$1);
832
+ const VariableMappingsSchema$1 = z.record(propertyKeySchema$1, VariableMappingSchema$1);
833
+ const ComponentVariablesSchema$1 = z.record(z.string().regex(/^[a-zA-Z0-9-_]{1,54}$/), // Here the key is <variableName>_<nanoidId> so we need to allow for a longer length
834
+ ComponentVariableSchema$1);
1046
835
  const ComponentSettingsSchema$1 = z.object({
1047
836
  variableDefinitions: ComponentVariablesSchema$1,
1048
837
  thumbnailId: z.enum(THUMBNAIL_IDS$1).optional(),
@@ -1050,65 +839,12 @@ const ComponentSettingsSchema$1 = z.object({
1050
839
  variableMappings: VariableMappingsSchema$1.optional(),
1051
840
  patternPropertyDefinitions: PatternPropertyDefinitionsSchema$1.optional(),
1052
841
  });
1053
- const UsedComponentsSchema$1 = z.array(z.object({
1054
- sys: z.object({
1055
- type: z.literal('Link'),
1056
- id: z.string(),
1057
- linkType: z.literal('Entry'),
1058
- }),
1059
- }));
1060
- const breakpointsRefinement$1 = (value, ctx) => {
1061
- if (!value.length || value[0].query !== '*') {
1062
- ctx.addIssue({
1063
- code: z.ZodIssueCode.custom,
1064
- message: `The first breakpoint should include the following attributes: { "query": "*" }`,
1065
- });
1066
- }
1067
- const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
1068
- // check if the current breakpoint id is found in the rest of the array
1069
- const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
1070
- return breakpointIndex !== currentBreakpointIndex;
1071
- });
1072
- if (hasDuplicateIds) {
1073
- ctx.addIssue({
1074
- code: z.ZodIssueCode.custom,
1075
- message: `Breakpoint IDs must be unique`,
1076
- });
1077
- }
1078
- // Extract the queries boundary by removing the special characters around it
1079
- const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
1080
- // sort updates queries array in place so we need to create a copy
1081
- const originalQueries = [...queries];
1082
- queries.sort((q1, q2) => {
1083
- if (q1 === '*') {
1084
- return -1;
1085
- }
1086
- if (q2 === '*') {
1087
- return 1;
1088
- }
1089
- return q1 > q2 ? -1 : 1;
1090
- });
1091
- if (originalQueries.join('') !== queries.join('')) {
1092
- ctx.addIssue({
1093
- code: z.ZodIssueCode.custom,
1094
- message: `Breakpoints should be ordered from largest to smallest pixel value`,
1095
- });
1096
- }
1097
- };
1098
- const ComponentTreeSchema$1 = z
1099
- .object({
1100
- breakpoints: z.array(BreakpointSchema$1).superRefine(breakpointsRefinement$1),
1101
- children: z.array(ComponentTreeNodeSchema$1),
1102
- schemaVersion: SchemaVersions$1,
1103
- })
1104
- .strict();
1105
- const localeWrapper$1 = (fieldSchema) => z.record(z.string(), fieldSchema);
1106
842
  z.object({
1107
843
  componentTree: localeWrapper$1(ComponentTreeSchema$1),
1108
844
  dataSource: localeWrapper$1(DataSourceSchema$1),
1109
845
  unboundValues: localeWrapper$1(UnboundValuesSchema$1),
1110
846
  usedComponents: localeWrapper$1(UsedComponentsSchema$1).optional(),
1111
- componentSettings: localeWrapper$1(ComponentSettingsSchema$1).optional(),
847
+ componentSettings: localeWrapper$1(ComponentSettingsSchema$1),
1112
848
  });
1113
849
 
1114
850
  z.object({
@@ -1392,6 +1128,51 @@ let DebugLogger$1 = class DebugLogger {
1392
1128
  DebugLogger$1.instance = null;
1393
1129
  DebugLogger$1.getInstance();
1394
1130
 
1131
+ const findOutermostCoordinates = (first, second) => {
1132
+ return {
1133
+ top: Math.min(first.top, second.top),
1134
+ right: Math.max(first.right, second.right),
1135
+ bottom: Math.max(first.bottom, second.bottom),
1136
+ left: Math.min(first.left, second.left),
1137
+ };
1138
+ };
1139
+ const getElementCoordinates = (element) => {
1140
+ const rect = element.getBoundingClientRect();
1141
+ /**
1142
+ * If element does not have children, or element has it's own width or height,
1143
+ * return the element's coordinates.
1144
+ */
1145
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
1146
+ return rect;
1147
+ }
1148
+ const rects = [];
1149
+ /**
1150
+ * If element has children, or element does not have it's own width and height,
1151
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
1152
+ * as the coordinate of the element.
1153
+ *
1154
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
1155
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
1156
+ */
1157
+ for (const child of element.children) {
1158
+ const childRect = getElementCoordinates(child);
1159
+ if (childRect.width !== 0 || childRect.height !== 0) {
1160
+ const { top, right, bottom, left } = childRect;
1161
+ rects.push({ top, right, bottom, left });
1162
+ }
1163
+ }
1164
+ if (rects.length === 0) {
1165
+ return rect;
1166
+ }
1167
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
1168
+ return DOMRect.fromRect({
1169
+ x: left,
1170
+ y: top,
1171
+ height: bottom - top,
1172
+ width: right - left,
1173
+ });
1174
+ };
1175
+
1395
1176
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1396
1177
  const isLinkToAsset = (variable) => {
1397
1178
  if (!variable)
@@ -1803,7 +1584,7 @@ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1803
1584
  if (children.length) {
1804
1585
  return '100%';
1805
1586
  }
1806
- return EMPTY_CONTAINER_HEIGHT;
1587
+ return EMPTY_CONTAINER_HEIGHT$1;
1807
1588
  };
1808
1589
 
1809
1590
  function getOptimizedImageUrl(url, width, quality, format) {
@@ -2234,10 +2015,10 @@ const tryParseMessage = (event) => {
2234
2015
  throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
2235
2016
  }
2236
2017
  // check eventData.eventType
2237
- const supportedEventTypes = Object.values(INCOMING_EVENTS);
2018
+ const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
2238
2019
  if (!supportedEventTypes.includes(eventData.eventType)) {
2239
2020
  // Expected message: This message is handled in the EntityStore to store fetched entities
2240
- if (eventData.eventType !== PostMessageMethods$2.REQUESTED_ENTITIES) {
2021
+ if (eventData.eventType !== PostMessageMethods$3.REQUESTED_ENTITIES) {
2241
2022
  throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
2242
2023
  }
2243
2024
  }
@@ -2502,7 +2283,7 @@ class EditorEntityStore extends EntityStoreBase {
2502
2283
  }
2503
2284
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2504
2285
  const newPromise = new Promise((resolve, reject) => {
2505
- const unsubscribe = this.subscribe(PostMessageMethods$2.REQUESTED_ENTITIES, (message) => {
2286
+ const unsubscribe = this.subscribe(PostMessageMethods$3.REQUESTED_ENTITIES, (message) => {
2506
2287
  const messageIds = [
2507
2288
  ...message.entities.map((entity) => entity.sys.id),
2508
2289
  ...(message.missingEntityIds ?? []),
@@ -2524,7 +2305,7 @@ class EditorEntityStore extends EntityStoreBase {
2524
2305
  ids.forEach((id) => this.cleanupPromise(id));
2525
2306
  unsubscribe();
2526
2307
  }, this.timeoutDuration);
2527
- this.sendMessage(PostMessageMethods$2.REQUEST_ENTITIES, {
2308
+ this.sendMessage(PostMessageMethods$3.REQUEST_ENTITIES, {
2528
2309
  entityIds: missing,
2529
2310
  entityType: type,
2530
2311
  locale: this.locale,
@@ -2567,133 +2348,626 @@ class EditorEntityStore extends EntityStoreBase {
2567
2348
  return this.fetchEntity('Entry', ids, skipCache);
2568
2349
  }
2569
2350
  }
2570
-
2571
- function transformAssetFileToUrl(fieldValue) {
2572
- return fieldValue && typeof fieldValue == 'object' && fieldValue.url
2573
- ? fieldValue.url
2574
- : fieldValue;
2351
+
2352
+ function transformAssetFileToUrl(fieldValue) {
2353
+ return fieldValue && typeof fieldValue == 'object' && fieldValue.url
2354
+ ? fieldValue.url
2355
+ : fieldValue;
2356
+ }
2357
+
2358
+ // The default of 3s in the EditorEntityStore is sometimes timing out and
2359
+ // leads to not rendering bound content and assemblies.
2360
+ const REQUEST_TIMEOUT = 10000;
2361
+ class EditorModeEntityStore extends EditorEntityStore {
2362
+ constructor({ entities, locale }) {
2363
+ console.debug(`[experiences-sdk-react] Initializing editor entity store with ${entities.length} entities for locale ${locale}.`, { entities });
2364
+ const subscribe = (method, cb) => {
2365
+ const handleMessage = (event) => {
2366
+ const data = JSON.parse(event.data);
2367
+ if (typeof data !== 'object' || !data)
2368
+ return;
2369
+ if (data.source !== 'composability-app')
2370
+ return;
2371
+ if (data.eventType === method) {
2372
+ cb(data.payload);
2373
+ }
2374
+ };
2375
+ if (typeof window !== 'undefined') {
2376
+ window.addEventListener('message', handleMessage);
2377
+ }
2378
+ return () => {
2379
+ if (typeof window !== 'undefined') {
2380
+ window.removeEventListener('message', handleMessage);
2381
+ }
2382
+ };
2383
+ };
2384
+ super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
2385
+ this.locale = locale;
2386
+ }
2387
+ /**
2388
+ * This function collects and returns the list of requested entries and assets. Additionally, it checks
2389
+ * upfront whether any async fetching logic is actually happening. If not, it returns a plain `false` value, so we
2390
+ * can detect this early and avoid unnecessary re-renders.
2391
+ * @param entityLinks
2392
+ * @returns false if no async fetching is happening, otherwise a promise that resolves when all entities are fetched
2393
+ */
2394
+ async fetchEntities({ missingEntryIds, missingAssetIds, skipCache = false, }) {
2395
+ // Entries and assets will be stored in entryMap and assetMap
2396
+ await Promise.all([
2397
+ super.fetchEntries(missingEntryIds, skipCache),
2398
+ super.fetchAssets(missingAssetIds, skipCache),
2399
+ ]);
2400
+ }
2401
+ getMissingEntityIds(entityLinks) {
2402
+ const entryLinks = entityLinks.filter((link) => link.sys?.linkType === 'Entry');
2403
+ const assetLinks = entityLinks.filter((link) => link.sys?.linkType === 'Asset');
2404
+ const uniqueEntryIds = [...new Set(entryLinks.map((link) => link.sys.id))];
2405
+ const uniqueAssetIds = [...new Set(assetLinks.map((link) => link.sys.id))];
2406
+ const { missing: missingEntryIds } = this.getEntitiesFromMap('Entry', uniqueEntryIds);
2407
+ const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
2408
+ return { missingEntryIds, missingAssetIds };
2409
+ }
2410
+ getValue(entityLinkOrEntity, path) {
2411
+ const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2412
+ if (!entity) {
2413
+ return;
2414
+ }
2415
+ const fieldValue = get(entity, path);
2416
+ return transformAssetFileToUrl(fieldValue);
2417
+ }
2418
+ }
2419
+
2420
+ var VisualEditorMode$1;
2421
+ (function (VisualEditorMode) {
2422
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
2423
+ VisualEditorMode["InjectScript"] = "injectScript";
2424
+ })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2425
+
2426
+ class DeepReference {
2427
+ constructor({ path, dataSource }) {
2428
+ const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2429
+ this.originalPath = path;
2430
+ this.entityId = dataSource[key].sys.id;
2431
+ this.entityLink = dataSource[key];
2432
+ this.field = field;
2433
+ this.referentField = referentField;
2434
+ }
2435
+ get headEntityId() {
2436
+ return this.entityId;
2437
+ }
2438
+ /**
2439
+ * Extracts referent from the path, using EntityStore as source of
2440
+ * entities during the resolution path.
2441
+ */
2442
+ extractReferent(entityStore) {
2443
+ const headEntity = entityStore.getEntityFromLink(this.entityLink);
2444
+ const maybeReferentLink = headEntity?.fields[this.field];
2445
+ if (undefined === maybeReferentLink) {
2446
+ // field references nothing (or even field doesn't exist)
2447
+ return undefined;
2448
+ }
2449
+ if (!isLink(maybeReferentLink)) {
2450
+ // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2451
+ // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2452
+ // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2453
+ return undefined;
2454
+ }
2455
+ return maybeReferentLink;
2456
+ }
2457
+ static from(opt) {
2458
+ return new DeepReference(opt);
2459
+ }
2460
+ }
2461
+ function gatherDeepReferencesFromTree(startingNode, dataSource) {
2462
+ const deepReferences = [];
2463
+ treeVisit(startingNode, (node) => {
2464
+ if (!node.data.props)
2465
+ return;
2466
+ for (const [, variableMapping] of Object.entries(node.data.props)) {
2467
+ if (variableMapping.type !== 'BoundValue')
2468
+ continue;
2469
+ if (!isDeepPath(variableMapping.path))
2470
+ continue;
2471
+ deepReferences.push(DeepReference.from({
2472
+ path: variableMapping.path,
2473
+ dataSource,
2474
+ }));
2475
+ }
2476
+ });
2477
+ return deepReferences;
2478
+ }
2479
+
2480
+ const useDraggedItemStore = create((set) => ({
2481
+ draggedItem: undefined,
2482
+ hoveredComponentId: undefined,
2483
+ domRect: undefined,
2484
+ componentId: '',
2485
+ isDraggingOnCanvas: false,
2486
+ onBeforeCaptureId: '',
2487
+ mouseX: 0,
2488
+ mouseY: 0,
2489
+ scrollY: 0,
2490
+ setComponentId(id) {
2491
+ set({ componentId: id });
2492
+ },
2493
+ setHoveredComponentId(id) {
2494
+ set({ hoveredComponentId: id });
2495
+ },
2496
+ updateItem: (item) => {
2497
+ set({ draggedItem: item });
2498
+ },
2499
+ setDraggingOnCanvas: (isDraggingOnCanvas) => {
2500
+ set({ isDraggingOnCanvas });
2501
+ },
2502
+ setOnBeforeCaptureId: (onBeforeCaptureId) => {
2503
+ set({ onBeforeCaptureId });
2504
+ },
2505
+ setMousePosition(x, y) {
2506
+ set({ mouseX: x, mouseY: y });
2507
+ },
2508
+ setDomRect(domRect) {
2509
+ set({ domRect });
2510
+ },
2511
+ setScrollY(y) {
2512
+ set({ scrollY: y });
2513
+ },
2514
+ }));
2515
+
2516
+ const SCROLL_STATES = {
2517
+ Start: 'scrollStart',
2518
+ IsScrolling: 'isScrolling',
2519
+ End: 'scrollEnd',
2520
+ };
2521
+ const OUTGOING_EVENTS = {
2522
+ Connected: 'connected',
2523
+ DesignTokens: 'registerDesignTokens',
2524
+ RegisteredBreakpoints: 'registeredBreakpoints',
2525
+ MouseMove: 'mouseMove',
2526
+ NewHoveredElement: 'newHoveredElement',
2527
+ ComponentSelected: 'componentSelected',
2528
+ RegisteredComponents: 'registeredComponents',
2529
+ RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
2530
+ ComponentDragCanceled: 'componentDragCanceled',
2531
+ ComponentDropped: 'componentDropped',
2532
+ ComponentMoved: 'componentMoved',
2533
+ CanvasReload: 'canvasReload',
2534
+ UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
2535
+ CanvasScroll: 'canvasScrolling',
2536
+ CanvasError: 'canvasError',
2537
+ ComponentMoveStarted: 'componentMoveStarted',
2538
+ ComponentMoveEnded: 'componentMoveEnded',
2539
+ OutsideCanvasClick: 'outsideCanvasClick',
2540
+ SDKFeatures: 'sdkFeatures',
2541
+ RequestEntities: 'REQUEST_ENTITIES',
2542
+ };
2543
+ const INCOMING_EVENTS = {
2544
+ RequestEditorMode: 'requestEditorMode',
2545
+ RequestReadOnlyMode: 'requestReadOnlyMode',
2546
+ ExperienceUpdated: 'componentTreeUpdated',
2547
+ ComponentDraggingChanged: 'componentDraggingChanged',
2548
+ ComponentDragCanceled: 'componentDragCanceled',
2549
+ ComponentDragStarted: 'componentDragStarted',
2550
+ ComponentDragEnded: 'componentDragEnded',
2551
+ ComponentMoveEnded: 'componentMoveEnded',
2552
+ CanvasResized: 'canvasResized',
2553
+ SelectComponent: 'selectComponent',
2554
+ HoverComponent: 'hoverComponent',
2555
+ UpdatedEntity: 'updatedEntity',
2556
+ AssembliesAdded: 'assembliesAdded',
2557
+ AssembliesRegistered: 'assembliesRegistered',
2558
+ MouseMove: 'mouseMove',
2559
+ RequestedEntities: 'REQUESTED_ENTITIES',
2560
+ };
2561
+ const INTERNAL_EVENTS = {
2562
+ ComponentsRegistered: 'cfComponentsRegistered',
2563
+ VisualEditorInitialize: 'cfVisualEditorInitialize',
2564
+ };
2565
+ const VISUAL_EDITOR_EVENTS = {
2566
+ Ready: 'cfVisualEditorReady',
2567
+ };
2568
+ /**
2569
+ * These modes are ONLY intended to be internally used within the context of
2570
+ * editing an experience inside of Contentful Studio. i.e. these modes
2571
+ * intentionally do not include preview/delivery modes.
2572
+ */
2573
+ var StudioCanvasMode$2;
2574
+ (function (StudioCanvasMode) {
2575
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
2576
+ StudioCanvasMode["EDITOR"] = "editorMode";
2577
+ StudioCanvasMode["NONE"] = "none";
2578
+ })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
2579
+ const CONTENTFUL_COMPONENTS$1 = {
2580
+ section: {
2581
+ id: 'contentful-section',
2582
+ name: 'Section',
2583
+ },
2584
+ container: {
2585
+ id: 'contentful-container',
2586
+ name: 'Container',
2587
+ },
2588
+ columns: {
2589
+ id: 'contentful-columns',
2590
+ name: 'Columns',
2591
+ },
2592
+ singleColumn: {
2593
+ id: 'contentful-single-column',
2594
+ name: 'Column',
2595
+ },
2596
+ button: {
2597
+ id: 'contentful-button',
2598
+ name: 'Button',
2599
+ },
2600
+ heading: {
2601
+ id: 'contentful-heading',
2602
+ name: 'Heading',
2603
+ },
2604
+ image: {
2605
+ id: 'contentful-image',
2606
+ name: 'Image',
2607
+ },
2608
+ richText: {
2609
+ id: 'contentful-richText',
2610
+ name: 'Rich Text',
2611
+ },
2612
+ text: {
2613
+ id: 'contentful-text',
2614
+ name: 'Text',
2615
+ },
2616
+ divider: {
2617
+ id: 'contentful-divider',
2618
+ name: 'Divider',
2619
+ },
2620
+ carousel: {
2621
+ id: 'contentful-carousel',
2622
+ name: 'Carousel',
2623
+ },
2624
+ };
2625
+ const ASSEMBLY_NODE_TYPE = 'assembly';
2626
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
2627
+ const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
2628
+ const ASSEMBLY_NODE_TYPES = [ASSEMBLY_NODE_TYPE, ASSEMBLY_BLOCK_NODE_TYPE];
2629
+ const EMPTY_CONTAINER_HEIGHT = '80px';
2630
+ const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
2631
+ var PostMessageMethods$2;
2632
+ (function (PostMessageMethods) {
2633
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2634
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2635
+ })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
2636
+
2637
+ const DRAGGABLE_HEIGHT = 30;
2638
+ const DRAGGABLE_WIDTH = 50;
2639
+ const DRAG_PADDING = 4;
2640
+ const ROOT_ID = 'root';
2641
+ const COMPONENT_LIST_ID = 'component-list';
2642
+ const NEW_COMPONENT_ID = 'ctfl-new-draggable';
2643
+ const CTFL_ZONE_ID = 'data-ctfl-zone-id';
2644
+ const CTFL_DRAGGING_ELEMENT = 'data-ctfl-dragging-element';
2645
+ const HITBOX = {
2646
+ WIDTH: 70,
2647
+ HEIGHT: 20,
2648
+ INITIAL_OFFSET: 10,
2649
+ OFFSET_INCREMENT: 8,
2650
+ MIN_HEIGHT: 45,
2651
+ MIN_DEPTH_HEIGHT: 20,
2652
+ DEEP_ZONE: 5,
2653
+ };
2654
+ var TreeAction;
2655
+ (function (TreeAction) {
2656
+ TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
2657
+ TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
2658
+ TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
2659
+ TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
2660
+ TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
2661
+ TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
2662
+ })(TreeAction || (TreeAction = {}));
2663
+ var HitboxDirection;
2664
+ (function (HitboxDirection) {
2665
+ HitboxDirection[HitboxDirection["TOP"] = 0] = "TOP";
2666
+ HitboxDirection[HitboxDirection["LEFT"] = 1] = "LEFT";
2667
+ HitboxDirection[HitboxDirection["RIGHT"] = 2] = "RIGHT";
2668
+ HitboxDirection[HitboxDirection["BOTTOM"] = 3] = "BOTTOM";
2669
+ HitboxDirection[HitboxDirection["SELF_VERTICAL"] = 4] = "SELF_VERTICAL";
2670
+ HitboxDirection[HitboxDirection["SELF_HORIZONTAL"] = 5] = "SELF_HORIZONTAL";
2671
+ })(HitboxDirection || (HitboxDirection = {}));
2672
+ var DraggablePosition;
2673
+ (function (DraggablePosition) {
2674
+ DraggablePosition[DraggablePosition["CENTERED"] = 0] = "CENTERED";
2675
+ DraggablePosition[DraggablePosition["MOUSE_POSITION"] = 1] = "MOUSE_POSITION";
2676
+ })(DraggablePosition || (DraggablePosition = {}));
2677
+
2678
+ function useDraggablePosition({ draggableId, draggableRef, position }) {
2679
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
2680
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
2681
+ const preDragDomRect = useDraggedItemStore((state) => state.domRect);
2682
+ useEffect(() => {
2683
+ const el = draggableRef?.current ??
2684
+ document.querySelector(`[${CTFL_DRAGGING_ELEMENT}][data-cf-node-id="${draggableId}"]`);
2685
+ if (!isDraggingOnCanvas || draggingId !== draggableId || !el) {
2686
+ return;
2687
+ }
2688
+ const isCentered = position === DraggablePosition.CENTERED || !preDragDomRect;
2689
+ const domRect = isCentered ? el.getBoundingClientRect() : preDragDomRect;
2690
+ const { mouseX, mouseY } = useDraggedItemStore.getState();
2691
+ const top = isCentered ? mouseY - domRect.height / 2 : domRect.top;
2692
+ const left = isCentered ? mouseX - domRect.width / 2 : domRect.left;
2693
+ el.style.position = 'fixed';
2694
+ el.style.left = `${left}px`;
2695
+ el.style.top = `${top}px`;
2696
+ el.style.width = `${domRect.width}px`;
2697
+ el.style.height = `${domRect.height}px`;
2698
+ }, [draggableRef, draggableId, isDraggingOnCanvas, draggingId, position, preDragDomRect]);
2699
+ }
2700
+
2701
+ function getStyle$2(style, snapshot) {
2702
+ if (!snapshot.isDropAnimating) {
2703
+ return style;
2704
+ }
2705
+ return {
2706
+ ...style,
2707
+ // cannot be 0, but make it super tiny
2708
+ transitionDuration: `0.001s`,
2709
+ };
2710
+ }
2711
+ const DraggableContainer = ({ id }) => {
2712
+ const ref = useRef(null);
2713
+ useDraggablePosition({
2714
+ draggableId: id,
2715
+ draggableRef: ref,
2716
+ position: DraggablePosition.CENTERED,
2717
+ });
2718
+ return (React.createElement("div", { id: COMPONENT_LIST_ID, style: {
2719
+ position: 'absolute',
2720
+ top: 0,
2721
+ left: 0,
2722
+ pointerEvents: 'none',
2723
+ zIndex: -1,
2724
+ } },
2725
+ React.createElement(Droppable, { droppableId: COMPONENT_LIST_ID, isDropDisabled: true }, (provided) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef },
2726
+ React.createElement(Draggable, { draggableId: id, key: id, index: 0 }, (provided, snapshot) => (React.createElement("div", { id: NEW_COMPONENT_ID, "data-ctfl-dragging-element": true, ref: (node) => {
2727
+ provided.innerRef(node);
2728
+ ref.current = node;
2729
+ }, ...provided.draggableProps, ...provided.dragHandleProps, style: {
2730
+ ...getStyle$2(provided.draggableProps.style, snapshot),
2731
+ width: DRAGGABLE_WIDTH,
2732
+ height: DRAGGABLE_HEIGHT,
2733
+ pointerEvents: 'none',
2734
+ } }))),
2735
+ provided.placeholder)))));
2736
+ };
2737
+
2738
+ function getItemFromTree(id, node) {
2739
+ // Check if the current node's id matches the search id
2740
+ if (node.data.id === id) {
2741
+ return node;
2742
+ }
2743
+ // Recursively search through each child
2744
+ for (const child of node.children) {
2745
+ const foundNode = getItemFromTree(id, child);
2746
+ if (foundNode) {
2747
+ // Node found in children
2748
+ return foundNode;
2749
+ }
2750
+ }
2751
+ // If the node is not found in this branch of the tree, return undefined
2752
+ return undefined;
2753
+ }
2754
+ function findDepthById(node, id, currentDepth = 1) {
2755
+ if (node.data.id === id) {
2756
+ return currentDepth;
2757
+ }
2758
+ // If the node has children, check each one
2759
+ for (const child of node.children) {
2760
+ const childDepth = findDepthById(child, id, currentDepth + 1);
2761
+ if (childDepth !== -1) {
2762
+ return childDepth; // Found the node in a child
2763
+ }
2764
+ }
2765
+ return -1; // Node not found in this branch
2766
+ }
2767
+ const getChildFromTree = (parentId, index, node) => {
2768
+ // Check if the current node's id matches the search id
2769
+ if (node.data.id === parentId) {
2770
+ return node.children[index];
2771
+ }
2772
+ // Recursively search through each child
2773
+ for (const child of node.children) {
2774
+ const foundNode = getChildFromTree(parentId, index, child);
2775
+ if (foundNode) {
2776
+ // Node found in children
2777
+ return foundNode;
2778
+ }
2779
+ }
2780
+ // If the node is not found in this branch of the tree, return undefined
2781
+ return undefined;
2782
+ };
2783
+ const getItem = (selector, tree) => {
2784
+ return getItemFromTree(selector.id, {
2785
+ type: 'block',
2786
+ data: {
2787
+ id: ROOT_ID,
2788
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2789
+ },
2790
+ children: tree.root.children,
2791
+ });
2792
+ };
2793
+ const getItemDepthFromNode = (selector, node) => {
2794
+ return findDepthById(node, selector.id);
2795
+ };
2796
+
2797
+ function updateNode(nodeId, updatedNode, node) {
2798
+ if (node.data.id === nodeId) {
2799
+ node.data = updatedNode.data;
2800
+ return;
2801
+ }
2802
+ node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
2803
+ }
2804
+ function replaceNode(indexToReplace, updatedNode, node) {
2805
+ if (node.data.id === updatedNode.parentId) {
2806
+ node.children = [
2807
+ ...node.children.slice(0, indexToReplace),
2808
+ updatedNode,
2809
+ ...node.children.slice(indexToReplace + 1),
2810
+ ];
2811
+ return;
2812
+ }
2813
+ node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
2814
+ }
2815
+ function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
2816
+ if (node.data.id === parentNodeId) {
2817
+ const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
2818
+ node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
2819
+ return;
2820
+ }
2821
+ node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
2822
+ }
2823
+ function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
2824
+ if (node.data.id === parentNodeId) {
2825
+ node.children = [
2826
+ ...node.children.slice(0, indexToAdd),
2827
+ nodeToAdd,
2828
+ ...node.children.slice(indexToAdd),
2829
+ ];
2830
+ return;
2831
+ }
2832
+ node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
2833
+ }
2834
+ function reorderChildNode(oldIndex, newIndex, parentNodeId, node) {
2835
+ if (node.data.id === parentNodeId) {
2836
+ // Remove the child from the old position
2837
+ const [childToMove] = node.children.splice(oldIndex, 1);
2838
+ // Insert the child at the new position
2839
+ node.children.splice(newIndex, 0, childToMove);
2840
+ return;
2841
+ }
2842
+ node.children.forEach((childNode) => reorderChildNode(oldIndex, newIndex, parentNodeId, childNode));
2843
+ }
2844
+ function reparentChildNode(oldIndex, newIndex, sourceNodeId, destinationNodeId, node) {
2845
+ const nodeToMove = getChildFromTree(sourceNodeId, oldIndex, node);
2846
+ if (!nodeToMove) {
2847
+ return;
2848
+ }
2849
+ removeChildNode(oldIndex, nodeToMove.data.id, sourceNodeId, node);
2850
+ addChildNode(newIndex, destinationNodeId, nodeToMove, node);
2575
2851
  }
2576
2852
 
2577
- // The default of 3s in the EditorEntityStore is sometimes timing out and
2578
- // leads to not rendering bound content and assemblies.
2579
- const REQUEST_TIMEOUT = 10000;
2580
- class EditorModeEntityStore extends EditorEntityStore {
2581
- constructor({ entities, locale }) {
2582
- console.debug(`[experiences-sdk-react] Initializing editor entity store with ${entities.length} entities for locale ${locale}.`, { entities });
2583
- const subscribe = (method, cb) => {
2584
- const handleMessage = (event) => {
2585
- const data = JSON.parse(event.data);
2586
- if (typeof data !== 'object' || !data)
2587
- return;
2588
- if (data.source !== 'composability-app')
2589
- return;
2590
- if (data.eventType === method) {
2591
- cb(data.payload);
2592
- }
2593
- };
2594
- if (typeof window !== 'undefined') {
2595
- window.addEventListener('message', handleMessage);
2596
- }
2597
- return () => {
2598
- if (typeof window !== 'undefined') {
2599
- window.removeEventListener('message', handleMessage);
2600
- }
2601
- };
2602
- };
2603
- super({ entities, sendMessage, subscribe, locale, timeoutDuration: REQUEST_TIMEOUT });
2604
- this.locale = locale;
2605
- }
2606
- /**
2607
- * This function collects and returns the list of requested entries and assets. Additionally, it checks
2608
- * upfront whether any async fetching logic is actually happening. If not, it returns a plain `false` value, so we
2609
- * can detect this early and avoid unnecessary re-renders.
2610
- * @param entityLinks
2611
- * @returns false if no async fetching is happening, otherwise a promise that resolves when all entities are fetched
2612
- */
2613
- async fetchEntities({ missingEntryIds, missingAssetIds, skipCache = false, }) {
2614
- // Entries and assets will be stored in entryMap and assetMap
2615
- await Promise.all([
2616
- super.fetchEntries(missingEntryIds, skipCache),
2617
- super.fetchAssets(missingAssetIds, skipCache),
2618
- ]);
2619
- }
2620
- getMissingEntityIds(entityLinks) {
2621
- const entryLinks = entityLinks.filter((link) => link.sys?.linkType === 'Entry');
2622
- const assetLinks = entityLinks.filter((link) => link.sys?.linkType === 'Asset');
2623
- const uniqueEntryIds = [...new Set(entryLinks.map((link) => link.sys.id))];
2624
- const uniqueAssetIds = [...new Set(assetLinks.map((link) => link.sys.id))];
2625
- const { missing: missingEntryIds } = this.getEntitiesFromMap('Entry', uniqueEntryIds);
2626
- const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
2627
- return { missingEntryIds, missingAssetIds };
2853
+ function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
2854
+ if (nodeAdded) {
2855
+ return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
2628
2856
  }
2629
- getValue(entityLinkOrEntity, path) {
2630
- const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2631
- if (!entity) {
2632
- return;
2857
+ const item = getItem({ id: child.data.id }, tree);
2858
+ if (item) {
2859
+ const parentNode = getItem({ id: item.parentId }, tree);
2860
+ if (!parentNode) {
2861
+ return null;
2633
2862
  }
2634
- const fieldValue = get(entity, path);
2635
- return transformAssetFileToUrl(fieldValue);
2863
+ const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
2864
+ return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
2636
2865
  }
2866
+ return {
2867
+ type: TreeAction.REPLACE_NODE,
2868
+ originalId: currentNode.children[index].data.id,
2869
+ indexToReplace: index,
2870
+ node: child,
2871
+ };
2637
2872
  }
2638
-
2639
- var VisualEditorMode$1;
2640
- (function (VisualEditorMode) {
2641
- VisualEditorMode["LazyLoad"] = "lazyLoad";
2642
- VisualEditorMode["InjectScript"] = "injectScript";
2643
- })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2644
-
2645
- class DeepReference {
2646
- constructor({ path, dataSource }) {
2647
- const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2648
- this.originalPath = path;
2649
- this.entityId = dataSource[key].sys.id;
2650
- this.entityLink = dataSource[key];
2651
- this.field = field;
2652
- this.referentField = referentField;
2873
+ function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
2874
+ if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
2875
+ return {
2876
+ type: TreeAction.REORDER_NODE,
2877
+ sourceIndex: originalIndex,
2878
+ destinationIndex: index,
2879
+ parentNodeId,
2880
+ };
2653
2881
  }
2654
- get headEntityId() {
2655
- return this.entityId;
2882
+ return null;
2883
+ }
2884
+ function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
2885
+ // In the end, this map contains the list of nodes that are not present
2886
+ // in the updated tree and must be removed
2887
+ const map = new Map();
2888
+ if (!currentNode || !updatedNode) {
2889
+ return differences;
2656
2890
  }
2891
+ // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
2892
+ const currentNodeCount = currentNode.children.length;
2893
+ const updatedNodeCount = updatedNode.children.length;
2894
+ const nodeRemoved = currentNodeCount > updatedNodeCount;
2895
+ const nodeAdded = currentNodeCount < updatedNodeCount;
2896
+ const parentNodeId = updatedNode.data.id;
2657
2897
  /**
2658
- * Extracts referent from the path, using EntityStore as source of
2659
- * entities during the resolution path.
2898
+ * The data of the current node has changed, we need to update
2899
+ * this node to reflect the data change. (design, content, unbound values)
2660
2900
  */
2661
- extractReferent(entityStore) {
2662
- const headEntity = entityStore.getEntityFromLink(this.entityLink);
2663
- const maybeReferentLink = headEntity?.fields[this.field];
2664
- if (undefined === maybeReferentLink) {
2665
- // field references nothing (or even field doesn't exist)
2666
- return undefined;
2667
- }
2668
- if (!isLink(maybeReferentLink)) {
2669
- // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2670
- // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2671
- // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2672
- return undefined;
2673
- }
2674
- return maybeReferentLink;
2675
- }
2676
- static from(opt) {
2677
- return new DeepReference(opt);
2901
+ if (!isEqual(currentNode.data, updatedNode.data)) {
2902
+ differences.push({
2903
+ type: TreeAction.UPDATE_NODE,
2904
+ nodeId: currentNode.data.id,
2905
+ node: updatedNode,
2906
+ });
2678
2907
  }
2679
- }
2680
- function gatherDeepReferencesFromTree(startingNode, dataSource) {
2681
- const deepReferences = [];
2682
- treeVisit(startingNode, (node) => {
2683
- if (!node.data.props)
2908
+ // Map children of the first tree by their ID
2909
+ currentNode.children.forEach((child, index) => map.set(child.data.id, index));
2910
+ // Compare with the second tree
2911
+ updatedNode.children.forEach((child, index) => {
2912
+ const childId = child.data.id;
2913
+ // The original tree does not have this node in the updated tree.
2914
+ if (!map.has(childId)) {
2915
+ const diff = missingNodeAction({
2916
+ index,
2917
+ child,
2918
+ nodeAdded,
2919
+ parentNodeId,
2920
+ tree: originalTree,
2921
+ currentNode,
2922
+ });
2923
+ if (diff?.type === TreeAction.REPLACE_NODE) {
2924
+ // Remove it from the deletion map to avoid adding another REMOVE_NODE action
2925
+ map.delete(diff.originalId);
2926
+ }
2927
+ return differences.push(diff);
2928
+ }
2929
+ const originalIndex = map.get(childId);
2930
+ const diff = matchingNodeAction({
2931
+ index,
2932
+ originalIndex,
2933
+ nodeAdded,
2934
+ nodeRemoved,
2935
+ parentNodeId,
2936
+ });
2937
+ differences.push(diff);
2938
+ map.delete(childId);
2939
+ compareNodes({
2940
+ currentNode: currentNode.children[originalIndex],
2941
+ updatedNode: child,
2942
+ originalTree,
2943
+ differences,
2944
+ });
2945
+ });
2946
+ map.forEach((index, key) => {
2947
+ // If the node count of the entire tree doesn't signify
2948
+ // a node was removed, don't add that as a diff
2949
+ if (!nodeRemoved) {
2684
2950
  return;
2685
- for (const [, variableMapping] of Object.entries(node.data.props)) {
2686
- if (variableMapping.type !== 'BoundValue')
2687
- continue;
2688
- if (!isDeepPath(variableMapping.path))
2689
- continue;
2690
- deepReferences.push(DeepReference.from({
2691
- path: variableMapping.path,
2692
- dataSource,
2693
- }));
2694
2951
  }
2952
+ // Remaining nodes in the map are removed in the second tree
2953
+ differences.push({
2954
+ type: TreeAction.REMOVE_NODE,
2955
+ indexToRemove: index,
2956
+ parentNodeId,
2957
+ idToRemove: key,
2958
+ });
2695
2959
  });
2696
- return deepReferences;
2960
+ return differences;
2961
+ }
2962
+ function getTreeDiffs(tree1, tree2, originalTree) {
2963
+ const differences = [];
2964
+ compareNodes({
2965
+ currentNode: tree1,
2966
+ updatedNode: tree2,
2967
+ originalTree,
2968
+ differences,
2969
+ });
2970
+ return differences.filter((diff) => diff);
2697
2971
  }
2698
2972
 
2699
2973
  const useTreeStore = create((set, get) => ({
@@ -2727,6 +3001,20 @@ const useTreeStore = create((set, get) => ({
2727
3001
  });
2728
3002
  }));
2729
3003
  },
3004
+ /**
3005
+ * NOTE: this is for debugging purposes only as it causes ugly canvas flash.
3006
+ *
3007
+ * Force updates entire tree. Usually shouldn't be used as updateTree()
3008
+ * uses smart update algorithm based on diffs. But for troubleshooting
3009
+ * you may want to force update the tree so leaving this in.
3010
+ */
3011
+ updateTreeForced: (tree) => {
3012
+ set({
3013
+ tree,
3014
+ // Breakpoints must be updated, as we receive completely new tree with possibly new breakpoints
3015
+ breakpoints: tree?.root?.data?.breakpoints || [],
3016
+ });
3017
+ },
2730
3018
  updateTree: (tree) => {
2731
3019
  const currentTree = get().tree;
2732
3020
  /**
@@ -2772,6 +3060,21 @@ const useTreeStore = create((set, get) => ({
2772
3060
  state.breakpoints = tree?.root?.data?.breakpoints || [];
2773
3061
  }));
2774
3062
  },
3063
+ addChild: (index, parentId, node) => {
3064
+ set(produce((state) => {
3065
+ addChildNode(index, parentId, node, state.tree.root);
3066
+ }));
3067
+ },
3068
+ reorderChildren: (destinationIndex, destinationParentId, sourceIndex) => {
3069
+ set(produce((state) => {
3070
+ reorderChildNode(sourceIndex, destinationIndex, destinationParentId, state.tree.root);
3071
+ }));
3072
+ },
3073
+ reparentChild: (destinationIndex, destinationParentId, sourceIndex, sourceParentId) => {
3074
+ set(produce((state) => {
3075
+ reparentChildNode(sourceIndex, destinationIndex, sourceParentId, destinationParentId, state.tree.root);
3076
+ }));
3077
+ },
2775
3078
  }));
2776
3079
  const hasBreakpointDiffs = (currentTree, newTree) => {
2777
3080
  const currentBreakpoints = currentTree?.root?.data?.breakpoints ?? [];
@@ -2789,8 +3092,8 @@ const cloneDeepAsPOJO = (obj) => {
2789
3092
  return JSON.parse(JSON.stringify(obj));
2790
3093
  };
2791
3094
 
2792
- var css_248z$a = ".RootRenderer-module_rootContainer__9UawM {\n position: relative;\n display: flex;\n flex-direction: column;\n}\n\nbody {\n margin: 0;\n}\n\nhtml {\n -ms-overflow-style: none; /* Internet Explorer 10+ */\n scrollbar-width: none; /* Firefox */\n}\n\nhtml::-webkit-scrollbar {\n display: none;\n}\n";
2793
- var styles$2 = {"rootContainer":"RootRenderer-module_rootContainer__9UawM"};
3095
+ var css_248z$a = ".render-module_hitbox__l4ysJ {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 10px;\n z-index: 1000000;\n}\n\n.render-module_hitboxLower__tgsA1 {\n position: absolute;\n bottom: -20px;\n left: 0;\n width: 100%;\n height: 20px;\n z-index: 1000000;\n}\n\n.render-module_canvasBottomSpacer__JuxVh {\n position: absolute;\n width: 100%;\n height: 50px;\n}\n\n.render-module_container__-C3d7 {\n position: relative;\n display: flex;\n flex-direction: column;\n}\n\nbody {\n margin: 0;\n}\n\nhtml {\n -ms-overflow-style: none; /* Internet Explorer 10+ */\n scrollbar-width: none; /* Firefox */\n}\n\nhtml::-webkit-scrollbar {\n display: none;\n}\n";
3096
+ var styles$3 = {"hitbox":"render-module_hitbox__l4ysJ","hitboxLower":"render-module_hitboxLower__tgsA1","canvasBottomSpacer":"render-module_canvasBottomSpacer__JuxVh","container":"render-module_container__-C3d7"};
2794
3097
  styleInject(css_248z$a);
2795
3098
 
2796
3099
  // TODO: In order to support integrations without React, we should extract this heavy logic into simple
@@ -2829,6 +3132,66 @@ const useBreakpoints = (breakpoints) => {
2829
3132
  return { resolveDesignValue };
2830
3133
  };
2831
3134
 
3135
+ /**
3136
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
3137
+ * and sends the DOM Rect to the client app
3138
+ */
3139
+ const sendSelectedComponentCoordinates = (instanceId) => {
3140
+ const selection = getSelectionNodes(instanceId);
3141
+ if (selection?.target) {
3142
+ const sendUpdateSelectedComponentCoordinates = () => {
3143
+ sendMessage(OUTGOING_EVENTS.UpdateSelectedComponentCoordinates, {
3144
+ selectedNodeCoordinates: getElementCoordinates(selection.target),
3145
+ selectedAssemblyChildCoordinates: selection.patternChild
3146
+ ? getElementCoordinates(selection.patternChild)
3147
+ : undefined,
3148
+ parentCoordinates: selection.parent ? getElementCoordinates(selection.parent) : undefined,
3149
+ });
3150
+ };
3151
+ // If the target contains an image, wait for this image to be loaded before sending the coordinates
3152
+ const childImage = selection.target.querySelector('img');
3153
+ if (childImage) {
3154
+ const handleImageLoad = () => {
3155
+ sendUpdateSelectedComponentCoordinates();
3156
+ childImage.removeEventListener('load', handleImageLoad);
3157
+ };
3158
+ childImage.addEventListener('load', handleImageLoad);
3159
+ }
3160
+ sendUpdateSelectedComponentCoordinates();
3161
+ }
3162
+ };
3163
+ const getSelectionNodes = (instanceId) => {
3164
+ if (!instanceId)
3165
+ return;
3166
+ let selectedNode = document.querySelector(`[data-cf-node-id="${instanceId}"]`);
3167
+ let selectedPatternChild = null;
3168
+ let selectedParent = null;
3169
+ // Use RegEx instead of split to match the last occurrence of '---' in the instanceId instead of the first one
3170
+ const idMatch = instanceId.match(/(.*)---(.*)/);
3171
+ const rootNodeId = idMatch?.[1] ?? instanceId;
3172
+ const nodeLocation = idMatch?.[2];
3173
+ const isNestedPattern = nodeLocation && selectedNode?.dataset?.cfNodeBlockType === ASSEMBLY_NODE_TYPE;
3174
+ const isPatternChild = !isNestedPattern && nodeLocation;
3175
+ if (isPatternChild) {
3176
+ // For pattern child nodes, render the pattern itself as selected component
3177
+ selectedPatternChild = selectedNode;
3178
+ selectedNode = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3179
+ }
3180
+ else if (isNestedPattern) {
3181
+ // For nested patterns, return the upper pattern as parent
3182
+ selectedParent = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3183
+ }
3184
+ else {
3185
+ // Find the next valid parent of the selected element
3186
+ selectedParent = selectedNode?.parentElement ?? null;
3187
+ // Ensure that the selection parent is a VisualEditorBlock
3188
+ while (selectedParent && !selectedParent.dataset?.cfNodeId) {
3189
+ selectedParent = selectedParent?.parentElement;
3190
+ }
3191
+ }
3192
+ return { target: selectedNode, patternChild: selectedPatternChild, parent: selectedParent };
3193
+ };
3194
+
2832
3195
  // Note: During development, the hot reloading might empty this and it
2833
3196
  // stays empty leading to not rendering assemblies. Ideally, this is
2834
3197
  // integrated into the state machine to keep track of its state.
@@ -2862,10 +3225,14 @@ const useEditorStore = create((set, get) => ({
2862
3225
  dataSource: {},
2863
3226
  hyperLinkPattern: undefined,
2864
3227
  unboundValues: {},
3228
+ selectedNodeId: null,
2865
3229
  locale: null,
2866
3230
  setHyperLinkPattern: (pattern) => {
2867
3231
  set({ hyperLinkPattern: pattern });
2868
3232
  },
3233
+ setSelectedNodeId: (id) => {
3234
+ set({ selectedNodeId: id });
3235
+ },
2869
3236
  setDataSource(data) {
2870
3237
  const dataSource = get().dataSource;
2871
3238
  const newDataSource = { ...dataSource, ...data };
@@ -2897,7 +3264,6 @@ const useEditorStore = create((set, get) => ({
2897
3264
  var css_248z$8 = "/* Initially added with PR #253 for each component, this is now a global setting\n * It is recommended on MDN to use this as a default for layouting.\n*/\n* {\n box-sizing: border-box;\n}\n";
2898
3265
  styleInject(css_248z$8);
2899
3266
 
2900
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2901
3267
  /**
2902
3268
  * These modes are ONLY intended to be internally used within the context of
2903
3269
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -3026,17 +3392,16 @@ const PrimitiveValueSchema = z.union([
3026
3392
  z.record(z.any(), z.any()),
3027
3393
  z.undefined(),
3028
3394
  ]);
3395
+ const UsedComponentsSchema = z.array(z.object({
3396
+ sys: z.object({
3397
+ type: z.literal('Link'),
3398
+ id: z.string(),
3399
+ linkType: z.literal('Entry'),
3400
+ }),
3401
+ }));
3029
3402
  const uuidKeySchema = z
3030
3403
  .string()
3031
3404
  .regex(/^[a-zA-Z0-9-_]{1,21}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,21}$/' });
3032
- /**
3033
- * Property keys for imported components have a limit of 32 characters (to be implemented) while
3034
- * property keys for patterns have a limit of 54 characters (<32-char-variabl-name>_<21-char-nanoid-id>).
3035
- * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
3036
- */
3037
- const propertyKeySchema = z
3038
- .string()
3039
- .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
3040
3405
  const DataSourceSchema = z.record(uuidKeySchema, z.object({
3041
3406
  sys: z.object({
3042
3407
  type: z.literal('Link'),
@@ -3044,7 +3409,62 @@ const DataSourceSchema = z.record(uuidKeySchema, z.object({
3044
3409
  linkType: z.enum(['Entry', 'Asset']),
3045
3410
  }),
3046
3411
  }));
3412
+ const UnboundValuesSchema = z.record(uuidKeySchema, z.object({
3413
+ value: PrimitiveValueSchema,
3414
+ }));
3415
+ /**
3416
+ * Property keys for imported components have a limit of 32 characters (to be implemented) while
3417
+ * property keys for patterns have a limit of 54 characters (<32-char-variable-name>_<21-char-nanoid-id>).
3418
+ * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
3419
+ */
3420
+ const propertyKeySchema = z
3421
+ .string()
3422
+ .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
3423
+ const ComponentTreeNodeIdSchema = z
3424
+ .string()
3425
+ .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
3426
+ const breakpointsRefinement = (value, ctx) => {
3427
+ if (!value.length || value[0].query !== '*') {
3428
+ ctx.addIssue({
3429
+ code: z.ZodIssueCode.custom,
3430
+ message: `The first breakpoint should include the following attributes: { "query": "*" }`,
3431
+ });
3432
+ }
3433
+ const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
3434
+ // check if the current breakpoint id is found in the rest of the array
3435
+ const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
3436
+ return breakpointIndex !== currentBreakpointIndex;
3437
+ });
3438
+ if (hasDuplicateIds) {
3439
+ ctx.addIssue({
3440
+ code: z.ZodIssueCode.custom,
3441
+ message: `Breakpoint IDs must be unique`,
3442
+ });
3443
+ }
3444
+ // Extract the queries boundary by removing the special characters around it
3445
+ const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
3446
+ // sort updates queries array in place so we need to create a copy
3447
+ const originalQueries = [...queries];
3448
+ queries.sort((q1, q2) => {
3449
+ if (q1 === '*') {
3450
+ return -1;
3451
+ }
3452
+ if (q2 === '*') {
3453
+ return 1;
3454
+ }
3455
+ return q1 > q2 ? -1 : 1;
3456
+ });
3457
+ if (originalQueries.join('') !== queries.join('')) {
3458
+ ctx.addIssue({
3459
+ code: z.ZodIssueCode.custom,
3460
+ message: `Breakpoints should be ordered from largest to smallest pixel value`,
3461
+ });
3462
+ }
3463
+ };
3047
3464
  const ValuesByBreakpointSchema = z.record(z.lazy(() => PrimitiveValueSchema));
3465
+ const BindingSourceTypeEnumSchema = z
3466
+ .array(z.enum(['entry', 'asset', 'manual', 'experience']))
3467
+ .nonempty();
3048
3468
  const DesignValueSchema = z
3049
3469
  .object({
3050
3470
  type: z.literal('DesignValue'),
@@ -3077,8 +3497,6 @@ const ComponentValueSchema = z
3077
3497
  key: z.string(),
3078
3498
  })
3079
3499
  .strict();
3080
- // TODO: finalize schema structure before release
3081
- // https://contentful.atlassian.net/browse/LUMOS-523
3082
3500
  const NoValueSchema = z.object({ type: z.literal('NoValue') }).strict();
3083
3501
  const ComponentPropertyValueSchema = z.discriminatedUnion('type', [
3084
3502
  DesignValueSchema,
@@ -3090,41 +3508,12 @@ const ComponentPropertyValueSchema = z.discriminatedUnion('type', [
3090
3508
  ]);
3091
3509
  // TODO: finalize schema structure before release
3092
3510
  // https://contentful.atlassian.net/browse/LUMOS-523
3093
- const VariableMappingSchema = z.object({
3094
- patternPropertyDefinitionId: propertyKeySchema,
3095
- type: z.literal('ContentTypeMapping'),
3096
- pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
3097
- });
3098
- const VariableMappingsSchema = z.record(propertyKeySchema, VariableMappingSchema);
3099
- // TODO: finalize schema structure before release
3100
- // https://contentful.atlassian.net/browse/LUMOS-523
3101
- const PatternPropertyDefinitionSchema = z.object({
3102
- defaultValue: z
3103
- .record(z.string(), z.object({
3104
- sys: z.object({
3105
- type: z.literal('Link'),
3106
- id: z.string(),
3107
- linkType: z.enum(['Entry']),
3108
- }),
3109
- }))
3110
- .optional(),
3111
- contentTypes: z.record(z.string(), z.object({
3112
- sys: z.object({
3113
- type: z.literal('Link'),
3114
- id: z.string(),
3115
- linkType: z.enum(['ContentType']),
3116
- }),
3117
- })),
3118
- });
3119
- const PatternPropertyDefinitionsSchema = z.record(propertyKeySchema, PatternPropertyDefinitionSchema);
3120
- // TODO: finalize schema structure before release
3121
- // https://contentful.atlassian.net/browse/LUMOS-523
3122
3511
  const PatternPropertySchema = z.object({
3123
3512
  type: z.literal('BoundValue'),
3124
3513
  path: z.string(),
3125
3514
  contentType: z.string(),
3126
3515
  });
3127
- const PatternPropertysSchema = z.record(propertyKeySchema, PatternPropertySchema);
3516
+ const PatternPropertiesSchema = z.record(propertyKeySchema, PatternPropertySchema);
3128
3517
  const BreakpointSchema = z
3129
3518
  .object({
3130
3519
  id: propertyKeySchema,
@@ -3134,12 +3523,6 @@ const BreakpointSchema = z
3134
3523
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
3135
3524
  })
3136
3525
  .strict();
3137
- const UnboundValuesSchema = z.record(uuidKeySchema, z.object({
3138
- value: PrimitiveValueSchema,
3139
- }));
3140
- const ComponentTreeNodeIdSchema = z
3141
- .string()
3142
- .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
3143
3526
  // Use helper schema to define a recursive schema with its type correctly below
3144
3527
  const BaseComponentTreeNodeSchema = z.object({
3145
3528
  id: ComponentTreeNodeIdSchema.optional(),
@@ -3147,14 +3530,8 @@ const BaseComponentTreeNodeSchema = z.object({
3147
3530
  displayName: z.string().optional(),
3148
3531
  slotId: z.string().optional(),
3149
3532
  variables: z.record(propertyKeySchema, ComponentPropertyValueSchema),
3150
- patternProperties: PatternPropertysSchema.optional(),
3151
- });
3152
- const ComponentTreeNodeSchema = BaseComponentTreeNodeSchema.extend({
3153
- children: z.lazy(() => ComponentTreeNodeSchema.array()),
3533
+ patternProperties: PatternPropertiesSchema.optional(),
3154
3534
  });
3155
- const BindingSourceTypeEnumSchema = z
3156
- .array(z.enum(['entry', 'asset', 'manual', 'experience']))
3157
- .nonempty();
3158
3535
  const ComponentVariableSchema = z.object({
3159
3536
  displayName: z.string().optional(),
3160
3537
  type: DefinitionPropertyTypeSchema,
@@ -3175,8 +3552,25 @@ const ComponentVariableSchema = z.object({
3175
3552
  })
3176
3553
  .optional(),
3177
3554
  });
3178
- const ComponentVariablesSchema = z.record(z.string().regex(/^[a-zA-Z0-9-_]{1,54}$/), // Here the key is <variableName>_<nanoidId> so we need to allow for a longer length
3179
- ComponentVariableSchema);
3555
+ const ComponentTreeNodeSchema = BaseComponentTreeNodeSchema.extend({
3556
+ children: z.lazy(() => ComponentTreeNodeSchema.array()),
3557
+ });
3558
+ const ComponentTreeSchema = z
3559
+ .object({
3560
+ breakpoints: z.array(BreakpointSchema).superRefine(breakpointsRefinement),
3561
+ children: z.array(ComponentTreeNodeSchema),
3562
+ schemaVersion: SchemaVersions,
3563
+ })
3564
+ .strict();
3565
+ const localeWrapper = (fieldSchema) => z.record(z.string(), fieldSchema);
3566
+
3567
+ z.object({
3568
+ componentTree: localeWrapper(ComponentTreeSchema),
3569
+ dataSource: localeWrapper(DataSourceSchema),
3570
+ unboundValues: localeWrapper(UnboundValuesSchema),
3571
+ usedComponents: localeWrapper(UsedComponentsSchema).optional(),
3572
+ });
3573
+
3180
3574
  const THUMBNAIL_IDS = [
3181
3575
  'columns',
3182
3576
  'columnsPlusRight',
@@ -3202,6 +3596,37 @@ const THUMBNAIL_IDS = [
3202
3596
  'textColumns',
3203
3597
  'duplex',
3204
3598
  ];
3599
+ // TODO: finalize schema structure before release
3600
+ // https://contentful.atlassian.net/browse/LUMOS-523
3601
+ const VariableMappingSchema = z.object({
3602
+ patternPropertyDefinitionId: propertyKeySchema,
3603
+ type: z.literal('ContentTypeMapping'),
3604
+ pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
3605
+ });
3606
+ // TODO: finalize schema structure before release
3607
+ // https://contentful.atlassian.net/browse/LUMOS-523
3608
+ const PatternPropertyDefinitionSchema = z.object({
3609
+ defaultValue: z
3610
+ .record(z.string(), z.object({
3611
+ sys: z.object({
3612
+ type: z.literal('Link'),
3613
+ id: z.string(),
3614
+ linkType: z.enum(['Entry']),
3615
+ }),
3616
+ }))
3617
+ .optional(),
3618
+ contentTypes: z.record(z.string(), z.object({
3619
+ sys: z.object({
3620
+ type: z.literal('Link'),
3621
+ id: z.string(),
3622
+ linkType: z.enum(['ContentType']),
3623
+ }),
3624
+ })),
3625
+ });
3626
+ const PatternPropertyDefinitionsSchema = z.record(propertyKeySchema, PatternPropertyDefinitionSchema);
3627
+ const VariableMappingsSchema = z.record(propertyKeySchema, VariableMappingSchema);
3628
+ const ComponentVariablesSchema = z.record(z.string().regex(/^[a-zA-Z0-9-_]{1,54}$/), // Here the key is <variableName>_<nanoidId> so we need to allow for a longer length
3629
+ ComponentVariableSchema);
3205
3630
  const ComponentSettingsSchema = z.object({
3206
3631
  variableDefinitions: ComponentVariablesSchema,
3207
3632
  thumbnailId: z.enum(THUMBNAIL_IDS).optional(),
@@ -3209,65 +3634,12 @@ const ComponentSettingsSchema = z.object({
3209
3634
  variableMappings: VariableMappingsSchema.optional(),
3210
3635
  patternPropertyDefinitions: PatternPropertyDefinitionsSchema.optional(),
3211
3636
  });
3212
- const UsedComponentsSchema = z.array(z.object({
3213
- sys: z.object({
3214
- type: z.literal('Link'),
3215
- id: z.string(),
3216
- linkType: z.literal('Entry'),
3217
- }),
3218
- }));
3219
- const breakpointsRefinement = (value, ctx) => {
3220
- if (!value.length || value[0].query !== '*') {
3221
- ctx.addIssue({
3222
- code: z.ZodIssueCode.custom,
3223
- message: `The first breakpoint should include the following attributes: { "query": "*" }`,
3224
- });
3225
- }
3226
- const hasDuplicateIds = value.some((currentBreakpoint, currentBreakpointIndex) => {
3227
- // check if the current breakpoint id is found in the rest of the array
3228
- const breakpointIndex = value.findIndex((breakpoint) => breakpoint.id === currentBreakpoint.id);
3229
- return breakpointIndex !== currentBreakpointIndex;
3230
- });
3231
- if (hasDuplicateIds) {
3232
- ctx.addIssue({
3233
- code: z.ZodIssueCode.custom,
3234
- message: `Breakpoint IDs must be unique`,
3235
- });
3236
- }
3237
- // Extract the queries boundary by removing the special characters around it
3238
- const queries = value.map((bp) => bp.query === '*' ? bp.query : parseInt(bp.query.replace(/px|<|>/, '')));
3239
- // sort updates queries array in place so we need to create a copy
3240
- const originalQueries = [...queries];
3241
- queries.sort((q1, q2) => {
3242
- if (q1 === '*') {
3243
- return -1;
3244
- }
3245
- if (q2 === '*') {
3246
- return 1;
3247
- }
3248
- return q1 > q2 ? -1 : 1;
3249
- });
3250
- if (originalQueries.join('') !== queries.join('')) {
3251
- ctx.addIssue({
3252
- code: z.ZodIssueCode.custom,
3253
- message: `Breakpoints should be ordered from largest to smallest pixel value`,
3254
- });
3255
- }
3256
- };
3257
- const ComponentTreeSchema = z
3258
- .object({
3259
- breakpoints: z.array(BreakpointSchema).superRefine(breakpointsRefinement),
3260
- children: z.array(ComponentTreeNodeSchema),
3261
- schemaVersion: SchemaVersions,
3262
- })
3263
- .strict();
3264
- const localeWrapper = (fieldSchema) => z.record(z.string(), fieldSchema);
3265
3637
  z.object({
3266
3638
  componentTree: localeWrapper(ComponentTreeSchema),
3267
3639
  dataSource: localeWrapper(DataSourceSchema),
3268
3640
  unboundValues: localeWrapper(UnboundValuesSchema),
3269
3641
  usedComponents: localeWrapper(UsedComponentsSchema).optional(),
3270
- componentSettings: localeWrapper(ComponentSettingsSchema).optional(),
3642
+ componentSettings: localeWrapper(ComponentSettingsSchema),
3271
3643
  });
3272
3644
 
3273
3645
  z.object({
@@ -3444,8 +3816,8 @@ var VisualEditorMode;
3444
3816
  VisualEditorMode["InjectScript"] = "injectScript";
3445
3817
  })(VisualEditorMode || (VisualEditorMode = {}));
3446
3818
 
3447
- var css_248z$2 = ".contentful-container {\n position: relative;\n display: flex;\n box-sizing: border-box;\n pointer-events: all;\n}\n\n.contentful-container::-webkit-scrollbar {\n display: none; /* Safari and Chrome */\n}\n\n.cf-container-wrapper {\n position: relative;\n width: 100%;\n}\n\n.contentful-container:after {\n content: '';\n display: block;\n position: absolute;\n pointer-events: none;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow-x: clip;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 1;\n}\n\n.contentful-section-label:after {\n content: 'Section';\n}\n\n.contentful-container-label:after {\n content: 'Container';\n}\n\n/* used by ContentfulSectionAsHyperlink.tsx */\n\n.contentful-container-link,\n.contentful-container-link:active,\n.contentful-container-link:visited,\n.contentful-container-link:hover,\n.contentful-container-link:read-write,\n.contentful-container-link:focus-visible {\n color: inherit;\n text-decoration: unset;\n outline: unset;\n}\n";
3448
- styleInject(css_248z$2);
3819
+ var css_248z$2$1 = ".contentful-container {\n position: relative;\n display: flex;\n box-sizing: border-box;\n pointer-events: all;\n}\n\n.contentful-container::-webkit-scrollbar {\n display: none; /* Safari and Chrome */\n}\n\n.cf-single-column-wrapper {\n position: relative;\n}\n\n.cf-container-wrapper {\n position: relative;\n width: 100%;\n}\n\n.contentful-container:after {\n content: '';\n display: block;\n position: absolute;\n pointer-events: none;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow-x: clip;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 1;\n}\n\n.contentful-section-label:after {\n content: 'Section';\n}\n\n.contentful-container-label:after {\n content: 'Container';\n}\n\n/* used by ContentfulSectionAsHyperlink.tsx */\n\n.contentful-container-link,\n.contentful-container-link:active,\n.contentful-container-link:visited,\n.contentful-container-link:hover,\n.contentful-container-link:read-write,\n.contentful-container-link:focus-visible {\n color: inherit;\n text-decoration: unset;\n outline: unset;\n}\n";
3820
+ styleInject(css_248z$2$1);
3449
3821
 
3450
3822
  const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave, onMouseDown, onClick, flex, flexBasis, flexShrink, flexDirection, gap, justifyContent, justifyItems, justifySelf, alignItems, alignSelf, alignContent, order, flexWrap, flexGrow, className, cssStyles, ...props }, ref) => {
3451
3823
  return (React.createElement("div", { id: id, ref: ref, style: {
@@ -3469,14 +3841,34 @@ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave,
3469
3841
  });
3470
3842
  Flex.displayName = 'Flex';
3471
3843
 
3472
- var css_248z$1$1 = ".cf-divider {\n display: contents;\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.cf-divider hr {\n border: none;\n}\n";
3844
+ var css_248z$1$1 = ".cf-divider {\n display: contents;\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.cf-divider hr {\n border: none;\n}\n\n/* For the editor mode add this 10px tolerance to make it easier picking up the divider component.\n * Using the DND zone as precondition makes sure that we don't render this pseudo element in delivery. */\n\n[data-ctfl-zone-id='root'] .cf-divider::before {\n content: \"\";\n position: absolute;\n top: -5px;\n left: -5px;\n bottom: -5px;\n right: -5px;\n pointer-events: all;\n}\n";
3473
3845
  styleInject(css_248z$1$1);
3474
3846
 
3475
- var css_248z$9 = ".cf-columns {\n display: flex;\n gap: 24px;\n grid-template-columns: repeat(12, 1fr);\n flex-direction: column;\n min-height: 0; /* NEW */\n min-width: 0; /* NEW; needed for Firefox */\n}\n\n@media (min-width: 768px) {\n .cf-columns {\n display: grid;\n }\n}\n\n.cf-single-column-wrapper {\n position: relative;\n}\n\n.cf-single-column-wrapper:after {\n content: '';\n display: block;\n position: absolute;\n pointer-events: none;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow-x: clip;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 1;\n}\n\n.cf-single-column-label:after {\n content: 'Column';\n}\n";
3847
+ var css_248z$9 = ".cf-columns {\n display: flex;\n gap: 24px;\n grid-template-columns: repeat(12, 1fr);\n flex-direction: column;\n min-height: 0; /* NEW */\n min-width: 0; /* NEW; needed for Firefox */\n}\n\n@media (min-width: 768px) {\n .cf-columns {\n display: grid;\n }\n}\n\n.cf-single-column-wrapper {\n position: relative;\n display: flex;\n}\n\n.cf-single-column {\n pointer-events: all;\n}\n\n.cf-single-column-wrapper:after {\n content: '';\n display: block;\n position: absolute;\n pointer-events: none;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n overflow-x: clip;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 12px;\n color: var(--exp-builder-gray400);\n z-index: 1;\n}\n\n.cf-single-column-label:after {\n content: 'Column';\n}\n";
3476
3848
  styleInject(css_248z$9);
3477
3849
 
3850
+ const ColumnWrapper = forwardRef((props, ref) => {
3851
+ return (React.createElement("div", { ref: ref, ...props, style: {
3852
+ ...(props.style || {}),
3853
+ display: 'grid',
3854
+ gridTemplateColumns: 'repeat(12, [col-start] 1fr)',
3855
+ } }, props.children));
3856
+ });
3857
+ ColumnWrapper.displayName = 'ColumnWrapper';
3858
+
3478
3859
  const assemblyStyle = { display: 'contents' };
3860
+ // Feel free to do any magic as regards variable definitions for assemblies
3861
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
3479
3862
  const Assembly = (props) => {
3863
+ if (props.editorMode) {
3864
+ const { node, dragProps, ...editorModeProps } = props;
3865
+ return props.renderDropzone(node, {
3866
+ ...editorModeProps,
3867
+ ['data-test-id']: 'contentful-assembly',
3868
+ className: props.className,
3869
+ dragProps,
3870
+ });
3871
+ }
3480
3872
  // Using a display contents so assembly content/children
3481
3873
  // can appear as if they are direct children of the div wrapper's parent
3482
3874
  return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
@@ -3499,6 +3891,75 @@ const useEntityStore = create((set) => ({
3499
3891
  },
3500
3892
  }));
3501
3893
 
3894
+ class DragState {
3895
+ constructor() {
3896
+ this.isDragStartedOnParent = false;
3897
+ this.isDraggingItem = false;
3898
+ }
3899
+ get isDragging() {
3900
+ return this.isDraggingItem;
3901
+ }
3902
+ get isDraggingOnParent() {
3903
+ return this.isDragStartedOnParent;
3904
+ }
3905
+ updateIsDragging(isDraggingItem) {
3906
+ this.isDraggingItem = isDraggingItem;
3907
+ }
3908
+ updateIsDragStartedOnParent(isDragStartedOnParent) {
3909
+ this.isDragStartedOnParent = isDragStartedOnParent;
3910
+ }
3911
+ resetState() {
3912
+ this.isDraggingItem = false;
3913
+ this.isDragStartedOnParent = false;
3914
+ }
3915
+ }
3916
+
3917
+ class SimulateDnD extends DragState {
3918
+ constructor() {
3919
+ super();
3920
+ this.draggingElement = null;
3921
+ }
3922
+ setupDrag() {
3923
+ this.updateIsDragStartedOnParent(true);
3924
+ }
3925
+ startDrag(coordX, coordY) {
3926
+ this.draggingElement = document.getElementById(NEW_COMPONENT_ID);
3927
+ this.updateIsDragging(true);
3928
+ this.simulateMouseEvent(coordX, coordY, 'mousedown');
3929
+ }
3930
+ updateDrag(coordX, coordY) {
3931
+ if (!this.draggingElement) {
3932
+ this.draggingElement = document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`);
3933
+ }
3934
+ this.simulateMouseEvent(coordX, coordY);
3935
+ }
3936
+ endDrag(coordX, coordY) {
3937
+ this.simulateMouseEvent(coordX, coordY, 'mouseup');
3938
+ this.reset();
3939
+ }
3940
+ reset() {
3941
+ this.draggingElement = null;
3942
+ this.resetState();
3943
+ }
3944
+ simulateMouseEvent(coordX, coordY, eventName = 'mousemove') {
3945
+ if (!this.draggingElement) {
3946
+ return;
3947
+ }
3948
+ const options = {
3949
+ bubbles: true,
3950
+ cancelable: true,
3951
+ view: window,
3952
+ pageX: 0,
3953
+ pageY: 0,
3954
+ clientX: coordX,
3955
+ clientY: coordY,
3956
+ };
3957
+ const event = new MouseEvent(eventName, options);
3958
+ this.draggingElement.dispatchEvent(event);
3959
+ }
3960
+ }
3961
+ var SimulateDnD$1 = new SimulateDnD();
3962
+
3502
3963
  function useEditorSubscriber() {
3503
3964
  const entityStore = useEntityStore((state) => state.entityStore);
3504
3965
  const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
@@ -3512,7 +3973,14 @@ function useEditorSubscriber() {
3512
3973
  const setLocale = useEditorStore((state) => state.setLocale);
3513
3974
  const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
3514
3975
  const setDataSource = useEditorStore((state) => state.setDataSource);
3976
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
3977
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3515
3978
  const resetEntityStore = useEntityStore((state) => state.resetEntityStore);
3979
+ const setComponentId = useDraggedItemStore((state) => state.setComponentId);
3980
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
3981
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
3982
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
3983
+ const setScrollY = useDraggedItemStore((state) => state.setScrollY);
3516
3984
  const reloadApp = () => {
3517
3985
  sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
3518
3986
  // Wait a moment to ensure that the message was sent
@@ -3613,12 +4081,12 @@ function useEditorSubscriber() {
3613
4081
  }
3614
4082
  const eventData = tryParseMessage(event);
3615
4083
  console.debug(`[experiences-sdk-react::onMessage] Received message [${eventData.eventType}]`, eventData);
3616
- if (eventData.eventType === PostMessageMethods$3.REQUESTED_ENTITIES) {
4084
+ if (eventData.eventType === PostMessageMethods$2.REQUESTED_ENTITIES) {
3617
4085
  // Expected message: This message is handled in the EntityStore to store fetched entities
3618
4086
  return;
3619
4087
  }
3620
4088
  switch (eventData.eventType) {
3621
- case INCOMING_EVENTS$1.ExperienceUpdated: {
4089
+ case INCOMING_EVENTS.ExperienceUpdated: {
3622
4090
  const { tree, locale, changedNode, changedValueType, assemblies } = eventData.payload;
3623
4091
  // Make sure to first store the assemblies before setting the tree and thus triggering a rerender
3624
4092
  if (assemblies) {
@@ -3662,7 +4130,7 @@ function useEditorSubscriber() {
3662
4130
  updateTree(tree);
3663
4131
  break;
3664
4132
  }
3665
- case INCOMING_EVENTS$1.AssembliesRegistered: {
4133
+ case INCOMING_EVENTS.AssembliesRegistered: {
3666
4134
  const { assemblies } = eventData.payload;
3667
4135
  assemblies.forEach((definition) => {
3668
4136
  addComponentRegistration({
@@ -3672,7 +4140,7 @@ function useEditorSubscriber() {
3672
4140
  });
3673
4141
  break;
3674
4142
  }
3675
- case INCOMING_EVENTS$1.AssembliesAdded: {
4143
+ case INCOMING_EVENTS.AssembliesAdded: {
3676
4144
  const { assembly, assemblyDefinition, } = eventData.payload;
3677
4145
  entityStore.updateEntity(assembly);
3678
4146
  // Using a Map here to avoid setting state and rerending all existing assemblies when a new assembly is added
@@ -3689,7 +4157,28 @@ function useEditorSubscriber() {
3689
4157
  }
3690
4158
  break;
3691
4159
  }
3692
- case INCOMING_EVENTS$1.UpdatedEntity: {
4160
+ case INCOMING_EVENTS.CanvasResized: {
4161
+ const { selectedNodeId } = eventData.payload;
4162
+ if (selectedNodeId) {
4163
+ sendSelectedComponentCoordinates(selectedNodeId);
4164
+ }
4165
+ break;
4166
+ }
4167
+ case INCOMING_EVENTS.HoverComponent: {
4168
+ const { hoveredNodeId } = eventData.payload;
4169
+ setHoveredComponentId(hoveredNodeId);
4170
+ break;
4171
+ }
4172
+ case INCOMING_EVENTS.ComponentDraggingChanged: {
4173
+ const { isDragging } = eventData.payload;
4174
+ if (!isDragging) {
4175
+ setComponentId('');
4176
+ setDraggingOnCanvas(false);
4177
+ SimulateDnD$1.reset();
4178
+ }
4179
+ break;
4180
+ }
4181
+ case INCOMING_EVENTS.UpdatedEntity: {
3693
4182
  const { entity: updatedEntity, shouldRerender } = eventData.payload;
3694
4183
  if (updatedEntity) {
3695
4184
  const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
@@ -3702,7 +4191,52 @@ function useEditorSubscriber() {
3702
4191
  }
3703
4192
  break;
3704
4193
  }
3705
- case INCOMING_EVENTS$1.RequestEditorMode: {
4194
+ case INCOMING_EVENTS.RequestEditorMode: {
4195
+ break;
4196
+ }
4197
+ case INCOMING_EVENTS.ComponentDragCanceled: {
4198
+ if (SimulateDnD$1.isDragging) {
4199
+ //simulate a mouseup event to cancel the drag
4200
+ SimulateDnD$1.endDrag(0, 0);
4201
+ }
4202
+ break;
4203
+ }
4204
+ case INCOMING_EVENTS.ComponentDragStarted: {
4205
+ const { id, isAssembly } = eventData.payload;
4206
+ SimulateDnD$1.setupDrag();
4207
+ setComponentId(`${id}:${isAssembly}` || '');
4208
+ setDraggingOnCanvas(true);
4209
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4210
+ nodeId: '',
4211
+ });
4212
+ break;
4213
+ }
4214
+ case INCOMING_EVENTS.ComponentDragEnded: {
4215
+ SimulateDnD$1.reset();
4216
+ setComponentId('');
4217
+ setDraggingOnCanvas(false);
4218
+ break;
4219
+ }
4220
+ case INCOMING_EVENTS.SelectComponent: {
4221
+ const { selectedNodeId: nodeId } = eventData.payload;
4222
+ setSelectedNodeId(nodeId);
4223
+ sendSelectedComponentCoordinates(nodeId);
4224
+ break;
4225
+ }
4226
+ case INCOMING_EVENTS.MouseMove: {
4227
+ const { mouseX, mouseY } = eventData.payload;
4228
+ setMousePosition(mouseX, mouseY);
4229
+ if (SimulateDnD$1.isDraggingOnParent && !SimulateDnD$1.isDragging) {
4230
+ SimulateDnD$1.startDrag(mouseX, mouseY);
4231
+ }
4232
+ else {
4233
+ SimulateDnD$1.updateDrag(mouseX, mouseY);
4234
+ }
4235
+ break;
4236
+ }
4237
+ case INCOMING_EVENTS.ComponentMoveEnded: {
4238
+ const { mouseX, mouseY } = eventData.payload;
4239
+ SimulateDnD$1.endDrag(mouseX, mouseY);
3706
4240
  break;
3707
4241
  }
3708
4242
  default:
@@ -3715,8 +4249,11 @@ function useEditorSubscriber() {
3715
4249
  };
3716
4250
  }, [
3717
4251
  entityStore,
4252
+ setComponentId,
4253
+ setDraggingOnCanvas,
3718
4254
  setDataSource,
3719
4255
  setLocale,
4256
+ setSelectedNodeId,
3720
4257
  dataSource,
3721
4258
  areEntitiesFetched,
3722
4259
  fetchMissingEntities,
@@ -3724,92 +4261,328 @@ function useEditorSubscriber() {
3724
4261
  unboundValues,
3725
4262
  updateTree,
3726
4263
  updateNodesByUpdatedEntity,
4264
+ setMousePosition,
3727
4265
  resetEntityStore,
4266
+ setHoveredComponentId,
3728
4267
  ]);
4268
+ /*
4269
+ * Handles on scroll business
4270
+ */
4271
+ useEffect(() => {
4272
+ let timeoutId = 0;
4273
+ let isScrolling = false;
4274
+ const onScroll = () => {
4275
+ setScrollY(window.scrollY);
4276
+ if (isScrolling === false) {
4277
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.Start);
4278
+ }
4279
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.IsScrolling);
4280
+ isScrolling = true;
4281
+ clearTimeout(timeoutId);
4282
+ timeoutId = window.setTimeout(() => {
4283
+ if (isScrolling === false) {
4284
+ return;
4285
+ }
4286
+ isScrolling = false;
4287
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.End);
4288
+ /**
4289
+ * On scroll end, send new co-ordinates of selected node
4290
+ */
4291
+ if (selectedNodeId) {
4292
+ sendSelectedComponentCoordinates(selectedNodeId);
4293
+ }
4294
+ }, 150);
4295
+ };
4296
+ window.addEventListener('scroll', onScroll, { capture: true, passive: true });
4297
+ return () => {
4298
+ window.removeEventListener('scroll', onScroll, { capture: true });
4299
+ clearTimeout(timeoutId);
4300
+ };
4301
+ }, [selectedNodeId, setScrollY]);
3729
4302
  }
3730
4303
 
3731
- const CircularDependencyErrorPlaceholder = ({ node, wrappingPatternIds, }) => {
3732
- const entityStore = useEntityStore((state) => state.entityStore);
3733
- return (React.createElement("div", { "data-cf-node-id": node.data.id, "data-cf-node-block-id": node.data.blockId, "data-cf-node-block-type": node.type, "data-cf-node-error": "circular-pattern-dependency", style: {
3734
- border: '1px solid red',
3735
- background: 'rgba(255, 0, 0, 0.1)',
3736
- padding: '1rem 1rem 0 1rem',
3737
- width: '100%',
3738
- height: '100%',
3739
- } },
3740
- "Circular usage of patterns detected:",
3741
- React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
3742
- const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
3743
- const entry = entityStore.getEntityFromLink(entryLink);
3744
- const entryTitle = entry?.fields?.title;
3745
- const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
3746
- return React.createElement("li", { key: patternId }, text);
3747
- }))));
4304
+ const onComponentMoved = (options) => {
4305
+ sendMessage(OUTGOING_EVENTS.ComponentMoved, options);
3748
4306
  };
3749
4307
 
3750
- class ImportedComponentError extends Error {
3751
- constructor(message) {
3752
- super(message);
3753
- this.name = 'ImportedComponentError';
3754
- }
3755
- }
3756
- class ExperienceSDKError extends Error {
3757
- constructor(message) {
3758
- super(message);
3759
- this.name = 'ExperienceSDKError';
3760
- }
3761
- }
3762
- class ImportedComponentErrorBoundary extends React.Component {
3763
- componentDidCatch(error, _errorInfo) {
3764
- if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
3765
- // This error was already handled by a nested error boundary and should be passed upwards
3766
- // We have to do this as we wrap every component on every layer with this error boundary and
3767
- // thus an error deep in the tree bubbles through many layers of error boundaries.
3768
- throw error;
3769
- }
3770
- // Differentiate between custom and SDK-provided components for error tracking
3771
- const ErrorClass = isContentfulComponent(this.props.componentId)
3772
- ? ExperienceSDKError
3773
- : ImportedComponentError;
3774
- const err = new ErrorClass(error.message);
3775
- err.stack = error.stack;
3776
- throw err;
3777
- }
3778
- render() {
3779
- return this.props.children;
3780
- }
3781
- }
4308
+ const generateId = (type) => `${type}-${v4()}`;
3782
4309
 
3783
- const MissingComponentPlaceholder = ({ blockId }) => {
3784
- return (React.createElement("div", { style: {
3785
- border: '1px solid red',
3786
- width: '100%',
3787
- height: '100%',
3788
- } },
3789
- "Missing component '",
3790
- blockId,
3791
- "'"));
4310
+ const createTreeNode = ({ blockId, parentId, slotId }) => {
4311
+ const node = {
4312
+ type: 'block',
4313
+ data: {
4314
+ id: generateId(blockId),
4315
+ blockId,
4316
+ slotId,
4317
+ props: {},
4318
+ dataSource: {},
4319
+ breakpoints: [],
4320
+ unboundValues: {},
4321
+ },
4322
+ parentId,
4323
+ children: [],
4324
+ };
4325
+ return node;
3792
4326
  };
3793
4327
 
3794
- var css_248z$1 = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
3795
- var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
3796
- styleInject(css_248z$1);
4328
+ const onComponentDropped = ({ node, index, parentBlockId, parentType, parentId, }) => {
4329
+ sendMessage(OUTGOING_EVENTS.ComponentDropped, {
4330
+ node,
4331
+ index: index ?? node.children.length,
4332
+ parentNode: {
4333
+ type: parentType,
4334
+ data: {
4335
+ blockId: parentBlockId,
4336
+ id: parentId,
4337
+ },
4338
+ },
4339
+ });
4340
+ };
3797
4341
 
3798
- const useComponentRegistration = (node) => {
3799
- return useMemo(() => {
3800
- let registration = componentRegistry.get(node.data.blockId);
3801
- if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
3802
- registration = createAssemblyRegistration({
3803
- definitionId: node.data.blockId,
3804
- component: Assembly,
4342
+ const onDrop = ({ destinationIndex, componentType, destinationZoneId, data, slotId, }) => {
4343
+ const parentId = destinationZoneId;
4344
+ const parentNode = getItem({ id: parentId }, data);
4345
+ const parentIsRoot = parentId === ROOT_ID;
4346
+ const emptyComponentData = {
4347
+ type: 'block',
4348
+ parentId,
4349
+ children: [],
4350
+ data: {
4351
+ blockId: componentType,
4352
+ id: generateId(componentType),
4353
+ slotId,
4354
+ breakpoints: [],
4355
+ dataSource: {},
4356
+ props: {},
4357
+ unboundValues: {},
4358
+ },
4359
+ };
4360
+ onComponentDropped({
4361
+ node: emptyComponentData,
4362
+ index: destinationIndex,
4363
+ parentType: parentIsRoot ? 'root' : parentNode?.type,
4364
+ parentBlockId: parentNode?.data.blockId,
4365
+ parentId: parentIsRoot ? 'root' : parentId,
4366
+ });
4367
+ };
4368
+
4369
+ /**
4370
+ * Parses a droppable zone ID into a node ID and slot ID.
4371
+ *
4372
+ * The slot ID is optional and only present if the component implements multiple drop zones.
4373
+ *
4374
+ * @param zoneId - Expected formats are `nodeId` or `nodeId|slotId`.
4375
+ */
4376
+ const parseZoneId = (zoneId) => {
4377
+ const [nodeId, slotId] = zoneId.includes('|') ? zoneId.split('|') : [zoneId, undefined];
4378
+ return { nodeId, slotId };
4379
+ };
4380
+
4381
+ function useCanvasInteractions() {
4382
+ const tree = useTreeStore((state) => state.tree);
4383
+ const reorderChildren = useTreeStore((state) => state.reorderChildren);
4384
+ const reparentChild = useTreeStore((state) => state.reparentChild);
4385
+ const addChild = useTreeStore((state) => state.addChild);
4386
+ const onAddComponent = (droppedItem) => {
4387
+ const { destination, draggableId } = droppedItem;
4388
+ if (!destination) {
4389
+ return;
4390
+ }
4391
+ /**
4392
+ * We only have the draggableId as information about the new component being dropped.
4393
+ * So we need to split it to get the blockId and the isAssembly flag.
4394
+ */
4395
+ const [blockId, isAssembly] = draggableId.split(':');
4396
+ const { nodeId: parentId, slotId } = parseZoneId(destination.droppableId);
4397
+ const droppingOnRoot = parentId === ROOT_ID;
4398
+ const isValidRootComponent = blockId === CONTENTFUL_COMPONENTS$1.container.id;
4399
+ let node = createTreeNode({ blockId, parentId, slotId });
4400
+ if (droppingOnRoot && !isValidRootComponent) {
4401
+ const wrappingContainer = createTreeNode({
4402
+ blockId: CONTENTFUL_COMPONENTS$1.container.id,
4403
+ parentId,
3805
4404
  });
4405
+ const childNode = createTreeNode({
4406
+ blockId,
4407
+ parentId: wrappingContainer.data.id,
4408
+ });
4409
+ node = wrappingContainer;
4410
+ node.children = [childNode];
3806
4411
  }
3807
- if (!registration) {
3808
- console.warn(`Component registration not found for component with id: "${node.data.blockId}". The registered component might have been removed from the code. To proceed, remove the component manually from the layers tab.`);
3809
- return undefined;
4412
+ /**
4413
+ * isAssembly comes from a string ID so we need to check if it's 'true' or 'false'
4414
+ * in string format.
4415
+ */
4416
+ if (isAssembly === 'false') {
4417
+ addChild(destination.index, parentId, node);
3810
4418
  }
3811
- return registration;
3812
- }, [node]);
4419
+ onDrop({
4420
+ data: tree,
4421
+ componentType: blockId,
4422
+ destinationIndex: destination.index,
4423
+ destinationZoneId: parentId,
4424
+ slotId,
4425
+ });
4426
+ };
4427
+ const onMoveComponent = (droppedItem) => {
4428
+ const { destination, source, draggableId } = droppedItem;
4429
+ if (!destination || !source) {
4430
+ return;
4431
+ }
4432
+ if (destination.droppableId === source.droppableId) {
4433
+ reorderChildren(destination.index, destination.droppableId, source.index);
4434
+ }
4435
+ if (destination.droppableId !== source.droppableId) {
4436
+ reparentChild(destination.index, destination.droppableId, source.index, source.droppableId);
4437
+ }
4438
+ onComponentMoved({
4439
+ nodeId: draggableId,
4440
+ destinationIndex: destination.index,
4441
+ destinationParentId: destination.droppableId,
4442
+ sourceIndex: source.index,
4443
+ sourceParentId: source.droppableId,
4444
+ });
4445
+ };
4446
+ return { onAddComponent, onMoveComponent };
4447
+ }
4448
+
4449
+ const TestDNDContainer = ({ onDragEnd, onBeforeDragStart, onDragStart, onDragUpdate, children, }) => {
4450
+ const handleDragStart = (event) => {
4451
+ const draggedItem = event.nativeEvent;
4452
+ const start = {
4453
+ mode: draggedItem.mode,
4454
+ draggableId: draggedItem.draggableId,
4455
+ type: draggedItem.type,
4456
+ source: draggedItem.source,
4457
+ };
4458
+ onBeforeDragStart(start);
4459
+ onDragStart(start, {});
4460
+ };
4461
+ const handleDrag = (event) => {
4462
+ const draggedItem = event.nativeEvent;
4463
+ const update = {
4464
+ mode: draggedItem.mode,
4465
+ draggableId: draggedItem.draggableId,
4466
+ type: draggedItem.type,
4467
+ source: draggedItem.source,
4468
+ destination: draggedItem.destination,
4469
+ combine: draggedItem.combine,
4470
+ };
4471
+ onDragUpdate(update, {});
4472
+ };
4473
+ const handleDragEnd = (event) => {
4474
+ const draggedItem = event.nativeEvent;
4475
+ const result = {
4476
+ mode: draggedItem.mode,
4477
+ draggableId: draggedItem.draggableId,
4478
+ type: draggedItem.type,
4479
+ source: draggedItem.source,
4480
+ destination: draggedItem.destination,
4481
+ combine: draggedItem.combine,
4482
+ reason: draggedItem.reason,
4483
+ };
4484
+ onDragEnd(result, {});
4485
+ };
4486
+ return (React.createElement("div", { "data-test-id": "dnd-context-substitute", onDragStart: handleDragStart, onDrag: handleDrag, onDragEnd: handleDragEnd }, children));
4487
+ };
4488
+
4489
+ const DNDProvider = ({ children }) => {
4490
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4491
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
4492
+ const setOnBeforeCaptureId = useDraggedItemStore((state) => state.setOnBeforeCaptureId);
4493
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4494
+ const updateItem = useDraggedItemStore((state) => state.updateItem);
4495
+ const { onAddComponent, onMoveComponent } = useCanvasInteractions();
4496
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4497
+ const prevSelectedNodeId = useRef(null);
4498
+ const isTestRun = typeof window !== 'undefined' && Object.prototype.hasOwnProperty.call(window, 'Cypress');
4499
+ const beforeDragStart = ({ source }) => {
4500
+ prevSelectedNodeId.current = selectedNodeId;
4501
+ // Unselect the current node when dragging and remove the outline
4502
+ setSelectedNodeId('');
4503
+ // Set dragging state here to make sure that DnD capture phase has completed
4504
+ setDraggingOnCanvas(true);
4505
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4506
+ nodeId: '',
4507
+ });
4508
+ if (source.droppableId !== COMPONENT_LIST_ID) {
4509
+ sendMessage(OUTGOING_EVENTS.ComponentMoveStarted, undefined);
4510
+ }
4511
+ };
4512
+ const beforeCapture = ({ draggableId }) => {
4513
+ setOnBeforeCaptureId(draggableId);
4514
+ };
4515
+ const dragStart = (start) => {
4516
+ updateItem(start);
4517
+ };
4518
+ const dragUpdate = (update) => {
4519
+ updateItem(update);
4520
+ };
4521
+ const dragEnd = (dropResult) => {
4522
+ setDraggingOnCanvas(false);
4523
+ setOnBeforeCaptureId('');
4524
+ updateItem();
4525
+ SimulateDnD$1.reset();
4526
+ // If the component is being dropped onto itself, do nothing
4527
+ // This can happen from an apparent race condition where the hovering zone gets set
4528
+ // to the component after its dropped.
4529
+ if (dropResult.destination?.droppableId === dropResult.draggableId) {
4530
+ return;
4531
+ }
4532
+ if (!dropResult.destination) {
4533
+ if (!draggedItem?.destination) {
4534
+ // User cancel drag
4535
+ sendMessage(OUTGOING_EVENTS.ComponentDragCanceled, undefined);
4536
+ //select the previously selected node if drag was canceled
4537
+ if (prevSelectedNodeId.current) {
4538
+ setSelectedNodeId(prevSelectedNodeId.current);
4539
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4540
+ nodeId: prevSelectedNodeId.current,
4541
+ });
4542
+ prevSelectedNodeId.current = null;
4543
+ }
4544
+ return;
4545
+ }
4546
+ // Use the destination from the draggedItem (when clicking the canvas)
4547
+ dropResult.destination = draggedItem.destination;
4548
+ }
4549
+ // New component added to canvas
4550
+ if (dropResult.source.droppableId.startsWith('component-list')) {
4551
+ onAddComponent(dropResult);
4552
+ }
4553
+ else {
4554
+ onMoveComponent(dropResult);
4555
+ }
4556
+ // If a node was previously selected prior to dragging, re-select it
4557
+ setSelectedNodeId(dropResult.draggableId);
4558
+ sendMessage(OUTGOING_EVENTS.ComponentMoveEnded, undefined);
4559
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4560
+ nodeId: dropResult.draggableId,
4561
+ });
4562
+ };
4563
+ return (React.createElement(DragDropContext, { onBeforeCapture: beforeCapture, onDragUpdate: dragUpdate, onBeforeDragStart: beforeDragStart, onDragStart: dragStart, onDragEnd: dragEnd }, isTestRun ? (React.createElement(TestDNDContainer, { onDragEnd: dragEnd, onBeforeDragStart: beforeDragStart, onDragStart: dragStart, onDragUpdate: dragUpdate }, children)) : (children)));
4564
+ };
4565
+
4566
+ /**
4567
+ * This hook gets the element co-ordinates of a specified element in the DOM
4568
+ * and sends the DOM Rect to the client app
4569
+ */
4570
+ const useSelectedInstanceCoordinates = ({ node }) => {
4571
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4572
+ useEffect(() => {
4573
+ if (selectedNodeId !== node.data.id) {
4574
+ return;
4575
+ }
4576
+ // Allows the drop animation to finish before
4577
+ // calculating the components coordinates
4578
+ setTimeout(() => {
4579
+ sendSelectedComponentCoordinates(node.data.id);
4580
+ }, 10);
4581
+ }, [node, selectedNodeId]);
4582
+ const selectedElement = node.data.id
4583
+ ? document.querySelector(`[data-cf-node-id="${selectedNodeId}"]`)
4584
+ : undefined;
4585
+ return selectedElement ? getElementCoordinates(selectedElement) : null;
3813
4586
  };
3814
4587
 
3815
4588
  /**
@@ -3857,13 +4630,15 @@ const getUnboundValues = ({ key, fallback, unboundValues, }) => {
3857
4630
  return get$1(unboundValues, lodashPath, fallback);
3858
4631
  };
3859
4632
 
3860
- const useComponentProps = ({ node, resolveDesignValue, definition, options, }) => {
4633
+ const useComponentProps = ({ node, areEntitiesFetched, resolveDesignValue, renderDropzone, definition, options, userIsDragging, requiresDragWrapper, }) => {
3861
4634
  const unboundValues = useEditorStore((state) => state.unboundValues);
3862
4635
  const hyperlinkPattern = useEditorStore((state) => state.hyperLinkPattern);
3863
4636
  const locale = useEditorStore((state) => state.locale);
3864
4637
  const dataSource = useEditorStore((state) => state.dataSource);
3865
4638
  const entityStore = useEntityStore((state) => state.entityStore);
3866
- const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
4639
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
4640
+ const nodeRect = useDraggedItemStore((state) => state.domRect);
4641
+ const isEmptyZone = !node.children.length;
3867
4642
  const props = useMemo(() => {
3868
4643
  const propsBase = {
3869
4644
  cfSsrClassName: node.data.props.cfSsrClassName
@@ -3953,126 +4728,1133 @@ const useComponentProps = ({ node, resolveDesignValue, definition, options, }) =
3953
4728
  return { ...acc };
3954
4729
  }
3955
4730
  }, {});
4731
+ const slotProps = {};
4732
+ if (definition.slots) {
4733
+ for (const slotId in definition.slots) {
4734
+ slotProps[slotId] = renderDropzone(node, {
4735
+ zoneId: [node.data.id, slotId].join('|'),
4736
+ });
4737
+ }
4738
+ }
3956
4739
  return {
3957
4740
  ...propsBase,
3958
4741
  ...extractedProps,
4742
+ ...slotProps,
3959
4743
  };
3960
4744
  }, [
3961
4745
  hyperlinkPattern,
3962
4746
  node,
3963
- locale,
3964
- definition,
3965
- resolveDesignValue,
3966
- dataSource,
4747
+ locale,
4748
+ definition,
4749
+ resolveDesignValue,
4750
+ dataSource,
4751
+ areEntitiesFetched,
4752
+ unboundValues,
4753
+ entityStore,
4754
+ renderDropzone,
4755
+ ]);
4756
+ const cfStyles = useMemo(() => buildCfStyles(props), [props]);
4757
+ const cfVisibility = props['cfVisibility'];
4758
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
4759
+ const isSingleColumn = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
4760
+ const isStructureComponent = isContentfulStructureComponent(node?.data.blockId);
4761
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
4762
+ const { overrideStyles, wrapperStyles } = useMemo(() => {
4763
+ // Move size styles to the wrapping div and override the component styles
4764
+ const overrideStyles = {};
4765
+ const wrapperStyles = { width: options?.wrapContainerWidth };
4766
+ if (requiresDragWrapper) {
4767
+ // when element is marked by user as not-visible, on that element the node `display: none !important`
4768
+ // will be set and it will disappear. However, when such a node has a wrapper div, the wrapper
4769
+ // should not have any css properties (at least not ones which force size), as such div should
4770
+ // simply be a zero height wrapper around element with `display: none !important`.
4771
+ // Hence we guard all wrapperStyles with `cfVisibility` check.
4772
+ if (cfVisibility && cfStyles.width)
4773
+ wrapperStyles.width = cfStyles.width;
4774
+ if (cfVisibility && cfStyles.height)
4775
+ wrapperStyles.height = cfStyles.height;
4776
+ if (cfVisibility && cfStyles.maxWidth)
4777
+ wrapperStyles.maxWidth = cfStyles.maxWidth;
4778
+ if (cfVisibility && cfStyles.margin)
4779
+ wrapperStyles.margin = cfStyles.margin;
4780
+ }
4781
+ // Override component styles to fill the wrapper
4782
+ if (wrapperStyles.width)
4783
+ overrideStyles.width = '100%';
4784
+ if (wrapperStyles.height)
4785
+ overrideStyles.height = '100%';
4786
+ if (wrapperStyles.margin)
4787
+ overrideStyles.margin = '0';
4788
+ if (wrapperStyles.maxWidth)
4789
+ overrideStyles.maxWidth = 'none';
4790
+ // Prevent the dragging element from changing sizes when it has a percentage width or height
4791
+ if (draggingId === node.data.id && nodeRect) {
4792
+ if (requiresDragWrapper) {
4793
+ if (isPercentValue(cfStyles.width))
4794
+ wrapperStyles.maxWidth = nodeRect.width;
4795
+ if (isPercentValue(cfStyles.height))
4796
+ wrapperStyles.maxHeight = nodeRect.height;
4797
+ }
4798
+ else {
4799
+ if (isPercentValue(cfStyles.width))
4800
+ overrideStyles.maxWidth = nodeRect.width;
4801
+ if (isPercentValue(cfStyles.height))
4802
+ overrideStyles.maxHeight = nodeRect.height;
4803
+ }
4804
+ }
4805
+ return { overrideStyles, wrapperStyles };
4806
+ }, [
4807
+ cfStyles,
4808
+ options?.wrapContainerWidth,
4809
+ requiresDragWrapper,
4810
+ node.data.id,
4811
+ draggingId,
4812
+ nodeRect,
4813
+ cfVisibility,
4814
+ ]);
4815
+ // Styles that will be applied to the component element
4816
+ // This has to be memoized to avoid recreating the styles in useEditorModeClassName on every render
4817
+ const componentStyles = useMemo(() => ({
4818
+ ...cfStyles,
4819
+ ...overrideStyles,
4820
+ ...(isEmptyZone &&
4821
+ isStructureWithRelativeHeight(node?.data.blockId, cfStyles.height) && {
4822
+ minHeight: EMPTY_CONTAINER_HEIGHT,
4823
+ }),
4824
+ ...(userIsDragging &&
4825
+ isStructureComponent &&
4826
+ !isSingleColumn &&
4827
+ !isAssemblyBlock && {
4828
+ padding: addExtraDropzonePadding(cfStyles.padding?.toString() || '0 0 0 0'),
4829
+ }),
4830
+ }), [
4831
+ cfStyles,
4832
+ isAssemblyBlock,
4833
+ isEmptyZone,
4834
+ isSingleColumn,
4835
+ isStructureComponent,
4836
+ node?.data.blockId,
4837
+ overrideStyles,
4838
+ userIsDragging,
4839
+ ]);
4840
+ const componentClass = useEditorModeClassName({
4841
+ styles: componentStyles,
4842
+ nodeId: node.data.id,
4843
+ });
4844
+ const sharedProps = {
4845
+ 'data-cf-node-id': node.data.id,
4846
+ 'data-cf-node-block-id': node.data.blockId,
4847
+ 'data-cf-node-block-type': node.type,
4848
+ className: props.cfSsrClassName ?? componentClass,
4849
+ ...(definition?.children ? { children: renderDropzone(node) } : {}),
4850
+ };
4851
+ const customComponentProps = {
4852
+ ...sharedProps,
4853
+ // Allows custom components to render differently in the editor. This needs to be activated
4854
+ // through options as the component has to be aware of this prop to not cause any React warnings.
4855
+ ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4856
+ ...sanitizeNodeProps(props),
4857
+ };
4858
+ const structuralOrPatternComponentProps = {
4859
+ ...sharedProps,
4860
+ editorMode: true,
4861
+ node,
4862
+ renderDropzone,
4863
+ };
4864
+ return {
4865
+ componentProps: isStructureComponent || isPatternNode
4866
+ ? structuralOrPatternComponentProps
4867
+ : customComponentProps,
4868
+ componentStyles,
4869
+ wrapperStyles,
4870
+ };
4871
+ };
4872
+ const addExtraDropzonePadding = (padding) => padding
4873
+ .split(' ')
4874
+ .map((value) => parseFloat(value) === 0 ? `${DRAG_PADDING}px` : `calc(${value} + ${DRAG_PADDING}px)`)
4875
+ .join(' ');
4876
+ const isPercentValue = (value) => typeof value === 'string' && value.endsWith('%');
4877
+
4878
+ class ImportedComponentError extends Error {
4879
+ constructor(message) {
4880
+ super(message);
4881
+ this.name = 'ImportedComponentError';
4882
+ }
4883
+ }
4884
+ class ExperienceSDKError extends Error {
4885
+ constructor(message) {
4886
+ super(message);
4887
+ this.name = 'ExperienceSDKError';
4888
+ }
4889
+ }
4890
+ class ImportedComponentErrorBoundary extends React.Component {
4891
+ componentDidCatch(error, _errorInfo) {
4892
+ if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
4893
+ // This error was already handled by a nested error boundary and should be passed upwards
4894
+ // We have to do this as we wrap every component on every layer with this error boundary and
4895
+ // thus an error deep in the tree bubbles through many layers of error boundaries.
4896
+ throw error;
4897
+ }
4898
+ // Differentiate between custom and SDK-provided components for error tracking
4899
+ const ErrorClass = isContentfulComponent(this.props.componentId)
4900
+ ? ExperienceSDKError
4901
+ : ImportedComponentError;
4902
+ const err = new ErrorClass(error.message);
4903
+ err.stack = error.stack;
4904
+ throw err;
4905
+ }
4906
+ render() {
4907
+ return this.props.children;
4908
+ }
4909
+ }
4910
+
4911
+ const MissingComponentPlaceholder = ({ blockId }) => {
4912
+ return (React.createElement("div", { style: {
4913
+ border: '1px solid red',
4914
+ width: '100%',
4915
+ height: '100%',
4916
+ } },
4917
+ "Missing component '",
4918
+ blockId,
4919
+ "'"));
4920
+ };
4921
+
4922
+ const CircularDependencyErrorPlaceholder = forwardRef(({ wrappingPatternIds, ...props }, ref) => {
4923
+ const entityStore = useEntityStore((state) => state.entityStore);
4924
+ return (React.createElement("div", { ...props,
4925
+ // Pass through ref to avoid DND errors being logged
4926
+ ref: ref, "data-cf-node-error": "circular-pattern-dependency", style: {
4927
+ border: '1px solid red',
4928
+ background: 'rgba(255, 0, 0, 0.1)',
4929
+ padding: '1rem 1rem 0 1rem',
4930
+ width: '100%',
4931
+ height: '100%',
4932
+ } },
4933
+ "Circular usage of patterns detected:",
4934
+ React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
4935
+ const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
4936
+ const entry = entityStore.getEntityFromLink(entryLink);
4937
+ const entryTitle = entry?.fields?.title;
4938
+ const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
4939
+ return React.createElement("li", { key: patternId }, text);
4940
+ }))));
4941
+ });
4942
+ CircularDependencyErrorPlaceholder.displayName = 'CircularDependencyErrorPlaceholder';
4943
+
4944
+ const useComponent = ({ node, resolveDesignValue, renderDropzone, userIsDragging, wrappingPatternIds, }) => {
4945
+ const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
4946
+ const tree = useTreeStore((state) => state.tree);
4947
+ const componentRegistration = useMemo(() => {
4948
+ let registration = componentRegistry.get(node.data.blockId);
4949
+ if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
4950
+ registration = createAssemblyRegistration({
4951
+ definitionId: node.data.blockId,
4952
+ component: Assembly,
4953
+ });
4954
+ }
4955
+ if (!registration) {
4956
+ console.warn(`Component registration not found for component with id: "${node.data.blockId}". The registered component might have been removed from the code. To proceed, remove the component manually from the layers tab.`);
4957
+ return undefined;
4958
+ }
4959
+ return registration;
4960
+ }, [node]);
4961
+ const componentId = node.data.id;
4962
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
4963
+ const isPatternComponent = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
4964
+ const parentComponentNode = getItem({ id: node.parentId }, tree);
4965
+ const isNestedPattern = isPatternNode &&
4966
+ [ASSEMBLY_BLOCK_NODE_TYPE, ASSEMBLY_NODE_TYPE].includes(parentComponentNode?.type ?? '');
4967
+ const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
4968
+ const requiresDragWrapper = !isPatternNode && !isStructureComponent && !componentRegistration?.options?.wrapComponent;
4969
+ const { componentProps, wrapperStyles } = useComponentProps({
4970
+ node,
3967
4971
  areEntitiesFetched,
3968
- unboundValues,
3969
- entityStore,
3970
- ]);
3971
- const cfStyles = useMemo(() => buildCfStyles(props), [props]);
3972
- // Styles that will be applied to the component element
3973
- const componentStyles = useMemo(() => ({
3974
- ...cfStyles,
3975
- ...(!node.children.length &&
3976
- isStructureWithRelativeHeight(node.data.blockId, cfStyles.height) && {
3977
- minHeight: EMPTY_CONTAINER_HEIGHT$1,
3978
- }),
3979
- }), [cfStyles, node.children.length, node.data.blockId]);
3980
- const cfCsrClassName = useEditorModeClassName({
3981
- styles: componentStyles,
3982
- nodeId: node.data.id,
4972
+ resolveDesignValue,
4973
+ renderDropzone,
4974
+ definition: componentRegistration?.definition,
4975
+ options: componentRegistration?.options,
4976
+ userIsDragging,
4977
+ requiresDragWrapper,
3983
4978
  });
3984
- const componentProps = useMemo(() => {
3985
- const sharedProps = {
3986
- 'data-cf-node-id': node.data.id,
3987
- 'data-cf-node-block-id': node.data.blockId,
3988
- 'data-cf-node-block-type': node.type,
3989
- className: props.cfSsrClassName ?? cfCsrClassName,
4979
+ const elementToRender = (props) => {
4980
+ const { dragProps = {} } = props || {};
4981
+ const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps;
4982
+ const { 'data-cf-node-block-id': dataCfNodeBlockId, 'data-cf-node-block-type': dataCfNodeBlockType, 'data-cf-node-id': dataCfNodeId, } = componentProps;
4983
+ const refCallback = (refNode) => {
4984
+ if (innerRef && refNode)
4985
+ innerRef(refNode);
3990
4986
  };
3991
- // Only pass `editorMode` and `node` to structure components and assembly root nodes.
3992
- const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
3993
- if (isStructureComponent) {
3994
- return {
3995
- ...sharedProps,
3996
- editorMode: true,
3997
- node,
3998
- };
4987
+ if (!componentRegistration) {
4988
+ return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4989
+ }
4990
+ if (node.data.blockId && wrappingPatternIds.has(node.data.blockId)) {
4991
+ return (React.createElement(CircularDependencyErrorPlaceholder, { ref: refCallback, "data-cf-node-id": dataCfNodeId, "data-cf-node-block-id": dataCfNodeBlockId, "data-cf-node-block-type": dataCfNodeBlockType, wrappingPatternIds: wrappingPatternIds }));
3999
4992
  }
4993
+ const element = React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, {
4994
+ ...componentProps,
4995
+ dragProps,
4996
+ }));
4997
+ if (!requiresDragWrapper) {
4998
+ return element;
4999
+ }
5000
+ return (React.createElement(Tag, { ...rest, style: { ...style, ...wrapperStyles }, ref: refCallback, "data-cf-node-id": dataCfNodeId, "data-cf-node-block-id": dataCfNodeBlockId, "data-cf-node-block-type": dataCfNodeBlockType },
5001
+ ToolTipAndPlaceholder,
5002
+ element));
5003
+ };
5004
+ return {
5005
+ node,
5006
+ parentComponentNode,
5007
+ isAssembly: isPatternNode,
5008
+ isPatternNode,
5009
+ isPatternComponent,
5010
+ isNestedPattern,
5011
+ componentId,
5012
+ elementToRender,
5013
+ definition: componentRegistration?.definition,
5014
+ };
5015
+ };
5016
+
5017
+ const calcOffsetLeft = (parentElement, placeholderWidth, nodeWidth) => {
5018
+ if (!parentElement) {
5019
+ return 0;
5020
+ }
5021
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5022
+ if (alignItems === 'center') {
5023
+ return -(placeholderWidth - nodeWidth) / 2;
5024
+ }
5025
+ if (alignItems === 'end') {
5026
+ return -placeholderWidth + nodeWidth + 2;
5027
+ }
5028
+ return 0;
5029
+ };
5030
+ const calcOffsetTop = (parentElement, placeholderHeight, nodeHeight) => {
5031
+ if (!parentElement) {
5032
+ return 0;
5033
+ }
5034
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5035
+ if (alignItems === 'center') {
5036
+ return -(placeholderHeight - nodeHeight) / 2;
5037
+ }
5038
+ if (alignItems === 'end') {
5039
+ return -placeholderHeight + nodeHeight + 2;
5040
+ }
5041
+ return 0;
5042
+ };
5043
+ const getPaddingOffset = (element) => {
5044
+ const paddingLeft = parseFloat(window.getComputedStyle(element).paddingLeft);
5045
+ const paddingRight = parseFloat(window.getComputedStyle(element).paddingRight);
5046
+ const paddingTop = parseFloat(window.getComputedStyle(element).paddingTop);
5047
+ const paddingBottom = parseFloat(window.getComputedStyle(element).paddingBottom);
5048
+ const horizontalOffset = paddingLeft + paddingRight;
5049
+ const verticalOffset = paddingTop + paddingBottom;
5050
+ return [horizontalOffset, verticalOffset];
5051
+ };
5052
+ /**
5053
+ * Calculate the size and position of the dropzone indicator
5054
+ * when dragging a new component onto the canvas
5055
+ */
5056
+ const calcNewComponentStyles = (params) => {
5057
+ const { destinationIndex, elementIndex, dropzoneElementId, id, direction, totalIndexes } = params;
5058
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5059
+ const isHorizontal = direction === 'horizontal';
5060
+ const isRightAlign = isHorizontal && isEnd;
5061
+ const isBottomAlign = !isHorizontal && isEnd;
5062
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5063
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5064
+ if (!dropzone || !element) {
5065
+ return emptyStyles;
5066
+ }
5067
+ const elementSizes = element.getBoundingClientRect();
5068
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5069
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5070
+ const width = isHorizontal ? DRAGGABLE_WIDTH : dropzoneSizes.width - horizontalPadding;
5071
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : DRAGGABLE_HEIGHT;
5072
+ const top = isHorizontal
5073
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5074
+ : -height;
5075
+ const left = isHorizontal
5076
+ ? -width
5077
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5078
+ return {
5079
+ width,
5080
+ height,
5081
+ top: !isBottomAlign ? top : 'unset',
5082
+ right: isRightAlign ? -width : 'unset',
5083
+ bottom: isBottomAlign ? -height : 'unset',
5084
+ left: !isRightAlign ? left : 'unset',
5085
+ };
5086
+ };
5087
+ /**
5088
+ * Calculate the size and position of the dropzone indicator
5089
+ * when moving an existing component on the canvas
5090
+ */
5091
+ const calcMovementStyles = (params) => {
5092
+ const { destinationIndex, sourceIndex, destinationId, sourceId, elementIndex, dropzoneElementId, id, direction, totalIndexes, draggableId, } = params;
5093
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5094
+ const isHorizontal = direction === 'horizontal';
5095
+ const isSameZone = destinationId === sourceId;
5096
+ const isBelowSourceIndex = destinationIndex > sourceIndex;
5097
+ const isRightAlign = isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5098
+ const isBottomAlign = !isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5099
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5100
+ const draggable = document.querySelector(`[data-rfd-draggable-id="${draggableId}"]`);
5101
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5102
+ if (!dropzone || !element || !draggable) {
5103
+ return emptyStyles;
5104
+ }
5105
+ const elementSizes = element.getBoundingClientRect();
5106
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5107
+ const draggableSizes = draggable.getBoundingClientRect();
5108
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5109
+ const width = isHorizontal ? draggableSizes.width : dropzoneSizes.width - horizontalPadding;
5110
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : draggableSizes.height;
5111
+ const top = isHorizontal
5112
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5113
+ : -height;
5114
+ const left = isHorizontal
5115
+ ? -width
5116
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5117
+ return {
5118
+ width,
5119
+ height,
5120
+ top: !isBottomAlign ? top : 'unset',
5121
+ right: isRightAlign ? -width : 'unset',
5122
+ bottom: isBottomAlign ? -height : 'unset',
5123
+ left: !isRightAlign ? left : 'unset',
5124
+ };
5125
+ };
5126
+ const emptyStyles = { width: 0, height: 0 };
5127
+ const calcPlaceholderStyles = (params) => {
5128
+ const { isDraggingOver, sourceId } = params;
5129
+ if (!isDraggingOver) {
5130
+ return emptyStyles;
5131
+ }
5132
+ if (sourceId === COMPONENT_LIST_ID) {
5133
+ return calcNewComponentStyles(params);
5134
+ }
5135
+ return calcMovementStyles(params);
5136
+ };
5137
+ const Placeholder = (props) => {
5138
+ const sourceIndex = useDraggedItemStore((state) => state.draggedItem?.source.index) ?? -1;
5139
+ const draggableId = useDraggedItemStore((state) => state.draggedItem?.draggableId) ?? '';
5140
+ const sourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId) ?? '';
5141
+ const destinationIndex = useDraggedItemStore((state) => state.draggedItem?.destination?.index) ?? -1;
5142
+ const destinationId = useDraggedItemStore((state) => state.draggedItem?.destination?.droppableId) ?? '';
5143
+ const { elementIndex, totalIndexes, isDraggingOver } = props;
5144
+ const isActive = destinationIndex === elementIndex;
5145
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5146
+ const isVisible = isEnd || isActive;
5147
+ const isComponentList = destinationId === COMPONENT_LIST_ID;
5148
+ return (!isComponentList &&
5149
+ isDraggingOver &&
5150
+ isVisible && (React.createElement("div", { style: {
5151
+ ...calcPlaceholderStyles({
5152
+ ...props,
5153
+ sourceId,
5154
+ sourceIndex,
5155
+ destinationId,
5156
+ destinationIndex,
5157
+ draggableId,
5158
+ }),
5159
+ backgroundColor: 'rgba(var(--exp-builder-blue300-rgb), 0.5)',
5160
+ position: 'absolute',
5161
+ pointerEvents: 'none',
5162
+ } })));
5163
+ };
5164
+
5165
+ var css_248z$2 = ".styles-module_hitbox__i3wKV {\n position: fixed;\n pointer-events: all;\n}\n";
5166
+ var styles$2 = {"hitbox":"styles-module_hitbox__i3wKV"};
5167
+ styleInject(css_248z$2);
5168
+
5169
+ const useZoneStore = create()((set) => ({
5170
+ zones: {},
5171
+ hoveringZone: '',
5172
+ setHoveringZone(zoneId) {
5173
+ set({
5174
+ hoveringZone: zoneId,
5175
+ });
5176
+ },
5177
+ upsertZone(id, data) {
5178
+ set(produce((state) => {
5179
+ state.zones[id] = { ...(state.zones[id] || {}), ...data };
5180
+ }));
5181
+ },
5182
+ }));
5183
+
5184
+ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_HEIGHT, DEEP_ZONE } = HITBOX;
5185
+ const calcOffsetDepth = (depth) => {
5186
+ return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
5187
+ };
5188
+ const getHitboxStyles = ({ direction, zoneDepth, domRect, scrollY, offsetRect, }) => {
5189
+ if (!domRect) {
4000
5190
  return {
4001
- ...sharedProps,
4002
- // Allows custom components to render differently in the editor. This needs to be activated
4003
- // through options as the component has to be aware of this prop to not cause any React warnings.
4004
- ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4005
- ...sanitizeNodeProps(props),
5191
+ display: 'none',
4006
5192
  };
4007
- }, [cfCsrClassName, node, options?.enableCustomEditorView, props]);
4008
- return { componentProps };
5193
+ }
5194
+ const { width, height, top, left, bottom, right } = domRect;
5195
+ const { height: offsetHeight, width: offsetWidth } = offsetRect || { height: 0, width: 0 };
5196
+ const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
5197
+ const isDeepZone = zoneDepth > DEEP_ZONE;
5198
+ const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
5199
+ switch (direction) {
5200
+ case HitboxDirection.TOP:
5201
+ return {
5202
+ width,
5203
+ height: HEIGHT,
5204
+ top: top + offsetHeight - calcOffsetDepth(zoneDepth) - scrollY,
5205
+ left,
5206
+ zIndex: 100 + zoneDepth,
5207
+ };
5208
+ case HitboxDirection.BOTTOM:
5209
+ return {
5210
+ width,
5211
+ height: HEIGHT,
5212
+ top: bottom + offsetHeight + calcOffsetDepth(zoneDepth) - scrollY,
5213
+ left,
5214
+ zIndex: 100 + zoneDepth,
5215
+ };
5216
+ case HitboxDirection.LEFT:
5217
+ return {
5218
+ width: WIDTH,
5219
+ height: height - HEIGHT,
5220
+ left: left + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
5221
+ top: top + HEIGHT / 2 - scrollY,
5222
+ zIndex: 100 + zoneDepth,
5223
+ };
5224
+ case HitboxDirection.RIGHT:
5225
+ return {
5226
+ width: WIDTH,
5227
+ height: height - HEIGHT,
5228
+ left: right + offsetWidth + calcOffsetDepth(zoneDepth) - WIDTH / 2,
5229
+ top: top + HEIGHT / 2 - scrollY,
5230
+ zIndex: 100 + zoneDepth,
5231
+ };
5232
+ case HitboxDirection.SELF_VERTICAL: {
5233
+ if (isAboveMaxHeight && !isDeepZone) {
5234
+ return { display: 'none' };
5235
+ }
5236
+ const selfHeight = isDeepZone ? MIN_DEPTH_HEIGHT : MIN_HEIGHT;
5237
+ return {
5238
+ width,
5239
+ height: selfHeight,
5240
+ left,
5241
+ top: top + height / 2 - selfHeight / 2 - scrollY,
5242
+ zIndex: 1000 + zoneDepth,
5243
+ };
5244
+ }
5245
+ case HitboxDirection.SELF_HORIZONTAL: {
5246
+ if (width > DRAGGABLE_WIDTH) {
5247
+ return { display: 'none' };
5248
+ }
5249
+ return {
5250
+ width: width - DRAGGABLE_WIDTH * 2,
5251
+ height,
5252
+ left: left + DRAGGABLE_WIDTH,
5253
+ top: top - scrollY,
5254
+ zIndex: 1000 + zoneDepth,
5255
+ };
5256
+ }
5257
+ default:
5258
+ return {};
5259
+ }
4009
5260
  };
4010
5261
 
4011
- function EditorBlock({ node, resolveDesignValue, wrappingPatternIds: parentWrappingPatternIds = new Set(), }) {
4012
- const isRootAssemblyNode = node.type === ASSEMBLY_NODE_TYPE;
4013
- const wrappingPatternIds = useMemo(() => {
4014
- if (isRootAssemblyNode && node.data.blockId) {
4015
- return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5262
+ const Hitboxes = ({ zoneId, parentZoneId, isEmptyZone }) => {
5263
+ const tree = useTreeStore((state) => state.tree);
5264
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5265
+ const scrollY = useDraggedItemStore((state) => state.scrollY);
5266
+ const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
5267
+ const zones = useZoneStore((state) => state.zones);
5268
+ const hoveringZone = useZoneStore((state) => state.hoveringZone);
5269
+ const isHoveringZone = hoveringZone === zoneId;
5270
+ const hitboxContainer = useMemo(() => {
5271
+ return document.querySelector('[data-ctfl-hitboxes]');
5272
+ }, []);
5273
+ const domRect = useMemo(() => {
5274
+ if (!isDraggingOnCanvas)
5275
+ return;
5276
+ return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
5277
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5278
+ }, [zoneId, isDraggingOnCanvas]);
5279
+ // Use the size of the cloned dragging element to offset the position of the hitboxes
5280
+ // So that when dragging causes a dropzone to expand, the hitboxes will be in the correct position
5281
+ const offsetRect = useMemo(() => {
5282
+ if (!isDraggingOnCanvas || isEmptyZone || !isHoveringZone)
5283
+ return;
5284
+ return document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`)?.getBoundingClientRect();
5285
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5286
+ }, [isEmptyZone, isHoveringZone, isDraggingOnCanvas]);
5287
+ const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
5288
+ const isVertical = zoneDirection === 'vertical';
5289
+ const isRoot = parentZoneId === ROOT_ID;
5290
+ const { slotId: parentSlotId } = parseZoneId(parentZoneId);
5291
+ const getStyles = useCallback((direction) => getHitboxStyles({
5292
+ direction,
5293
+ zoneDepth,
5294
+ domRect,
5295
+ scrollY,
5296
+ offsetRect,
5297
+ }), [zoneDepth, domRect, scrollY, offsetRect]);
5298
+ const renderFinalRootHitbox = () => {
5299
+ if (!isRoot)
5300
+ return null;
5301
+ return (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) }));
5302
+ };
5303
+ const renderSurroundingHitboxes = () => {
5304
+ if (isRoot || parentSlotId)
5305
+ return null;
5306
+ return (React.createElement(React.Fragment, null,
5307
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
5308
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })));
5309
+ };
5310
+ const ActiveHitboxes = (React.createElement(React.Fragment, null,
5311
+ React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
5312
+ renderSurroundingHitboxes(),
5313
+ renderFinalRootHitbox()));
5314
+ if (!hitboxContainer) {
5315
+ return null;
5316
+ }
5317
+ return createPortal(ActiveHitboxes, hitboxContainer);
5318
+ };
5319
+
5320
+ const isRelativePreviewSize = (width) => {
5321
+ // For now, we solely allow 100% as relative value
5322
+ return width === '100%';
5323
+ };
5324
+ const getTooltipPositions = ({ previewSize, tooltipRect, coordinates, }) => {
5325
+ if (!coordinates || !tooltipRect) {
5326
+ return { display: 'none' };
5327
+ }
5328
+ /**
5329
+ * By default, the tooltip floats to the left of the element
5330
+ */
5331
+ const newTooltipStyles = { display: 'flex' };
5332
+ // If the preview size is relative, we don't change the floating direction
5333
+ if (!isRelativePreviewSize(previewSize)) {
5334
+ const previewSizeMatch = previewSize.match(/(\d{1,})px/);
5335
+ if (!previewSizeMatch) {
5336
+ return { display: 'none' };
4016
5337
  }
4017
- return parentWrappingPatternIds;
4018
- }, [isRootAssemblyNode, node, parentWrappingPatternIds]);
4019
- const componentRegistration = useComponentRegistration(node);
4020
- if (!componentRegistration) {
4021
- return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4022
- }
4023
- if (isRootAssemblyNode && node.data.blockId && parentWrappingPatternIds.has(node.data.blockId)) {
4024
- return (React.createElement(CircularDependencyErrorPlaceholder, { node: node, wrappingPatternIds: wrappingPatternIds }));
4025
- }
4026
- const slotNodes = {};
4027
- for (const slotId in componentRegistration.definition.slots) {
4028
- const nodes = node.children.filter((child) => child.data.slotId === slotId);
4029
- slotNodes[slotId] =
4030
- nodes.length === 0 ? (React.createElement("div", { className: styles$1.emptySlot })) : (React.createElement(React.Fragment, null, nodes.map((slotChildNode) => (React.createElement(EditorBlock, { key: slotChildNode.data.id, node: slotChildNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))));
4031
- }
4032
- const children = componentRegistration.definition.children
4033
- ? node.children
4034
- .filter((node) => node.data.slotId === undefined)
4035
- .map((childNode) => (React.createElement(EditorBlock, { key: childNode.data.id, node: childNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))
4036
- : null;
4037
- return (React.createElement(RegistrationComponent, { node: node, resolveDesignValue: resolveDesignValue, componentRegistration: componentRegistration, slotNodes: slotNodes }, children));
5338
+ const previewSizePx = parseInt(previewSizeMatch[1]);
5339
+ /**
5340
+ * If the element is at the right edge of the canvas, and the element isn't wide enough to fit the tooltip width,
5341
+ * we float the tooltip to the right of the element.
5342
+ */
5343
+ if (tooltipRect.width > previewSizePx - coordinates.right &&
5344
+ tooltipRect.width > coordinates.width) {
5345
+ newTooltipStyles['float'] = 'right';
5346
+ }
5347
+ }
5348
+ const tooltipHeight = tooltipRect.height === 0 ? 32 : tooltipRect.height;
5349
+ /**
5350
+ * For elements with small heights, we don't want the tooltip covering the content in the element,
5351
+ * so we show the tooltip at the top or bottom.
5352
+ */
5353
+ if (tooltipHeight * 2 > coordinates.height) {
5354
+ /**
5355
+ * If there's enough space for the tooltip at the top of the element, we show the tooltip at the top of the element,
5356
+ * else we show the tooltip at the bottom.
5357
+ */
5358
+ if (tooltipHeight < coordinates.top) {
5359
+ newTooltipStyles['bottom'] = coordinates.height;
5360
+ }
5361
+ else {
5362
+ newTooltipStyles['top'] = coordinates.height;
5363
+ }
5364
+ }
5365
+ /**
5366
+ * If the component draws outside of the borders of the canvas to the left we move the tooltip to the right
5367
+ * so that it is fully visible.
5368
+ */
5369
+ if (coordinates.left < 0) {
5370
+ newTooltipStyles['left'] = -coordinates.left;
5371
+ }
5372
+ /**
5373
+ * If for any reason, the element's top is negative, we show the tooltip at the bottom
5374
+ */
5375
+ if (coordinates.top < 0) {
5376
+ newTooltipStyles['top'] = coordinates.height;
5377
+ }
5378
+ return newTooltipStyles;
5379
+ };
5380
+
5381
+ var css_248z$1 = ".styles-module_DraggableComponent__oyE7Q,\n.styles-module_Dropzone__3R-sm:not(.styles-module_isSlot__HI9yO) {\n position: relative;\n transition: background-color 0.2s;\n pointer-events: all;\n box-sizing: border-box;\n cursor: grab;\n}\n\n.styles-module_DraggableComponent__oyE7Q:before,\n.styles-module_Dropzone__3R-sm:not(.styles-module_isSlot__HI9yO):before {\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n outline-offset: -2px;\n outline: 2px solid transparent;\n z-index: 1;\n transition: outline 0.2s;\n pointer-events: none;\n}\n\n.styles-module_DraggableComponent__oyE7Q.styles-module_isDragging__hldL4.styles-module_Dropzone__3R-sm:before {\n outline-offset: -1px;\n}\n\n.styles-module_DraggableComponent__oyE7Q.styles-module_isDragging__hldL4.styles-module_Dropzone__3R-sm {\n pointer-events: all;\n}\n\n.styles-module_Dropzone__3R-sm.styles-module_fullHeight__afMfT {\n height: 100%;\n}\n\n.styles-module_Dropzone__3R-sm.styles-module_fullWidth__Od117 {\n width: 100%;\n}\n\n.styles-module_isRoot__c-c-x,\n.styles-module_isEmptyCanvas__Mm6Al {\n flex: 1;\n}\n\n.styles-module_isEmptyZone__XZ1Ej {\n min-height: 80px;\n min-width: 80px;\n}\n\n.styles-module_isDragging__hldL4:not(.styles-module_isRoot__c-c-x):not(.styles-module_DraggableClone__CdKIH):before {\n outline: 2px dashed var(--exp-builder-gray300);\n}\n\n.styles-module_Dropzone__3R-sm.styles-module_isDestination__sE70P:not(.styles-module_isRoot__c-c-x):before {\n transition:\n outline 0.2s,\n background-color 0.2s;\n outline: 2px dashed var(--exp-builder-blue400);\n background-color: rgba(var(--exp-builder-blue100-rgb), 0.5);\n z-index: 2;\n}\n\n.styles-module_DraggableClone__CdKIH:before {\n outline: 2px solid var(--exp-builder-blue500);\n}\n\n.styles-module_DropzoneClone__xiT8j,\n.styles-module_DraggableClone__CdKIH,\n.styles-module_DropzoneClone__xiT8j *,\n.styles-module_DraggableClone__CdKIH * {\n pointer-events: none !important;\n}\n\n.styles-module_DraggableComponent__oyE7Q:not(.styles-module_isDragging__hldL4) :not(.styles-module_DraggableComponent__oyE7Q) {\n pointer-events: none;\n}\n\n.styles-module_isDraggingThisComponent__yCZTp {\n overflow: hidden;\n}\n\n.styles-module_isSelected__c2QEJ:before {\n outline: 2px solid transparent !important;\n}\n\n.styles-module_tooltipWrapper__kqvmR {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n z-index: 10;\n pointer-events: none;\n}\n\n.styles-module_DraggableComponent__oyE7Q.styles-module_isDragging__hldL4 .styles-module_tooltipWrapper__kqvmR {\n display: none;\n}\n\n.styles-module_overlay__knwhE {\n position: absolute;\n display: flex;\n align-items: center;\n min-width: max-content;\n height: 24px;\n z-index: 2;\n font-family: var(--exp-builder-font-stack-primary);\n font-size: 14px;\n font-weight: 500;\n background-color: var(--exp-builder-gray500);\n color: var(--exp-builder-color-white);\n padding: 4px 12px 4px 12px;\n transition: opacity 0.1s;\n opacity: 0;\n text-wrap: nowrap;\n}\n\n.styles-module_overlayContainer__lUsiC {\n opacity: 0;\n}\n\n.styles-module_overlayAssembly__3BKl4 {\n background-color: var(--exp-builder-purple600);\n}\n\n.styles-module_isDragging__hldL4 > .styles-module_overlay__knwhE,\n.styles-module_isDragging__hldL4 > .styles-module_overlayContainer__lUsiC {\n opacity: 0 !important;\n}\n\n.styles-module_isDragging__hldL4:not(.styles-module_Dropzone__3R-sm):before {\n outline: 2px solid transparent !important;\n}\n\n.styles-module_isHoveringComponent__f7G5m > div > .styles-module_overlay__knwhE,\n.styles-module_DraggableComponent__oyE7Q:hover:not(:has(.styles-module_DraggableComponent__oyE7Q:hover)) > div > .styles-module_overlay__knwhE {\n opacity: 1;\n}\n\n/* hovering related component in layers tab */\n\n.styles-module_DraggableComponent__oyE7Q:has(.styles-module_isHoveringComponent__f7G5m):not(.styles-module_isAssemblyBlock__goT9z):before,\n.styles-module_DraggableComponent__oyE7Q:has(.styles-module_isHoveringComponent__f7G5m):not(.styles-module_isAssemblyBlock__goT9z) .styles-module_DraggableComponent__oyE7Q:not(.styles-module_isHoveringComponent__f7G5m):not(.styles-module_isAssemblyBlock__goT9z):before,\n.styles-module_isHoveringComponent__f7G5m:not(.styles-module_isAssemblyBlock__goT9z) .styles-module_DraggableComponent__oyE7Q:not(.styles-module_isAssemblyBlock__goT9z):before,\n\n.styles-module_DraggableComponent__oyE7Q:not(.styles-module_isAssemblyBlock__goT9z):not(.styles-module_isDragging__hldL4):hover:before,\n.styles-module_DraggableComponent__oyE7Q:not(.styles-module_isDragging__hldL4):hover .styles-module_DraggableComponent__oyE7Q:before {\n outline: 2px dashed var(--exp-builder-gray500);\n}\n\n/* hovering component in layers tab */\n\n.styles-module_isHoveringComponent__f7G5m:not(.styles-module_isAssemblyBlock__goT9z):before,\n\n.styles-module_DraggableComponent__oyE7Q:not(.styles-module_isAssemblyBlock__goT9z):not(.styles-module_isDragging__hldL4):hover:not(:has(.styles-module_DraggableComponent__oyE7Q:hover)):before {\n outline: 2px solid var(--exp-builder-gray500);\n}\n\n/* hovering related pattern in layers tab */\n\n.styles-module_isAssemblyBlock__goT9z:has(.styles-module_isHoveringComponent__f7G5m):before,\n.styles-module_isAssemblyBlock__goT9z:has(.styles-module_isHoveringComponent__f7G5m) .styles-module_isAssemblyBlock__goT9z:not(.styles-module_isHoveringComponent__f7G5m):before,\n.styles-module_isHoveringComponent__f7G5m .styles-module_isAssemblyBlock__goT9z:before,\n\n.styles-module_isAssemblyBlock__goT9z:hover:before,\n.styles-module_isAssemblyBlock__goT9z:hover .styles-module_DraggableComponent__oyE7Q:before,\n.styles-module_DraggableComponent__oyE7Q:not(.styles-module_isDragging__hldL4):hover .styles-module_isAssemblyBlock__goT9z:before {\n outline: 2px dashed var(--exp-builder-purple600);\n}\n\n/* hovering pattern in layers tab */\n\n.styles-module_isAssemblyBlock__goT9z.styles-module_isHoveringComponent__f7G5m:before,\n\n.styles-module_isAssemblyBlock__goT9z:hover:not(:has(.styles-module_DraggableComponent__oyE7Q:hover)):before {\n outline: 2px solid var(--exp-builder-purple600);\n}\n";
5382
+ var styles$1 = {"DraggableComponent":"styles-module_DraggableComponent__oyE7Q","Dropzone":"styles-module_Dropzone__3R-sm","isSlot":"styles-module_isSlot__HI9yO","isDragging":"styles-module_isDragging__hldL4","fullHeight":"styles-module_fullHeight__afMfT","fullWidth":"styles-module_fullWidth__Od117","isRoot":"styles-module_isRoot__c-c-x","isEmptyCanvas":"styles-module_isEmptyCanvas__Mm6Al","isEmptyZone":"styles-module_isEmptyZone__XZ1Ej","DraggableClone":"styles-module_DraggableClone__CdKIH","isDestination":"styles-module_isDestination__sE70P","DropzoneClone":"styles-module_DropzoneClone__xiT8j","isDraggingThisComponent":"styles-module_isDraggingThisComponent__yCZTp","isSelected":"styles-module_isSelected__c2QEJ","tooltipWrapper":"styles-module_tooltipWrapper__kqvmR","overlay":"styles-module_overlay__knwhE","overlayContainer":"styles-module_overlayContainer__lUsiC","overlayAssembly":"styles-module_overlayAssembly__3BKl4","isHoveringComponent":"styles-module_isHoveringComponent__f7G5m","isAssemblyBlock":"styles-module_isAssemblyBlock__goT9z"};
5383
+ styleInject(css_248z$1);
5384
+
5385
+ const Tooltip = ({ coordinates, id, label, isAssemblyBlock, isContainer, isSelected, }) => {
5386
+ const tooltipRef = useRef(null);
5387
+ const previewSize = '100%'; // This should be based on breakpoints and added to usememo dependency array
5388
+ const tooltipStyles = useMemo(() => {
5389
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect();
5390
+ const draggableRect = document
5391
+ .querySelector(`[data-ctfl-draggable-id="${id}"]`)
5392
+ ?.getBoundingClientRect();
5393
+ const newTooltipStyles = getTooltipPositions({
5394
+ previewSize,
5395
+ tooltipRect,
5396
+ coordinates: draggableRect,
5397
+ });
5398
+ return newTooltipStyles;
5399
+ // Ignore eslint because we intentionally want to trigger this whenever a user clicks on a container/component which is tracked by these coordinates of the component being clicked being changed
5400
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5401
+ }, [coordinates, id, tooltipRef.current]);
5402
+ if (isSelected) {
5403
+ return null;
5404
+ }
5405
+ return (React.createElement("div", { "data-tooltip": true, className: styles$1.tooltipWrapper },
5406
+ React.createElement("div", { "data-tooltip": true, ref: tooltipRef, style: tooltipStyles, className: classNames(styles$1.overlay, {
5407
+ [styles$1.overlayContainer]: isContainer,
5408
+ [styles$1.overlayAssembly]: isAssemblyBlock,
5409
+ }) }, label)));
5410
+ };
5411
+
5412
+ function useSingleColumn(node, resolveDesignValue) {
5413
+ const tree = useTreeStore((store) => store.tree);
5414
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5415
+ const isWrapped = useMemo(() => {
5416
+ if (!node.parentId || !isSingleColumn) {
5417
+ return false;
5418
+ }
5419
+ const parentNode = getItem({ id: node.parentId }, tree);
5420
+ if (!parentNode || parentNode.data.blockId !== CONTENTFUL_COMPONENTS$1.columns.id) {
5421
+ return false;
5422
+ }
5423
+ const { cfWrapColumns } = parentNode.data.props;
5424
+ if (cfWrapColumns.type !== 'DesignValue') {
5425
+ return false;
5426
+ }
5427
+ return resolveDesignValue(cfWrapColumns.valuesByBreakpoint);
5428
+ }, [tree, node, isSingleColumn, resolveDesignValue]);
5429
+ return {
5430
+ isSingleColumn,
5431
+ isWrapped,
5432
+ };
4038
5433
  }
4039
- const RegistrationComponent = ({ node, resolveDesignValue, componentRegistration, slotNodes, children, }) => {
4040
- const { componentProps } = useComponentProps({
4041
- node,
5434
+
5435
+ function getStyle$1(style, snapshot) {
5436
+ if (!snapshot.isDropAnimating) {
5437
+ return style;
5438
+ }
5439
+ return {
5440
+ ...style,
5441
+ // cannot be 0, but make it super tiny
5442
+ transitionDuration: `0.001s`,
5443
+ };
5444
+ }
5445
+ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, index, zoneId, userIsDragging, placeholder, wrappingPatternIds, }) => {
5446
+ const { slotId } = parseZoneId(zoneId);
5447
+ const ref = useRef(null);
5448
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
5449
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
5450
+ const { node, componentId, elementToRender, definition, isPatternNode, isPatternComponent, isNestedPattern, } = useComponent({
5451
+ node: rawNode,
4042
5452
  resolveDesignValue,
4043
- definition: componentRegistration.definition,
4044
- options: componentRegistration.options,
5453
+ renderDropzone,
5454
+ userIsDragging,
5455
+ wrappingPatternIds,
4045
5456
  });
4046
- return React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, { ...componentProps, ...slotNodes }, children));
5457
+ const { isSingleColumn, isWrapped } = useSingleColumn(node, resolveDesignValue);
5458
+ const setDomRect = useDraggedItemStore((state) => state.setDomRect);
5459
+ const isHoveredComponent = useDraggedItemStore((state) => state.hoveredComponentId === componentId);
5460
+ const coordinates = useSelectedInstanceCoordinates({ node });
5461
+ const displayName = node.data.displayName || rawNode.data.displayName || definition?.name;
5462
+ const testId = `draggable-${node.data.blockId ?? 'node'}`;
5463
+ const isSelected = node.data.id === selectedNodeId;
5464
+ const isContainer = node.data.blockId === CONTENTFUL_COMPONENTS$1.container.id;
5465
+ const isSlotComponent = Boolean(node.data.slotId);
5466
+ const isDragDisabled = isNestedPattern || isPatternComponent || (isSingleColumn && isWrapped) || isSlotComponent;
5467
+ const isEmptyZone = useMemo(() => {
5468
+ return !node.children.filter((node) => node.data.slotId === slotId).length;
5469
+ }, [node.children, slotId]);
5470
+ useDraggablePosition({
5471
+ draggableId: componentId,
5472
+ draggableRef: ref,
5473
+ position: DraggablePosition.MOUSE_POSITION,
5474
+ });
5475
+ const onClick = (e) => {
5476
+ e.stopPropagation();
5477
+ if (!userIsDragging) {
5478
+ setSelectedNodeId(node.data.id);
5479
+ // if it is the assembly directly we just want to select it as a normal component
5480
+ if (isPatternNode) {
5481
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5482
+ nodeId: node.data.id,
5483
+ });
5484
+ return;
5485
+ }
5486
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5487
+ assembly: node.data.assembly,
5488
+ nodeId: node.data.id,
5489
+ });
5490
+ }
5491
+ };
5492
+ const onMouseOver = (e) => {
5493
+ e.stopPropagation();
5494
+ if (userIsDragging)
5495
+ return;
5496
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {
5497
+ nodeId: componentId,
5498
+ });
5499
+ };
5500
+ const onMouseDown = (e) => {
5501
+ if (isDragDisabled) {
5502
+ return;
5503
+ }
5504
+ e.stopPropagation();
5505
+ setDomRect(e.currentTarget.getBoundingClientRect());
5506
+ };
5507
+ const ToolTipAndPlaceholder = (React.createElement(React.Fragment, null,
5508
+ React.createElement(Tooltip, { id: componentId, coordinates: coordinates, isAssemblyBlock: isPatternNode || isPatternComponent, isContainer: isContainer, isSelected: isSelected, label: displayName || 'No label specified' }),
5509
+ React.createElement(Placeholder, { ...placeholder, id: componentId }),
5510
+ userIsDragging && !isPatternComponent && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
5511
+ return (React.createElement(Draggable, { key: componentId, draggableId: componentId, index: index, isDragDisabled: isDragDisabled, disableInteractiveElementBlocking: true }, (provided, snapshot) => elementToRender({
5512
+ dragProps: {
5513
+ ...provided.draggableProps,
5514
+ ...provided.dragHandleProps,
5515
+ 'data-ctfl-draggable-id': componentId,
5516
+ 'data-test-id': testId,
5517
+ innerRef: (refNode) => {
5518
+ provided?.innerRef(refNode);
5519
+ ref.current = refNode;
5520
+ },
5521
+ className: classNames(styles$1.DraggableComponent, {
5522
+ [styles$1.isAssemblyBlock]: isPatternComponent || isPatternNode,
5523
+ [styles$1.isDragging]: snapshot?.isDragging || userIsDragging,
5524
+ [styles$1.isSelected]: isSelected,
5525
+ [styles$1.isHoveringComponent]: isHoveredComponent,
5526
+ }),
5527
+ style: getStyle$1(provided.draggableProps.style, snapshot),
5528
+ onMouseDown,
5529
+ onMouseOver,
5530
+ onClick,
5531
+ ToolTipAndPlaceholder,
5532
+ },
5533
+ })));
4047
5534
  };
4048
5535
 
4049
- var css_248z = ".EmptyCanvasMessage-module_empty-canvas-container__7K-0l {\n height: 200px;\n display: flex;\n width: 100%;\n align-items: center;\n justify-content: center;\n flex-direction: row;\n transition: all 0.2s;\n color: var(--exp-builder-gray400);\n font-size: var(--exp-builder-font-size-l);\n font-family: var(--exp-builder-font-stack-primary);\n outline: 2px dashed var(--exp-builder-gray400);\n outline-offset: -2px;\n}\n\n.EmptyCanvasMessage-module_empty-canvas-icon__EztFr rect {\n fill: var(--exp-builder-gray400);\n}\n\n.EmptyCanvasMessage-module_empty-canvas-label__cbIrR {\n margin-left: var(--exp-builder-spacing-s);\n}\n";
4050
- var styles = {"empty-canvas-container":"EmptyCanvasMessage-module_empty-canvas-container__7K-0l","empty-canvas-icon":"EmptyCanvasMessage-module_empty-canvas-icon__EztFr","empty-canvas-label":"EmptyCanvasMessage-module_empty-canvas-label__cbIrR"};
5536
+ var css_248z = ".EmptyContainer-module_container__XPH5b {\n height: 200px;\n display: flex;\n width: 100%;\n position: absolute;\n align-items: center;\n justify-content: center;\n flex-direction: row;\n transition: all 0.2s;\n color: var(--exp-builder-gray400);\n font-size: var(--exp-builder-font-size-l);\n font-family: var(--exp-builder-font-stack-primary);\n outline: 2px dashed var(--exp-builder-gray400);\n outline-offset: -2px;\n}\n\n.EmptyContainer-module_highlight__lcICy:hover {\n outline: 2px dashed var(--exp-builder-blue500);\n background-color: rgba(var(--exp-builder-blue100-rgb), 0.5);\n cursor: grabbing;\n}\n\n.EmptyContainer-module_icon__82-2O rect {\n fill: var(--exp-builder-gray400);\n}\n\n.EmptyContainer-module_label__4TxRa {\n margin-left: var(--exp-builder-spacing-s);\n}\n";
5537
+ var styles = {"container":"EmptyContainer-module_container__XPH5b","highlight":"EmptyContainer-module_highlight__lcICy","icon":"EmptyContainer-module_icon__82-2O","label":"EmptyContainer-module_label__4TxRa"};
4051
5538
  styleInject(css_248z);
4052
5539
 
4053
- const EmptyCanvasMessage = () => {
4054
- return (React.createElement("div", { className: styles['empty-canvas-container'], "data-type": "empty-container" },
4055
- React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles['empty-canvas-icon'] },
5540
+ const EmptyContainer = ({ isDragging }) => {
5541
+ return (React.createElement("div", { className: classNames(styles.container, {
5542
+ [styles.highlight]: isDragging,
5543
+ }), "data-type": "empty-container" },
5544
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles.icon },
4056
5545
  React.createElement("rect", { width: "11.676", height: "11.676", x: "18.512", y: ".153", rx: "1.621", transform: "rotate(45 18.512 .153)" }),
4057
5546
  React.createElement("rect", { width: "11.676", height: "11.676", x: "9.254", y: "9.139", rx: "1.621", transform: "rotate(45 9.254 9.139)" }),
4058
5547
  React.createElement("rect", { width: "11.676", height: "11.676", x: "18.011", y: "18.625", rx: "1.621", transform: "rotate(45 18.01 18.625)" }),
4059
5548
  React.createElement("rect", { width: "11.676", height: "11.676", x: "30.557", y: "10.131", rx: "1.621", transform: "rotate(60 30.557 10.13)" }),
4060
5549
  React.createElement("path", { fill: "#fff", stroke: "#fff", strokeWidth: ".243", d: "M31.113 17.038a.463.463 0 0 0-.683-.517l-1.763 1.032-1.033-1.763a.464.464 0 0 0-.8.469l1.034 1.763-1.763 1.033a.463.463 0 1 0 .468.8l1.763-1.033 1.033 1.763a.463.463 0 1 0 .8-.469l-1.033-1.763 1.763-1.033a.463.463 0 0 0 .214-.282Z" })),
4061
- React.createElement("span", { className: styles['empty-canvas-label'] }, "Add components to begin")));
5550
+ React.createElement("span", { className: styles.label }, "Add components to begin")));
5551
+ };
5552
+
5553
+ const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
5554
+ const zone = useZoneStore((state) => state.zones);
5555
+ const upsertZone = useZoneStore((state) => state.upsertZone);
5556
+ useEffect(() => {
5557
+ function getDirection() {
5558
+ if (!node || !node.data.blockId) {
5559
+ return 'vertical';
5560
+ }
5561
+ if (!isContentfulStructureComponent(node.data.blockId)) {
5562
+ return 'vertical';
5563
+ }
5564
+ if (node.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id) {
5565
+ return 'horizontal';
5566
+ }
5567
+ const designValues = node.data.props['cfFlexDirection'];
5568
+ if (!designValues || !resolveDesignValue || designValues.type !== 'DesignValue') {
5569
+ return 'vertical';
5570
+ }
5571
+ const direction = resolveDesignValue(designValues.valuesByBreakpoint);
5572
+ if (direction === 'row') {
5573
+ return 'horizontal';
5574
+ }
5575
+ return 'vertical';
5576
+ }
5577
+ upsertZone(zoneId, { direction: getDirection() });
5578
+ }, [node, resolveDesignValue, zoneId, upsertZone]);
5579
+ return zone[zoneId]?.direction || 'vertical';
5580
+ };
5581
+
5582
+ function getStyle(style = {}, snapshot) {
5583
+ if (!snapshot?.isDropAnimating) {
5584
+ return style;
5585
+ }
5586
+ return {
5587
+ ...style,
5588
+ // cannot be 0, but make it super tiny
5589
+ transitionDuration: `0.001s`,
5590
+ };
5591
+ }
5592
+ const EditorBlockClone = ({ node: rawNode, resolveDesignValue, snapshot, provided, renderDropzone, wrappingPatternIds, }) => {
5593
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5594
+ const { node, elementToRender } = useComponent({
5595
+ node: rawNode,
5596
+ resolveDesignValue,
5597
+ renderDropzone,
5598
+ userIsDragging,
5599
+ wrappingPatternIds,
5600
+ });
5601
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5602
+ return elementToRender({
5603
+ dragProps: {
5604
+ ...provided?.draggableProps,
5605
+ ...provided?.dragHandleProps,
5606
+ 'data-ctfl-dragging-element': 'true',
5607
+ innerRef: provided?.innerRef,
5608
+ className: classNames(styles$1.DraggableComponent, styles$1.DraggableClone, {
5609
+ [styles$1.isAssemblyBlock]: isAssemblyBlock,
5610
+ [styles$1.isDragging]: snapshot?.isDragging,
5611
+ }),
5612
+ style: getStyle(provided?.draggableProps.style, snapshot),
5613
+ },
5614
+ });
4062
5615
  };
4063
5616
 
4064
- const RootRenderer = () => {
5617
+ const getHtmlDragProps = (dragProps) => {
5618
+ if (dragProps) {
5619
+ const { ToolTipAndPlaceholder, Tag, innerRef, wrapComponent, ...htmlDragProps } = dragProps;
5620
+ return htmlDragProps;
5621
+ }
5622
+ return {};
5623
+ };
5624
+ const getHtmlComponentProps = (props) => {
5625
+ if (props) {
5626
+ const { editorMode, renderDropzone, node, ...htmlProps } = props;
5627
+ return htmlProps;
5628
+ }
5629
+ return {};
5630
+ };
5631
+
5632
+ function DropzoneClone({ node, zoneId, resolveDesignValue, WrapperComponent = 'div', renderDropzone, dragProps, wrappingPatternIds, ...rest }) {
5633
+ const tree = useTreeStore((state) => state.tree);
5634
+ const content = node?.children || tree.root?.children || [];
5635
+ const { slotId } = parseZoneId(zoneId);
5636
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5637
+ const htmlProps = getHtmlComponentProps(rest);
5638
+ const isRootZone = zoneId === ROOT_ID;
5639
+ if (!resolveDesignValue) {
5640
+ return null;
5641
+ }
5642
+ return (React.createElement(WrapperComponent, { ...htmlDraggableProps, ...htmlProps, className: classNames(dragProps?.className, styles$1.Dropzone, styles$1.DropzoneClone, rest.className, {
5643
+ [styles$1.isRoot]: isRootZone,
5644
+ [styles$1.isEmptyZone]: !content.length,
5645
+ }), "data-ctfl-slot-id": slotId, ref: (refNode) => {
5646
+ if (dragProps?.innerRef) {
5647
+ dragProps.innerRef(refNode);
5648
+ }
5649
+ } }, content
5650
+ .filter((node) => node.data.slotId === slotId)
5651
+ .map((item) => {
5652
+ const componentId = item.data.id;
5653
+ return (React.createElement(EditorBlockClone, { key: componentId, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds }));
5654
+ })));
5655
+ }
5656
+
5657
+ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', dragProps, wrappingPatternIds: parentWrappingPatternIds = new Set(), ...rest }) {
5658
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5659
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
5660
+ const isDraggingNewComponent = useDraggedItemStore((state) => Boolean(state.componentId));
5661
+ const isHoveringZone = useZoneStore((state) => state.hoveringZone === zoneId);
5662
+ const tree = useTreeStore((state) => state.tree);
5663
+ const content = node?.children || tree.root?.children || [];
5664
+ const { slotId } = parseZoneId(zoneId);
5665
+ const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
5666
+ const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
5667
+ const draggedNode = useMemo(() => {
5668
+ if (!draggedItem)
5669
+ return;
5670
+ return getItem({ id: draggedItem.draggableId }, tree);
5671
+ }, [draggedItem, tree]);
5672
+ const isRootZone = zoneId === ROOT_ID;
5673
+ const isDestination = draggedDestinationId === zoneId;
5674
+ const isEmptyCanvas = isRootZone && !content.length;
5675
+ const isAssembly = ASSEMBLY_NODE_TYPES.includes(node?.type || '');
5676
+ const isRootAssembly = node?.type === ASSEMBLY_NODE_TYPE;
5677
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5678
+ const htmlProps = getHtmlComponentProps(rest);
5679
+ const wrappingPatternIds = useMemo(() => {
5680
+ // On the top level, the node is not defined. If the root blockId is not the default string,
5681
+ // we assume that it is the entry ID of the experience/ pattern to properly detect circular dependencies
5682
+ if (!node && tree.root.data.blockId && tree.root.data.blockId !== ROOT_ID) {
5683
+ return new Set([tree.root.data.blockId, ...parentWrappingPatternIds]);
5684
+ }
5685
+ if (isRootAssembly && node?.data.blockId) {
5686
+ return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5687
+ }
5688
+ return parentWrappingPatternIds;
5689
+ }, [isRootAssembly, node, parentWrappingPatternIds, tree.root.data.blockId]);
5690
+ // To avoid a circular dependency, we create the recursive rendering function here and trickle it down
5691
+ const renderDropzone = useCallback((node, props) => {
5692
+ return (React.createElement(Dropzone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds, ...props }));
5693
+ }, [wrappingPatternIds, resolveDesignValue]);
5694
+ const renderClonedDropzone = useCallback((node, props) => {
5695
+ return (React.createElement(DropzoneClone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds, ...props }));
5696
+ }, [resolveDesignValue, wrappingPatternIds]);
5697
+ const isDropzoneEnabled = useMemo(() => {
5698
+ const isColumns = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
5699
+ const isDraggingSingleColumn = draggedNode?.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5700
+ const isParentOfDraggedNode = node?.data.id === draggedNode?.parentId;
5701
+ // If dragging a single column, only enable the dropzone of the parent
5702
+ // columns component
5703
+ if (isDraggingSingleColumn && isColumns && isParentOfDraggedNode) {
5704
+ return true;
5705
+ }
5706
+ // If dragging a single column, disable dropzones for any component besides
5707
+ // the parent of the dragged single column
5708
+ if (isDraggingSingleColumn && !isParentOfDraggedNode) {
5709
+ return false;
5710
+ }
5711
+ // Disable dropzone for Columns component
5712
+ if (isColumns) {
5713
+ return false;
5714
+ }
5715
+ // Disable dropzone for Assembly
5716
+ if (isAssembly) {
5717
+ return false;
5718
+ }
5719
+ // Enable dropzone for the non-root hovered zones if component is not allowed on root
5720
+ if (!isDraggingNewComponent &&
5721
+ !isComponentAllowedOnRoot({ type: draggedNode?.type, componentId: draggedNode?.data.blockId })) {
5722
+ return isHoveringZone && !isRootZone;
5723
+ }
5724
+ // Enable dropzone for the hovered zone only
5725
+ return isHoveringZone;
5726
+ }, [isAssembly, isHoveringZone, isRootZone, isDraggingNewComponent, draggedNode, node]);
5727
+ if (!resolveDesignValue) {
5728
+ return null;
5729
+ }
5730
+ const isPatternWrapperComponentFullHeight = isRootAssembly
5731
+ ? node.children.length === 1 &&
5732
+ resolveDesignValue(node?.children[0]?.data.props.cfHeight?.valuesByBreakpoint ?? {}, 'cfHeight') === '100%'
5733
+ : false;
5734
+ const isPatternWrapperComponentFullWidth = isRootAssembly
5735
+ ? node.children.length === 1 &&
5736
+ resolveDesignValue(node?.children[0]?.data.props.cfWidth?.valuesByBreakpoint ?? {}, 'cfWidth') === '100%'
5737
+ : false;
5738
+ return (React.createElement(Droppable, { droppableId: zoneId, direction: direction, isDropDisabled: !isDropzoneEnabled, renderClone: (provided, snapshot, rubic) => (React.createElement(EditorBlockClone, { node: content[rubic.source.index], resolveDesignValue: resolveDesignValue, provided: provided, snapshot: snapshot, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds })) }, (provided, snapshot) => {
5739
+ return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ...htmlDraggableProps, ...htmlProps, ref: (refNode) => {
5740
+ if (dragProps?.innerRef) {
5741
+ dragProps.innerRef(refNode);
5742
+ }
5743
+ provided?.innerRef(refNode);
5744
+ }, id: zoneId, "data-ctfl-zone-id": zoneId, "data-ctfl-slot-id": slotId, className: classNames(dragProps?.className, styles$1.Dropzone, className, {
5745
+ [styles$1.isEmptyCanvas]: isEmptyCanvas,
5746
+ [styles$1.isDragging]: userIsDragging,
5747
+ [styles$1.isDestination]: isDestination && !isAssembly,
5748
+ [styles$1.isRoot]: isRootZone,
5749
+ [styles$1.isEmptyZone]: !content.length,
5750
+ [styles$1.isSlot]: Boolean(slotId),
5751
+ [styles$1.fullHeight]: isPatternWrapperComponentFullHeight,
5752
+ [styles$1.fullWidth]: isPatternWrapperComponentFullWidth,
5753
+ }) },
5754
+ isEmptyCanvas ? (React.createElement(EmptyContainer, { isDragging: isRootZone && userIsDragging })) : (content
5755
+ .filter((node) => node.data.slotId === slotId)
5756
+ .map((item, i) => (React.createElement(EditorBlock, { placeholder: {
5757
+ isDraggingOver: snapshot?.isDraggingOver,
5758
+ totalIndexes: content.length,
5759
+ elementIndex: i,
5760
+ dropzoneElementId: zoneId,
5761
+ direction,
5762
+ }, index: i, zoneId: zoneId, key: item.data.id, userIsDragging: userIsDragging, draggingNewComponent: isDraggingNewComponent, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds })))),
5763
+ provided?.placeholder,
5764
+ dragProps?.ToolTipAndPlaceholder));
5765
+ }));
5766
+ }
5767
+
5768
+ const RootRenderer = ({ onChange }) => {
4065
5769
  useEditorSubscriber();
5770
+ const dragItem = useDraggedItemStore((state) => state.componentId);
5771
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5772
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4066
5773
  const breakpoints = useTreeStore((state) => state.breakpoints);
5774
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4067
5775
  const containerRef = useRef(null);
4068
5776
  const { resolveDesignValue } = useBreakpoints(breakpoints);
5777
+ const [containerStyles, setContainerStyles] = useState({});
4069
5778
  const tree = useTreeStore((state) => state.tree);
4070
- // If the root blockId is defined but not the default string, it is the entry ID
4071
- // of the experience/ pattern to properly detect circular dependencies.
4072
- const rootBlockId = tree.root.data.blockId ?? ROOT_ID;
4073
- const wrappingPatternIds = rootBlockId !== ROOT_ID ? new Set([rootBlockId]) : new Set();
4074
- return (React.createElement(React.Fragment, null,
4075
- React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer, ref: containerRef }, !tree.root.children.length ? (React.createElement(EmptyCanvasMessage, null)) : (tree.root.children.map((topLevelChildNode) => (React.createElement(EditorBlock, { key: topLevelChildNode.data.id, node: topLevelChildNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))))));
5779
+ const handleMouseOver = useCallback(() => {
5780
+ // Remove hover state set by UI when mouse is over canvas
5781
+ setHoveredComponentId();
5782
+ // Remove hover styling from components in the layers tab
5783
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {});
5784
+ }, [setHoveredComponentId]);
5785
+ const handleClickOutside = useCallback((e) => {
5786
+ const element = e.target;
5787
+ const isRoot = element.getAttribute('data-ctfl-zone-id') === ROOT_ID;
5788
+ const clickedOnCanvas = element.closest(`[data-ctfl-root]`);
5789
+ if (clickedOnCanvas && !isRoot) {
5790
+ return;
5791
+ }
5792
+ sendMessage(OUTGOING_EVENTS.OutsideCanvasClick, {
5793
+ outsideCanvasClick: true,
5794
+ });
5795
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5796
+ nodeId: '',
5797
+ });
5798
+ setSelectedNodeId('');
5799
+ }, [setSelectedNodeId]);
5800
+ const handleResizeCanvas = useCallback(() => {
5801
+ const parentElement = containerRef.current?.parentElement;
5802
+ if (!parentElement) {
5803
+ return;
5804
+ }
5805
+ let siblingHeight = 0;
5806
+ for (const child of parentElement.children) {
5807
+ if (!child.hasAttribute('data-ctfl-root')) {
5808
+ siblingHeight += child.getBoundingClientRect().height;
5809
+ }
5810
+ }
5811
+ if (!siblingHeight) {
5812
+ /**
5813
+ * DRAGGABLE_HEIGHT is subtracted here due to an uninteded scrolling effect
5814
+ * when dragging a new component onto the canvas
5815
+ *
5816
+ * The DRAGGABLE_HEIGHT is then added as margin bottom to offset this value
5817
+ * so that visually there is no difference to the user.
5818
+ */
5819
+ setContainerStyles({
5820
+ minHeight: `${window.innerHeight - DRAGGABLE_HEIGHT}px`,
5821
+ });
5822
+ return;
5823
+ }
5824
+ setContainerStyles({
5825
+ minHeight: `${window.innerHeight - siblingHeight}px`,
5826
+ });
5827
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5828
+ }, [containerRef.current]);
5829
+ useEffect(() => {
5830
+ if (onChange)
5831
+ onChange(tree);
5832
+ }, [tree, onChange]);
5833
+ useEffect(() => {
5834
+ window.addEventListener('mouseover', handleMouseOver);
5835
+ return () => {
5836
+ window.removeEventListener('mouseover', handleMouseOver);
5837
+ };
5838
+ }, [handleMouseOver]);
5839
+ useEffect(() => {
5840
+ document.addEventListener('click', handleClickOutside);
5841
+ return () => {
5842
+ document.removeEventListener('click', handleClickOutside);
5843
+ };
5844
+ }, [handleClickOutside]);
5845
+ useEffect(() => {
5846
+ handleResizeCanvas();
5847
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5848
+ }, [containerRef.current]);
5849
+ return (React.createElement(DNDProvider, null,
5850
+ dragItem && React.createElement(DraggableContainer, { id: dragItem }),
5851
+ React.createElement("div", { "data-ctfl-root": true, className: styles$3.container, ref: containerRef, style: containerStyles },
5852
+ userIsDragging && React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitbox }),
5853
+ React.createElement(Dropzone, { zoneId: ROOT_ID, resolveDesignValue: resolveDesignValue }),
5854
+ userIsDragging && (React.createElement(React.Fragment, null,
5855
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitboxLower }),
5856
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.canvasBottomSpacer })))),
5857
+ React.createElement("div", { "data-ctfl-hitboxes": true })));
4076
5858
  };
4077
5859
 
4078
5860
  const useInitializeEditor = () => {
@@ -4116,11 +5898,38 @@ const useInitializeEditor = () => {
4116
5898
  const VisualEditorRoot = ({ experience }) => {
4117
5899
  const initialized = useInitializeEditor();
4118
5900
  const setHyperLinkPattern = useEditorStore((state) => state.setHyperLinkPattern);
5901
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
5902
+ const setHoveringZone = useZoneStore((state) => state.setHoveringZone);
4119
5903
  useEffect(() => {
4120
5904
  if (experience?.hyperlinkPattern) {
4121
5905
  setHyperLinkPattern(experience.hyperlinkPattern);
4122
5906
  }
4123
5907
  }, [experience?.hyperlinkPattern, setHyperLinkPattern]);
5908
+ useEffect(() => {
5909
+ const onMouseMove = (e) => {
5910
+ setMousePosition(e.clientX, e.clientY);
5911
+ const target = e.target;
5912
+ const zoneId = target.closest(`[${CTFL_ZONE_ID}]`)?.getAttribute(CTFL_ZONE_ID);
5913
+ if (zoneId) {
5914
+ setHoveringZone(zoneId);
5915
+ }
5916
+ if (!SimulateDnD$1.isDragging) {
5917
+ return;
5918
+ }
5919
+ if (target.id === NEW_COMPONENT_ID) {
5920
+ return;
5921
+ }
5922
+ SimulateDnD$1.updateDrag(e.clientX, e.clientY);
5923
+ sendMessage(OUTGOING_EVENTS.MouseMove, {
5924
+ clientX: e.pageX - window.scrollX,
5925
+ clientY: e.pageY - window.scrollY,
5926
+ });
5927
+ };
5928
+ document.addEventListener('mousemove', onMouseMove);
5929
+ return () => {
5930
+ document.removeEventListener('mousemove', onMouseMove);
5931
+ };
5932
+ }, [setHoveringZone, setMousePosition]);
4124
5933
  if (!initialized)
4125
5934
  return null;
4126
5935
  return React.createElement(RootRenderer, null);