@contentful/experiences-visual-editor-react 1.40.0-alpha-20250604T0813-1f6e699.0 → 1.40.0-dev-20250604T0933-fc115de.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,269 +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, debounce } 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
- CanvasGeometryUpdated: 'canvasGeometryUpdated',
232
- };
233
18
  const INCOMING_EVENTS$1 = {
234
19
  RequestEditorMode: 'requestEditorMode',
235
20
  RequestReadOnlyMode: 'requestReadOnlyMode',
236
21
  ExperienceUpdated: 'componentTreeUpdated',
237
- /** @deprecated will be removed when dropping backward compatibility for old DND */
238
22
  ComponentDraggingChanged: 'componentDraggingChanged',
239
- /** @deprecated will be removed when dropping backward compatibility for old DND */
240
23
  ComponentDragCanceled: 'componentDragCanceled',
241
- /** @deprecated will be removed when dropping backward compatibility for old DND */
242
24
  ComponentDragStarted: 'componentDragStarted',
243
- /** @deprecated will be removed when dropping backward compatibility for old DND */
244
25
  ComponentDragEnded: 'componentDragEnded',
245
- /** @deprecated will be removed when dropping backward compatibility for old DND */
246
26
  ComponentMoveEnded: 'componentMoveEnded',
247
- /** @deprecated will be removed when dropping backward compatibility for old DND */
248
27
  CanvasResized: 'canvasResized',
249
- /** @deprecated will be removed when dropping backward compatibility for old DND */
250
28
  SelectComponent: 'selectComponent',
251
- /** @deprecated will be removed when dropping backward compatibility for old DND */
252
29
  HoverComponent: 'hoverComponent',
253
30
  UpdatedEntity: 'updatedEntity',
254
31
  AssembliesAdded: 'assembliesAdded',
255
32
  AssembliesRegistered: 'assembliesRegistered',
256
- /** @deprecated will be removed when dropping backward compatibility for old DND */
257
33
  MouseMove: 'mouseMove',
258
34
  RequestedEntities: 'REQUESTED_ENTITIES',
259
35
  };
260
- const INTERNAL_EVENTS = {
261
- ComponentsRegistered: 'cfComponentsRegistered',
262
- VisualEditorInitialize: 'cfVisualEditorInitialize',
263
- };
264
- const VISUAL_EDITOR_EVENTS = {
265
- Ready: 'cfVisualEditorReady',
266
- };
267
36
  /**
268
37
  * These modes are ONLY intended to be internally used within the context of
269
38
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -275,56 +44,7 @@ var StudioCanvasMode$3;
275
44
  StudioCanvasMode["EDITOR"] = "editorMode";
276
45
  StudioCanvasMode["NONE"] = "none";
277
46
  })(StudioCanvasMode$3 || (StudioCanvasMode$3 = {}));
278
- const ASSEMBLY_NODE_TYPE = 'assembly';
279
- const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
280
- const EMPTY_CONTAINER_HEIGHT$1 = '80px';
281
- const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
282
- var PostMessageMethods$3;
283
- (function (PostMessageMethods) {
284
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
285
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
286
- })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
287
-
288
- /** @deprecated will be removed when dropping backward compatibility for old DND */
289
- const INCOMING_EVENTS = {
290
- RequestEditorMode: 'requestEditorMode',
291
- RequestReadOnlyMode: 'requestReadOnlyMode',
292
- ExperienceUpdated: 'componentTreeUpdated',
293
- /** @deprecated will be removed when dropping backward compatibility for old DND */
294
- ComponentDraggingChanged: 'componentDraggingChanged',
295
- /** @deprecated will be removed when dropping backward compatibility for old DND */
296
- ComponentDragCanceled: 'componentDragCanceled',
297
- /** @deprecated will be removed when dropping backward compatibility for old DND */
298
- ComponentDragStarted: 'componentDragStarted',
299
- /** @deprecated will be removed when dropping backward compatibility for old DND */
300
- ComponentDragEnded: 'componentDragEnded',
301
- /** @deprecated will be removed when dropping backward compatibility for old DND */
302
- ComponentMoveEnded: 'componentMoveEnded',
303
- /** @deprecated will be removed when dropping backward compatibility for old DND */
304
- CanvasResized: 'canvasResized',
305
- /** @deprecated will be removed when dropping backward compatibility for old DND */
306
- SelectComponent: 'selectComponent',
307
- /** @deprecated will be removed when dropping backward compatibility for old DND */
308
- HoverComponent: 'hoverComponent',
309
- UpdatedEntity: 'updatedEntity',
310
- AssembliesAdded: 'assembliesAdded',
311
- AssembliesRegistered: 'assembliesRegistered',
312
- /** @deprecated will be removed when dropping backward compatibility for old DND */
313
- MouseMove: 'mouseMove',
314
- RequestedEntities: 'REQUESTED_ENTITIES',
315
- };
316
- /**
317
- * These modes are ONLY intended to be internally used within the context of
318
- * editing an experience inside of Contentful Studio. i.e. these modes
319
- * intentionally do not include preview/delivery modes.
320
- */
321
- var StudioCanvasMode$2;
322
- (function (StudioCanvasMode) {
323
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
324
- StudioCanvasMode["EDITOR"] = "editorMode";
325
- StudioCanvasMode["NONE"] = "none";
326
- })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
327
- const CONTENTFUL_COMPONENTS$1 = {
47
+ const CONTENTFUL_COMPONENTS$2 = {
328
48
  section: {
329
49
  id: 'contentful-section',
330
50
  name: 'Section',
@@ -370,6 +90,9 @@ const CONTENTFUL_COMPONENTS$1 = {
370
90
  name: 'Carousel',
371
91
  },
372
92
  };
93
+ const ASSEMBLY_NODE_TYPE$1 = 'assembly';
94
+ const ASSEMBLY_DEFAULT_CATEGORY$1 = 'Assemblies';
95
+ const ASSEMBLY_BLOCK_NODE_TYPE$1 = 'assemblyBlock';
373
96
  const CF_STYLE_ATTRIBUTES = [
374
97
  'cfVisibility',
375
98
  'cfHorizontalAlignment',
@@ -412,24 +135,30 @@ const CF_STYLE_ATTRIBUTES = [
412
135
  'cfBackgroundImageAlignmentVertical',
413
136
  'cfBackgroundImageAlignmentHorizontal',
414
137
  ];
415
- const EMPTY_CONTAINER_HEIGHT = '80px';
138
+ const EMPTY_CONTAINER_HEIGHT$1 = '80px';
416
139
  const DEFAULT_IMAGE_WIDTH = '500px';
417
- var PostMessageMethods$2;
140
+ var PostMessageMethods$3;
418
141
  (function (PostMessageMethods) {
419
142
  PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
420
143
  PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
421
- })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
144
+ })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
422
145
  const SUPPORTED_IMAGE_FORMATS = ['jpg', 'png', 'webp', 'gif', 'avif'];
423
146
 
424
147
  const structureComponentIds = new Set([
425
- CONTENTFUL_COMPONENTS$1.section.id,
426
- CONTENTFUL_COMPONENTS$1.columns.id,
427
- CONTENTFUL_COMPONENTS$1.container.id,
428
- 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,
429
152
  ]);
430
- 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 ?? '');
431
156
  const isContentfulStructureComponent = (componentId) => structureComponentIds.has((componentId ?? ''));
432
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;
433
162
  const isStructureWithRelativeHeight = (componentId, height) => {
434
163
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
435
164
  };
@@ -451,6 +180,10 @@ const builtInStyles = {
451
180
  value: 'end',
452
181
  displayName: 'Align right',
453
182
  },
183
+ {
184
+ value: 'stretch',
185
+ displayName: 'Stretch',
186
+ },
454
187
  ],
455
188
  },
456
189
  type: 'Text',
@@ -474,6 +207,10 @@ const builtInStyles = {
474
207
  value: 'end',
475
208
  displayName: 'Align bottom',
476
209
  },
210
+ {
211
+ value: 'stretch',
212
+ displayName: 'Stretch',
213
+ },
477
214
  ],
478
215
  },
479
216
  type: 'Text',
@@ -1881,7 +1618,7 @@ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1881
1618
  if (children.length) {
1882
1619
  return '100%';
1883
1620
  }
1884
- return EMPTY_CONTAINER_HEIGHT;
1621
+ return EMPTY_CONTAINER_HEIGHT$1;
1885
1622
  };
1886
1623
 
1887
1624
  function getOptimizedImageUrl(url, width, quality, format) {
@@ -2312,10 +2049,10 @@ const tryParseMessage = (event) => {
2312
2049
  throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
2313
2050
  }
2314
2051
  // check eventData.eventType
2315
- const supportedEventTypes = Object.values(INCOMING_EVENTS);
2052
+ const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
2316
2053
  if (!supportedEventTypes.includes(eventData.eventType)) {
2317
2054
  // Expected message: This message is handled in the EntityStore to store fetched entities
2318
- if (eventData.eventType !== PostMessageMethods$2.REQUESTED_ENTITIES) {
2055
+ if (eventData.eventType !== PostMessageMethods$3.REQUESTED_ENTITIES) {
2319
2056
  throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
2320
2057
  }
2321
2058
  }
@@ -2580,7 +2317,7 @@ class EditorEntityStore extends EntityStoreBase {
2580
2317
  }
2581
2318
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2582
2319
  const newPromise = new Promise((resolve, reject) => {
2583
- const unsubscribe = this.subscribe(PostMessageMethods$2.REQUESTED_ENTITIES, (message) => {
2320
+ const unsubscribe = this.subscribe(PostMessageMethods$3.REQUESTED_ENTITIES, (message) => {
2584
2321
  const messageIds = [
2585
2322
  ...message.entities.map((entity) => entity.sys.id),
2586
2323
  ...(message.missingEntityIds ?? []),
@@ -2602,7 +2339,7 @@ class EditorEntityStore extends EntityStoreBase {
2602
2339
  ids.forEach((id) => this.cleanupPromise(id));
2603
2340
  unsubscribe();
2604
2341
  }, this.timeoutDuration);
2605
- this.sendMessage(PostMessageMethods$2.REQUEST_ENTITIES, {
2342
+ this.sendMessage(PostMessageMethods$3.REQUEST_ENTITIES, {
2606
2343
  entityIds: missing,
2607
2344
  entityType: type,
2608
2345
  locale: this.locale,
@@ -2704,74 +2441,567 @@ class EditorModeEntityStore extends EditorEntityStore {
2704
2441
  const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
2705
2442
  return { missingEntryIds, missingAssetIds };
2706
2443
  }
2707
- getValue(entityLinkOrEntity, path) {
2708
- const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2709
- if (!entity) {
2710
- return;
2711
- }
2712
- const fieldValue = get(entity, path);
2713
- return transformAssetFileToUrl(fieldValue);
2444
+ getValue(entityLinkOrEntity, path) {
2445
+ const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2446
+ if (!entity) {
2447
+ return;
2448
+ }
2449
+ const fieldValue = get(entity, path);
2450
+ return transformAssetFileToUrl(fieldValue);
2451
+ }
2452
+ }
2453
+
2454
+ var VisualEditorMode$1;
2455
+ (function (VisualEditorMode) {
2456
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
2457
+ VisualEditorMode["InjectScript"] = "injectScript";
2458
+ })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2459
+
2460
+ class DeepReference {
2461
+ constructor({ path, dataSource }) {
2462
+ const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2463
+ this.originalPath = path;
2464
+ this.entityId = dataSource[key].sys.id;
2465
+ this.entityLink = dataSource[key];
2466
+ this.field = field;
2467
+ this.referentField = referentField;
2468
+ }
2469
+ get headEntityId() {
2470
+ return this.entityId;
2471
+ }
2472
+ /**
2473
+ * Extracts referent from the path, using EntityStore as source of
2474
+ * entities during the resolution path.
2475
+ */
2476
+ extractReferent(entityStore) {
2477
+ const headEntity = entityStore.getEntityFromLink(this.entityLink);
2478
+ const maybeReferentLink = headEntity?.fields[this.field];
2479
+ if (undefined === maybeReferentLink) {
2480
+ // field references nothing (or even field doesn't exist)
2481
+ return undefined;
2482
+ }
2483
+ if (!isLink(maybeReferentLink)) {
2484
+ // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2485
+ // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2486
+ // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2487
+ return undefined;
2488
+ }
2489
+ return maybeReferentLink;
2490
+ }
2491
+ static from(opt) {
2492
+ return new DeepReference(opt);
2493
+ }
2494
+ }
2495
+ function gatherDeepReferencesFromTree(startingNode, dataSource) {
2496
+ const deepReferences = [];
2497
+ treeVisit(startingNode, (node) => {
2498
+ if (!node.data.props)
2499
+ return;
2500
+ for (const [, variableMapping] of Object.entries(node.data.props)) {
2501
+ if (variableMapping.type !== 'BoundValue')
2502
+ continue;
2503
+ if (!isDeepPath(variableMapping.path))
2504
+ continue;
2505
+ deepReferences.push(DeepReference.from({
2506
+ path: variableMapping.path,
2507
+ dataSource,
2508
+ }));
2509
+ }
2510
+ });
2511
+ return deepReferences;
2512
+ }
2513
+
2514
+ const useDraggedItemStore = create((set) => ({
2515
+ draggedItem: undefined,
2516
+ hoveredComponentId: undefined,
2517
+ domRect: undefined,
2518
+ componentId: '',
2519
+ isDraggingOnCanvas: false,
2520
+ onBeforeCaptureId: '',
2521
+ mouseX: 0,
2522
+ mouseY: 0,
2523
+ scrollY: 0,
2524
+ setComponentId(id) {
2525
+ set({ componentId: id });
2526
+ },
2527
+ setHoveredComponentId(id) {
2528
+ set({ hoveredComponentId: id });
2529
+ },
2530
+ updateItem: (item) => {
2531
+ set({ draggedItem: item });
2532
+ },
2533
+ setDraggingOnCanvas: (isDraggingOnCanvas) => {
2534
+ set({ isDraggingOnCanvas });
2535
+ },
2536
+ setOnBeforeCaptureId: (onBeforeCaptureId) => {
2537
+ set({ onBeforeCaptureId });
2538
+ },
2539
+ setMousePosition(x, y) {
2540
+ set({ mouseX: x, mouseY: y });
2541
+ },
2542
+ setDomRect(domRect) {
2543
+ set({ domRect });
2544
+ },
2545
+ setScrollY(y) {
2546
+ set({ scrollY: y });
2547
+ },
2548
+ }));
2549
+
2550
+ const SCROLL_STATES = {
2551
+ Start: 'scrollStart',
2552
+ IsScrolling: 'isScrolling',
2553
+ End: 'scrollEnd',
2554
+ };
2555
+ const OUTGOING_EVENTS = {
2556
+ Connected: 'connected',
2557
+ DesignTokens: 'registerDesignTokens',
2558
+ RegisteredBreakpoints: 'registeredBreakpoints',
2559
+ MouseMove: 'mouseMove',
2560
+ NewHoveredElement: 'newHoveredElement',
2561
+ ComponentSelected: 'componentSelected',
2562
+ RegisteredComponents: 'registeredComponents',
2563
+ RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
2564
+ ComponentDragCanceled: 'componentDragCanceled',
2565
+ ComponentDropped: 'componentDropped',
2566
+ ComponentMoved: 'componentMoved',
2567
+ CanvasReload: 'canvasReload',
2568
+ UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
2569
+ CanvasScroll: 'canvasScrolling',
2570
+ CanvasError: 'canvasError',
2571
+ ComponentMoveStarted: 'componentMoveStarted',
2572
+ ComponentMoveEnded: 'componentMoveEnded',
2573
+ OutsideCanvasClick: 'outsideCanvasClick',
2574
+ SDKFeatures: 'sdkFeatures',
2575
+ RequestEntities: 'REQUEST_ENTITIES',
2576
+ };
2577
+ const INCOMING_EVENTS = {
2578
+ RequestEditorMode: 'requestEditorMode',
2579
+ RequestReadOnlyMode: 'requestReadOnlyMode',
2580
+ ExperienceUpdated: 'componentTreeUpdated',
2581
+ ComponentDraggingChanged: 'componentDraggingChanged',
2582
+ ComponentDragCanceled: 'componentDragCanceled',
2583
+ ComponentDragStarted: 'componentDragStarted',
2584
+ ComponentDragEnded: 'componentDragEnded',
2585
+ ComponentMoveEnded: 'componentMoveEnded',
2586
+ CanvasResized: 'canvasResized',
2587
+ SelectComponent: 'selectComponent',
2588
+ HoverComponent: 'hoverComponent',
2589
+ UpdatedEntity: 'updatedEntity',
2590
+ AssembliesAdded: 'assembliesAdded',
2591
+ AssembliesRegistered: 'assembliesRegistered',
2592
+ MouseMove: 'mouseMove',
2593
+ RequestedEntities: 'REQUESTED_ENTITIES',
2594
+ };
2595
+ const INTERNAL_EVENTS = {
2596
+ ComponentsRegistered: 'cfComponentsRegistered',
2597
+ VisualEditorInitialize: 'cfVisualEditorInitialize',
2598
+ };
2599
+ const VISUAL_EDITOR_EVENTS = {
2600
+ Ready: 'cfVisualEditorReady',
2601
+ };
2602
+ /**
2603
+ * These modes are ONLY intended to be internally used within the context of
2604
+ * editing an experience inside of Contentful Studio. i.e. these modes
2605
+ * intentionally do not include preview/delivery modes.
2606
+ */
2607
+ var StudioCanvasMode$2;
2608
+ (function (StudioCanvasMode) {
2609
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
2610
+ StudioCanvasMode["EDITOR"] = "editorMode";
2611
+ StudioCanvasMode["NONE"] = "none";
2612
+ })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
2613
+ const CONTENTFUL_COMPONENTS$1 = {
2614
+ section: {
2615
+ id: 'contentful-section',
2616
+ name: 'Section',
2617
+ },
2618
+ container: {
2619
+ id: 'contentful-container',
2620
+ name: 'Container',
2621
+ },
2622
+ columns: {
2623
+ id: 'contentful-columns',
2624
+ name: 'Columns',
2625
+ },
2626
+ singleColumn: {
2627
+ id: 'contentful-single-column',
2628
+ name: 'Column',
2629
+ },
2630
+ button: {
2631
+ id: 'contentful-button',
2632
+ name: 'Button',
2633
+ },
2634
+ heading: {
2635
+ id: 'contentful-heading',
2636
+ name: 'Heading',
2637
+ },
2638
+ image: {
2639
+ id: 'contentful-image',
2640
+ name: 'Image',
2641
+ },
2642
+ richText: {
2643
+ id: 'contentful-richText',
2644
+ name: 'Rich Text',
2645
+ },
2646
+ text: {
2647
+ id: 'contentful-text',
2648
+ name: 'Text',
2649
+ },
2650
+ divider: {
2651
+ id: 'contentful-divider',
2652
+ name: 'Divider',
2653
+ },
2654
+ carousel: {
2655
+ id: 'contentful-carousel',
2656
+ name: 'Carousel',
2657
+ },
2658
+ };
2659
+ const ASSEMBLY_NODE_TYPE = 'assembly';
2660
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
2661
+ const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
2662
+ const ASSEMBLY_NODE_TYPES = [ASSEMBLY_NODE_TYPE, ASSEMBLY_BLOCK_NODE_TYPE];
2663
+ const EMPTY_CONTAINER_HEIGHT = '80px';
2664
+ const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
2665
+ var PostMessageMethods$2;
2666
+ (function (PostMessageMethods) {
2667
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2668
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2669
+ })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
2670
+
2671
+ const DRAGGABLE_HEIGHT = 30;
2672
+ const DRAGGABLE_WIDTH = 50;
2673
+ const DRAG_PADDING = 4;
2674
+ const ROOT_ID = 'root';
2675
+ const COMPONENT_LIST_ID = 'component-list';
2676
+ const NEW_COMPONENT_ID = 'ctfl-new-draggable';
2677
+ const CTFL_ZONE_ID = 'data-ctfl-zone-id';
2678
+ const CTFL_DRAGGING_ELEMENT = 'data-ctfl-dragging-element';
2679
+ const HITBOX = {
2680
+ WIDTH: 70,
2681
+ HEIGHT: 20,
2682
+ INITIAL_OFFSET: 10,
2683
+ OFFSET_INCREMENT: 8,
2684
+ MIN_HEIGHT: 45,
2685
+ MIN_DEPTH_HEIGHT: 20,
2686
+ DEEP_ZONE: 5,
2687
+ };
2688
+ var TreeAction;
2689
+ (function (TreeAction) {
2690
+ TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
2691
+ TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
2692
+ TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
2693
+ TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
2694
+ TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
2695
+ TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
2696
+ })(TreeAction || (TreeAction = {}));
2697
+ var HitboxDirection;
2698
+ (function (HitboxDirection) {
2699
+ HitboxDirection[HitboxDirection["TOP"] = 0] = "TOP";
2700
+ HitboxDirection[HitboxDirection["LEFT"] = 1] = "LEFT";
2701
+ HitboxDirection[HitboxDirection["RIGHT"] = 2] = "RIGHT";
2702
+ HitboxDirection[HitboxDirection["BOTTOM"] = 3] = "BOTTOM";
2703
+ HitboxDirection[HitboxDirection["SELF_VERTICAL"] = 4] = "SELF_VERTICAL";
2704
+ HitboxDirection[HitboxDirection["SELF_HORIZONTAL"] = 5] = "SELF_HORIZONTAL";
2705
+ })(HitboxDirection || (HitboxDirection = {}));
2706
+ var DraggablePosition;
2707
+ (function (DraggablePosition) {
2708
+ DraggablePosition[DraggablePosition["CENTERED"] = 0] = "CENTERED";
2709
+ DraggablePosition[DraggablePosition["MOUSE_POSITION"] = 1] = "MOUSE_POSITION";
2710
+ })(DraggablePosition || (DraggablePosition = {}));
2711
+
2712
+ function useDraggablePosition({ draggableId, draggableRef, position }) {
2713
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
2714
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
2715
+ const preDragDomRect = useDraggedItemStore((state) => state.domRect);
2716
+ useEffect(() => {
2717
+ const el = draggableRef?.current ??
2718
+ document.querySelector(`[${CTFL_DRAGGING_ELEMENT}][data-cf-node-id="${draggableId}"]`);
2719
+ if (!isDraggingOnCanvas || draggingId !== draggableId || !el) {
2720
+ return;
2721
+ }
2722
+ const isCentered = position === DraggablePosition.CENTERED || !preDragDomRect;
2723
+ const domRect = isCentered ? el.getBoundingClientRect() : preDragDomRect;
2724
+ const { mouseX, mouseY } = useDraggedItemStore.getState();
2725
+ const top = isCentered ? mouseY - domRect.height / 2 : domRect.top;
2726
+ const left = isCentered ? mouseX - domRect.width / 2 : domRect.left;
2727
+ el.style.position = 'fixed';
2728
+ el.style.left = `${left}px`;
2729
+ el.style.top = `${top}px`;
2730
+ el.style.width = `${domRect.width}px`;
2731
+ el.style.height = `${domRect.height}px`;
2732
+ }, [draggableRef, draggableId, isDraggingOnCanvas, draggingId, position, preDragDomRect]);
2733
+ }
2734
+
2735
+ function getStyle$2(style, snapshot) {
2736
+ if (!snapshot.isDropAnimating) {
2737
+ return style;
2738
+ }
2739
+ return {
2740
+ ...style,
2741
+ // cannot be 0, but make it super tiny
2742
+ transitionDuration: `0.001s`,
2743
+ };
2744
+ }
2745
+ const DraggableContainer = ({ id }) => {
2746
+ const ref = useRef(null);
2747
+ useDraggablePosition({
2748
+ draggableId: id,
2749
+ draggableRef: ref,
2750
+ position: DraggablePosition.CENTERED,
2751
+ });
2752
+ return (React.createElement("div", { id: COMPONENT_LIST_ID, style: {
2753
+ position: 'absolute',
2754
+ top: 0,
2755
+ left: 0,
2756
+ pointerEvents: 'none',
2757
+ zIndex: -1,
2758
+ } },
2759
+ React.createElement(Droppable, { droppableId: COMPONENT_LIST_ID, isDropDisabled: true }, (provided) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef },
2760
+ 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) => {
2761
+ provided.innerRef(node);
2762
+ ref.current = node;
2763
+ }, ...provided.draggableProps, ...provided.dragHandleProps, style: {
2764
+ ...getStyle$2(provided.draggableProps.style, snapshot),
2765
+ width: DRAGGABLE_WIDTH,
2766
+ height: DRAGGABLE_HEIGHT,
2767
+ pointerEvents: 'none',
2768
+ } }))),
2769
+ provided.placeholder)))));
2770
+ };
2771
+
2772
+ function getItemFromTree(id, node) {
2773
+ // Check if the current node's id matches the search id
2774
+ if (node.data.id === id) {
2775
+ return node;
2776
+ }
2777
+ // Recursively search through each child
2778
+ for (const child of node.children) {
2779
+ const foundNode = getItemFromTree(id, child);
2780
+ if (foundNode) {
2781
+ // Node found in children
2782
+ return foundNode;
2783
+ }
2784
+ }
2785
+ // If the node is not found in this branch of the tree, return undefined
2786
+ return undefined;
2787
+ }
2788
+ function findDepthById(node, id, currentDepth = 1) {
2789
+ if (node.data.id === id) {
2790
+ return currentDepth;
2791
+ }
2792
+ // If the node has children, check each one
2793
+ for (const child of node.children) {
2794
+ const childDepth = findDepthById(child, id, currentDepth + 1);
2795
+ if (childDepth !== -1) {
2796
+ return childDepth; // Found the node in a child
2797
+ }
2798
+ }
2799
+ return -1; // Node not found in this branch
2800
+ }
2801
+ const getChildFromTree = (parentId, index, node) => {
2802
+ // Check if the current node's id matches the search id
2803
+ if (node.data.id === parentId) {
2804
+ return node.children[index];
2805
+ }
2806
+ // Recursively search through each child
2807
+ for (const child of node.children) {
2808
+ const foundNode = getChildFromTree(parentId, index, child);
2809
+ if (foundNode) {
2810
+ // Node found in children
2811
+ return foundNode;
2812
+ }
2813
+ }
2814
+ // If the node is not found in this branch of the tree, return undefined
2815
+ return undefined;
2816
+ };
2817
+ const getItem = (selector, tree) => {
2818
+ return getItemFromTree(selector.id, {
2819
+ type: 'block',
2820
+ data: {
2821
+ id: ROOT_ID,
2822
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2823
+ },
2824
+ children: tree.root.children,
2825
+ });
2826
+ };
2827
+ const getItemDepthFromNode = (selector, node) => {
2828
+ return findDepthById(node, selector.id);
2829
+ };
2830
+
2831
+ function updateNode(nodeId, updatedNode, node) {
2832
+ if (node.data.id === nodeId) {
2833
+ node.data = updatedNode.data;
2834
+ return;
2835
+ }
2836
+ node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
2837
+ }
2838
+ function replaceNode(indexToReplace, updatedNode, node) {
2839
+ if (node.data.id === updatedNode.parentId) {
2840
+ node.children = [
2841
+ ...node.children.slice(0, indexToReplace),
2842
+ updatedNode,
2843
+ ...node.children.slice(indexToReplace + 1),
2844
+ ];
2845
+ return;
2846
+ }
2847
+ node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
2848
+ }
2849
+ function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
2850
+ if (node.data.id === parentNodeId) {
2851
+ const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
2852
+ node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
2853
+ return;
2854
+ }
2855
+ node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
2856
+ }
2857
+ function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
2858
+ if (node.data.id === parentNodeId) {
2859
+ node.children = [
2860
+ ...node.children.slice(0, indexToAdd),
2861
+ nodeToAdd,
2862
+ ...node.children.slice(indexToAdd),
2863
+ ];
2864
+ return;
2865
+ }
2866
+ node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
2867
+ }
2868
+ function reorderChildNode(oldIndex, newIndex, parentNodeId, node) {
2869
+ if (node.data.id === parentNodeId) {
2870
+ // Remove the child from the old position
2871
+ const [childToMove] = node.children.splice(oldIndex, 1);
2872
+ // Insert the child at the new position
2873
+ node.children.splice(newIndex, 0, childToMove);
2874
+ return;
2875
+ }
2876
+ node.children.forEach((childNode) => reorderChildNode(oldIndex, newIndex, parentNodeId, childNode));
2877
+ }
2878
+ function reparentChildNode(oldIndex, newIndex, sourceNodeId, destinationNodeId, node) {
2879
+ const nodeToMove = getChildFromTree(sourceNodeId, oldIndex, node);
2880
+ if (!nodeToMove) {
2881
+ return;
2714
2882
  }
2883
+ removeChildNode(oldIndex, nodeToMove.data.id, sourceNodeId, node);
2884
+ addChildNode(newIndex, destinationNodeId, nodeToMove, node);
2715
2885
  }
2716
2886
 
2717
- var VisualEditorMode$1;
2718
- (function (VisualEditorMode) {
2719
- VisualEditorMode["LazyLoad"] = "lazyLoad";
2720
- VisualEditorMode["InjectScript"] = "injectScript";
2721
- })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2722
-
2723
- class DeepReference {
2724
- constructor({ path, dataSource }) {
2725
- const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2726
- this.originalPath = path;
2727
- this.entityId = dataSource[key].sys.id;
2728
- this.entityLink = dataSource[key];
2729
- this.field = field;
2730
- this.referentField = referentField;
2731
- }
2732
- get headEntityId() {
2733
- return this.entityId;
2887
+ function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
2888
+ if (nodeAdded) {
2889
+ return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
2734
2890
  }
2735
- /**
2736
- * Extracts referent from the path, using EntityStore as source of
2737
- * entities during the resolution path.
2738
- */
2739
- extractReferent(entityStore) {
2740
- const headEntity = entityStore.getEntityFromLink(this.entityLink);
2741
- const maybeReferentLink = headEntity?.fields[this.field];
2742
- if (undefined === maybeReferentLink) {
2743
- // field references nothing (or even field doesn't exist)
2744
- return undefined;
2745
- }
2746
- if (!isLink(maybeReferentLink)) {
2747
- // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2748
- // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2749
- // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2750
- return undefined;
2891
+ const item = getItem({ id: child.data.id }, tree);
2892
+ if (item) {
2893
+ const parentNode = getItem({ id: item.parentId }, tree);
2894
+ if (!parentNode) {
2895
+ return null;
2751
2896
  }
2752
- return maybeReferentLink;
2897
+ const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
2898
+ return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
2753
2899
  }
2754
- static from(opt) {
2755
- return new DeepReference(opt);
2900
+ return {
2901
+ type: TreeAction.REPLACE_NODE,
2902
+ originalId: currentNode.children[index].data.id,
2903
+ indexToReplace: index,
2904
+ node: child,
2905
+ };
2906
+ }
2907
+ function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
2908
+ if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
2909
+ return {
2910
+ type: TreeAction.REORDER_NODE,
2911
+ sourceIndex: originalIndex,
2912
+ destinationIndex: index,
2913
+ parentNodeId,
2914
+ };
2756
2915
  }
2916
+ return null;
2757
2917
  }
2758
- function gatherDeepReferencesFromTree(startingNode, dataSource) {
2759
- const deepReferences = [];
2760
- treeVisit(startingNode, (node) => {
2761
- if (!node.data.props)
2918
+ function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
2919
+ // In the end, this map contains the list of nodes that are not present
2920
+ // in the updated tree and must be removed
2921
+ const map = new Map();
2922
+ if (!currentNode || !updatedNode) {
2923
+ return differences;
2924
+ }
2925
+ // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
2926
+ const currentNodeCount = currentNode.children.length;
2927
+ const updatedNodeCount = updatedNode.children.length;
2928
+ const nodeRemoved = currentNodeCount > updatedNodeCount;
2929
+ const nodeAdded = currentNodeCount < updatedNodeCount;
2930
+ const parentNodeId = updatedNode.data.id;
2931
+ /**
2932
+ * The data of the current node has changed, we need to update
2933
+ * this node to reflect the data change. (design, content, unbound values)
2934
+ */
2935
+ if (!isEqual(currentNode.data, updatedNode.data)) {
2936
+ differences.push({
2937
+ type: TreeAction.UPDATE_NODE,
2938
+ nodeId: currentNode.data.id,
2939
+ node: updatedNode,
2940
+ });
2941
+ }
2942
+ // Map children of the first tree by their ID
2943
+ currentNode.children.forEach((child, index) => map.set(child.data.id, index));
2944
+ // Compare with the second tree
2945
+ updatedNode.children.forEach((child, index) => {
2946
+ const childId = child.data.id;
2947
+ // The original tree does not have this node in the updated tree.
2948
+ if (!map.has(childId)) {
2949
+ const diff = missingNodeAction({
2950
+ index,
2951
+ child,
2952
+ nodeAdded,
2953
+ parentNodeId,
2954
+ tree: originalTree,
2955
+ currentNode,
2956
+ });
2957
+ if (diff?.type === TreeAction.REPLACE_NODE) {
2958
+ // Remove it from the deletion map to avoid adding another REMOVE_NODE action
2959
+ map.delete(diff.originalId);
2960
+ }
2961
+ return differences.push(diff);
2962
+ }
2963
+ const originalIndex = map.get(childId);
2964
+ const diff = matchingNodeAction({
2965
+ index,
2966
+ originalIndex,
2967
+ nodeAdded,
2968
+ nodeRemoved,
2969
+ parentNodeId,
2970
+ });
2971
+ differences.push(diff);
2972
+ map.delete(childId);
2973
+ compareNodes({
2974
+ currentNode: currentNode.children[originalIndex],
2975
+ updatedNode: child,
2976
+ originalTree,
2977
+ differences,
2978
+ });
2979
+ });
2980
+ map.forEach((index, key) => {
2981
+ // If the node count of the entire tree doesn't signify
2982
+ // a node was removed, don't add that as a diff
2983
+ if (!nodeRemoved) {
2762
2984
  return;
2763
- for (const [, variableMapping] of Object.entries(node.data.props)) {
2764
- if (variableMapping.type !== 'BoundValue')
2765
- continue;
2766
- if (!isDeepPath(variableMapping.path))
2767
- continue;
2768
- deepReferences.push(DeepReference.from({
2769
- path: variableMapping.path,
2770
- dataSource,
2771
- }));
2772
2985
  }
2986
+ // Remaining nodes in the map are removed in the second tree
2987
+ differences.push({
2988
+ type: TreeAction.REMOVE_NODE,
2989
+ indexToRemove: index,
2990
+ parentNodeId,
2991
+ idToRemove: key,
2992
+ });
2773
2993
  });
2774
- return deepReferences;
2994
+ return differences;
2995
+ }
2996
+ function getTreeDiffs(tree1, tree2, originalTree) {
2997
+ const differences = [];
2998
+ compareNodes({
2999
+ currentNode: tree1,
3000
+ updatedNode: tree2,
3001
+ originalTree,
3002
+ differences,
3003
+ });
3004
+ return differences.filter((diff) => diff);
2775
3005
  }
2776
3006
 
2777
3007
  const useTreeStore = create((set, get) => ({
@@ -2805,6 +3035,20 @@ const useTreeStore = create((set, get) => ({
2805
3035
  });
2806
3036
  }));
2807
3037
  },
3038
+ /**
3039
+ * NOTE: this is for debugging purposes only as it causes ugly canvas flash.
3040
+ *
3041
+ * Force updates entire tree. Usually shouldn't be used as updateTree()
3042
+ * uses smart update algorithm based on diffs. But for troubleshooting
3043
+ * you may want to force update the tree so leaving this in.
3044
+ */
3045
+ updateTreeForced: (tree) => {
3046
+ set({
3047
+ tree,
3048
+ // Breakpoints must be updated, as we receive completely new tree with possibly new breakpoints
3049
+ breakpoints: tree?.root?.data?.breakpoints || [],
3050
+ });
3051
+ },
2808
3052
  updateTree: (tree) => {
2809
3053
  const currentTree = get().tree;
2810
3054
  /**
@@ -2850,6 +3094,21 @@ const useTreeStore = create((set, get) => ({
2850
3094
  state.breakpoints = tree?.root?.data?.breakpoints || [];
2851
3095
  }));
2852
3096
  },
3097
+ addChild: (index, parentId, node) => {
3098
+ set(produce((state) => {
3099
+ addChildNode(index, parentId, node, state.tree.root);
3100
+ }));
3101
+ },
3102
+ reorderChildren: (destinationIndex, destinationParentId, sourceIndex) => {
3103
+ set(produce((state) => {
3104
+ reorderChildNode(sourceIndex, destinationIndex, destinationParentId, state.tree.root);
3105
+ }));
3106
+ },
3107
+ reparentChild: (destinationIndex, destinationParentId, sourceIndex, sourceParentId) => {
3108
+ set(produce((state) => {
3109
+ reparentChildNode(sourceIndex, destinationIndex, sourceParentId, destinationParentId, state.tree.root);
3110
+ }));
3111
+ },
2853
3112
  }));
2854
3113
  const hasBreakpointDiffs = (currentTree, newTree) => {
2855
3114
  const currentBreakpoints = currentTree?.root?.data?.breakpoints ?? [];
@@ -2867,8 +3126,8 @@ const cloneDeepAsPOJO = (obj) => {
2867
3126
  return JSON.parse(JSON.stringify(obj));
2868
3127
  };
2869
3128
 
2870
- 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";
2871
- var styles$2 = {"rootContainer":"RootRenderer-module_rootContainer__9UawM"};
3129
+ 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";
3130
+ var styles$3 = {"hitbox":"render-module_hitbox__l4ysJ","hitboxLower":"render-module_hitboxLower__tgsA1","canvasBottomSpacer":"render-module_canvasBottomSpacer__JuxVh","container":"render-module_container__-C3d7"};
2872
3131
  styleInject(css_248z$a);
2873
3132
 
2874
3133
  // TODO: In order to support integrations without React, we should extract this heavy logic into simple
@@ -2907,6 +3166,66 @@ const useBreakpoints = (breakpoints) => {
2907
3166
  return { resolveDesignValue };
2908
3167
  };
2909
3168
 
3169
+ /**
3170
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
3171
+ * and sends the DOM Rect to the client app
3172
+ */
3173
+ const sendSelectedComponentCoordinates = (instanceId) => {
3174
+ const selection = getSelectionNodes(instanceId);
3175
+ if (selection?.target) {
3176
+ const sendUpdateSelectedComponentCoordinates = () => {
3177
+ sendMessage(OUTGOING_EVENTS.UpdateSelectedComponentCoordinates, {
3178
+ selectedNodeCoordinates: getElementCoordinates(selection.target),
3179
+ selectedAssemblyChildCoordinates: selection.patternChild
3180
+ ? getElementCoordinates(selection.patternChild)
3181
+ : undefined,
3182
+ parentCoordinates: selection.parent ? getElementCoordinates(selection.parent) : undefined,
3183
+ });
3184
+ };
3185
+ // If the target contains an image, wait for this image to be loaded before sending the coordinates
3186
+ const childImage = selection.target.querySelector('img');
3187
+ if (childImage) {
3188
+ const handleImageLoad = () => {
3189
+ sendUpdateSelectedComponentCoordinates();
3190
+ childImage.removeEventListener('load', handleImageLoad);
3191
+ };
3192
+ childImage.addEventListener('load', handleImageLoad);
3193
+ }
3194
+ sendUpdateSelectedComponentCoordinates();
3195
+ }
3196
+ };
3197
+ const getSelectionNodes = (instanceId) => {
3198
+ if (!instanceId)
3199
+ return;
3200
+ let selectedNode = document.querySelector(`[data-cf-node-id="${instanceId}"]`);
3201
+ let selectedPatternChild = null;
3202
+ let selectedParent = null;
3203
+ // Use RegEx instead of split to match the last occurrence of '---' in the instanceId instead of the first one
3204
+ const idMatch = instanceId.match(/(.*)---(.*)/);
3205
+ const rootNodeId = idMatch?.[1] ?? instanceId;
3206
+ const nodeLocation = idMatch?.[2];
3207
+ const isNestedPattern = nodeLocation && selectedNode?.dataset?.cfNodeBlockType === ASSEMBLY_NODE_TYPE;
3208
+ const isPatternChild = !isNestedPattern && nodeLocation;
3209
+ if (isPatternChild) {
3210
+ // For pattern child nodes, render the pattern itself as selected component
3211
+ selectedPatternChild = selectedNode;
3212
+ selectedNode = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3213
+ }
3214
+ else if (isNestedPattern) {
3215
+ // For nested patterns, return the upper pattern as parent
3216
+ selectedParent = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3217
+ }
3218
+ else {
3219
+ // Find the next valid parent of the selected element
3220
+ selectedParent = selectedNode?.parentElement ?? null;
3221
+ // Ensure that the selection parent is a VisualEditorBlock
3222
+ while (selectedParent && !selectedParent.dataset?.cfNodeId) {
3223
+ selectedParent = selectedParent?.parentElement;
3224
+ }
3225
+ }
3226
+ return { target: selectedNode, patternChild: selectedPatternChild, parent: selectedParent };
3227
+ };
3228
+
2910
3229
  // Note: During development, the hot reloading might empty this and it
2911
3230
  // stays empty leading to not rendering assemblies. Ideally, this is
2912
3231
  // integrated into the state machine to keep track of its state.
@@ -2940,10 +3259,14 @@ const useEditorStore = create((set, get) => ({
2940
3259
  dataSource: {},
2941
3260
  hyperLinkPattern: undefined,
2942
3261
  unboundValues: {},
3262
+ selectedNodeId: null,
2943
3263
  locale: null,
2944
3264
  setHyperLinkPattern: (pattern) => {
2945
3265
  set({ hyperLinkPattern: pattern });
2946
3266
  },
3267
+ setSelectedNodeId: (id) => {
3268
+ set({ selectedNodeId: id });
3269
+ },
2947
3270
  setDataSource(data) {
2948
3271
  const dataSource = get().dataSource;
2949
3272
  const newDataSource = { ...dataSource, ...data };
@@ -2975,7 +3298,6 @@ const useEditorStore = create((set, get) => ({
2975
3298
  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";
2976
3299
  styleInject(css_248z$8);
2977
3300
 
2978
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2979
3301
  /**
2980
3302
  * These modes are ONLY intended to be internally used within the context of
2981
3303
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -3554,8 +3876,8 @@ var VisualEditorMode;
3554
3876
  VisualEditorMode["InjectScript"] = "injectScript";
3555
3877
  })(VisualEditorMode || (VisualEditorMode = {}));
3556
3878
 
3557
- 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";
3558
- styleInject(css_248z$2);
3879
+ 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";
3880
+ styleInject(css_248z$2$1);
3559
3881
 
3560
3882
  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) => {
3561
3883
  return (React.createElement("div", { id: id, ref: ref, style: {
@@ -3579,14 +3901,34 @@ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave,
3579
3901
  });
3580
3902
  Flex.displayName = 'Flex';
3581
3903
 
3582
- 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";
3904
+ 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";
3583
3905
  styleInject(css_248z$1$1);
3584
3906
 
3585
- 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";
3907
+ 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";
3586
3908
  styleInject(css_248z$9);
3587
3909
 
3910
+ const ColumnWrapper = forwardRef((props, ref) => {
3911
+ return (React.createElement("div", { ref: ref, ...props, style: {
3912
+ ...(props.style || {}),
3913
+ display: 'grid',
3914
+ gridTemplateColumns: 'repeat(12, [col-start] 1fr)',
3915
+ } }, props.children));
3916
+ });
3917
+ ColumnWrapper.displayName = 'ColumnWrapper';
3918
+
3588
3919
  const assemblyStyle = { display: 'contents' };
3920
+ // Feel free to do any magic as regards variable definitions for assemblies
3921
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
3589
3922
  const Assembly = (props) => {
3923
+ if (props.editorMode) {
3924
+ const { node, dragProps, ...editorModeProps } = props;
3925
+ return props.renderDropzone(node, {
3926
+ ...editorModeProps,
3927
+ ['data-test-id']: 'contentful-assembly',
3928
+ className: props.className,
3929
+ dragProps,
3930
+ });
3931
+ }
3590
3932
  // Using a display contents so assembly content/children
3591
3933
  // can appear as if they are direct children of the div wrapper's parent
3592
3934
  return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
@@ -3609,6 +3951,75 @@ const useEntityStore = create((set) => ({
3609
3951
  },
3610
3952
  }));
3611
3953
 
3954
+ class DragState {
3955
+ constructor() {
3956
+ this.isDragStartedOnParent = false;
3957
+ this.isDraggingItem = false;
3958
+ }
3959
+ get isDragging() {
3960
+ return this.isDraggingItem;
3961
+ }
3962
+ get isDraggingOnParent() {
3963
+ return this.isDragStartedOnParent;
3964
+ }
3965
+ updateIsDragging(isDraggingItem) {
3966
+ this.isDraggingItem = isDraggingItem;
3967
+ }
3968
+ updateIsDragStartedOnParent(isDragStartedOnParent) {
3969
+ this.isDragStartedOnParent = isDragStartedOnParent;
3970
+ }
3971
+ resetState() {
3972
+ this.isDraggingItem = false;
3973
+ this.isDragStartedOnParent = false;
3974
+ }
3975
+ }
3976
+
3977
+ class SimulateDnD extends DragState {
3978
+ constructor() {
3979
+ super();
3980
+ this.draggingElement = null;
3981
+ }
3982
+ setupDrag() {
3983
+ this.updateIsDragStartedOnParent(true);
3984
+ }
3985
+ startDrag(coordX, coordY) {
3986
+ this.draggingElement = document.getElementById(NEW_COMPONENT_ID);
3987
+ this.updateIsDragging(true);
3988
+ this.simulateMouseEvent(coordX, coordY, 'mousedown');
3989
+ }
3990
+ updateDrag(coordX, coordY) {
3991
+ if (!this.draggingElement) {
3992
+ this.draggingElement = document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`);
3993
+ }
3994
+ this.simulateMouseEvent(coordX, coordY);
3995
+ }
3996
+ endDrag(coordX, coordY) {
3997
+ this.simulateMouseEvent(coordX, coordY, 'mouseup');
3998
+ this.reset();
3999
+ }
4000
+ reset() {
4001
+ this.draggingElement = null;
4002
+ this.resetState();
4003
+ }
4004
+ simulateMouseEvent(coordX, coordY, eventName = 'mousemove') {
4005
+ if (!this.draggingElement) {
4006
+ return;
4007
+ }
4008
+ const options = {
4009
+ bubbles: true,
4010
+ cancelable: true,
4011
+ view: window,
4012
+ pageX: 0,
4013
+ pageY: 0,
4014
+ clientX: coordX,
4015
+ clientY: coordY,
4016
+ };
4017
+ const event = new MouseEvent(eventName, options);
4018
+ this.draggingElement.dispatchEvent(event);
4019
+ }
4020
+ }
4021
+ var SimulateDnD$1 = new SimulateDnD();
4022
+
3612
4023
  function useEditorSubscriber() {
3613
4024
  const entityStore = useEntityStore((state) => state.entityStore);
3614
4025
  const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
@@ -3622,7 +4033,14 @@ function useEditorSubscriber() {
3622
4033
  const setLocale = useEditorStore((state) => state.setLocale);
3623
4034
  const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
3624
4035
  const setDataSource = useEditorStore((state) => state.setDataSource);
4036
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4037
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3625
4038
  const resetEntityStore = useEntityStore((state) => state.resetEntityStore);
4039
+ const setComponentId = useDraggedItemStore((state) => state.setComponentId);
4040
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4041
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4042
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
4043
+ const setScrollY = useDraggedItemStore((state) => state.setScrollY);
3626
4044
  const reloadApp = () => {
3627
4045
  sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
3628
4046
  // Wait a moment to ensure that the message was sent
@@ -3723,12 +4141,12 @@ function useEditorSubscriber() {
3723
4141
  }
3724
4142
  const eventData = tryParseMessage(event);
3725
4143
  console.debug(`[experiences-sdk-react::onMessage] Received message [${eventData.eventType}]`, eventData);
3726
- if (eventData.eventType === PostMessageMethods$3.REQUESTED_ENTITIES) {
4144
+ if (eventData.eventType === PostMessageMethods$2.REQUESTED_ENTITIES) {
3727
4145
  // Expected message: This message is handled in the EntityStore to store fetched entities
3728
4146
  return;
3729
4147
  }
3730
4148
  switch (eventData.eventType) {
3731
- case INCOMING_EVENTS$1.ExperienceUpdated: {
4149
+ case INCOMING_EVENTS.ExperienceUpdated: {
3732
4150
  const { tree, locale, changedNode, changedValueType, assemblies } = eventData.payload;
3733
4151
  // Make sure to first store the assemblies before setting the tree and thus triggering a rerender
3734
4152
  if (assemblies) {
@@ -3772,7 +4190,7 @@ function useEditorSubscriber() {
3772
4190
  updateTree(tree);
3773
4191
  break;
3774
4192
  }
3775
- case INCOMING_EVENTS$1.AssembliesRegistered: {
4193
+ case INCOMING_EVENTS.AssembliesRegistered: {
3776
4194
  const { assemblies } = eventData.payload;
3777
4195
  assemblies.forEach((definition) => {
3778
4196
  addComponentRegistration({
@@ -3782,7 +4200,7 @@ function useEditorSubscriber() {
3782
4200
  });
3783
4201
  break;
3784
4202
  }
3785
- case INCOMING_EVENTS$1.AssembliesAdded: {
4203
+ case INCOMING_EVENTS.AssembliesAdded: {
3786
4204
  const { assembly, assemblyDefinition, } = eventData.payload;
3787
4205
  entityStore.updateEntity(assembly);
3788
4206
  // Using a Map here to avoid setting state and rerending all existing assemblies when a new assembly is added
@@ -3799,7 +4217,28 @@ function useEditorSubscriber() {
3799
4217
  }
3800
4218
  break;
3801
4219
  }
3802
- case INCOMING_EVENTS$1.UpdatedEntity: {
4220
+ case INCOMING_EVENTS.CanvasResized: {
4221
+ const { selectedNodeId } = eventData.payload;
4222
+ if (selectedNodeId) {
4223
+ sendSelectedComponentCoordinates(selectedNodeId);
4224
+ }
4225
+ break;
4226
+ }
4227
+ case INCOMING_EVENTS.HoverComponent: {
4228
+ const { hoveredNodeId } = eventData.payload;
4229
+ setHoveredComponentId(hoveredNodeId);
4230
+ break;
4231
+ }
4232
+ case INCOMING_EVENTS.ComponentDraggingChanged: {
4233
+ const { isDragging } = eventData.payload;
4234
+ if (!isDragging) {
4235
+ setComponentId('');
4236
+ setDraggingOnCanvas(false);
4237
+ SimulateDnD$1.reset();
4238
+ }
4239
+ break;
4240
+ }
4241
+ case INCOMING_EVENTS.UpdatedEntity: {
3803
4242
  const { entity: updatedEntity, shouldRerender } = eventData.payload;
3804
4243
  if (updatedEntity) {
3805
4244
  const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
@@ -3812,7 +4251,52 @@ function useEditorSubscriber() {
3812
4251
  }
3813
4252
  break;
3814
4253
  }
3815
- case INCOMING_EVENTS$1.RequestEditorMode: {
4254
+ case INCOMING_EVENTS.RequestEditorMode: {
4255
+ break;
4256
+ }
4257
+ case INCOMING_EVENTS.ComponentDragCanceled: {
4258
+ if (SimulateDnD$1.isDragging) {
4259
+ //simulate a mouseup event to cancel the drag
4260
+ SimulateDnD$1.endDrag(0, 0);
4261
+ }
4262
+ break;
4263
+ }
4264
+ case INCOMING_EVENTS.ComponentDragStarted: {
4265
+ const { id, isAssembly } = eventData.payload;
4266
+ SimulateDnD$1.setupDrag();
4267
+ setComponentId(`${id}:${isAssembly}` || '');
4268
+ setDraggingOnCanvas(true);
4269
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4270
+ nodeId: '',
4271
+ });
4272
+ break;
4273
+ }
4274
+ case INCOMING_EVENTS.ComponentDragEnded: {
4275
+ SimulateDnD$1.reset();
4276
+ setComponentId('');
4277
+ setDraggingOnCanvas(false);
4278
+ break;
4279
+ }
4280
+ case INCOMING_EVENTS.SelectComponent: {
4281
+ const { selectedNodeId: nodeId } = eventData.payload;
4282
+ setSelectedNodeId(nodeId);
4283
+ sendSelectedComponentCoordinates(nodeId);
4284
+ break;
4285
+ }
4286
+ case INCOMING_EVENTS.MouseMove: {
4287
+ const { mouseX, mouseY } = eventData.payload;
4288
+ setMousePosition(mouseX, mouseY);
4289
+ if (SimulateDnD$1.isDraggingOnParent && !SimulateDnD$1.isDragging) {
4290
+ SimulateDnD$1.startDrag(mouseX, mouseY);
4291
+ }
4292
+ else {
4293
+ SimulateDnD$1.updateDrag(mouseX, mouseY);
4294
+ }
4295
+ break;
4296
+ }
4297
+ case INCOMING_EVENTS.ComponentMoveEnded: {
4298
+ const { mouseX, mouseY } = eventData.payload;
4299
+ SimulateDnD$1.endDrag(mouseX, mouseY);
3816
4300
  break;
3817
4301
  }
3818
4302
  default:
@@ -3825,8 +4309,11 @@ function useEditorSubscriber() {
3825
4309
  };
3826
4310
  }, [
3827
4311
  entityStore,
4312
+ setComponentId,
4313
+ setDraggingOnCanvas,
3828
4314
  setDataSource,
3829
4315
  setLocale,
4316
+ setSelectedNodeId,
3830
4317
  dataSource,
3831
4318
  areEntitiesFetched,
3832
4319
  fetchMissingEntities,
@@ -3834,92 +4321,328 @@ function useEditorSubscriber() {
3834
4321
  unboundValues,
3835
4322
  updateTree,
3836
4323
  updateNodesByUpdatedEntity,
4324
+ setMousePosition,
3837
4325
  resetEntityStore,
4326
+ setHoveredComponentId,
3838
4327
  ]);
4328
+ /*
4329
+ * Handles on scroll business
4330
+ */
4331
+ useEffect(() => {
4332
+ let timeoutId = 0;
4333
+ let isScrolling = false;
4334
+ const onScroll = () => {
4335
+ setScrollY(window.scrollY);
4336
+ if (isScrolling === false) {
4337
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.Start);
4338
+ }
4339
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.IsScrolling);
4340
+ isScrolling = true;
4341
+ clearTimeout(timeoutId);
4342
+ timeoutId = window.setTimeout(() => {
4343
+ if (isScrolling === false) {
4344
+ return;
4345
+ }
4346
+ isScrolling = false;
4347
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.End);
4348
+ /**
4349
+ * On scroll end, send new co-ordinates of selected node
4350
+ */
4351
+ if (selectedNodeId) {
4352
+ sendSelectedComponentCoordinates(selectedNodeId);
4353
+ }
4354
+ }, 150);
4355
+ };
4356
+ window.addEventListener('scroll', onScroll, { capture: true, passive: true });
4357
+ return () => {
4358
+ window.removeEventListener('scroll', onScroll, { capture: true });
4359
+ clearTimeout(timeoutId);
4360
+ };
4361
+ }, [selectedNodeId, setScrollY]);
3839
4362
  }
3840
4363
 
3841
- const CircularDependencyErrorPlaceholder = ({ node, wrappingPatternIds, }) => {
3842
- const entityStore = useEntityStore((state) => state.entityStore);
3843
- 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: {
3844
- border: '1px solid red',
3845
- background: 'rgba(255, 0, 0, 0.1)',
3846
- padding: '1rem 1rem 0 1rem',
3847
- width: '100%',
3848
- height: '100%',
3849
- } },
3850
- "Circular usage of patterns detected:",
3851
- React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
3852
- const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
3853
- const entry = entityStore.getEntityFromLink(entryLink);
3854
- const entryTitle = entry?.fields?.title;
3855
- const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
3856
- return React.createElement("li", { key: patternId }, text);
3857
- }))));
4364
+ const onComponentMoved = (options) => {
4365
+ sendMessage(OUTGOING_EVENTS.ComponentMoved, options);
3858
4366
  };
3859
4367
 
3860
- class ImportedComponentError extends Error {
3861
- constructor(message) {
3862
- super(message);
3863
- this.name = 'ImportedComponentError';
3864
- }
3865
- }
3866
- class ExperienceSDKError extends Error {
3867
- constructor(message) {
3868
- super(message);
3869
- this.name = 'ExperienceSDKError';
3870
- }
3871
- }
3872
- class ImportedComponentErrorBoundary extends React.Component {
3873
- componentDidCatch(error, _errorInfo) {
3874
- if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
3875
- // This error was already handled by a nested error boundary and should be passed upwards
3876
- // We have to do this as we wrap every component on every layer with this error boundary and
3877
- // thus an error deep in the tree bubbles through many layers of error boundaries.
3878
- throw error;
3879
- }
3880
- // Differentiate between custom and SDK-provided components for error tracking
3881
- const ErrorClass = isContentfulComponent(this.props.componentId)
3882
- ? ExperienceSDKError
3883
- : ImportedComponentError;
3884
- const err = new ErrorClass(error.message);
3885
- err.stack = error.stack;
3886
- throw err;
3887
- }
3888
- render() {
3889
- return this.props.children;
3890
- }
3891
- }
4368
+ const generateId = (type) => `${type}-${v4()}`;
3892
4369
 
3893
- const MissingComponentPlaceholder = ({ blockId }) => {
3894
- return (React.createElement("div", { style: {
3895
- border: '1px solid red',
3896
- width: '100%',
3897
- height: '100%',
3898
- } },
3899
- "Missing component '",
3900
- blockId,
3901
- "'"));
4370
+ const createTreeNode = ({ blockId, parentId, slotId }) => {
4371
+ const node = {
4372
+ type: 'block',
4373
+ data: {
4374
+ id: generateId(blockId),
4375
+ blockId,
4376
+ slotId,
4377
+ props: {},
4378
+ dataSource: {},
4379
+ breakpoints: [],
4380
+ unboundValues: {},
4381
+ },
4382
+ parentId,
4383
+ children: [],
4384
+ };
4385
+ return node;
3902
4386
  };
3903
4387
 
3904
- var css_248z$1 = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
3905
- var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
3906
- styleInject(css_248z$1);
4388
+ const onComponentDropped = ({ node, index, parentBlockId, parentType, parentId, }) => {
4389
+ sendMessage(OUTGOING_EVENTS.ComponentDropped, {
4390
+ node,
4391
+ index: index ?? node.children.length,
4392
+ parentNode: {
4393
+ type: parentType,
4394
+ data: {
4395
+ blockId: parentBlockId,
4396
+ id: parentId,
4397
+ },
4398
+ },
4399
+ });
4400
+ };
3907
4401
 
3908
- const useComponentRegistration = (node) => {
3909
- return useMemo(() => {
3910
- let registration = componentRegistry.get(node.data.blockId);
3911
- if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
3912
- registration = createAssemblyRegistration({
3913
- definitionId: node.data.blockId,
3914
- component: Assembly,
4402
+ const onDrop = ({ destinationIndex, componentType, destinationZoneId, data, slotId, }) => {
4403
+ const parentId = destinationZoneId;
4404
+ const parentNode = getItem({ id: parentId }, data);
4405
+ const parentIsRoot = parentId === ROOT_ID;
4406
+ const emptyComponentData = {
4407
+ type: 'block',
4408
+ parentId,
4409
+ children: [],
4410
+ data: {
4411
+ blockId: componentType,
4412
+ id: generateId(componentType),
4413
+ slotId,
4414
+ breakpoints: [],
4415
+ dataSource: {},
4416
+ props: {},
4417
+ unboundValues: {},
4418
+ },
4419
+ };
4420
+ onComponentDropped({
4421
+ node: emptyComponentData,
4422
+ index: destinationIndex,
4423
+ parentType: parentIsRoot ? 'root' : parentNode?.type,
4424
+ parentBlockId: parentNode?.data.blockId,
4425
+ parentId: parentIsRoot ? 'root' : parentId,
4426
+ });
4427
+ };
4428
+
4429
+ /**
4430
+ * Parses a droppable zone ID into a node ID and slot ID.
4431
+ *
4432
+ * The slot ID is optional and only present if the component implements multiple drop zones.
4433
+ *
4434
+ * @param zoneId - Expected formats are `nodeId` or `nodeId|slotId`.
4435
+ */
4436
+ const parseZoneId = (zoneId) => {
4437
+ const [nodeId, slotId] = zoneId.includes('|') ? zoneId.split('|') : [zoneId, undefined];
4438
+ return { nodeId, slotId };
4439
+ };
4440
+
4441
+ function useCanvasInteractions() {
4442
+ const tree = useTreeStore((state) => state.tree);
4443
+ const reorderChildren = useTreeStore((state) => state.reorderChildren);
4444
+ const reparentChild = useTreeStore((state) => state.reparentChild);
4445
+ const addChild = useTreeStore((state) => state.addChild);
4446
+ const onAddComponent = (droppedItem) => {
4447
+ const { destination, draggableId } = droppedItem;
4448
+ if (!destination) {
4449
+ return;
4450
+ }
4451
+ /**
4452
+ * We only have the draggableId as information about the new component being dropped.
4453
+ * So we need to split it to get the blockId and the isAssembly flag.
4454
+ */
4455
+ const [blockId, isAssembly] = draggableId.split(':');
4456
+ const { nodeId: parentId, slotId } = parseZoneId(destination.droppableId);
4457
+ const droppingOnRoot = parentId === ROOT_ID;
4458
+ const isValidRootComponent = blockId === CONTENTFUL_COMPONENTS$1.container.id;
4459
+ let node = createTreeNode({ blockId, parentId, slotId });
4460
+ if (droppingOnRoot && !isValidRootComponent) {
4461
+ const wrappingContainer = createTreeNode({
4462
+ blockId: CONTENTFUL_COMPONENTS$1.container.id,
4463
+ parentId,
4464
+ });
4465
+ const childNode = createTreeNode({
4466
+ blockId,
4467
+ parentId: wrappingContainer.data.id,
3915
4468
  });
4469
+ node = wrappingContainer;
4470
+ node.children = [childNode];
3916
4471
  }
3917
- if (!registration) {
3918
- 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.`);
3919
- return undefined;
4472
+ /**
4473
+ * isAssembly comes from a string ID so we need to check if it's 'true' or 'false'
4474
+ * in string format.
4475
+ */
4476
+ if (isAssembly === 'false') {
4477
+ addChild(destination.index, parentId, node);
3920
4478
  }
3921
- return registration;
3922
- }, [node]);
4479
+ onDrop({
4480
+ data: tree,
4481
+ componentType: blockId,
4482
+ destinationIndex: destination.index,
4483
+ destinationZoneId: parentId,
4484
+ slotId,
4485
+ });
4486
+ };
4487
+ const onMoveComponent = (droppedItem) => {
4488
+ const { destination, source, draggableId } = droppedItem;
4489
+ if (!destination || !source) {
4490
+ return;
4491
+ }
4492
+ if (destination.droppableId === source.droppableId) {
4493
+ reorderChildren(destination.index, destination.droppableId, source.index);
4494
+ }
4495
+ if (destination.droppableId !== source.droppableId) {
4496
+ reparentChild(destination.index, destination.droppableId, source.index, source.droppableId);
4497
+ }
4498
+ onComponentMoved({
4499
+ nodeId: draggableId,
4500
+ destinationIndex: destination.index,
4501
+ destinationParentId: destination.droppableId,
4502
+ sourceIndex: source.index,
4503
+ sourceParentId: source.droppableId,
4504
+ });
4505
+ };
4506
+ return { onAddComponent, onMoveComponent };
4507
+ }
4508
+
4509
+ const TestDNDContainer = ({ onDragEnd, onBeforeDragStart, onDragStart, onDragUpdate, children, }) => {
4510
+ const handleDragStart = (event) => {
4511
+ const draggedItem = event.nativeEvent;
4512
+ const start = {
4513
+ mode: draggedItem.mode,
4514
+ draggableId: draggedItem.draggableId,
4515
+ type: draggedItem.type,
4516
+ source: draggedItem.source,
4517
+ };
4518
+ onBeforeDragStart(start);
4519
+ onDragStart(start, {});
4520
+ };
4521
+ const handleDrag = (event) => {
4522
+ const draggedItem = event.nativeEvent;
4523
+ const update = {
4524
+ mode: draggedItem.mode,
4525
+ draggableId: draggedItem.draggableId,
4526
+ type: draggedItem.type,
4527
+ source: draggedItem.source,
4528
+ destination: draggedItem.destination,
4529
+ combine: draggedItem.combine,
4530
+ };
4531
+ onDragUpdate(update, {});
4532
+ };
4533
+ const handleDragEnd = (event) => {
4534
+ const draggedItem = event.nativeEvent;
4535
+ const result = {
4536
+ mode: draggedItem.mode,
4537
+ draggableId: draggedItem.draggableId,
4538
+ type: draggedItem.type,
4539
+ source: draggedItem.source,
4540
+ destination: draggedItem.destination,
4541
+ combine: draggedItem.combine,
4542
+ reason: draggedItem.reason,
4543
+ };
4544
+ onDragEnd(result, {});
4545
+ };
4546
+ return (React.createElement("div", { "data-test-id": "dnd-context-substitute", onDragStart: handleDragStart, onDrag: handleDrag, onDragEnd: handleDragEnd }, children));
4547
+ };
4548
+
4549
+ const DNDProvider = ({ children }) => {
4550
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4551
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
4552
+ const setOnBeforeCaptureId = useDraggedItemStore((state) => state.setOnBeforeCaptureId);
4553
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4554
+ const updateItem = useDraggedItemStore((state) => state.updateItem);
4555
+ const { onAddComponent, onMoveComponent } = useCanvasInteractions();
4556
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4557
+ const prevSelectedNodeId = useRef(null);
4558
+ const isTestRun = typeof window !== 'undefined' && Object.prototype.hasOwnProperty.call(window, 'Cypress');
4559
+ const beforeDragStart = ({ source }) => {
4560
+ prevSelectedNodeId.current = selectedNodeId;
4561
+ // Unselect the current node when dragging and remove the outline
4562
+ setSelectedNodeId('');
4563
+ // Set dragging state here to make sure that DnD capture phase has completed
4564
+ setDraggingOnCanvas(true);
4565
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4566
+ nodeId: '',
4567
+ });
4568
+ if (source.droppableId !== COMPONENT_LIST_ID) {
4569
+ sendMessage(OUTGOING_EVENTS.ComponentMoveStarted, undefined);
4570
+ }
4571
+ };
4572
+ const beforeCapture = ({ draggableId }) => {
4573
+ setOnBeforeCaptureId(draggableId);
4574
+ };
4575
+ const dragStart = (start) => {
4576
+ updateItem(start);
4577
+ };
4578
+ const dragUpdate = (update) => {
4579
+ updateItem(update);
4580
+ };
4581
+ const dragEnd = (dropResult) => {
4582
+ setDraggingOnCanvas(false);
4583
+ setOnBeforeCaptureId('');
4584
+ updateItem();
4585
+ SimulateDnD$1.reset();
4586
+ // If the component is being dropped onto itself, do nothing
4587
+ // This can happen from an apparent race condition where the hovering zone gets set
4588
+ // to the component after its dropped.
4589
+ if (dropResult.destination?.droppableId === dropResult.draggableId) {
4590
+ return;
4591
+ }
4592
+ if (!dropResult.destination) {
4593
+ if (!draggedItem?.destination) {
4594
+ // User cancel drag
4595
+ sendMessage(OUTGOING_EVENTS.ComponentDragCanceled, undefined);
4596
+ //select the previously selected node if drag was canceled
4597
+ if (prevSelectedNodeId.current) {
4598
+ setSelectedNodeId(prevSelectedNodeId.current);
4599
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4600
+ nodeId: prevSelectedNodeId.current,
4601
+ });
4602
+ prevSelectedNodeId.current = null;
4603
+ }
4604
+ return;
4605
+ }
4606
+ // Use the destination from the draggedItem (when clicking the canvas)
4607
+ dropResult.destination = draggedItem.destination;
4608
+ }
4609
+ // New component added to canvas
4610
+ if (dropResult.source.droppableId.startsWith('component-list')) {
4611
+ onAddComponent(dropResult);
4612
+ }
4613
+ else {
4614
+ onMoveComponent(dropResult);
4615
+ }
4616
+ // If a node was previously selected prior to dragging, re-select it
4617
+ setSelectedNodeId(dropResult.draggableId);
4618
+ sendMessage(OUTGOING_EVENTS.ComponentMoveEnded, undefined);
4619
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4620
+ nodeId: dropResult.draggableId,
4621
+ });
4622
+ };
4623
+ 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)));
4624
+ };
4625
+
4626
+ /**
4627
+ * This hook gets the element co-ordinates of a specified element in the DOM
4628
+ * and sends the DOM Rect to the client app
4629
+ */
4630
+ const useSelectedInstanceCoordinates = ({ node }) => {
4631
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4632
+ useEffect(() => {
4633
+ if (selectedNodeId !== node.data.id) {
4634
+ return;
4635
+ }
4636
+ // Allows the drop animation to finish before
4637
+ // calculating the components coordinates
4638
+ setTimeout(() => {
4639
+ sendSelectedComponentCoordinates(node.data.id);
4640
+ }, 10);
4641
+ }, [node, selectedNodeId]);
4642
+ const selectedElement = node.data.id
4643
+ ? document.querySelector(`[data-cf-node-id="${selectedNodeId}"]`)
4644
+ : undefined;
4645
+ return selectedElement ? getElementCoordinates(selectedElement) : null;
3923
4646
  };
3924
4647
 
3925
4648
  /**
@@ -3967,13 +4690,15 @@ const getUnboundValues = ({ key, fallback, unboundValues, }) => {
3967
4690
  return get$1(unboundValues, lodashPath, fallback);
3968
4691
  };
3969
4692
 
3970
- const useComponentProps = ({ node, resolveDesignValue, definition, options, }) => {
4693
+ const useComponentProps = ({ node, areEntitiesFetched, resolveDesignValue, renderDropzone, definition, options, userIsDragging, requiresDragWrapper, }) => {
3971
4694
  const unboundValues = useEditorStore((state) => state.unboundValues);
3972
4695
  const hyperlinkPattern = useEditorStore((state) => state.hyperLinkPattern);
3973
4696
  const locale = useEditorStore((state) => state.locale);
3974
4697
  const dataSource = useEditorStore((state) => state.dataSource);
3975
4698
  const entityStore = useEntityStore((state) => state.entityStore);
3976
- const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
4699
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
4700
+ const nodeRect = useDraggedItemStore((state) => state.domRect);
4701
+ const isEmptyZone = !node.children.length;
3977
4702
  const props = useMemo(() => {
3978
4703
  const propsBase = {
3979
4704
  cfSsrClassName: node.data.props.cfSsrClassName
@@ -4063,223 +4788,1133 @@ const useComponentProps = ({ node, resolveDesignValue, definition, options, }) =
4063
4788
  return { ...acc };
4064
4789
  }
4065
4790
  }, {});
4791
+ const slotProps = {};
4792
+ if (definition.slots) {
4793
+ for (const slotId in definition.slots) {
4794
+ slotProps[slotId] = renderDropzone(node, {
4795
+ zoneId: [node.data.id, slotId].join('|'),
4796
+ });
4797
+ }
4798
+ }
4066
4799
  return {
4067
4800
  ...propsBase,
4068
4801
  ...extractedProps,
4802
+ ...slotProps,
4069
4803
  };
4070
4804
  }, [
4071
4805
  hyperlinkPattern,
4072
4806
  node,
4073
- locale,
4074
- definition,
4075
- resolveDesignValue,
4076
- dataSource,
4807
+ locale,
4808
+ definition,
4809
+ resolveDesignValue,
4810
+ dataSource,
4811
+ areEntitiesFetched,
4812
+ unboundValues,
4813
+ entityStore,
4814
+ renderDropzone,
4815
+ ]);
4816
+ const cfStyles = useMemo(() => buildCfStyles(props), [props]);
4817
+ const cfVisibility = props['cfVisibility'];
4818
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
4819
+ const isSingleColumn = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
4820
+ const isStructureComponent = isContentfulStructureComponent(node?.data.blockId);
4821
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
4822
+ const { overrideStyles, wrapperStyles } = useMemo(() => {
4823
+ // Move size styles to the wrapping div and override the component styles
4824
+ const overrideStyles = {};
4825
+ const wrapperStyles = { width: options?.wrapContainerWidth };
4826
+ if (requiresDragWrapper) {
4827
+ // when element is marked by user as not-visible, on that element the node `display: none !important`
4828
+ // will be set and it will disappear. However, when such a node has a wrapper div, the wrapper
4829
+ // should not have any css properties (at least not ones which force size), as such div should
4830
+ // simply be a zero height wrapper around element with `display: none !important`.
4831
+ // Hence we guard all wrapperStyles with `cfVisibility` check.
4832
+ if (cfVisibility && cfStyles.width)
4833
+ wrapperStyles.width = cfStyles.width;
4834
+ if (cfVisibility && cfStyles.height)
4835
+ wrapperStyles.height = cfStyles.height;
4836
+ if (cfVisibility && cfStyles.maxWidth)
4837
+ wrapperStyles.maxWidth = cfStyles.maxWidth;
4838
+ if (cfVisibility && cfStyles.margin)
4839
+ wrapperStyles.margin = cfStyles.margin;
4840
+ }
4841
+ // Override component styles to fill the wrapper
4842
+ if (wrapperStyles.width)
4843
+ overrideStyles.width = '100%';
4844
+ if (wrapperStyles.height)
4845
+ overrideStyles.height = '100%';
4846
+ if (wrapperStyles.margin)
4847
+ overrideStyles.margin = '0';
4848
+ if (wrapperStyles.maxWidth)
4849
+ overrideStyles.maxWidth = 'none';
4850
+ // Prevent the dragging element from changing sizes when it has a percentage width or height
4851
+ if (draggingId === node.data.id && nodeRect) {
4852
+ if (requiresDragWrapper) {
4853
+ if (isPercentValue(cfStyles.width))
4854
+ wrapperStyles.maxWidth = nodeRect.width;
4855
+ if (isPercentValue(cfStyles.height))
4856
+ wrapperStyles.maxHeight = nodeRect.height;
4857
+ }
4858
+ else {
4859
+ if (isPercentValue(cfStyles.width))
4860
+ overrideStyles.maxWidth = nodeRect.width;
4861
+ if (isPercentValue(cfStyles.height))
4862
+ overrideStyles.maxHeight = nodeRect.height;
4863
+ }
4864
+ }
4865
+ return { overrideStyles, wrapperStyles };
4866
+ }, [
4867
+ cfStyles,
4868
+ options?.wrapContainerWidth,
4869
+ requiresDragWrapper,
4870
+ node.data.id,
4871
+ draggingId,
4872
+ nodeRect,
4873
+ cfVisibility,
4874
+ ]);
4875
+ // Styles that will be applied to the component element
4876
+ // This has to be memoized to avoid recreating the styles in useEditorModeClassName on every render
4877
+ const componentStyles = useMemo(() => ({
4878
+ ...cfStyles,
4879
+ ...overrideStyles,
4880
+ ...(isEmptyZone &&
4881
+ isStructureWithRelativeHeight(node?.data.blockId, cfStyles.height) && {
4882
+ minHeight: EMPTY_CONTAINER_HEIGHT,
4883
+ }),
4884
+ ...(userIsDragging &&
4885
+ isStructureComponent &&
4886
+ !isSingleColumn &&
4887
+ !isAssemblyBlock && {
4888
+ padding: addExtraDropzonePadding(cfStyles.padding?.toString() || '0 0 0 0'),
4889
+ }),
4890
+ }), [
4891
+ cfStyles,
4892
+ isAssemblyBlock,
4893
+ isEmptyZone,
4894
+ isSingleColumn,
4895
+ isStructureComponent,
4896
+ node?.data.blockId,
4897
+ overrideStyles,
4898
+ userIsDragging,
4899
+ ]);
4900
+ const componentClass = useEditorModeClassName({
4901
+ styles: componentStyles,
4902
+ nodeId: node.data.id,
4903
+ });
4904
+ const sharedProps = {
4905
+ 'data-cf-node-id': node.data.id,
4906
+ 'data-cf-node-block-id': node.data.blockId,
4907
+ 'data-cf-node-block-type': node.type,
4908
+ className: props.cfSsrClassName ?? componentClass,
4909
+ ...(definition?.children ? { children: renderDropzone(node) } : {}),
4910
+ };
4911
+ const customComponentProps = {
4912
+ ...sharedProps,
4913
+ // Allows custom components to render differently in the editor. This needs to be activated
4914
+ // through options as the component has to be aware of this prop to not cause any React warnings.
4915
+ ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4916
+ ...sanitizeNodeProps(props),
4917
+ };
4918
+ const structuralOrPatternComponentProps = {
4919
+ ...sharedProps,
4920
+ editorMode: true,
4921
+ node,
4922
+ renderDropzone,
4923
+ };
4924
+ return {
4925
+ componentProps: isStructureComponent || isPatternNode
4926
+ ? structuralOrPatternComponentProps
4927
+ : customComponentProps,
4928
+ componentStyles,
4929
+ wrapperStyles,
4930
+ };
4931
+ };
4932
+ const addExtraDropzonePadding = (padding) => padding
4933
+ .split(' ')
4934
+ .map((value) => parseFloat(value) === 0 ? `${DRAG_PADDING}px` : `calc(${value} + ${DRAG_PADDING}px)`)
4935
+ .join(' ');
4936
+ const isPercentValue = (value) => typeof value === 'string' && value.endsWith('%');
4937
+
4938
+ class ImportedComponentError extends Error {
4939
+ constructor(message) {
4940
+ super(message);
4941
+ this.name = 'ImportedComponentError';
4942
+ }
4943
+ }
4944
+ class ExperienceSDKError extends Error {
4945
+ constructor(message) {
4946
+ super(message);
4947
+ this.name = 'ExperienceSDKError';
4948
+ }
4949
+ }
4950
+ class ImportedComponentErrorBoundary extends React.Component {
4951
+ componentDidCatch(error, _errorInfo) {
4952
+ if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
4953
+ // This error was already handled by a nested error boundary and should be passed upwards
4954
+ // We have to do this as we wrap every component on every layer with this error boundary and
4955
+ // thus an error deep in the tree bubbles through many layers of error boundaries.
4956
+ throw error;
4957
+ }
4958
+ // Differentiate between custom and SDK-provided components for error tracking
4959
+ const ErrorClass = isContentfulComponent(this.props.componentId)
4960
+ ? ExperienceSDKError
4961
+ : ImportedComponentError;
4962
+ const err = new ErrorClass(error.message);
4963
+ err.stack = error.stack;
4964
+ throw err;
4965
+ }
4966
+ render() {
4967
+ return this.props.children;
4968
+ }
4969
+ }
4970
+
4971
+ const MissingComponentPlaceholder = ({ blockId }) => {
4972
+ return (React.createElement("div", { style: {
4973
+ border: '1px solid red',
4974
+ width: '100%',
4975
+ height: '100%',
4976
+ } },
4977
+ "Missing component '",
4978
+ blockId,
4979
+ "'"));
4980
+ };
4981
+
4982
+ const CircularDependencyErrorPlaceholder = forwardRef(({ wrappingPatternIds, ...props }, ref) => {
4983
+ const entityStore = useEntityStore((state) => state.entityStore);
4984
+ return (React.createElement("div", { ...props,
4985
+ // Pass through ref to avoid DND errors being logged
4986
+ ref: ref, "data-cf-node-error": "circular-pattern-dependency", style: {
4987
+ border: '1px solid red',
4988
+ background: 'rgba(255, 0, 0, 0.1)',
4989
+ padding: '1rem 1rem 0 1rem',
4990
+ width: '100%',
4991
+ height: '100%',
4992
+ } },
4993
+ "Circular usage of patterns detected:",
4994
+ React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
4995
+ const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
4996
+ const entry = entityStore.getEntityFromLink(entryLink);
4997
+ const entryTitle = entry?.fields?.title;
4998
+ const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
4999
+ return React.createElement("li", { key: patternId }, text);
5000
+ }))));
5001
+ });
5002
+ CircularDependencyErrorPlaceholder.displayName = 'CircularDependencyErrorPlaceholder';
5003
+
5004
+ const useComponent = ({ node, resolveDesignValue, renderDropzone, userIsDragging, wrappingPatternIds, }) => {
5005
+ const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
5006
+ const tree = useTreeStore((state) => state.tree);
5007
+ const componentRegistration = useMemo(() => {
5008
+ let registration = componentRegistry.get(node.data.blockId);
5009
+ if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
5010
+ registration = createAssemblyRegistration({
5011
+ definitionId: node.data.blockId,
5012
+ component: Assembly,
5013
+ });
5014
+ }
5015
+ if (!registration) {
5016
+ 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.`);
5017
+ return undefined;
5018
+ }
5019
+ return registration;
5020
+ }, [node]);
5021
+ const componentId = node.data.id;
5022
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
5023
+ const isPatternComponent = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5024
+ const parentComponentNode = getItem({ id: node.parentId }, tree);
5025
+ const isNestedPattern = isPatternNode &&
5026
+ [ASSEMBLY_BLOCK_NODE_TYPE, ASSEMBLY_NODE_TYPE].includes(parentComponentNode?.type ?? '');
5027
+ const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
5028
+ const requiresDragWrapper = !isPatternNode && !isStructureComponent && !componentRegistration?.options?.wrapComponent;
5029
+ const { componentProps, wrapperStyles } = useComponentProps({
5030
+ node,
4077
5031
  areEntitiesFetched,
4078
- unboundValues,
4079
- entityStore,
4080
- ]);
4081
- const cfStyles = useMemo(() => buildCfStyles(props), [props]);
4082
- // Styles that will be applied to the component element
4083
- const componentStyles = useMemo(() => ({
4084
- ...cfStyles,
4085
- ...(!node.children.length &&
4086
- isStructureWithRelativeHeight(node.data.blockId, cfStyles.height) && {
4087
- minHeight: EMPTY_CONTAINER_HEIGHT$1,
4088
- }),
4089
- }), [cfStyles, node.children.length, node.data.blockId]);
4090
- const cfCsrClassName = useEditorModeClassName({
4091
- styles: componentStyles,
4092
- nodeId: node.data.id,
5032
+ resolveDesignValue,
5033
+ renderDropzone,
5034
+ definition: componentRegistration?.definition,
5035
+ options: componentRegistration?.options,
5036
+ userIsDragging,
5037
+ requiresDragWrapper,
4093
5038
  });
4094
- const componentProps = useMemo(() => {
4095
- const sharedProps = {
4096
- 'data-cf-node-id': node.data.id,
4097
- 'data-cf-node-block-id': node.data.blockId,
4098
- 'data-cf-node-block-type': node.type,
4099
- className: props.cfSsrClassName ?? cfCsrClassName,
5039
+ const elementToRender = (props) => {
5040
+ const { dragProps = {} } = props || {};
5041
+ const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps;
5042
+ const { 'data-cf-node-block-id': dataCfNodeBlockId, 'data-cf-node-block-type': dataCfNodeBlockType, 'data-cf-node-id': dataCfNodeId, } = componentProps;
5043
+ const refCallback = (refNode) => {
5044
+ if (innerRef && refNode)
5045
+ innerRef(refNode);
4100
5046
  };
4101
- // Only pass `editorMode` and `node` to structure components and assembly root nodes.
4102
- const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
4103
- if (isStructureComponent) {
4104
- return {
4105
- ...sharedProps,
4106
- editorMode: true,
4107
- node,
4108
- };
5047
+ if (!componentRegistration) {
5048
+ return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4109
5049
  }
5050
+ if (node.data.blockId && wrappingPatternIds.has(node.data.blockId)) {
5051
+ 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 }));
5052
+ }
5053
+ const element = React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, {
5054
+ ...componentProps,
5055
+ dragProps,
5056
+ }));
5057
+ if (!requiresDragWrapper) {
5058
+ return element;
5059
+ }
5060
+ 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 },
5061
+ ToolTipAndPlaceholder,
5062
+ element));
5063
+ };
5064
+ return {
5065
+ node,
5066
+ parentComponentNode,
5067
+ isAssembly: isPatternNode,
5068
+ isPatternNode,
5069
+ isPatternComponent,
5070
+ isNestedPattern,
5071
+ componentId,
5072
+ elementToRender,
5073
+ definition: componentRegistration?.definition,
5074
+ };
5075
+ };
5076
+
5077
+ const calcOffsetLeft = (parentElement, placeholderWidth, nodeWidth) => {
5078
+ if (!parentElement) {
5079
+ return 0;
5080
+ }
5081
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5082
+ if (alignItems === 'center') {
5083
+ return -(placeholderWidth - nodeWidth) / 2;
5084
+ }
5085
+ if (alignItems === 'end') {
5086
+ return -placeholderWidth + nodeWidth + 2;
5087
+ }
5088
+ return 0;
5089
+ };
5090
+ const calcOffsetTop = (parentElement, placeholderHeight, nodeHeight) => {
5091
+ if (!parentElement) {
5092
+ return 0;
5093
+ }
5094
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5095
+ if (alignItems === 'center') {
5096
+ return -(placeholderHeight - nodeHeight) / 2;
5097
+ }
5098
+ if (alignItems === 'end') {
5099
+ return -placeholderHeight + nodeHeight + 2;
5100
+ }
5101
+ return 0;
5102
+ };
5103
+ const getPaddingOffset = (element) => {
5104
+ const paddingLeft = parseFloat(window.getComputedStyle(element).paddingLeft);
5105
+ const paddingRight = parseFloat(window.getComputedStyle(element).paddingRight);
5106
+ const paddingTop = parseFloat(window.getComputedStyle(element).paddingTop);
5107
+ const paddingBottom = parseFloat(window.getComputedStyle(element).paddingBottom);
5108
+ const horizontalOffset = paddingLeft + paddingRight;
5109
+ const verticalOffset = paddingTop + paddingBottom;
5110
+ return [horizontalOffset, verticalOffset];
5111
+ };
5112
+ /**
5113
+ * Calculate the size and position of the dropzone indicator
5114
+ * when dragging a new component onto the canvas
5115
+ */
5116
+ const calcNewComponentStyles = (params) => {
5117
+ const { destinationIndex, elementIndex, dropzoneElementId, id, direction, totalIndexes } = params;
5118
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5119
+ const isHorizontal = direction === 'horizontal';
5120
+ const isRightAlign = isHorizontal && isEnd;
5121
+ const isBottomAlign = !isHorizontal && isEnd;
5122
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5123
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5124
+ if (!dropzone || !element) {
5125
+ return emptyStyles;
5126
+ }
5127
+ const elementSizes = element.getBoundingClientRect();
5128
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5129
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5130
+ const width = isHorizontal ? DRAGGABLE_WIDTH : dropzoneSizes.width - horizontalPadding;
5131
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : DRAGGABLE_HEIGHT;
5132
+ const top = isHorizontal
5133
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5134
+ : -height;
5135
+ const left = isHorizontal
5136
+ ? -width
5137
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5138
+ return {
5139
+ width,
5140
+ height,
5141
+ top: !isBottomAlign ? top : 'unset',
5142
+ right: isRightAlign ? -width : 'unset',
5143
+ bottom: isBottomAlign ? -height : 'unset',
5144
+ left: !isRightAlign ? left : 'unset',
5145
+ };
5146
+ };
5147
+ /**
5148
+ * Calculate the size and position of the dropzone indicator
5149
+ * when moving an existing component on the canvas
5150
+ */
5151
+ const calcMovementStyles = (params) => {
5152
+ const { destinationIndex, sourceIndex, destinationId, sourceId, elementIndex, dropzoneElementId, id, direction, totalIndexes, draggableId, } = params;
5153
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5154
+ const isHorizontal = direction === 'horizontal';
5155
+ const isSameZone = destinationId === sourceId;
5156
+ const isBelowSourceIndex = destinationIndex > sourceIndex;
5157
+ const isRightAlign = isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5158
+ const isBottomAlign = !isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5159
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5160
+ const draggable = document.querySelector(`[data-rfd-draggable-id="${draggableId}"]`);
5161
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5162
+ if (!dropzone || !element || !draggable) {
5163
+ return emptyStyles;
5164
+ }
5165
+ const elementSizes = element.getBoundingClientRect();
5166
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5167
+ const draggableSizes = draggable.getBoundingClientRect();
5168
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5169
+ const width = isHorizontal ? draggableSizes.width : dropzoneSizes.width - horizontalPadding;
5170
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : draggableSizes.height;
5171
+ const top = isHorizontal
5172
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5173
+ : -height;
5174
+ const left = isHorizontal
5175
+ ? -width
5176
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5177
+ return {
5178
+ width,
5179
+ height,
5180
+ top: !isBottomAlign ? top : 'unset',
5181
+ right: isRightAlign ? -width : 'unset',
5182
+ bottom: isBottomAlign ? -height : 'unset',
5183
+ left: !isRightAlign ? left : 'unset',
5184
+ };
5185
+ };
5186
+ const emptyStyles = { width: 0, height: 0 };
5187
+ const calcPlaceholderStyles = (params) => {
5188
+ const { isDraggingOver, sourceId } = params;
5189
+ if (!isDraggingOver) {
5190
+ return emptyStyles;
5191
+ }
5192
+ if (sourceId === COMPONENT_LIST_ID) {
5193
+ return calcNewComponentStyles(params);
5194
+ }
5195
+ return calcMovementStyles(params);
5196
+ };
5197
+ const Placeholder = (props) => {
5198
+ const sourceIndex = useDraggedItemStore((state) => state.draggedItem?.source.index) ?? -1;
5199
+ const draggableId = useDraggedItemStore((state) => state.draggedItem?.draggableId) ?? '';
5200
+ const sourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId) ?? '';
5201
+ const destinationIndex = useDraggedItemStore((state) => state.draggedItem?.destination?.index) ?? -1;
5202
+ const destinationId = useDraggedItemStore((state) => state.draggedItem?.destination?.droppableId) ?? '';
5203
+ const { elementIndex, totalIndexes, isDraggingOver } = props;
5204
+ const isActive = destinationIndex === elementIndex;
5205
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5206
+ const isVisible = isEnd || isActive;
5207
+ const isComponentList = destinationId === COMPONENT_LIST_ID;
5208
+ return (!isComponentList &&
5209
+ isDraggingOver &&
5210
+ isVisible && (React.createElement("div", { style: {
5211
+ ...calcPlaceholderStyles({
5212
+ ...props,
5213
+ sourceId,
5214
+ sourceIndex,
5215
+ destinationId,
5216
+ destinationIndex,
5217
+ draggableId,
5218
+ }),
5219
+ backgroundColor: 'rgba(var(--exp-builder-blue300-rgb), 0.5)',
5220
+ position: 'absolute',
5221
+ pointerEvents: 'none',
5222
+ } })));
5223
+ };
5224
+
5225
+ var css_248z$2 = ".styles-module_hitbox__i3wKV {\n position: fixed;\n pointer-events: all;\n}\n";
5226
+ var styles$2 = {"hitbox":"styles-module_hitbox__i3wKV"};
5227
+ styleInject(css_248z$2);
5228
+
5229
+ const useZoneStore = create()((set) => ({
5230
+ zones: {},
5231
+ hoveringZone: '',
5232
+ setHoveringZone(zoneId) {
5233
+ set({
5234
+ hoveringZone: zoneId,
5235
+ });
5236
+ },
5237
+ upsertZone(id, data) {
5238
+ set(produce((state) => {
5239
+ state.zones[id] = { ...(state.zones[id] || {}), ...data };
5240
+ }));
5241
+ },
5242
+ }));
5243
+
5244
+ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_HEIGHT, DEEP_ZONE } = HITBOX;
5245
+ const calcOffsetDepth = (depth) => {
5246
+ return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
5247
+ };
5248
+ const getHitboxStyles = ({ direction, zoneDepth, domRect, scrollY, offsetRect, }) => {
5249
+ if (!domRect) {
4110
5250
  return {
4111
- ...sharedProps,
4112
- // Allows custom components to render differently in the editor. This needs to be activated
4113
- // through options as the component has to be aware of this prop to not cause any React warnings.
4114
- ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4115
- ...sanitizeNodeProps(props),
5251
+ display: 'none',
4116
5252
  };
4117
- }, [cfCsrClassName, node, options?.enableCustomEditorView, props]);
4118
- return { componentProps };
5253
+ }
5254
+ const { width, height, top, left, bottom, right } = domRect;
5255
+ const { height: offsetHeight, width: offsetWidth } = offsetRect || { height: 0, width: 0 };
5256
+ const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
5257
+ const isDeepZone = zoneDepth > DEEP_ZONE;
5258
+ const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
5259
+ switch (direction) {
5260
+ case HitboxDirection.TOP:
5261
+ return {
5262
+ width,
5263
+ height: HEIGHT,
5264
+ top: top + offsetHeight - calcOffsetDepth(zoneDepth) - scrollY,
5265
+ left,
5266
+ zIndex: 100 + zoneDepth,
5267
+ };
5268
+ case HitboxDirection.BOTTOM:
5269
+ return {
5270
+ width,
5271
+ height: HEIGHT,
5272
+ top: bottom + offsetHeight + calcOffsetDepth(zoneDepth) - scrollY,
5273
+ left,
5274
+ zIndex: 100 + zoneDepth,
5275
+ };
5276
+ case HitboxDirection.LEFT:
5277
+ return {
5278
+ width: WIDTH,
5279
+ height: height - HEIGHT,
5280
+ left: left + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
5281
+ top: top + HEIGHT / 2 - scrollY,
5282
+ zIndex: 100 + zoneDepth,
5283
+ };
5284
+ case HitboxDirection.RIGHT:
5285
+ return {
5286
+ width: WIDTH,
5287
+ height: height - HEIGHT,
5288
+ left: right + offsetWidth + calcOffsetDepth(zoneDepth) - WIDTH / 2,
5289
+ top: top + HEIGHT / 2 - scrollY,
5290
+ zIndex: 100 + zoneDepth,
5291
+ };
5292
+ case HitboxDirection.SELF_VERTICAL: {
5293
+ if (isAboveMaxHeight && !isDeepZone) {
5294
+ return { display: 'none' };
5295
+ }
5296
+ const selfHeight = isDeepZone ? MIN_DEPTH_HEIGHT : MIN_HEIGHT;
5297
+ return {
5298
+ width,
5299
+ height: selfHeight,
5300
+ left,
5301
+ top: top + height / 2 - selfHeight / 2 - scrollY,
5302
+ zIndex: 1000 + zoneDepth,
5303
+ };
5304
+ }
5305
+ case HitboxDirection.SELF_HORIZONTAL: {
5306
+ if (width > DRAGGABLE_WIDTH) {
5307
+ return { display: 'none' };
5308
+ }
5309
+ return {
5310
+ width: width - DRAGGABLE_WIDTH * 2,
5311
+ height,
5312
+ left: left + DRAGGABLE_WIDTH,
5313
+ top: top - scrollY,
5314
+ zIndex: 1000 + zoneDepth,
5315
+ };
5316
+ }
5317
+ default:
5318
+ return {};
5319
+ }
4119
5320
  };
4120
5321
 
4121
- function EditorBlock({ node, resolveDesignValue, wrappingPatternIds: parentWrappingPatternIds = new Set(), }) {
4122
- const isRootAssemblyNode = node.type === ASSEMBLY_NODE_TYPE;
4123
- const wrappingPatternIds = useMemo(() => {
4124
- if (isRootAssemblyNode && node.data.blockId) {
4125
- return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5322
+ const Hitboxes = ({ zoneId, parentZoneId, isEmptyZone }) => {
5323
+ const tree = useTreeStore((state) => state.tree);
5324
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5325
+ const scrollY = useDraggedItemStore((state) => state.scrollY);
5326
+ const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
5327
+ const zones = useZoneStore((state) => state.zones);
5328
+ const hoveringZone = useZoneStore((state) => state.hoveringZone);
5329
+ const isHoveringZone = hoveringZone === zoneId;
5330
+ const hitboxContainer = useMemo(() => {
5331
+ return document.querySelector('[data-ctfl-hitboxes]');
5332
+ }, []);
5333
+ const domRect = useMemo(() => {
5334
+ if (!isDraggingOnCanvas)
5335
+ return;
5336
+ return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
5337
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5338
+ }, [zoneId, isDraggingOnCanvas]);
5339
+ // Use the size of the cloned dragging element to offset the position of the hitboxes
5340
+ // So that when dragging causes a dropzone to expand, the hitboxes will be in the correct position
5341
+ const offsetRect = useMemo(() => {
5342
+ if (!isDraggingOnCanvas || isEmptyZone || !isHoveringZone)
5343
+ return;
5344
+ return document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`)?.getBoundingClientRect();
5345
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5346
+ }, [isEmptyZone, isHoveringZone, isDraggingOnCanvas]);
5347
+ const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
5348
+ const isVertical = zoneDirection === 'vertical';
5349
+ const isRoot = parentZoneId === ROOT_ID;
5350
+ const { slotId: parentSlotId } = parseZoneId(parentZoneId);
5351
+ const getStyles = useCallback((direction) => getHitboxStyles({
5352
+ direction,
5353
+ zoneDepth,
5354
+ domRect,
5355
+ scrollY,
5356
+ offsetRect,
5357
+ }), [zoneDepth, domRect, scrollY, offsetRect]);
5358
+ const renderFinalRootHitbox = () => {
5359
+ if (!isRoot)
5360
+ return null;
5361
+ return (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) }));
5362
+ };
5363
+ const renderSurroundingHitboxes = () => {
5364
+ if (isRoot || parentSlotId)
5365
+ return null;
5366
+ return (React.createElement(React.Fragment, null,
5367
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
5368
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })));
5369
+ };
5370
+ const ActiveHitboxes = (React.createElement(React.Fragment, null,
5371
+ React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
5372
+ renderSurroundingHitboxes(),
5373
+ renderFinalRootHitbox()));
5374
+ if (!hitboxContainer) {
5375
+ return null;
5376
+ }
5377
+ return createPortal(ActiveHitboxes, hitboxContainer);
5378
+ };
5379
+
5380
+ const isRelativePreviewSize = (width) => {
5381
+ // For now, we solely allow 100% as relative value
5382
+ return width === '100%';
5383
+ };
5384
+ const getTooltipPositions = ({ previewSize, tooltipRect, coordinates, }) => {
5385
+ if (!coordinates || !tooltipRect) {
5386
+ return { display: 'none' };
5387
+ }
5388
+ /**
5389
+ * By default, the tooltip floats to the left of the element
5390
+ */
5391
+ const newTooltipStyles = { display: 'flex' };
5392
+ // If the preview size is relative, we don't change the floating direction
5393
+ if (!isRelativePreviewSize(previewSize)) {
5394
+ const previewSizeMatch = previewSize.match(/(\d{1,})px/);
5395
+ if (!previewSizeMatch) {
5396
+ return { display: 'none' };
4126
5397
  }
4127
- return parentWrappingPatternIds;
4128
- }, [isRootAssemblyNode, node, parentWrappingPatternIds]);
4129
- const componentRegistration = useComponentRegistration(node);
4130
- if (!componentRegistration) {
4131
- return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4132
- }
4133
- if (isRootAssemblyNode && node.data.blockId && parentWrappingPatternIds.has(node.data.blockId)) {
4134
- return (React.createElement(CircularDependencyErrorPlaceholder, { node: node, wrappingPatternIds: wrappingPatternIds }));
4135
- }
4136
- const slotNodes = {};
4137
- for (const slotId in componentRegistration.definition.slots) {
4138
- const nodes = node.children.filter((child) => child.data.slotId === slotId);
4139
- slotNodes[slotId] =
4140
- 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 })))));
4141
- }
4142
- const children = componentRegistration.definition.children
4143
- ? node.children
4144
- .filter((node) => node.data.slotId === undefined)
4145
- .map((childNode) => (React.createElement(EditorBlock, { key: childNode.data.id, node: childNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))
4146
- : null;
4147
- return (React.createElement(RegistrationComponent, { node: node, resolveDesignValue: resolveDesignValue, componentRegistration: componentRegistration, slotNodes: slotNodes }, children));
5398
+ const previewSizePx = parseInt(previewSizeMatch[1]);
5399
+ /**
5400
+ * If the element is at the right edge of the canvas, and the element isn't wide enough to fit the tooltip width,
5401
+ * we float the tooltip to the right of the element.
5402
+ */
5403
+ if (tooltipRect.width > previewSizePx - coordinates.right &&
5404
+ tooltipRect.width > coordinates.width) {
5405
+ newTooltipStyles['float'] = 'right';
5406
+ }
5407
+ }
5408
+ const tooltipHeight = tooltipRect.height === 0 ? 32 : tooltipRect.height;
5409
+ /**
5410
+ * For elements with small heights, we don't want the tooltip covering the content in the element,
5411
+ * so we show the tooltip at the top or bottom.
5412
+ */
5413
+ if (tooltipHeight * 2 > coordinates.height) {
5414
+ /**
5415
+ * If there's enough space for the tooltip at the top of the element, we show the tooltip at the top of the element,
5416
+ * else we show the tooltip at the bottom.
5417
+ */
5418
+ if (tooltipHeight < coordinates.top) {
5419
+ newTooltipStyles['bottom'] = coordinates.height;
5420
+ }
5421
+ else {
5422
+ newTooltipStyles['top'] = coordinates.height;
5423
+ }
5424
+ }
5425
+ /**
5426
+ * If the component draws outside of the borders of the canvas to the left we move the tooltip to the right
5427
+ * so that it is fully visible.
5428
+ */
5429
+ if (coordinates.left < 0) {
5430
+ newTooltipStyles['left'] = -coordinates.left;
5431
+ }
5432
+ /**
5433
+ * If for any reason, the element's top is negative, we show the tooltip at the bottom
5434
+ */
5435
+ if (coordinates.top < 0) {
5436
+ newTooltipStyles['top'] = coordinates.height;
5437
+ }
5438
+ return newTooltipStyles;
5439
+ };
5440
+
5441
+ 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";
5442
+ 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"};
5443
+ styleInject(css_248z$1);
5444
+
5445
+ const Tooltip = ({ coordinates, id, label, isAssemblyBlock, isContainer, isSelected, }) => {
5446
+ const tooltipRef = useRef(null);
5447
+ const previewSize = '100%'; // This should be based on breakpoints and added to usememo dependency array
5448
+ const tooltipStyles = useMemo(() => {
5449
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect();
5450
+ const draggableRect = document
5451
+ .querySelector(`[data-ctfl-draggable-id="${id}"]`)
5452
+ ?.getBoundingClientRect();
5453
+ const newTooltipStyles = getTooltipPositions({
5454
+ previewSize,
5455
+ tooltipRect,
5456
+ coordinates: draggableRect,
5457
+ });
5458
+ return newTooltipStyles;
5459
+ // 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
5460
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5461
+ }, [coordinates, id, tooltipRef.current]);
5462
+ if (isSelected) {
5463
+ return null;
5464
+ }
5465
+ return (React.createElement("div", { "data-tooltip": true, className: styles$1.tooltipWrapper },
5466
+ React.createElement("div", { "data-tooltip": true, ref: tooltipRef, style: tooltipStyles, className: classNames(styles$1.overlay, {
5467
+ [styles$1.overlayContainer]: isContainer,
5468
+ [styles$1.overlayAssembly]: isAssemblyBlock,
5469
+ }) }, label)));
5470
+ };
5471
+
5472
+ function useSingleColumn(node, resolveDesignValue) {
5473
+ const tree = useTreeStore((store) => store.tree);
5474
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5475
+ const isWrapped = useMemo(() => {
5476
+ if (!node.parentId || !isSingleColumn) {
5477
+ return false;
5478
+ }
5479
+ const parentNode = getItem({ id: node.parentId }, tree);
5480
+ if (!parentNode || parentNode.data.blockId !== CONTENTFUL_COMPONENTS$1.columns.id) {
5481
+ return false;
5482
+ }
5483
+ const { cfWrapColumns } = parentNode.data.props;
5484
+ if (cfWrapColumns.type !== 'DesignValue') {
5485
+ return false;
5486
+ }
5487
+ return resolveDesignValue(cfWrapColumns.valuesByBreakpoint);
5488
+ }, [tree, node, isSingleColumn, resolveDesignValue]);
5489
+ return {
5490
+ isSingleColumn,
5491
+ isWrapped,
5492
+ };
4148
5493
  }
4149
- const RegistrationComponent = ({ node, resolveDesignValue, componentRegistration, slotNodes, children, }) => {
4150
- const { componentProps } = useComponentProps({
4151
- node,
5494
+
5495
+ function getStyle$1(style, snapshot) {
5496
+ if (!snapshot.isDropAnimating) {
5497
+ return style;
5498
+ }
5499
+ return {
5500
+ ...style,
5501
+ // cannot be 0, but make it super tiny
5502
+ transitionDuration: `0.001s`,
5503
+ };
5504
+ }
5505
+ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, index, zoneId, userIsDragging, placeholder, wrappingPatternIds, }) => {
5506
+ const { slotId } = parseZoneId(zoneId);
5507
+ const ref = useRef(null);
5508
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
5509
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
5510
+ const { node, componentId, elementToRender, definition, isPatternNode, isPatternComponent, isNestedPattern, } = useComponent({
5511
+ node: rawNode,
4152
5512
  resolveDesignValue,
4153
- definition: componentRegistration.definition,
4154
- options: componentRegistration.options,
5513
+ renderDropzone,
5514
+ userIsDragging,
5515
+ wrappingPatternIds,
5516
+ });
5517
+ const { isSingleColumn, isWrapped } = useSingleColumn(node, resolveDesignValue);
5518
+ const setDomRect = useDraggedItemStore((state) => state.setDomRect);
5519
+ const isHoveredComponent = useDraggedItemStore((state) => state.hoveredComponentId === componentId);
5520
+ const coordinates = useSelectedInstanceCoordinates({ node });
5521
+ const displayName = node.data.displayName || rawNode.data.displayName || definition?.name;
5522
+ const testId = `draggable-${node.data.blockId ?? 'node'}`;
5523
+ const isSelected = node.data.id === selectedNodeId;
5524
+ const isContainer = node.data.blockId === CONTENTFUL_COMPONENTS$1.container.id;
5525
+ const isSlotComponent = Boolean(node.data.slotId);
5526
+ const isDragDisabled = isNestedPattern || isPatternComponent || (isSingleColumn && isWrapped) || isSlotComponent;
5527
+ const isEmptyZone = useMemo(() => {
5528
+ return !node.children.filter((node) => node.data.slotId === slotId).length;
5529
+ }, [node.children, slotId]);
5530
+ useDraggablePosition({
5531
+ draggableId: componentId,
5532
+ draggableRef: ref,
5533
+ position: DraggablePosition.MOUSE_POSITION,
4155
5534
  });
4156
- return React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, { ...componentProps, ...slotNodes }, children));
5535
+ const onClick = (e) => {
5536
+ e.stopPropagation();
5537
+ if (!userIsDragging) {
5538
+ setSelectedNodeId(node.data.id);
5539
+ // if it is the assembly directly we just want to select it as a normal component
5540
+ if (isPatternNode) {
5541
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5542
+ nodeId: node.data.id,
5543
+ });
5544
+ return;
5545
+ }
5546
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5547
+ assembly: node.data.assembly,
5548
+ nodeId: node.data.id,
5549
+ });
5550
+ }
5551
+ };
5552
+ const onMouseOver = (e) => {
5553
+ e.stopPropagation();
5554
+ if (userIsDragging)
5555
+ return;
5556
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {
5557
+ nodeId: componentId,
5558
+ });
5559
+ };
5560
+ const onMouseDown = (e) => {
5561
+ if (isDragDisabled) {
5562
+ return;
5563
+ }
5564
+ e.stopPropagation();
5565
+ setDomRect(e.currentTarget.getBoundingClientRect());
5566
+ };
5567
+ const ToolTipAndPlaceholder = (React.createElement(React.Fragment, null,
5568
+ React.createElement(Tooltip, { id: componentId, coordinates: coordinates, isAssemblyBlock: isPatternNode || isPatternComponent, isContainer: isContainer, isSelected: isSelected, label: displayName || 'No label specified' }),
5569
+ React.createElement(Placeholder, { ...placeholder, id: componentId }),
5570
+ userIsDragging && !isPatternComponent && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
5571
+ return (React.createElement(Draggable, { key: componentId, draggableId: componentId, index: index, isDragDisabled: isDragDisabled, disableInteractiveElementBlocking: true }, (provided, snapshot) => elementToRender({
5572
+ dragProps: {
5573
+ ...provided.draggableProps,
5574
+ ...provided.dragHandleProps,
5575
+ 'data-ctfl-draggable-id': componentId,
5576
+ 'data-test-id': testId,
5577
+ innerRef: (refNode) => {
5578
+ provided?.innerRef(refNode);
5579
+ ref.current = refNode;
5580
+ },
5581
+ className: classNames(styles$1.DraggableComponent, {
5582
+ [styles$1.isAssemblyBlock]: isPatternComponent || isPatternNode,
5583
+ [styles$1.isDragging]: snapshot?.isDragging || userIsDragging,
5584
+ [styles$1.isSelected]: isSelected,
5585
+ [styles$1.isHoveringComponent]: isHoveredComponent,
5586
+ }),
5587
+ style: getStyle$1(provided.draggableProps.style, snapshot),
5588
+ onMouseDown,
5589
+ onMouseOver,
5590
+ onClick,
5591
+ ToolTipAndPlaceholder,
5592
+ },
5593
+ })));
4157
5594
  };
4158
5595
 
4159
- 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";
4160
- 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"};
5596
+ 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";
5597
+ var styles = {"container":"EmptyContainer-module_container__XPH5b","highlight":"EmptyContainer-module_highlight__lcICy","icon":"EmptyContainer-module_icon__82-2O","label":"EmptyContainer-module_label__4TxRa"};
4161
5598
  styleInject(css_248z);
4162
5599
 
4163
- const EmptyCanvasMessage = () => {
4164
- return (React.createElement("div", { className: styles['empty-canvas-container'], "data-type": "empty-container" },
4165
- React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles['empty-canvas-icon'] },
5600
+ const EmptyContainer = ({ isDragging }) => {
5601
+ return (React.createElement("div", { className: classNames(styles.container, {
5602
+ [styles.highlight]: isDragging,
5603
+ }), "data-type": "empty-container" },
5604
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles.icon },
4166
5605
  React.createElement("rect", { width: "11.676", height: "11.676", x: "18.512", y: ".153", rx: "1.621", transform: "rotate(45 18.512 .153)" }),
4167
5606
  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)" }),
4168
5607
  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)" }),
4169
5608
  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)" }),
4170
5609
  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" })),
4171
- React.createElement("span", { className: styles['empty-canvas-label'] }, "Add components to begin")));
5610
+ React.createElement("span", { className: styles.label }, "Add components to begin")));
4172
5611
  };
4173
5612
 
4174
- /**
4175
- * This function gets the element co-ordinates of a specified component in the DOM and its parent
4176
- * and sends the DOM Rect to the client app.
4177
- */
4178
- const sendCanvasGeometryUpdatedMessage = async (tree, sourceEvent) => {
4179
- const nodeToCoordinatesMap = {};
4180
- await waitForAllImagesToBeLoaded();
4181
- collectNodeCoordinates(tree.root, nodeToCoordinatesMap);
4182
- sendMessage(OUTGOING_EVENTS.CanvasGeometryUpdated, {
4183
- size: {
4184
- width: document.documentElement.offsetWidth,
4185
- height: document.documentElement.offsetHeight,
5613
+ const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
5614
+ const zone = useZoneStore((state) => state.zones);
5615
+ const upsertZone = useZoneStore((state) => state.upsertZone);
5616
+ useEffect(() => {
5617
+ function getDirection() {
5618
+ if (!node || !node.data.blockId) {
5619
+ return 'vertical';
5620
+ }
5621
+ if (!isContentfulStructureComponent(node.data.blockId)) {
5622
+ return 'vertical';
5623
+ }
5624
+ if (node.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id) {
5625
+ return 'horizontal';
5626
+ }
5627
+ const designValues = node.data.props['cfFlexDirection'];
5628
+ if (!designValues || !resolveDesignValue || designValues.type !== 'DesignValue') {
5629
+ return 'vertical';
5630
+ }
5631
+ const direction = resolveDesignValue(designValues.valuesByBreakpoint);
5632
+ if (direction === 'row') {
5633
+ return 'horizontal';
5634
+ }
5635
+ return 'vertical';
5636
+ }
5637
+ upsertZone(zoneId, { direction: getDirection() });
5638
+ }, [node, resolveDesignValue, zoneId, upsertZone]);
5639
+ return zone[zoneId]?.direction || 'vertical';
5640
+ };
5641
+
5642
+ function getStyle(style = {}, snapshot) {
5643
+ if (!snapshot?.isDropAnimating) {
5644
+ return style;
5645
+ }
5646
+ return {
5647
+ ...style,
5648
+ // cannot be 0, but make it super tiny
5649
+ transitionDuration: `0.001s`,
5650
+ };
5651
+ }
5652
+ const EditorBlockClone = ({ node: rawNode, resolveDesignValue, snapshot, provided, renderDropzone, wrappingPatternIds, }) => {
5653
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5654
+ const { node, elementToRender } = useComponent({
5655
+ node: rawNode,
5656
+ resolveDesignValue,
5657
+ renderDropzone,
5658
+ userIsDragging,
5659
+ wrappingPatternIds,
5660
+ });
5661
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5662
+ return elementToRender({
5663
+ dragProps: {
5664
+ ...provided?.draggableProps,
5665
+ ...provided?.dragHandleProps,
5666
+ 'data-ctfl-dragging-element': 'true',
5667
+ innerRef: provided?.innerRef,
5668
+ className: classNames(styles$1.DraggableComponent, styles$1.DraggableClone, {
5669
+ [styles$1.isAssemblyBlock]: isAssemblyBlock,
5670
+ [styles$1.isDragging]: snapshot?.isDragging,
5671
+ }),
5672
+ style: getStyle(provided?.draggableProps.style, snapshot),
4186
5673
  },
4187
- nodes: nodeToCoordinatesMap,
4188
- sourceEvent,
4189
5674
  });
4190
5675
  };
4191
- const collectNodeCoordinates = (node, nodeToCoordinatesMap) => {
4192
- const selectedElement = document.querySelector(`[data-cf-node-id="${node.data.id}"]`);
4193
- if (selectedElement) {
4194
- const rect = getElementCoordinates(selectedElement);
4195
- nodeToCoordinatesMap[node.data.id] = {
4196
- coordinates: {
4197
- x: rect.x + window.scrollX,
4198
- y: rect.y + window.scrollY,
4199
- width: rect.width,
4200
- height: rect.height,
4201
- },
4202
- };
5676
+
5677
+ const getHtmlDragProps = (dragProps) => {
5678
+ if (dragProps) {
5679
+ const { ToolTipAndPlaceholder, Tag, innerRef, wrapComponent, ...htmlDragProps } = dragProps;
5680
+ return htmlDragProps;
4203
5681
  }
4204
- node.children.forEach((child) => collectNodeCoordinates(child, nodeToCoordinatesMap));
5682
+ return {};
4205
5683
  };
4206
- const waitForAllImagesToBeLoaded = () => {
4207
- // If the document contains an image, wait for this image to be loaded before collecting & sending all geometry data.
4208
- const allImageNodes = document.querySelectorAll('img');
4209
- return Promise.all(Array.from(allImageNodes).map((imageNode) => {
4210
- if (imageNode.complete) {
4211
- return Promise.resolve();
4212
- }
4213
- return new Promise((resolve, reject) => {
4214
- const handleImageLoad = (event) => {
4215
- imageNode.removeEventListener('load', handleImageLoad);
4216
- imageNode.removeEventListener('error', handleImageLoad);
4217
- if (event.type === 'error') {
4218
- console.warn('Image failed to load:', imageNode);
4219
- reject();
4220
- }
4221
- else {
4222
- resolve();
4223
- }
4224
- };
4225
- imageNode.addEventListener('load', handleImageLoad);
4226
- imageNode.addEventListener('error', handleImageLoad);
4227
- });
4228
- }));
5684
+ const getHtmlComponentProps = (props) => {
5685
+ if (props) {
5686
+ const { editorMode, renderDropzone, node, ...htmlProps } = props;
5687
+ return htmlProps;
5688
+ }
5689
+ return {};
4229
5690
  };
4230
5691
 
4231
- const useCanvasGeometryUpdates = ({ tree, rootContainerRef, }) => {
4232
- const debouncedUpdateGeometry = useMemo(() => debounce((tree, sourceEvent) => {
4233
- // When the DOM changed, we still need to wait for the next frame to ensure that
4234
- // rendering is complete (e.g. this is required when deleting a node).
4235
- window.requestAnimationFrame(() => {
4236
- sendCanvasGeometryUpdatedMessage(tree, sourceEvent);
4237
- });
4238
- }, 100, {
4239
- leading: true,
4240
- // To be sure, we recalculate it at the end of the frame again. Though, we couldn't
4241
- // yet show the need for this. So we might be able to drop this later to boost performance.
4242
- trailing: true,
4243
- }), []);
4244
- // Store tree in a ref to avoid the need to deactivate & reactivate the mutation observer
4245
- // when the tree changes. This is important to avoid missing out on some mutation events.
4246
- const treeRef = useRef(tree);
4247
- useEffect(() => {
4248
- treeRef.current = tree;
4249
- }, [tree]);
4250
- // Handling window resize events
4251
- useEffect(() => {
4252
- const resizeEventListener = () => debouncedUpdateGeometry(treeRef.current, 'resize');
4253
- window.addEventListener('resize', resizeEventListener);
4254
- return () => window.removeEventListener('resize', resizeEventListener);
4255
- }, [debouncedUpdateGeometry]);
4256
- // Handling DOM mutations
4257
- useEffect(() => {
4258
- if (!rootContainerRef.current)
4259
- return;
4260
- const observer = new MutationObserver(() => debouncedUpdateGeometry(treeRef.current, 'mutation'));
4261
- observer.observe(rootContainerRef.current, {
4262
- childList: true,
4263
- subtree: true,
4264
- attributes: true,
4265
- });
4266
- return () => observer.disconnect();
4267
- }, [debouncedUpdateGeometry, rootContainerRef]);
4268
- };
5692
+ function DropzoneClone({ node, zoneId, resolveDesignValue, WrapperComponent = 'div', renderDropzone, dragProps, wrappingPatternIds, ...rest }) {
5693
+ const tree = useTreeStore((state) => state.tree);
5694
+ const content = node?.children || tree.root?.children || [];
5695
+ const { slotId } = parseZoneId(zoneId);
5696
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5697
+ const htmlProps = getHtmlComponentProps(rest);
5698
+ const isRootZone = zoneId === ROOT_ID;
5699
+ if (!resolveDesignValue) {
5700
+ return null;
5701
+ }
5702
+ return (React.createElement(WrapperComponent, { ...htmlDraggableProps, ...htmlProps, className: classNames(dragProps?.className, styles$1.Dropzone, styles$1.DropzoneClone, rest.className, {
5703
+ [styles$1.isRoot]: isRootZone,
5704
+ [styles$1.isEmptyZone]: !content.length,
5705
+ }), "data-ctfl-slot-id": slotId, ref: (refNode) => {
5706
+ if (dragProps?.innerRef) {
5707
+ dragProps.innerRef(refNode);
5708
+ }
5709
+ } }, content
5710
+ .filter((node) => node.data.slotId === slotId)
5711
+ .map((item) => {
5712
+ const componentId = item.data.id;
5713
+ return (React.createElement(EditorBlockClone, { key: componentId, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds }));
5714
+ })));
5715
+ }
4269
5716
 
4270
- const RootRenderer = () => {
4271
- const rootContainerRef = useRef(null);
5717
+ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', dragProps, wrappingPatternIds: parentWrappingPatternIds = new Set(), ...rest }) {
5718
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5719
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
5720
+ const isDraggingNewComponent = useDraggedItemStore((state) => Boolean(state.componentId));
5721
+ const isHoveringZone = useZoneStore((state) => state.hoveringZone === zoneId);
4272
5722
  const tree = useTreeStore((state) => state.tree);
4273
- useCanvasGeometryUpdates({ tree, rootContainerRef });
5723
+ const content = node?.children || tree.root?.children || [];
5724
+ const { slotId } = parseZoneId(zoneId);
5725
+ const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
5726
+ const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
5727
+ const draggedNode = useMemo(() => {
5728
+ if (!draggedItem)
5729
+ return;
5730
+ return getItem({ id: draggedItem.draggableId }, tree);
5731
+ }, [draggedItem, tree]);
5732
+ const isRootZone = zoneId === ROOT_ID;
5733
+ const isDestination = draggedDestinationId === zoneId;
5734
+ const isEmptyCanvas = isRootZone && !content.length;
5735
+ const isAssembly = ASSEMBLY_NODE_TYPES.includes(node?.type || '');
5736
+ const isRootAssembly = node?.type === ASSEMBLY_NODE_TYPE;
5737
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5738
+ const htmlProps = getHtmlComponentProps(rest);
5739
+ const wrappingPatternIds = useMemo(() => {
5740
+ // On the top level, the node is not defined. If the root blockId is not the default string,
5741
+ // we assume that it is the entry ID of the experience/ pattern to properly detect circular dependencies
5742
+ if (!node && tree.root.data.blockId && tree.root.data.blockId !== ROOT_ID) {
5743
+ return new Set([tree.root.data.blockId, ...parentWrappingPatternIds]);
5744
+ }
5745
+ if (isRootAssembly && node?.data.blockId) {
5746
+ return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5747
+ }
5748
+ return parentWrappingPatternIds;
5749
+ }, [isRootAssembly, node, parentWrappingPatternIds, tree.root.data.blockId]);
5750
+ // To avoid a circular dependency, we create the recursive rendering function here and trickle it down
5751
+ const renderDropzone = useCallback((node, props) => {
5752
+ return (React.createElement(Dropzone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds, ...props }));
5753
+ }, [wrappingPatternIds, resolveDesignValue]);
5754
+ const renderClonedDropzone = useCallback((node, props) => {
5755
+ return (React.createElement(DropzoneClone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds, ...props }));
5756
+ }, [resolveDesignValue, wrappingPatternIds]);
5757
+ const isDropzoneEnabled = useMemo(() => {
5758
+ const isColumns = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
5759
+ const isDraggingSingleColumn = draggedNode?.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5760
+ const isParentOfDraggedNode = node?.data.id === draggedNode?.parentId;
5761
+ // If dragging a single column, only enable the dropzone of the parent
5762
+ // columns component
5763
+ if (isDraggingSingleColumn && isColumns && isParentOfDraggedNode) {
5764
+ return true;
5765
+ }
5766
+ // If dragging a single column, disable dropzones for any component besides
5767
+ // the parent of the dragged single column
5768
+ if (isDraggingSingleColumn && !isParentOfDraggedNode) {
5769
+ return false;
5770
+ }
5771
+ // Disable dropzone for Columns component
5772
+ if (isColumns) {
5773
+ return false;
5774
+ }
5775
+ // Disable dropzone for Assembly
5776
+ if (isAssembly) {
5777
+ return false;
5778
+ }
5779
+ // Enable dropzone for the non-root hovered zones if component is not allowed on root
5780
+ if (!isDraggingNewComponent &&
5781
+ !isComponentAllowedOnRoot({ type: draggedNode?.type, componentId: draggedNode?.data.blockId })) {
5782
+ return isHoveringZone && !isRootZone;
5783
+ }
5784
+ // Enable dropzone for the hovered zone only
5785
+ return isHoveringZone;
5786
+ }, [isAssembly, isHoveringZone, isRootZone, isDraggingNewComponent, draggedNode, node]);
5787
+ if (!resolveDesignValue) {
5788
+ return null;
5789
+ }
5790
+ const isPatternWrapperComponentFullHeight = isRootAssembly
5791
+ ? node.children.length === 1 &&
5792
+ resolveDesignValue(node?.children[0]?.data.props.cfHeight?.valuesByBreakpoint ?? {}, 'cfHeight') === '100%'
5793
+ : false;
5794
+ const isPatternWrapperComponentFullWidth = isRootAssembly
5795
+ ? node.children.length === 1 &&
5796
+ resolveDesignValue(node?.children[0]?.data.props.cfWidth?.valuesByBreakpoint ?? {}, 'cfWidth') === '100%'
5797
+ : false;
5798
+ 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) => {
5799
+ return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ...htmlDraggableProps, ...htmlProps, ref: (refNode) => {
5800
+ if (dragProps?.innerRef) {
5801
+ dragProps.innerRef(refNode);
5802
+ }
5803
+ provided?.innerRef(refNode);
5804
+ }, id: zoneId, "data-ctfl-zone-id": zoneId, "data-ctfl-slot-id": slotId, className: classNames(dragProps?.className, styles$1.Dropzone, className, {
5805
+ [styles$1.isEmptyCanvas]: isEmptyCanvas,
5806
+ [styles$1.isDragging]: userIsDragging,
5807
+ [styles$1.isDestination]: isDestination && !isAssembly,
5808
+ [styles$1.isRoot]: isRootZone,
5809
+ [styles$1.isEmptyZone]: !content.length,
5810
+ [styles$1.isSlot]: Boolean(slotId),
5811
+ [styles$1.fullHeight]: isPatternWrapperComponentFullHeight,
5812
+ [styles$1.fullWidth]: isPatternWrapperComponentFullWidth,
5813
+ }) },
5814
+ isEmptyCanvas ? (React.createElement(EmptyContainer, { isDragging: isRootZone && userIsDragging })) : (content
5815
+ .filter((node) => node.data.slotId === slotId)
5816
+ .map((item, i) => (React.createElement(EditorBlock, { placeholder: {
5817
+ isDraggingOver: snapshot?.isDraggingOver,
5818
+ totalIndexes: content.length,
5819
+ elementIndex: i,
5820
+ dropzoneElementId: zoneId,
5821
+ direction,
5822
+ }, index: i, zoneId: zoneId, key: item.data.id, userIsDragging: userIsDragging, draggingNewComponent: isDraggingNewComponent, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds })))),
5823
+ provided?.placeholder,
5824
+ dragProps?.ToolTipAndPlaceholder));
5825
+ }));
5826
+ }
5827
+
5828
+ const RootRenderer = ({ onChange }) => {
4274
5829
  useEditorSubscriber();
5830
+ const dragItem = useDraggedItemStore((state) => state.componentId);
5831
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5832
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4275
5833
  const breakpoints = useTreeStore((state) => state.breakpoints);
5834
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
5835
+ const containerRef = useRef(null);
4276
5836
  const { resolveDesignValue } = useBreakpoints(breakpoints);
4277
- // If the root blockId is defined but not the default string, it is the entry ID
4278
- // of the experience/ pattern to properly detect circular dependencies.
4279
- const rootBlockId = tree.root.data.blockId ?? ROOT_ID;
4280
- const wrappingPatternIds = rootBlockId !== ROOT_ID ? new Set([rootBlockId]) : new Set();
4281
- return (React.createElement(React.Fragment, null,
4282
- React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer, ref: rootContainerRef }, !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 })))))));
5837
+ const [containerStyles, setContainerStyles] = useState({});
5838
+ const tree = useTreeStore((state) => state.tree);
5839
+ const handleMouseOver = useCallback(() => {
5840
+ // Remove hover state set by UI when mouse is over canvas
5841
+ setHoveredComponentId();
5842
+ // Remove hover styling from components in the layers tab
5843
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {});
5844
+ }, [setHoveredComponentId]);
5845
+ const handleClickOutside = useCallback((e) => {
5846
+ const element = e.target;
5847
+ const isRoot = element.getAttribute('data-ctfl-zone-id') === ROOT_ID;
5848
+ const clickedOnCanvas = element.closest(`[data-ctfl-root]`);
5849
+ if (clickedOnCanvas && !isRoot) {
5850
+ return;
5851
+ }
5852
+ sendMessage(OUTGOING_EVENTS.OutsideCanvasClick, {
5853
+ outsideCanvasClick: true,
5854
+ });
5855
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5856
+ nodeId: '',
5857
+ });
5858
+ setSelectedNodeId('');
5859
+ }, [setSelectedNodeId]);
5860
+ const handleResizeCanvas = useCallback(() => {
5861
+ const parentElement = containerRef.current?.parentElement;
5862
+ if (!parentElement) {
5863
+ return;
5864
+ }
5865
+ let siblingHeight = 0;
5866
+ for (const child of parentElement.children) {
5867
+ if (!child.hasAttribute('data-ctfl-root')) {
5868
+ siblingHeight += child.getBoundingClientRect().height;
5869
+ }
5870
+ }
5871
+ if (!siblingHeight) {
5872
+ /**
5873
+ * DRAGGABLE_HEIGHT is subtracted here due to an uninteded scrolling effect
5874
+ * when dragging a new component onto the canvas
5875
+ *
5876
+ * The DRAGGABLE_HEIGHT is then added as margin bottom to offset this value
5877
+ * so that visually there is no difference to the user.
5878
+ */
5879
+ setContainerStyles({
5880
+ minHeight: `${window.innerHeight - DRAGGABLE_HEIGHT}px`,
5881
+ });
5882
+ return;
5883
+ }
5884
+ setContainerStyles({
5885
+ minHeight: `${window.innerHeight - siblingHeight}px`,
5886
+ });
5887
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5888
+ }, [containerRef.current]);
5889
+ useEffect(() => {
5890
+ if (onChange)
5891
+ onChange(tree);
5892
+ }, [tree, onChange]);
5893
+ useEffect(() => {
5894
+ window.addEventListener('mouseover', handleMouseOver);
5895
+ return () => {
5896
+ window.removeEventListener('mouseover', handleMouseOver);
5897
+ };
5898
+ }, [handleMouseOver]);
5899
+ useEffect(() => {
5900
+ document.addEventListener('click', handleClickOutside);
5901
+ return () => {
5902
+ document.removeEventListener('click', handleClickOutside);
5903
+ };
5904
+ }, [handleClickOutside]);
5905
+ useEffect(() => {
5906
+ handleResizeCanvas();
5907
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5908
+ }, [containerRef.current]);
5909
+ return (React.createElement(DNDProvider, null,
5910
+ dragItem && React.createElement(DraggableContainer, { id: dragItem }),
5911
+ React.createElement("div", { "data-ctfl-root": true, className: styles$3.container, ref: containerRef, style: containerStyles },
5912
+ userIsDragging && React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitbox }),
5913
+ React.createElement(Dropzone, { zoneId: ROOT_ID, resolveDesignValue: resolveDesignValue }),
5914
+ userIsDragging && (React.createElement(React.Fragment, null,
5915
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitboxLower }),
5916
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.canvasBottomSpacer })))),
5917
+ React.createElement("div", { "data-ctfl-hitboxes": true })));
4283
5918
  };
4284
5919
 
4285
5920
  const useInitializeEditor = () => {
@@ -4323,11 +5958,38 @@ const useInitializeEditor = () => {
4323
5958
  const VisualEditorRoot = ({ experience }) => {
4324
5959
  const initialized = useInitializeEditor();
4325
5960
  const setHyperLinkPattern = useEditorStore((state) => state.setHyperLinkPattern);
5961
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
5962
+ const setHoveringZone = useZoneStore((state) => state.setHoveringZone);
4326
5963
  useEffect(() => {
4327
5964
  if (experience?.hyperlinkPattern) {
4328
5965
  setHyperLinkPattern(experience.hyperlinkPattern);
4329
5966
  }
4330
5967
  }, [experience?.hyperlinkPattern, setHyperLinkPattern]);
5968
+ useEffect(() => {
5969
+ const onMouseMove = (e) => {
5970
+ setMousePosition(e.clientX, e.clientY);
5971
+ const target = e.target;
5972
+ const zoneId = target.closest(`[${CTFL_ZONE_ID}]`)?.getAttribute(CTFL_ZONE_ID);
5973
+ if (zoneId) {
5974
+ setHoveringZone(zoneId);
5975
+ }
5976
+ if (!SimulateDnD$1.isDragging) {
5977
+ return;
5978
+ }
5979
+ if (target.id === NEW_COMPONENT_ID) {
5980
+ return;
5981
+ }
5982
+ SimulateDnD$1.updateDrag(e.clientX, e.clientY);
5983
+ sendMessage(OUTGOING_EVENTS.MouseMove, {
5984
+ clientX: e.pageX - window.scrollX,
5985
+ clientY: e.pageY - window.scrollY,
5986
+ });
5987
+ };
5988
+ document.addEventListener('mousemove', onMouseMove);
5989
+ return () => {
5990
+ document.removeEventListener('mousemove', onMouseMove);
5991
+ };
5992
+ }, [setHoveringZone, setMousePosition]);
4331
5993
  if (!initialized)
4332
5994
  return null;
4333
5995
  return React.createElement(RootRenderer, null);