@contentful/experiences-visual-editor-react 1.39.0-alpha-20250528T1549-bd210e1.0 → 1.39.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,268 +1,38 @@
1
1
  import styleInject from 'style-inject';
2
- import React, { useState, useEffect, useCallback, forwardRef, useMemo, useLayoutEffect, useRef } from 'react';
3
- import { create } from 'zustand';
4
- import { produce } from 'immer';
5
- import { isEqual, omit, isArray, get as get$1 } from 'lodash-es';
2
+ import React, { useEffect, useRef, useState, useCallback, forwardRef, useLayoutEffect, useMemo } from 'react';
6
3
  import { z } from 'zod';
4
+ import { omit, isArray, isEqual, get as get$1 } from 'lodash-es';
7
5
  import md5 from 'md5';
8
6
  import { BLOCKS } from '@contentful/rich-text-types';
7
+ import { create } from 'zustand';
8
+ import { Droppable, Draggable, DragDropContext } from '@hello-pangea/dnd';
9
+ import { produce } from 'immer';
9
10
  import '@contentful/rich-text-react-renderer';
11
+ import { v4 } from 'uuid';
12
+ import { createPortal } from 'react-dom';
13
+ import classNames from 'classnames';
10
14
 
11
15
  var css_248z$b = "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\n/*\n * All of these variables are tokens from Forma-36 and should not be adjusted as these\n * are global variables that may affect multiple places.\n * As our customers may use other design libraries, we try to avoid overlapping global\n * variables by always using the prefix `--exp-builder-` inside this SDK.\n */\n\n:root {\n /* Color tokens from Forma 36: https://f36.contentful.com/tokens/color-system */\n --exp-builder-blue100: #e8f5ff;\n --exp-builder-blue200: #ceecff;\n --exp-builder-blue300: #98cbff;\n --exp-builder-blue400: #40a0ff;\n --exp-builder-blue500: #036fe3;\n --exp-builder-blue600: #0059c8;\n --exp-builder-blue700: #0041ab;\n --exp-builder-blue800: #003298;\n --exp-builder-blue900: #002a8e;\n --exp-builder-gray100: #f7f9fa;\n --exp-builder-gray200: #e7ebee;\n --exp-builder-gray300: #cfd9e0;\n --exp-builder-gray400: #aec1cc;\n --exp-builder-gray500: #67728a;\n --exp-builder-gray600: #5a657c;\n --exp-builder-gray700: #414d63;\n --exp-builder-gray800: #1b273a;\n --exp-builder-gray900: #111b2b;\n --exp-builder-purple600: #6c3ecf;\n --exp-builder-red200: #ffe0e0;\n --exp-builder-red800: #7f0010;\n --exp-builder-color-white: #ffffff;\n --exp-builder-glow-primary: 0px 0px 0px 3px #e8f5ff;\n\n /* RGB colors for applying opacity */\n --exp-builder-blue100-rgb: 232, 245, 255;\n --exp-builder-blue300-rgb: 152, 203, 255;\n\n /* Spacing tokens from Forma 36: https://f36.contentful.com/tokens/spacing */\n --exp-builder-spacing-s: 0.75rem;\n --exp-builder-spacing-2xs: 0.25rem;\n\n /* Typography tokens from Forma 36: https://f36.contentful.com/tokens/typography */\n --exp-builder-font-size-l: 1rem;\n --exp-builder-font-size-m: 0.875rem;\n --exp-builder-font-stack-primary: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n --exp-builder-line-height-condensed: 1.25;\n}\n";
12
16
  styleInject(css_248z$b);
13
17
 
14
- const ROOT_ID = 'root';
15
- var TreeAction;
16
- (function (TreeAction) {
17
- TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
18
- TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
19
- TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
20
- TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
21
- TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
22
- TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
23
- })(TreeAction || (TreeAction = {}));
24
-
25
- function updateNode(nodeId, updatedNode, node) {
26
- if (node.data.id === nodeId) {
27
- node.data = updatedNode.data;
28
- return;
29
- }
30
- node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
31
- }
32
- function replaceNode(indexToReplace, updatedNode, node) {
33
- if (node.data.id === updatedNode.parentId) {
34
- node.children = [
35
- ...node.children.slice(0, indexToReplace),
36
- updatedNode,
37
- ...node.children.slice(indexToReplace + 1),
38
- ];
39
- return;
40
- }
41
- node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
42
- }
43
- function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
44
- if (node.data.id === parentNodeId) {
45
- const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
46
- node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
47
- return;
48
- }
49
- node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
50
- }
51
- function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
52
- if (node.data.id === parentNodeId) {
53
- node.children = [
54
- ...node.children.slice(0, indexToAdd),
55
- nodeToAdd,
56
- ...node.children.slice(indexToAdd),
57
- ];
58
- return;
59
- }
60
- node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
61
- }
62
-
63
- function getItemFromTree(id, node) {
64
- // Check if the current node's id matches the search id
65
- if (node.data.id === id) {
66
- return node;
67
- }
68
- // Recursively search through each child
69
- for (const child of node.children) {
70
- const foundNode = getItemFromTree(id, child);
71
- if (foundNode) {
72
- // Node found in children
73
- return foundNode;
74
- }
75
- }
76
- // If the node is not found in this branch of the tree, return undefined
77
- return undefined;
78
- }
79
- const getItem = (selector, tree) => {
80
- return getItemFromTree(selector.id, {
81
- type: 'block',
82
- data: {
83
- id: ROOT_ID,
84
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
- },
86
- children: tree.root.children,
87
- });
88
- };
89
-
90
- function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
91
- if (nodeAdded) {
92
- return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
93
- }
94
- const item = getItem({ id: child.data.id }, tree);
95
- if (item) {
96
- const parentNode = getItem({ id: item.parentId }, tree);
97
- if (!parentNode) {
98
- return null;
99
- }
100
- const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
101
- return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
102
- }
103
- return {
104
- type: TreeAction.REPLACE_NODE,
105
- originalId: currentNode.children[index].data.id,
106
- indexToReplace: index,
107
- node: child,
108
- };
109
- }
110
- function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
111
- if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
112
- return {
113
- type: TreeAction.REORDER_NODE,
114
- sourceIndex: originalIndex,
115
- destinationIndex: index,
116
- parentNodeId,
117
- };
118
- }
119
- return null;
120
- }
121
- function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
122
- // In the end, this map contains the list of nodes that are not present
123
- // in the updated tree and must be removed
124
- const map = new Map();
125
- if (!currentNode || !updatedNode) {
126
- return differences;
127
- }
128
- // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
129
- const currentNodeCount = currentNode.children.length;
130
- const updatedNodeCount = updatedNode.children.length;
131
- const nodeRemoved = currentNodeCount > updatedNodeCount;
132
- const nodeAdded = currentNodeCount < updatedNodeCount;
133
- const parentNodeId = updatedNode.data.id;
134
- /**
135
- * The data of the current node has changed, we need to update
136
- * this node to reflect the data change. (design, content, unbound values)
137
- */
138
- if (!isEqual(currentNode.data, updatedNode.data)) {
139
- differences.push({
140
- type: TreeAction.UPDATE_NODE,
141
- nodeId: currentNode.data.id,
142
- node: updatedNode,
143
- });
144
- }
145
- // Map children of the first tree by their ID
146
- currentNode.children.forEach((child, index) => map.set(child.data.id, index));
147
- // Compare with the second tree
148
- updatedNode.children.forEach((child, index) => {
149
- const childId = child.data.id;
150
- // The original tree does not have this node in the updated tree.
151
- if (!map.has(childId)) {
152
- const diff = missingNodeAction({
153
- index,
154
- child,
155
- nodeAdded,
156
- parentNodeId,
157
- tree: originalTree,
158
- currentNode,
159
- });
160
- if (diff?.type === TreeAction.REPLACE_NODE) {
161
- // Remove it from the deletion map to avoid adding another REMOVE_NODE action
162
- map.delete(diff.originalId);
163
- }
164
- return differences.push(diff);
165
- }
166
- const originalIndex = map.get(childId);
167
- const diff = matchingNodeAction({
168
- index,
169
- originalIndex,
170
- nodeAdded,
171
- nodeRemoved,
172
- parentNodeId,
173
- });
174
- differences.push(diff);
175
- map.delete(childId);
176
- compareNodes({
177
- currentNode: currentNode.children[originalIndex],
178
- updatedNode: child,
179
- originalTree,
180
- differences,
181
- });
182
- });
183
- map.forEach((index, key) => {
184
- // If the node count of the entire tree doesn't signify
185
- // a node was removed, don't add that as a diff
186
- if (!nodeRemoved) {
187
- return;
188
- }
189
- // Remaining nodes in the map are removed in the second tree
190
- differences.push({
191
- type: TreeAction.REMOVE_NODE,
192
- indexToRemove: index,
193
- parentNodeId,
194
- idToRemove: key,
195
- });
196
- });
197
- return differences;
198
- }
199
- function getTreeDiffs(tree1, tree2, originalTree) {
200
- const differences = [];
201
- compareNodes({
202
- currentNode: tree1,
203
- updatedNode: tree2,
204
- originalTree,
205
- differences,
206
- });
207
- return differences.filter((diff) => diff);
208
- }
209
-
210
- /** @deprecated will be removed when dropping backward compatibility for old DND */
211
- const OUTGOING_EVENTS = {
212
- Connected: 'connected',
213
- DesignTokens: 'registerDesignTokens',
214
- RegisteredBreakpoints: 'registeredBreakpoints',
215
- /** @deprecated will be removed when dropping backward compatibility for old DND */
216
- MouseMove: 'mouseMove',
217
- /** @deprecated will be removed when dropping backward compatibility for old DND */
218
- ComponentSelected: 'componentSelected',
219
- RegisteredComponents: 'registeredComponents',
220
- RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
221
- CanvasReload: 'canvasReload',
222
- /** @deprecated will be removed when dropping backward compatibility for old DND */
223
- UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
224
- /** @deprecated will be removed when dropping backward compatibility for old DND */
225
- CanvasScroll: 'canvasScrolling',
226
- CanvasError: 'canvasError',
227
- /** @deprecated will be removed when dropping backward compatibility for old DND */
228
- OutsideCanvasClick: 'outsideCanvasClick',
229
- SDKFeatures: 'sdkFeatures',
230
- RequestEntities: 'REQUEST_ENTITIES',
231
- };
232
18
  const INCOMING_EVENTS$1 = {
233
19
  RequestEditorMode: 'requestEditorMode',
234
20
  RequestReadOnlyMode: 'requestReadOnlyMode',
235
21
  ExperienceUpdated: 'componentTreeUpdated',
236
- /** @deprecated will be removed when dropping backward compatibility for old DND */
237
22
  ComponentDraggingChanged: 'componentDraggingChanged',
238
- /** @deprecated will be removed when dropping backward compatibility for old DND */
239
23
  ComponentDragCanceled: 'componentDragCanceled',
240
- /** @deprecated will be removed when dropping backward compatibility for old DND */
241
24
  ComponentDragStarted: 'componentDragStarted',
242
- /** @deprecated will be removed when dropping backward compatibility for old DND */
243
25
  ComponentDragEnded: 'componentDragEnded',
244
- /** @deprecated will be removed when dropping backward compatibility for old DND */
245
26
  ComponentMoveEnded: 'componentMoveEnded',
246
- /** @deprecated will be removed when dropping backward compatibility for old DND */
247
27
  CanvasResized: 'canvasResized',
248
- /** @deprecated will be removed when dropping backward compatibility for old DND */
249
28
  SelectComponent: 'selectComponent',
250
- /** @deprecated will be removed when dropping backward compatibility for old DND */
251
29
  HoverComponent: 'hoverComponent',
252
30
  UpdatedEntity: 'updatedEntity',
253
31
  AssembliesAdded: 'assembliesAdded',
254
32
  AssembliesRegistered: 'assembliesRegistered',
255
- /** @deprecated will be removed when dropping backward compatibility for old DND */
256
33
  MouseMove: 'mouseMove',
257
34
  RequestedEntities: 'REQUESTED_ENTITIES',
258
35
  };
259
- const INTERNAL_EVENTS = {
260
- ComponentsRegistered: 'cfComponentsRegistered',
261
- VisualEditorInitialize: 'cfVisualEditorInitialize',
262
- };
263
- const VISUAL_EDITOR_EVENTS = {
264
- Ready: 'cfVisualEditorReady',
265
- };
266
36
  /**
267
37
  * These modes are ONLY intended to be internally used within the context of
268
38
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -274,56 +44,7 @@ var StudioCanvasMode$3;
274
44
  StudioCanvasMode["EDITOR"] = "editorMode";
275
45
  StudioCanvasMode["NONE"] = "none";
276
46
  })(StudioCanvasMode$3 || (StudioCanvasMode$3 = {}));
277
- const ASSEMBLY_NODE_TYPE = 'assembly';
278
- const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
279
- const EMPTY_CONTAINER_HEIGHT$1 = '80px';
280
- const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
281
- var PostMessageMethods$3;
282
- (function (PostMessageMethods) {
283
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
284
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
285
- })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
286
-
287
- /** @deprecated will be removed when dropping backward compatibility for old DND */
288
- const INCOMING_EVENTS = {
289
- RequestEditorMode: 'requestEditorMode',
290
- RequestReadOnlyMode: 'requestReadOnlyMode',
291
- ExperienceUpdated: 'componentTreeUpdated',
292
- /** @deprecated will be removed when dropping backward compatibility for old DND */
293
- ComponentDraggingChanged: 'componentDraggingChanged',
294
- /** @deprecated will be removed when dropping backward compatibility for old DND */
295
- ComponentDragCanceled: 'componentDragCanceled',
296
- /** @deprecated will be removed when dropping backward compatibility for old DND */
297
- ComponentDragStarted: 'componentDragStarted',
298
- /** @deprecated will be removed when dropping backward compatibility for old DND */
299
- ComponentDragEnded: 'componentDragEnded',
300
- /** @deprecated will be removed when dropping backward compatibility for old DND */
301
- ComponentMoveEnded: 'componentMoveEnded',
302
- /** @deprecated will be removed when dropping backward compatibility for old DND */
303
- CanvasResized: 'canvasResized',
304
- /** @deprecated will be removed when dropping backward compatibility for old DND */
305
- SelectComponent: 'selectComponent',
306
- /** @deprecated will be removed when dropping backward compatibility for old DND */
307
- HoverComponent: 'hoverComponent',
308
- UpdatedEntity: 'updatedEntity',
309
- AssembliesAdded: 'assembliesAdded',
310
- AssembliesRegistered: 'assembliesRegistered',
311
- /** @deprecated will be removed when dropping backward compatibility for old DND */
312
- MouseMove: 'mouseMove',
313
- RequestedEntities: 'REQUESTED_ENTITIES',
314
- };
315
- /**
316
- * These modes are ONLY intended to be internally used within the context of
317
- * editing an experience inside of Contentful Studio. i.e. these modes
318
- * intentionally do not include preview/delivery modes.
319
- */
320
- var StudioCanvasMode$2;
321
- (function (StudioCanvasMode) {
322
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
323
- StudioCanvasMode["EDITOR"] = "editorMode";
324
- StudioCanvasMode["NONE"] = "none";
325
- })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
326
- const CONTENTFUL_COMPONENTS$1 = {
47
+ const CONTENTFUL_COMPONENTS$2 = {
327
48
  section: {
328
49
  id: 'contentful-section',
329
50
  name: 'Section',
@@ -369,6 +90,9 @@ const CONTENTFUL_COMPONENTS$1 = {
369
90
  name: 'Carousel',
370
91
  },
371
92
  };
93
+ const ASSEMBLY_NODE_TYPE$1 = 'assembly';
94
+ const ASSEMBLY_DEFAULT_CATEGORY$1 = 'Assemblies';
95
+ const ASSEMBLY_BLOCK_NODE_TYPE$1 = 'assemblyBlock';
372
96
  const CF_STYLE_ATTRIBUTES = [
373
97
  'cfVisibility',
374
98
  'cfHorizontalAlignment',
@@ -411,24 +135,30 @@ const CF_STYLE_ATTRIBUTES = [
411
135
  'cfBackgroundImageAlignmentVertical',
412
136
  'cfBackgroundImageAlignmentHorizontal',
413
137
  ];
414
- const EMPTY_CONTAINER_HEIGHT = '80px';
138
+ const EMPTY_CONTAINER_HEIGHT$1 = '80px';
415
139
  const DEFAULT_IMAGE_WIDTH = '500px';
416
- var PostMessageMethods$2;
140
+ var PostMessageMethods$3;
417
141
  (function (PostMessageMethods) {
418
142
  PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
419
143
  PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
420
- })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
144
+ })(PostMessageMethods$3 || (PostMessageMethods$3 = {}));
421
145
  const SUPPORTED_IMAGE_FORMATS = ['jpg', 'png', 'webp', 'gif', 'avif'];
422
146
 
423
147
  const structureComponentIds = new Set([
424
- CONTENTFUL_COMPONENTS$1.section.id,
425
- CONTENTFUL_COMPONENTS$1.columns.id,
426
- CONTENTFUL_COMPONENTS$1.container.id,
427
- CONTENTFUL_COMPONENTS$1.singleColumn.id,
148
+ CONTENTFUL_COMPONENTS$2.section.id,
149
+ CONTENTFUL_COMPONENTS$2.columns.id,
150
+ CONTENTFUL_COMPONENTS$2.container.id,
151
+ CONTENTFUL_COMPONENTS$2.singleColumn.id,
428
152
  ]);
429
- const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$1).map((component) => component.id));
153
+ const patternTypes = new Set([ASSEMBLY_NODE_TYPE$1, ASSEMBLY_BLOCK_NODE_TYPE$1]);
154
+ const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$2).map((component) => component.id));
155
+ const isPatternComponent = (type) => patternTypes.has(type ?? '');
430
156
  const isContentfulStructureComponent = (componentId) => structureComponentIds.has((componentId ?? ''));
431
157
  const isContentfulComponent = (componentId) => allContentfulComponentIds.has((componentId ?? ''));
158
+ const isComponentAllowedOnRoot = ({ type, category, componentId }) => isPatternComponent(type) ||
159
+ category === ASSEMBLY_DEFAULT_CATEGORY$1 ||
160
+ isContentfulStructureComponent(componentId) ||
161
+ componentId === CONTENTFUL_COMPONENTS$2.divider.id;
432
162
  const isStructureWithRelativeHeight = (componentId, height) => {
433
163
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
434
164
  };
@@ -1351,6 +1081,32 @@ propertyName, resolveDesignTokens = true) => {
1351
1081
  };
1352
1082
 
1353
1083
  const CF_DEBUG_KEY$1 = 'cf_debug';
1084
+ /**
1085
+ * To ensure that the localStorage API can be used safely, we check
1086
+ * for availability (e.g. undefined in Node.js). Additionally, we
1087
+ * check if the localStorage can be used as some browsers throw a
1088
+ * SecurityError (e.g. Brave or Chromium with specific settings).
1089
+ */
1090
+ const checkLocalStorageAvailability$1 = () => {
1091
+ if (typeof localStorage === 'undefined' || localStorage === null) {
1092
+ return false;
1093
+ }
1094
+ try {
1095
+ // Attempt to set and remove an item to check if localStorage is enabled
1096
+ const TEST_KEY = 'cf_test_local_storage';
1097
+ localStorage.setItem(TEST_KEY, 'yes');
1098
+ if (localStorage.getItem(TEST_KEY) === 'yes') {
1099
+ localStorage.removeItem(TEST_KEY);
1100
+ return true;
1101
+ }
1102
+ else {
1103
+ return false;
1104
+ }
1105
+ }
1106
+ catch (_error) {
1107
+ return false;
1108
+ }
1109
+ };
1354
1110
  let DebugLogger$1 = class DebugLogger {
1355
1111
  constructor() {
1356
1112
  // Public methods for logging
@@ -1358,7 +1114,7 @@ let DebugLogger$1 = class DebugLogger {
1358
1114
  this.warn = this.logger('warn');
1359
1115
  this.log = this.logger('log');
1360
1116
  this.debug = this.logger('debug');
1361
- if (typeof localStorage === 'undefined') {
1117
+ if (!checkLocalStorageAvailability$1()) {
1362
1118
  this.enabled = false;
1363
1119
  return;
1364
1120
  }
@@ -1398,6 +1154,51 @@ let DebugLogger$1 = class DebugLogger {
1398
1154
  DebugLogger$1.instance = null;
1399
1155
  DebugLogger$1.getInstance();
1400
1156
 
1157
+ const findOutermostCoordinates = (first, second) => {
1158
+ return {
1159
+ top: Math.min(first.top, second.top),
1160
+ right: Math.max(first.right, second.right),
1161
+ bottom: Math.max(first.bottom, second.bottom),
1162
+ left: Math.min(first.left, second.left),
1163
+ };
1164
+ };
1165
+ const getElementCoordinates = (element) => {
1166
+ const rect = element.getBoundingClientRect();
1167
+ /**
1168
+ * If element does not have children, or element has it's own width or height,
1169
+ * return the element's coordinates.
1170
+ */
1171
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
1172
+ return rect;
1173
+ }
1174
+ const rects = [];
1175
+ /**
1176
+ * If element has children, or element does not have it's own width and height,
1177
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
1178
+ * as the coordinate of the element.
1179
+ *
1180
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
1181
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
1182
+ */
1183
+ for (const child of element.children) {
1184
+ const childRect = getElementCoordinates(child);
1185
+ if (childRect.width !== 0 || childRect.height !== 0) {
1186
+ const { top, right, bottom, left } = childRect;
1187
+ rects.push({ top, right, bottom, left });
1188
+ }
1189
+ }
1190
+ if (rects.length === 0) {
1191
+ return rect;
1192
+ }
1193
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
1194
+ return DOMRect.fromRect({
1195
+ x: left,
1196
+ y: top,
1197
+ height: bottom - top,
1198
+ width: right - left,
1199
+ });
1200
+ };
1201
+
1401
1202
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1402
1203
  const isLinkToAsset = (variable) => {
1403
1204
  if (!variable)
@@ -1809,7 +1610,7 @@ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1809
1610
  if (children.length) {
1810
1611
  return '100%';
1811
1612
  }
1812
- return EMPTY_CONTAINER_HEIGHT;
1613
+ return EMPTY_CONTAINER_HEIGHT$1;
1813
1614
  };
1814
1615
 
1815
1616
  function getOptimizedImageUrl(url, width, quality, format) {
@@ -2240,10 +2041,10 @@ const tryParseMessage = (event) => {
2240
2041
  throw new ParseError(`Field eventData.source must be equal to 'composability-app', instead of '${eventData.source}'`);
2241
2042
  }
2242
2043
  // check eventData.eventType
2243
- const supportedEventTypes = Object.values(INCOMING_EVENTS);
2044
+ const supportedEventTypes = Object.values(INCOMING_EVENTS$1);
2244
2045
  if (!supportedEventTypes.includes(eventData.eventType)) {
2245
2046
  // Expected message: This message is handled in the EntityStore to store fetched entities
2246
- if (eventData.eventType !== PostMessageMethods$2.REQUESTED_ENTITIES) {
2047
+ if (eventData.eventType !== PostMessageMethods$3.REQUESTED_ENTITIES) {
2247
2048
  throw new ParseError(`Field eventData.eventType must be one of the supported values: [${supportedEventTypes.join(', ')}]`);
2248
2049
  }
2249
2050
  }
@@ -2508,7 +2309,7 @@ class EditorEntityStore extends EntityStoreBase {
2508
2309
  }
2509
2310
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
2510
2311
  const newPromise = new Promise((resolve, reject) => {
2511
- const unsubscribe = this.subscribe(PostMessageMethods$2.REQUESTED_ENTITIES, (message) => {
2312
+ const unsubscribe = this.subscribe(PostMessageMethods$3.REQUESTED_ENTITIES, (message) => {
2512
2313
  const messageIds = [
2513
2314
  ...message.entities.map((entity) => entity.sys.id),
2514
2315
  ...(message.missingEntityIds ?? []),
@@ -2530,7 +2331,7 @@ class EditorEntityStore extends EntityStoreBase {
2530
2331
  ids.forEach((id) => this.cleanupPromise(id));
2531
2332
  unsubscribe();
2532
2333
  }, this.timeoutDuration);
2533
- this.sendMessage(PostMessageMethods$2.REQUEST_ENTITIES, {
2334
+ this.sendMessage(PostMessageMethods$3.REQUEST_ENTITIES, {
2534
2335
  entityIds: missing,
2535
2336
  entityType: type,
2536
2337
  locale: this.locale,
@@ -2632,74 +2433,567 @@ class EditorModeEntityStore extends EditorEntityStore {
2632
2433
  const { missing: missingAssetIds } = this.getEntitiesFromMap('Asset', uniqueAssetIds);
2633
2434
  return { missingEntryIds, missingAssetIds };
2634
2435
  }
2635
- getValue(entityLinkOrEntity, path) {
2636
- const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2637
- if (!entity) {
2638
- return;
2436
+ getValue(entityLinkOrEntity, path) {
2437
+ const entity = this.getEntryOrAsset(entityLinkOrEntity, path.join('/'));
2438
+ if (!entity) {
2439
+ return;
2440
+ }
2441
+ const fieldValue = get(entity, path);
2442
+ return transformAssetFileToUrl(fieldValue);
2443
+ }
2444
+ }
2445
+
2446
+ var VisualEditorMode$1;
2447
+ (function (VisualEditorMode) {
2448
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
2449
+ VisualEditorMode["InjectScript"] = "injectScript";
2450
+ })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2451
+
2452
+ class DeepReference {
2453
+ constructor({ path, dataSource }) {
2454
+ const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2455
+ this.originalPath = path;
2456
+ this.entityId = dataSource[key].sys.id;
2457
+ this.entityLink = dataSource[key];
2458
+ this.field = field;
2459
+ this.referentField = referentField;
2460
+ }
2461
+ get headEntityId() {
2462
+ return this.entityId;
2463
+ }
2464
+ /**
2465
+ * Extracts referent from the path, using EntityStore as source of
2466
+ * entities during the resolution path.
2467
+ */
2468
+ extractReferent(entityStore) {
2469
+ const headEntity = entityStore.getEntityFromLink(this.entityLink);
2470
+ const maybeReferentLink = headEntity?.fields[this.field];
2471
+ if (undefined === maybeReferentLink) {
2472
+ // field references nothing (or even field doesn't exist)
2473
+ return undefined;
2474
+ }
2475
+ if (!isLink(maybeReferentLink)) {
2476
+ // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2477
+ // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2478
+ // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2479
+ return undefined;
2480
+ }
2481
+ return maybeReferentLink;
2482
+ }
2483
+ static from(opt) {
2484
+ return new DeepReference(opt);
2485
+ }
2486
+ }
2487
+ function gatherDeepReferencesFromTree(startingNode, dataSource) {
2488
+ const deepReferences = [];
2489
+ treeVisit(startingNode, (node) => {
2490
+ if (!node.data.props)
2491
+ return;
2492
+ for (const [, variableMapping] of Object.entries(node.data.props)) {
2493
+ if (variableMapping.type !== 'BoundValue')
2494
+ continue;
2495
+ if (!isDeepPath(variableMapping.path))
2496
+ continue;
2497
+ deepReferences.push(DeepReference.from({
2498
+ path: variableMapping.path,
2499
+ dataSource,
2500
+ }));
2501
+ }
2502
+ });
2503
+ return deepReferences;
2504
+ }
2505
+
2506
+ const useDraggedItemStore = create((set) => ({
2507
+ draggedItem: undefined,
2508
+ hoveredComponentId: undefined,
2509
+ domRect: undefined,
2510
+ componentId: '',
2511
+ isDraggingOnCanvas: false,
2512
+ onBeforeCaptureId: '',
2513
+ mouseX: 0,
2514
+ mouseY: 0,
2515
+ scrollY: 0,
2516
+ setComponentId(id) {
2517
+ set({ componentId: id });
2518
+ },
2519
+ setHoveredComponentId(id) {
2520
+ set({ hoveredComponentId: id });
2521
+ },
2522
+ updateItem: (item) => {
2523
+ set({ draggedItem: item });
2524
+ },
2525
+ setDraggingOnCanvas: (isDraggingOnCanvas) => {
2526
+ set({ isDraggingOnCanvas });
2527
+ },
2528
+ setOnBeforeCaptureId: (onBeforeCaptureId) => {
2529
+ set({ onBeforeCaptureId });
2530
+ },
2531
+ setMousePosition(x, y) {
2532
+ set({ mouseX: x, mouseY: y });
2533
+ },
2534
+ setDomRect(domRect) {
2535
+ set({ domRect });
2536
+ },
2537
+ setScrollY(y) {
2538
+ set({ scrollY: y });
2539
+ },
2540
+ }));
2541
+
2542
+ const SCROLL_STATES = {
2543
+ Start: 'scrollStart',
2544
+ IsScrolling: 'isScrolling',
2545
+ End: 'scrollEnd',
2546
+ };
2547
+ const OUTGOING_EVENTS = {
2548
+ Connected: 'connected',
2549
+ DesignTokens: 'registerDesignTokens',
2550
+ RegisteredBreakpoints: 'registeredBreakpoints',
2551
+ MouseMove: 'mouseMove',
2552
+ NewHoveredElement: 'newHoveredElement',
2553
+ ComponentSelected: 'componentSelected',
2554
+ RegisteredComponents: 'registeredComponents',
2555
+ RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
2556
+ ComponentDragCanceled: 'componentDragCanceled',
2557
+ ComponentDropped: 'componentDropped',
2558
+ ComponentMoved: 'componentMoved',
2559
+ CanvasReload: 'canvasReload',
2560
+ UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
2561
+ CanvasScroll: 'canvasScrolling',
2562
+ CanvasError: 'canvasError',
2563
+ ComponentMoveStarted: 'componentMoveStarted',
2564
+ ComponentMoveEnded: 'componentMoveEnded',
2565
+ OutsideCanvasClick: 'outsideCanvasClick',
2566
+ SDKFeatures: 'sdkFeatures',
2567
+ RequestEntities: 'REQUEST_ENTITIES',
2568
+ };
2569
+ const INCOMING_EVENTS = {
2570
+ RequestEditorMode: 'requestEditorMode',
2571
+ RequestReadOnlyMode: 'requestReadOnlyMode',
2572
+ ExperienceUpdated: 'componentTreeUpdated',
2573
+ ComponentDraggingChanged: 'componentDraggingChanged',
2574
+ ComponentDragCanceled: 'componentDragCanceled',
2575
+ ComponentDragStarted: 'componentDragStarted',
2576
+ ComponentDragEnded: 'componentDragEnded',
2577
+ ComponentMoveEnded: 'componentMoveEnded',
2578
+ CanvasResized: 'canvasResized',
2579
+ SelectComponent: 'selectComponent',
2580
+ HoverComponent: 'hoverComponent',
2581
+ UpdatedEntity: 'updatedEntity',
2582
+ AssembliesAdded: 'assembliesAdded',
2583
+ AssembliesRegistered: 'assembliesRegistered',
2584
+ MouseMove: 'mouseMove',
2585
+ RequestedEntities: 'REQUESTED_ENTITIES',
2586
+ };
2587
+ const INTERNAL_EVENTS = {
2588
+ ComponentsRegistered: 'cfComponentsRegistered',
2589
+ VisualEditorInitialize: 'cfVisualEditorInitialize',
2590
+ };
2591
+ const VISUAL_EDITOR_EVENTS = {
2592
+ Ready: 'cfVisualEditorReady',
2593
+ };
2594
+ /**
2595
+ * These modes are ONLY intended to be internally used within the context of
2596
+ * editing an experience inside of Contentful Studio. i.e. these modes
2597
+ * intentionally do not include preview/delivery modes.
2598
+ */
2599
+ var StudioCanvasMode$2;
2600
+ (function (StudioCanvasMode) {
2601
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
2602
+ StudioCanvasMode["EDITOR"] = "editorMode";
2603
+ StudioCanvasMode["NONE"] = "none";
2604
+ })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
2605
+ const CONTENTFUL_COMPONENTS$1 = {
2606
+ section: {
2607
+ id: 'contentful-section',
2608
+ name: 'Section',
2609
+ },
2610
+ container: {
2611
+ id: 'contentful-container',
2612
+ name: 'Container',
2613
+ },
2614
+ columns: {
2615
+ id: 'contentful-columns',
2616
+ name: 'Columns',
2617
+ },
2618
+ singleColumn: {
2619
+ id: 'contentful-single-column',
2620
+ name: 'Column',
2621
+ },
2622
+ button: {
2623
+ id: 'contentful-button',
2624
+ name: 'Button',
2625
+ },
2626
+ heading: {
2627
+ id: 'contentful-heading',
2628
+ name: 'Heading',
2629
+ },
2630
+ image: {
2631
+ id: 'contentful-image',
2632
+ name: 'Image',
2633
+ },
2634
+ richText: {
2635
+ id: 'contentful-richText',
2636
+ name: 'Rich Text',
2637
+ },
2638
+ text: {
2639
+ id: 'contentful-text',
2640
+ name: 'Text',
2641
+ },
2642
+ divider: {
2643
+ id: 'contentful-divider',
2644
+ name: 'Divider',
2645
+ },
2646
+ carousel: {
2647
+ id: 'contentful-carousel',
2648
+ name: 'Carousel',
2649
+ },
2650
+ };
2651
+ const ASSEMBLY_NODE_TYPE = 'assembly';
2652
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
2653
+ const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
2654
+ const ASSEMBLY_NODE_TYPES = [ASSEMBLY_NODE_TYPE, ASSEMBLY_BLOCK_NODE_TYPE];
2655
+ const EMPTY_CONTAINER_HEIGHT = '80px';
2656
+ const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
2657
+ var PostMessageMethods$2;
2658
+ (function (PostMessageMethods) {
2659
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2660
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2661
+ })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
2662
+
2663
+ const DRAGGABLE_HEIGHT = 30;
2664
+ const DRAGGABLE_WIDTH = 50;
2665
+ const DRAG_PADDING = 4;
2666
+ const ROOT_ID = 'root';
2667
+ const COMPONENT_LIST_ID = 'component-list';
2668
+ const NEW_COMPONENT_ID = 'ctfl-new-draggable';
2669
+ const CTFL_ZONE_ID = 'data-ctfl-zone-id';
2670
+ const CTFL_DRAGGING_ELEMENT = 'data-ctfl-dragging-element';
2671
+ const HITBOX = {
2672
+ WIDTH: 70,
2673
+ HEIGHT: 20,
2674
+ INITIAL_OFFSET: 10,
2675
+ OFFSET_INCREMENT: 8,
2676
+ MIN_HEIGHT: 45,
2677
+ MIN_DEPTH_HEIGHT: 20,
2678
+ DEEP_ZONE: 5,
2679
+ };
2680
+ var TreeAction;
2681
+ (function (TreeAction) {
2682
+ TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
2683
+ TreeAction[TreeAction["ADD_NODE"] = 1] = "ADD_NODE";
2684
+ TreeAction[TreeAction["MOVE_NODE"] = 2] = "MOVE_NODE";
2685
+ TreeAction[TreeAction["UPDATE_NODE"] = 3] = "UPDATE_NODE";
2686
+ TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
2687
+ TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
2688
+ })(TreeAction || (TreeAction = {}));
2689
+ var HitboxDirection;
2690
+ (function (HitboxDirection) {
2691
+ HitboxDirection[HitboxDirection["TOP"] = 0] = "TOP";
2692
+ HitboxDirection[HitboxDirection["LEFT"] = 1] = "LEFT";
2693
+ HitboxDirection[HitboxDirection["RIGHT"] = 2] = "RIGHT";
2694
+ HitboxDirection[HitboxDirection["BOTTOM"] = 3] = "BOTTOM";
2695
+ HitboxDirection[HitboxDirection["SELF_VERTICAL"] = 4] = "SELF_VERTICAL";
2696
+ HitboxDirection[HitboxDirection["SELF_HORIZONTAL"] = 5] = "SELF_HORIZONTAL";
2697
+ })(HitboxDirection || (HitboxDirection = {}));
2698
+ var DraggablePosition;
2699
+ (function (DraggablePosition) {
2700
+ DraggablePosition[DraggablePosition["CENTERED"] = 0] = "CENTERED";
2701
+ DraggablePosition[DraggablePosition["MOUSE_POSITION"] = 1] = "MOUSE_POSITION";
2702
+ })(DraggablePosition || (DraggablePosition = {}));
2703
+
2704
+ function useDraggablePosition({ draggableId, draggableRef, position }) {
2705
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
2706
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
2707
+ const preDragDomRect = useDraggedItemStore((state) => state.domRect);
2708
+ useEffect(() => {
2709
+ const el = draggableRef?.current ??
2710
+ document.querySelector(`[${CTFL_DRAGGING_ELEMENT}][data-cf-node-id="${draggableId}"]`);
2711
+ if (!isDraggingOnCanvas || draggingId !== draggableId || !el) {
2712
+ return;
2713
+ }
2714
+ const isCentered = position === DraggablePosition.CENTERED || !preDragDomRect;
2715
+ const domRect = isCentered ? el.getBoundingClientRect() : preDragDomRect;
2716
+ const { mouseX, mouseY } = useDraggedItemStore.getState();
2717
+ const top = isCentered ? mouseY - domRect.height / 2 : domRect.top;
2718
+ const left = isCentered ? mouseX - domRect.width / 2 : domRect.left;
2719
+ el.style.position = 'fixed';
2720
+ el.style.left = `${left}px`;
2721
+ el.style.top = `${top}px`;
2722
+ el.style.width = `${domRect.width}px`;
2723
+ el.style.height = `${domRect.height}px`;
2724
+ }, [draggableRef, draggableId, isDraggingOnCanvas, draggingId, position, preDragDomRect]);
2725
+ }
2726
+
2727
+ function getStyle$2(style, snapshot) {
2728
+ if (!snapshot.isDropAnimating) {
2729
+ return style;
2730
+ }
2731
+ return {
2732
+ ...style,
2733
+ // cannot be 0, but make it super tiny
2734
+ transitionDuration: `0.001s`,
2735
+ };
2736
+ }
2737
+ const DraggableContainer = ({ id }) => {
2738
+ const ref = useRef(null);
2739
+ useDraggablePosition({
2740
+ draggableId: id,
2741
+ draggableRef: ref,
2742
+ position: DraggablePosition.CENTERED,
2743
+ });
2744
+ return (React.createElement("div", { id: COMPONENT_LIST_ID, style: {
2745
+ position: 'absolute',
2746
+ top: 0,
2747
+ left: 0,
2748
+ pointerEvents: 'none',
2749
+ zIndex: -1,
2750
+ } },
2751
+ React.createElement(Droppable, { droppableId: COMPONENT_LIST_ID, isDropDisabled: true }, (provided) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef },
2752
+ 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) => {
2753
+ provided.innerRef(node);
2754
+ ref.current = node;
2755
+ }, ...provided.draggableProps, ...provided.dragHandleProps, style: {
2756
+ ...getStyle$2(provided.draggableProps.style, snapshot),
2757
+ width: DRAGGABLE_WIDTH,
2758
+ height: DRAGGABLE_HEIGHT,
2759
+ pointerEvents: 'none',
2760
+ } }))),
2761
+ provided.placeholder)))));
2762
+ };
2763
+
2764
+ function getItemFromTree(id, node) {
2765
+ // Check if the current node's id matches the search id
2766
+ if (node.data.id === id) {
2767
+ return node;
2768
+ }
2769
+ // Recursively search through each child
2770
+ for (const child of node.children) {
2771
+ const foundNode = getItemFromTree(id, child);
2772
+ if (foundNode) {
2773
+ // Node found in children
2774
+ return foundNode;
2775
+ }
2776
+ }
2777
+ // If the node is not found in this branch of the tree, return undefined
2778
+ return undefined;
2779
+ }
2780
+ function findDepthById(node, id, currentDepth = 1) {
2781
+ if (node.data.id === id) {
2782
+ return currentDepth;
2783
+ }
2784
+ // If the node has children, check each one
2785
+ for (const child of node.children) {
2786
+ const childDepth = findDepthById(child, id, currentDepth + 1);
2787
+ if (childDepth !== -1) {
2788
+ return childDepth; // Found the node in a child
2789
+ }
2790
+ }
2791
+ return -1; // Node not found in this branch
2792
+ }
2793
+ const getChildFromTree = (parentId, index, node) => {
2794
+ // Check if the current node's id matches the search id
2795
+ if (node.data.id === parentId) {
2796
+ return node.children[index];
2797
+ }
2798
+ // Recursively search through each child
2799
+ for (const child of node.children) {
2800
+ const foundNode = getChildFromTree(parentId, index, child);
2801
+ if (foundNode) {
2802
+ // Node found in children
2803
+ return foundNode;
2804
+ }
2805
+ }
2806
+ // If the node is not found in this branch of the tree, return undefined
2807
+ return undefined;
2808
+ };
2809
+ const getItem = (selector, tree) => {
2810
+ return getItemFromTree(selector.id, {
2811
+ type: 'block',
2812
+ data: {
2813
+ id: ROOT_ID,
2814
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2815
+ },
2816
+ children: tree.root.children,
2817
+ });
2818
+ };
2819
+ const getItemDepthFromNode = (selector, node) => {
2820
+ return findDepthById(node, selector.id);
2821
+ };
2822
+
2823
+ function updateNode(nodeId, updatedNode, node) {
2824
+ if (node.data.id === nodeId) {
2825
+ node.data = updatedNode.data;
2826
+ return;
2827
+ }
2828
+ node.children.forEach((childNode) => updateNode(nodeId, updatedNode, childNode));
2829
+ }
2830
+ function replaceNode(indexToReplace, updatedNode, node) {
2831
+ if (node.data.id === updatedNode.parentId) {
2832
+ node.children = [
2833
+ ...node.children.slice(0, indexToReplace),
2834
+ updatedNode,
2835
+ ...node.children.slice(indexToReplace + 1),
2836
+ ];
2837
+ return;
2838
+ }
2839
+ node.children.forEach((childNode) => replaceNode(indexToReplace, updatedNode, childNode));
2840
+ }
2841
+ function removeChildNode(indexToRemove, nodeId, parentNodeId, node) {
2842
+ if (node.data.id === parentNodeId) {
2843
+ const childIndex = node.children.findIndex((child) => child.data.id === nodeId);
2844
+ node.children.splice(childIndex === -1 ? indexToRemove : childIndex, 1);
2845
+ return;
2846
+ }
2847
+ node.children.forEach((childNode) => removeChildNode(indexToRemove, nodeId, parentNodeId, childNode));
2848
+ }
2849
+ function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
2850
+ if (node.data.id === parentNodeId) {
2851
+ node.children = [
2852
+ ...node.children.slice(0, indexToAdd),
2853
+ nodeToAdd,
2854
+ ...node.children.slice(indexToAdd),
2855
+ ];
2856
+ return;
2857
+ }
2858
+ node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
2859
+ }
2860
+ function reorderChildNode(oldIndex, newIndex, parentNodeId, node) {
2861
+ if (node.data.id === parentNodeId) {
2862
+ // Remove the child from the old position
2863
+ const [childToMove] = node.children.splice(oldIndex, 1);
2864
+ // Insert the child at the new position
2865
+ node.children.splice(newIndex, 0, childToMove);
2866
+ return;
2867
+ }
2868
+ node.children.forEach((childNode) => reorderChildNode(oldIndex, newIndex, parentNodeId, childNode));
2869
+ }
2870
+ function reparentChildNode(oldIndex, newIndex, sourceNodeId, destinationNodeId, node) {
2871
+ const nodeToMove = getChildFromTree(sourceNodeId, oldIndex, node);
2872
+ if (!nodeToMove) {
2873
+ return;
2874
+ }
2875
+ removeChildNode(oldIndex, nodeToMove.data.id, sourceNodeId, node);
2876
+ addChildNode(newIndex, destinationNodeId, nodeToMove, node);
2877
+ }
2878
+
2879
+ function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
2880
+ if (nodeAdded) {
2881
+ return { type: TreeAction.ADD_NODE, indexToAdd: index, nodeToAdd: child, parentNodeId };
2882
+ }
2883
+ const item = getItem({ id: child.data.id }, tree);
2884
+ if (item) {
2885
+ const parentNode = getItem({ id: item.parentId }, tree);
2886
+ if (!parentNode) {
2887
+ return null;
2639
2888
  }
2640
- const fieldValue = get(entity, path);
2641
- return transformAssetFileToUrl(fieldValue);
2889
+ const sourceIndex = parentNode.children.findIndex((c) => c.data.id === child.data.id);
2890
+ return { type: TreeAction.MOVE_NODE, sourceIndex, destinationIndex: index, parentNodeId };
2642
2891
  }
2892
+ return {
2893
+ type: TreeAction.REPLACE_NODE,
2894
+ originalId: currentNode.children[index].data.id,
2895
+ indexToReplace: index,
2896
+ node: child,
2897
+ };
2643
2898
  }
2644
-
2645
- var VisualEditorMode$1;
2646
- (function (VisualEditorMode) {
2647
- VisualEditorMode["LazyLoad"] = "lazyLoad";
2648
- VisualEditorMode["InjectScript"] = "injectScript";
2649
- })(VisualEditorMode$1 || (VisualEditorMode$1 = {}));
2650
-
2651
- class DeepReference {
2652
- constructor({ path, dataSource }) {
2653
- const { key, field, referentField } = parseDataSourcePathWithL1DeepBindings(path);
2654
- this.originalPath = path;
2655
- this.entityId = dataSource[key].sys.id;
2656
- this.entityLink = dataSource[key];
2657
- this.field = field;
2658
- this.referentField = referentField;
2899
+ function matchingNodeAction({ index, originalIndex, nodeRemoved, nodeAdded, parentNodeId, }) {
2900
+ if (index !== originalIndex && !nodeRemoved && !nodeAdded) {
2901
+ return {
2902
+ type: TreeAction.REORDER_NODE,
2903
+ sourceIndex: originalIndex,
2904
+ destinationIndex: index,
2905
+ parentNodeId,
2906
+ };
2659
2907
  }
2660
- get headEntityId() {
2661
- return this.entityId;
2908
+ return null;
2909
+ }
2910
+ function compareNodes({ currentNode, updatedNode, originalTree, differences = [], }) {
2911
+ // In the end, this map contains the list of nodes that are not present
2912
+ // in the updated tree and must be removed
2913
+ const map = new Map();
2914
+ if (!currentNode || !updatedNode) {
2915
+ return differences;
2662
2916
  }
2917
+ // On each tree level, consider only the children of the current node to differentiate between added, removed, or replaced case
2918
+ const currentNodeCount = currentNode.children.length;
2919
+ const updatedNodeCount = updatedNode.children.length;
2920
+ const nodeRemoved = currentNodeCount > updatedNodeCount;
2921
+ const nodeAdded = currentNodeCount < updatedNodeCount;
2922
+ const parentNodeId = updatedNode.data.id;
2663
2923
  /**
2664
- * Extracts referent from the path, using EntityStore as source of
2665
- * entities during the resolution path.
2924
+ * The data of the current node has changed, we need to update
2925
+ * this node to reflect the data change. (design, content, unbound values)
2666
2926
  */
2667
- extractReferent(entityStore) {
2668
- const headEntity = entityStore.getEntityFromLink(this.entityLink);
2669
- const maybeReferentLink = headEntity?.fields[this.field];
2670
- if (undefined === maybeReferentLink) {
2671
- // field references nothing (or even field doesn't exist)
2672
- return undefined;
2673
- }
2674
- if (!isLink(maybeReferentLink)) {
2675
- // Scenario of "impostor referent", where one of the deepPath's segments is not a Link but some other type
2676
- // Under normal circumstance we expect field to be a Link, but it could be an "impostor"
2677
- // eg. `Text` or `Number` or anything like that; could be due to CT changes or manual path creation via CMA
2678
- return undefined;
2679
- }
2680
- return maybeReferentLink;
2681
- }
2682
- static from(opt) {
2683
- return new DeepReference(opt);
2927
+ if (!isEqual(currentNode.data, updatedNode.data)) {
2928
+ differences.push({
2929
+ type: TreeAction.UPDATE_NODE,
2930
+ nodeId: currentNode.data.id,
2931
+ node: updatedNode,
2932
+ });
2684
2933
  }
2685
- }
2686
- function gatherDeepReferencesFromTree(startingNode, dataSource) {
2687
- const deepReferences = [];
2688
- treeVisit(startingNode, (node) => {
2689
- if (!node.data.props)
2934
+ // Map children of the first tree by their ID
2935
+ currentNode.children.forEach((child, index) => map.set(child.data.id, index));
2936
+ // Compare with the second tree
2937
+ updatedNode.children.forEach((child, index) => {
2938
+ const childId = child.data.id;
2939
+ // The original tree does not have this node in the updated tree.
2940
+ if (!map.has(childId)) {
2941
+ const diff = missingNodeAction({
2942
+ index,
2943
+ child,
2944
+ nodeAdded,
2945
+ parentNodeId,
2946
+ tree: originalTree,
2947
+ currentNode,
2948
+ });
2949
+ if (diff?.type === TreeAction.REPLACE_NODE) {
2950
+ // Remove it from the deletion map to avoid adding another REMOVE_NODE action
2951
+ map.delete(diff.originalId);
2952
+ }
2953
+ return differences.push(diff);
2954
+ }
2955
+ const originalIndex = map.get(childId);
2956
+ const diff = matchingNodeAction({
2957
+ index,
2958
+ originalIndex,
2959
+ nodeAdded,
2960
+ nodeRemoved,
2961
+ parentNodeId,
2962
+ });
2963
+ differences.push(diff);
2964
+ map.delete(childId);
2965
+ compareNodes({
2966
+ currentNode: currentNode.children[originalIndex],
2967
+ updatedNode: child,
2968
+ originalTree,
2969
+ differences,
2970
+ });
2971
+ });
2972
+ map.forEach((index, key) => {
2973
+ // If the node count of the entire tree doesn't signify
2974
+ // a node was removed, don't add that as a diff
2975
+ if (!nodeRemoved) {
2690
2976
  return;
2691
- for (const [, variableMapping] of Object.entries(node.data.props)) {
2692
- if (variableMapping.type !== 'BoundValue')
2693
- continue;
2694
- if (!isDeepPath(variableMapping.path))
2695
- continue;
2696
- deepReferences.push(DeepReference.from({
2697
- path: variableMapping.path,
2698
- dataSource,
2699
- }));
2700
2977
  }
2978
+ // Remaining nodes in the map are removed in the second tree
2979
+ differences.push({
2980
+ type: TreeAction.REMOVE_NODE,
2981
+ indexToRemove: index,
2982
+ parentNodeId,
2983
+ idToRemove: key,
2984
+ });
2701
2985
  });
2702
- return deepReferences;
2986
+ return differences;
2987
+ }
2988
+ function getTreeDiffs(tree1, tree2, originalTree) {
2989
+ const differences = [];
2990
+ compareNodes({
2991
+ currentNode: tree1,
2992
+ updatedNode: tree2,
2993
+ originalTree,
2994
+ differences,
2995
+ });
2996
+ return differences.filter((diff) => diff);
2703
2997
  }
2704
2998
 
2705
2999
  const useTreeStore = create((set, get) => ({
@@ -2733,6 +3027,20 @@ const useTreeStore = create((set, get) => ({
2733
3027
  });
2734
3028
  }));
2735
3029
  },
3030
+ /**
3031
+ * NOTE: this is for debugging purposes only as it causes ugly canvas flash.
3032
+ *
3033
+ * Force updates entire tree. Usually shouldn't be used as updateTree()
3034
+ * uses smart update algorithm based on diffs. But for troubleshooting
3035
+ * you may want to force update the tree so leaving this in.
3036
+ */
3037
+ updateTreeForced: (tree) => {
3038
+ set({
3039
+ tree,
3040
+ // Breakpoints must be updated, as we receive completely new tree with possibly new breakpoints
3041
+ breakpoints: tree?.root?.data?.breakpoints || [],
3042
+ });
3043
+ },
2736
3044
  updateTree: (tree) => {
2737
3045
  const currentTree = get().tree;
2738
3046
  /**
@@ -2778,6 +3086,21 @@ const useTreeStore = create((set, get) => ({
2778
3086
  state.breakpoints = tree?.root?.data?.breakpoints || [];
2779
3087
  }));
2780
3088
  },
3089
+ addChild: (index, parentId, node) => {
3090
+ set(produce((state) => {
3091
+ addChildNode(index, parentId, node, state.tree.root);
3092
+ }));
3093
+ },
3094
+ reorderChildren: (destinationIndex, destinationParentId, sourceIndex) => {
3095
+ set(produce((state) => {
3096
+ reorderChildNode(sourceIndex, destinationIndex, destinationParentId, state.tree.root);
3097
+ }));
3098
+ },
3099
+ reparentChild: (destinationIndex, destinationParentId, sourceIndex, sourceParentId) => {
3100
+ set(produce((state) => {
3101
+ reparentChildNode(sourceIndex, destinationIndex, sourceParentId, destinationParentId, state.tree.root);
3102
+ }));
3103
+ },
2781
3104
  }));
2782
3105
  const hasBreakpointDiffs = (currentTree, newTree) => {
2783
3106
  const currentBreakpoints = currentTree?.root?.data?.breakpoints ?? [];
@@ -2795,8 +3118,8 @@ const cloneDeepAsPOJO = (obj) => {
2795
3118
  return JSON.parse(JSON.stringify(obj));
2796
3119
  };
2797
3120
 
2798
- 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";
2799
- var styles$2 = {"rootContainer":"RootRenderer-module_rootContainer__9UawM"};
3121
+ 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";
3122
+ var styles$3 = {"hitbox":"render-module_hitbox__l4ysJ","hitboxLower":"render-module_hitboxLower__tgsA1","canvasBottomSpacer":"render-module_canvasBottomSpacer__JuxVh","container":"render-module_container__-C3d7"};
2800
3123
  styleInject(css_248z$a);
2801
3124
 
2802
3125
  // TODO: In order to support integrations without React, we should extract this heavy logic into simple
@@ -2835,6 +3158,66 @@ const useBreakpoints = (breakpoints) => {
2835
3158
  return { resolveDesignValue };
2836
3159
  };
2837
3160
 
3161
+ /**
3162
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
3163
+ * and sends the DOM Rect to the client app
3164
+ */
3165
+ const sendSelectedComponentCoordinates = (instanceId) => {
3166
+ const selection = getSelectionNodes(instanceId);
3167
+ if (selection?.target) {
3168
+ const sendUpdateSelectedComponentCoordinates = () => {
3169
+ sendMessage(OUTGOING_EVENTS.UpdateSelectedComponentCoordinates, {
3170
+ selectedNodeCoordinates: getElementCoordinates(selection.target),
3171
+ selectedAssemblyChildCoordinates: selection.patternChild
3172
+ ? getElementCoordinates(selection.patternChild)
3173
+ : undefined,
3174
+ parentCoordinates: selection.parent ? getElementCoordinates(selection.parent) : undefined,
3175
+ });
3176
+ };
3177
+ // If the target contains an image, wait for this image to be loaded before sending the coordinates
3178
+ const childImage = selection.target.querySelector('img');
3179
+ if (childImage) {
3180
+ const handleImageLoad = () => {
3181
+ sendUpdateSelectedComponentCoordinates();
3182
+ childImage.removeEventListener('load', handleImageLoad);
3183
+ };
3184
+ childImage.addEventListener('load', handleImageLoad);
3185
+ }
3186
+ sendUpdateSelectedComponentCoordinates();
3187
+ }
3188
+ };
3189
+ const getSelectionNodes = (instanceId) => {
3190
+ if (!instanceId)
3191
+ return;
3192
+ let selectedNode = document.querySelector(`[data-cf-node-id="${instanceId}"]`);
3193
+ let selectedPatternChild = null;
3194
+ let selectedParent = null;
3195
+ // Use RegEx instead of split to match the last occurrence of '---' in the instanceId instead of the first one
3196
+ const idMatch = instanceId.match(/(.*)---(.*)/);
3197
+ const rootNodeId = idMatch?.[1] ?? instanceId;
3198
+ const nodeLocation = idMatch?.[2];
3199
+ const isNestedPattern = nodeLocation && selectedNode?.dataset?.cfNodeBlockType === ASSEMBLY_NODE_TYPE;
3200
+ const isPatternChild = !isNestedPattern && nodeLocation;
3201
+ if (isPatternChild) {
3202
+ // For pattern child nodes, render the pattern itself as selected component
3203
+ selectedPatternChild = selectedNode;
3204
+ selectedNode = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3205
+ }
3206
+ else if (isNestedPattern) {
3207
+ // For nested patterns, return the upper pattern as parent
3208
+ selectedParent = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3209
+ }
3210
+ else {
3211
+ // Find the next valid parent of the selected element
3212
+ selectedParent = selectedNode?.parentElement ?? null;
3213
+ // Ensure that the selection parent is a VisualEditorBlock
3214
+ while (selectedParent && !selectedParent.dataset?.cfNodeId) {
3215
+ selectedParent = selectedParent?.parentElement;
3216
+ }
3217
+ }
3218
+ return { target: selectedNode, patternChild: selectedPatternChild, parent: selectedParent };
3219
+ };
3220
+
2838
3221
  // Note: During development, the hot reloading might empty this and it
2839
3222
  // stays empty leading to not rendering assemblies. Ideally, this is
2840
3223
  // integrated into the state machine to keep track of its state.
@@ -2868,10 +3251,14 @@ const useEditorStore = create((set, get) => ({
2868
3251
  dataSource: {},
2869
3252
  hyperLinkPattern: undefined,
2870
3253
  unboundValues: {},
3254
+ selectedNodeId: null,
2871
3255
  locale: null,
2872
3256
  setHyperLinkPattern: (pattern) => {
2873
3257
  set({ hyperLinkPattern: pattern });
2874
3258
  },
3259
+ setSelectedNodeId: (id) => {
3260
+ set({ selectedNodeId: id });
3261
+ },
2875
3262
  setDataSource(data) {
2876
3263
  const dataSource = get().dataSource;
2877
3264
  const newDataSource = { ...dataSource, ...data };
@@ -2903,7 +3290,6 @@ const useEditorStore = create((set, get) => ({
2903
3290
  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";
2904
3291
  styleInject(css_248z$8);
2905
3292
 
2906
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2907
3293
  /**
2908
3294
  * These modes are ONLY intended to be internally used within the context of
2909
3295
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -3403,6 +3789,32 @@ var CodeNames;
3403
3789
  })(CodeNames || (CodeNames = {}));
3404
3790
 
3405
3791
  const CF_DEBUG_KEY = 'cf_debug';
3792
+ /**
3793
+ * To ensure that the localStorage API can be used safely, we check
3794
+ * for availability (e.g. undefined in Node.js). Additionally, we
3795
+ * check if the localStorage can be used as some browsers throw a
3796
+ * SecurityError (e.g. Brave or Chromium with specific settings).
3797
+ */
3798
+ const checkLocalStorageAvailability = () => {
3799
+ if (typeof localStorage === 'undefined' || localStorage === null) {
3800
+ return false;
3801
+ }
3802
+ try {
3803
+ // Attempt to set and remove an item to check if localStorage is enabled
3804
+ const TEST_KEY = 'cf_test_local_storage';
3805
+ localStorage.setItem(TEST_KEY, 'yes');
3806
+ if (localStorage.getItem(TEST_KEY) === 'yes') {
3807
+ localStorage.removeItem(TEST_KEY);
3808
+ return true;
3809
+ }
3810
+ else {
3811
+ return false;
3812
+ }
3813
+ }
3814
+ catch (_error) {
3815
+ return false;
3816
+ }
3817
+ };
3406
3818
  class DebugLogger {
3407
3819
  constructor() {
3408
3820
  // Public methods for logging
@@ -3410,7 +3822,7 @@ class DebugLogger {
3410
3822
  this.warn = this.logger('warn');
3411
3823
  this.log = this.logger('log');
3412
3824
  this.debug = this.logger('debug');
3413
- if (typeof localStorage === 'undefined') {
3825
+ if (!checkLocalStorageAvailability()) {
3414
3826
  this.enabled = false;
3415
3827
  return;
3416
3828
  }
@@ -3456,8 +3868,8 @@ var VisualEditorMode;
3456
3868
  VisualEditorMode["InjectScript"] = "injectScript";
3457
3869
  })(VisualEditorMode || (VisualEditorMode = {}));
3458
3870
 
3459
- 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";
3460
- styleInject(css_248z$2);
3871
+ 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";
3872
+ styleInject(css_248z$2$1);
3461
3873
 
3462
3874
  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) => {
3463
3875
  return (React.createElement("div", { id: id, ref: ref, style: {
@@ -3481,14 +3893,34 @@ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave,
3481
3893
  });
3482
3894
  Flex.displayName = 'Flex';
3483
3895
 
3484
- 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";
3896
+ 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";
3485
3897
  styleInject(css_248z$1$1);
3486
3898
 
3487
- 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";
3899
+ 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";
3488
3900
  styleInject(css_248z$9);
3489
3901
 
3902
+ const ColumnWrapper = forwardRef((props, ref) => {
3903
+ return (React.createElement("div", { ref: ref, ...props, style: {
3904
+ ...(props.style || {}),
3905
+ display: 'grid',
3906
+ gridTemplateColumns: 'repeat(12, [col-start] 1fr)',
3907
+ } }, props.children));
3908
+ });
3909
+ ColumnWrapper.displayName = 'ColumnWrapper';
3910
+
3490
3911
  const assemblyStyle = { display: 'contents' };
3912
+ // Feel free to do any magic as regards variable definitions for assemblies
3913
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
3491
3914
  const Assembly = (props) => {
3915
+ if (props.editorMode) {
3916
+ const { node, dragProps, ...editorModeProps } = props;
3917
+ return props.renderDropzone(node, {
3918
+ ...editorModeProps,
3919
+ ['data-test-id']: 'contentful-assembly',
3920
+ className: props.className,
3921
+ dragProps,
3922
+ });
3923
+ }
3492
3924
  // Using a display contents so assembly content/children
3493
3925
  // can appear as if they are direct children of the div wrapper's parent
3494
3926
  return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
@@ -3511,6 +3943,75 @@ const useEntityStore = create((set) => ({
3511
3943
  },
3512
3944
  }));
3513
3945
 
3946
+ class DragState {
3947
+ constructor() {
3948
+ this.isDragStartedOnParent = false;
3949
+ this.isDraggingItem = false;
3950
+ }
3951
+ get isDragging() {
3952
+ return this.isDraggingItem;
3953
+ }
3954
+ get isDraggingOnParent() {
3955
+ return this.isDragStartedOnParent;
3956
+ }
3957
+ updateIsDragging(isDraggingItem) {
3958
+ this.isDraggingItem = isDraggingItem;
3959
+ }
3960
+ updateIsDragStartedOnParent(isDragStartedOnParent) {
3961
+ this.isDragStartedOnParent = isDragStartedOnParent;
3962
+ }
3963
+ resetState() {
3964
+ this.isDraggingItem = false;
3965
+ this.isDragStartedOnParent = false;
3966
+ }
3967
+ }
3968
+
3969
+ class SimulateDnD extends DragState {
3970
+ constructor() {
3971
+ super();
3972
+ this.draggingElement = null;
3973
+ }
3974
+ setupDrag() {
3975
+ this.updateIsDragStartedOnParent(true);
3976
+ }
3977
+ startDrag(coordX, coordY) {
3978
+ this.draggingElement = document.getElementById(NEW_COMPONENT_ID);
3979
+ this.updateIsDragging(true);
3980
+ this.simulateMouseEvent(coordX, coordY, 'mousedown');
3981
+ }
3982
+ updateDrag(coordX, coordY) {
3983
+ if (!this.draggingElement) {
3984
+ this.draggingElement = document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`);
3985
+ }
3986
+ this.simulateMouseEvent(coordX, coordY);
3987
+ }
3988
+ endDrag(coordX, coordY) {
3989
+ this.simulateMouseEvent(coordX, coordY, 'mouseup');
3990
+ this.reset();
3991
+ }
3992
+ reset() {
3993
+ this.draggingElement = null;
3994
+ this.resetState();
3995
+ }
3996
+ simulateMouseEvent(coordX, coordY, eventName = 'mousemove') {
3997
+ if (!this.draggingElement) {
3998
+ return;
3999
+ }
4000
+ const options = {
4001
+ bubbles: true,
4002
+ cancelable: true,
4003
+ view: window,
4004
+ pageX: 0,
4005
+ pageY: 0,
4006
+ clientX: coordX,
4007
+ clientY: coordY,
4008
+ };
4009
+ const event = new MouseEvent(eventName, options);
4010
+ this.draggingElement.dispatchEvent(event);
4011
+ }
4012
+ }
4013
+ var SimulateDnD$1 = new SimulateDnD();
4014
+
3514
4015
  function useEditorSubscriber() {
3515
4016
  const entityStore = useEntityStore((state) => state.entityStore);
3516
4017
  const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
@@ -3524,7 +4025,14 @@ function useEditorSubscriber() {
3524
4025
  const setLocale = useEditorStore((state) => state.setLocale);
3525
4026
  const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
3526
4027
  const setDataSource = useEditorStore((state) => state.setDataSource);
4028
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4029
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3527
4030
  const resetEntityStore = useEntityStore((state) => state.resetEntityStore);
4031
+ const setComponentId = useDraggedItemStore((state) => state.setComponentId);
4032
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4033
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4034
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
4035
+ const setScrollY = useDraggedItemStore((state) => state.setScrollY);
3528
4036
  const reloadApp = () => {
3529
4037
  sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
3530
4038
  // Wait a moment to ensure that the message was sent
@@ -3625,12 +4133,12 @@ function useEditorSubscriber() {
3625
4133
  }
3626
4134
  const eventData = tryParseMessage(event);
3627
4135
  console.debug(`[experiences-sdk-react::onMessage] Received message [${eventData.eventType}]`, eventData);
3628
- if (eventData.eventType === PostMessageMethods$3.REQUESTED_ENTITIES) {
4136
+ if (eventData.eventType === PostMessageMethods$2.REQUESTED_ENTITIES) {
3629
4137
  // Expected message: This message is handled in the EntityStore to store fetched entities
3630
4138
  return;
3631
4139
  }
3632
4140
  switch (eventData.eventType) {
3633
- case INCOMING_EVENTS$1.ExperienceUpdated: {
4141
+ case INCOMING_EVENTS.ExperienceUpdated: {
3634
4142
  const { tree, locale, changedNode, changedValueType, assemblies } = eventData.payload;
3635
4143
  // Make sure to first store the assemblies before setting the tree and thus triggering a rerender
3636
4144
  if (assemblies) {
@@ -3674,7 +4182,7 @@ function useEditorSubscriber() {
3674
4182
  updateTree(tree);
3675
4183
  break;
3676
4184
  }
3677
- case INCOMING_EVENTS$1.AssembliesRegistered: {
4185
+ case INCOMING_EVENTS.AssembliesRegistered: {
3678
4186
  const { assemblies } = eventData.payload;
3679
4187
  assemblies.forEach((definition) => {
3680
4188
  addComponentRegistration({
@@ -3684,7 +4192,7 @@ function useEditorSubscriber() {
3684
4192
  });
3685
4193
  break;
3686
4194
  }
3687
- case INCOMING_EVENTS$1.AssembliesAdded: {
4195
+ case INCOMING_EVENTS.AssembliesAdded: {
3688
4196
  const { assembly, assemblyDefinition, } = eventData.payload;
3689
4197
  entityStore.updateEntity(assembly);
3690
4198
  // Using a Map here to avoid setting state and rerending all existing assemblies when a new assembly is added
@@ -3701,7 +4209,28 @@ function useEditorSubscriber() {
3701
4209
  }
3702
4210
  break;
3703
4211
  }
3704
- case INCOMING_EVENTS$1.UpdatedEntity: {
4212
+ case INCOMING_EVENTS.CanvasResized: {
4213
+ const { selectedNodeId } = eventData.payload;
4214
+ if (selectedNodeId) {
4215
+ sendSelectedComponentCoordinates(selectedNodeId);
4216
+ }
4217
+ break;
4218
+ }
4219
+ case INCOMING_EVENTS.HoverComponent: {
4220
+ const { hoveredNodeId } = eventData.payload;
4221
+ setHoveredComponentId(hoveredNodeId);
4222
+ break;
4223
+ }
4224
+ case INCOMING_EVENTS.ComponentDraggingChanged: {
4225
+ const { isDragging } = eventData.payload;
4226
+ if (!isDragging) {
4227
+ setComponentId('');
4228
+ setDraggingOnCanvas(false);
4229
+ SimulateDnD$1.reset();
4230
+ }
4231
+ break;
4232
+ }
4233
+ case INCOMING_EVENTS.UpdatedEntity: {
3705
4234
  const { entity: updatedEntity, shouldRerender } = eventData.payload;
3706
4235
  if (updatedEntity) {
3707
4236
  const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
@@ -3714,7 +4243,52 @@ function useEditorSubscriber() {
3714
4243
  }
3715
4244
  break;
3716
4245
  }
3717
- case INCOMING_EVENTS$1.RequestEditorMode: {
4246
+ case INCOMING_EVENTS.RequestEditorMode: {
4247
+ break;
4248
+ }
4249
+ case INCOMING_EVENTS.ComponentDragCanceled: {
4250
+ if (SimulateDnD$1.isDragging) {
4251
+ //simulate a mouseup event to cancel the drag
4252
+ SimulateDnD$1.endDrag(0, 0);
4253
+ }
4254
+ break;
4255
+ }
4256
+ case INCOMING_EVENTS.ComponentDragStarted: {
4257
+ const { id, isAssembly } = eventData.payload;
4258
+ SimulateDnD$1.setupDrag();
4259
+ setComponentId(`${id}:${isAssembly}` || '');
4260
+ setDraggingOnCanvas(true);
4261
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4262
+ nodeId: '',
4263
+ });
4264
+ break;
4265
+ }
4266
+ case INCOMING_EVENTS.ComponentDragEnded: {
4267
+ SimulateDnD$1.reset();
4268
+ setComponentId('');
4269
+ setDraggingOnCanvas(false);
4270
+ break;
4271
+ }
4272
+ case INCOMING_EVENTS.SelectComponent: {
4273
+ const { selectedNodeId: nodeId } = eventData.payload;
4274
+ setSelectedNodeId(nodeId);
4275
+ sendSelectedComponentCoordinates(nodeId);
4276
+ break;
4277
+ }
4278
+ case INCOMING_EVENTS.MouseMove: {
4279
+ const { mouseX, mouseY } = eventData.payload;
4280
+ setMousePosition(mouseX, mouseY);
4281
+ if (SimulateDnD$1.isDraggingOnParent && !SimulateDnD$1.isDragging) {
4282
+ SimulateDnD$1.startDrag(mouseX, mouseY);
4283
+ }
4284
+ else {
4285
+ SimulateDnD$1.updateDrag(mouseX, mouseY);
4286
+ }
4287
+ break;
4288
+ }
4289
+ case INCOMING_EVENTS.ComponentMoveEnded: {
4290
+ const { mouseX, mouseY } = eventData.payload;
4291
+ SimulateDnD$1.endDrag(mouseX, mouseY);
3718
4292
  break;
3719
4293
  }
3720
4294
  default:
@@ -3727,8 +4301,11 @@ function useEditorSubscriber() {
3727
4301
  };
3728
4302
  }, [
3729
4303
  entityStore,
4304
+ setComponentId,
4305
+ setDraggingOnCanvas,
3730
4306
  setDataSource,
3731
4307
  setLocale,
4308
+ setSelectedNodeId,
3732
4309
  dataSource,
3733
4310
  areEntitiesFetched,
3734
4311
  fetchMissingEntities,
@@ -3736,92 +4313,328 @@ function useEditorSubscriber() {
3736
4313
  unboundValues,
3737
4314
  updateTree,
3738
4315
  updateNodesByUpdatedEntity,
4316
+ setMousePosition,
3739
4317
  resetEntityStore,
4318
+ setHoveredComponentId,
3740
4319
  ]);
4320
+ /*
4321
+ * Handles on scroll business
4322
+ */
4323
+ useEffect(() => {
4324
+ let timeoutId = 0;
4325
+ let isScrolling = false;
4326
+ const onScroll = () => {
4327
+ setScrollY(window.scrollY);
4328
+ if (isScrolling === false) {
4329
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.Start);
4330
+ }
4331
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.IsScrolling);
4332
+ isScrolling = true;
4333
+ clearTimeout(timeoutId);
4334
+ timeoutId = window.setTimeout(() => {
4335
+ if (isScrolling === false) {
4336
+ return;
4337
+ }
4338
+ isScrolling = false;
4339
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.End);
4340
+ /**
4341
+ * On scroll end, send new co-ordinates of selected node
4342
+ */
4343
+ if (selectedNodeId) {
4344
+ sendSelectedComponentCoordinates(selectedNodeId);
4345
+ }
4346
+ }, 150);
4347
+ };
4348
+ window.addEventListener('scroll', onScroll, { capture: true, passive: true });
4349
+ return () => {
4350
+ window.removeEventListener('scroll', onScroll, { capture: true });
4351
+ clearTimeout(timeoutId);
4352
+ };
4353
+ }, [selectedNodeId, setScrollY]);
3741
4354
  }
3742
4355
 
3743
- const CircularDependencyErrorPlaceholder = ({ node, wrappingPatternIds, }) => {
3744
- const entityStore = useEntityStore((state) => state.entityStore);
3745
- 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: {
3746
- border: '1px solid red',
3747
- background: 'rgba(255, 0, 0, 0.1)',
3748
- padding: '1rem 1rem 0 1rem',
3749
- width: '100%',
3750
- height: '100%',
3751
- } },
3752
- "Circular usage of patterns detected:",
3753
- React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
3754
- const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
3755
- const entry = entityStore.getEntityFromLink(entryLink);
3756
- const entryTitle = entry?.fields?.title;
3757
- const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
3758
- return React.createElement("li", { key: patternId }, text);
3759
- }))));
4356
+ const onComponentMoved = (options) => {
4357
+ sendMessage(OUTGOING_EVENTS.ComponentMoved, options);
3760
4358
  };
3761
4359
 
3762
- class ImportedComponentError extends Error {
3763
- constructor(message) {
3764
- super(message);
3765
- this.name = 'ImportedComponentError';
3766
- }
3767
- }
3768
- class ExperienceSDKError extends Error {
3769
- constructor(message) {
3770
- super(message);
3771
- this.name = 'ExperienceSDKError';
3772
- }
3773
- }
3774
- class ImportedComponentErrorBoundary extends React.Component {
3775
- componentDidCatch(error, _errorInfo) {
3776
- if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
3777
- // This error was already handled by a nested error boundary and should be passed upwards
3778
- // We have to do this as we wrap every component on every layer with this error boundary and
3779
- // thus an error deep in the tree bubbles through many layers of error boundaries.
3780
- throw error;
3781
- }
3782
- // Differentiate between custom and SDK-provided components for error tracking
3783
- const ErrorClass = isContentfulComponent(this.props.componentId)
3784
- ? ExperienceSDKError
3785
- : ImportedComponentError;
3786
- const err = new ErrorClass(error.message);
3787
- err.stack = error.stack;
3788
- throw err;
3789
- }
3790
- render() {
3791
- return this.props.children;
3792
- }
3793
- }
4360
+ const generateId = (type) => `${type}-${v4()}`;
3794
4361
 
3795
- const MissingComponentPlaceholder = ({ blockId }) => {
3796
- return (React.createElement("div", { style: {
3797
- border: '1px solid red',
3798
- width: '100%',
3799
- height: '100%',
3800
- } },
3801
- "Missing component '",
3802
- blockId,
3803
- "'"));
4362
+ const createTreeNode = ({ blockId, parentId, slotId }) => {
4363
+ const node = {
4364
+ type: 'block',
4365
+ data: {
4366
+ id: generateId(blockId),
4367
+ blockId,
4368
+ slotId,
4369
+ props: {},
4370
+ dataSource: {},
4371
+ breakpoints: [],
4372
+ unboundValues: {},
4373
+ },
4374
+ parentId,
4375
+ children: [],
4376
+ };
4377
+ return node;
3804
4378
  };
3805
4379
 
3806
- var css_248z$1 = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
3807
- var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
3808
- styleInject(css_248z$1);
4380
+ const onComponentDropped = ({ node, index, parentBlockId, parentType, parentId, }) => {
4381
+ sendMessage(OUTGOING_EVENTS.ComponentDropped, {
4382
+ node,
4383
+ index: index ?? node.children.length,
4384
+ parentNode: {
4385
+ type: parentType,
4386
+ data: {
4387
+ blockId: parentBlockId,
4388
+ id: parentId,
4389
+ },
4390
+ },
4391
+ });
4392
+ };
3809
4393
 
3810
- const useComponentRegistration = (node) => {
3811
- return useMemo(() => {
3812
- let registration = componentRegistry.get(node.data.blockId);
3813
- if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
3814
- registration = createAssemblyRegistration({
3815
- definitionId: node.data.blockId,
3816
- component: Assembly,
4394
+ const onDrop = ({ destinationIndex, componentType, destinationZoneId, data, slotId, }) => {
4395
+ const parentId = destinationZoneId;
4396
+ const parentNode = getItem({ id: parentId }, data);
4397
+ const parentIsRoot = parentId === ROOT_ID;
4398
+ const emptyComponentData = {
4399
+ type: 'block',
4400
+ parentId,
4401
+ children: [],
4402
+ data: {
4403
+ blockId: componentType,
4404
+ id: generateId(componentType),
4405
+ slotId,
4406
+ breakpoints: [],
4407
+ dataSource: {},
4408
+ props: {},
4409
+ unboundValues: {},
4410
+ },
4411
+ };
4412
+ onComponentDropped({
4413
+ node: emptyComponentData,
4414
+ index: destinationIndex,
4415
+ parentType: parentIsRoot ? 'root' : parentNode?.type,
4416
+ parentBlockId: parentNode?.data.blockId,
4417
+ parentId: parentIsRoot ? 'root' : parentId,
4418
+ });
4419
+ };
4420
+
4421
+ /**
4422
+ * Parses a droppable zone ID into a node ID and slot ID.
4423
+ *
4424
+ * The slot ID is optional and only present if the component implements multiple drop zones.
4425
+ *
4426
+ * @param zoneId - Expected formats are `nodeId` or `nodeId|slotId`.
4427
+ */
4428
+ const parseZoneId = (zoneId) => {
4429
+ const [nodeId, slotId] = zoneId.includes('|') ? zoneId.split('|') : [zoneId, undefined];
4430
+ return { nodeId, slotId };
4431
+ };
4432
+
4433
+ function useCanvasInteractions() {
4434
+ const tree = useTreeStore((state) => state.tree);
4435
+ const reorderChildren = useTreeStore((state) => state.reorderChildren);
4436
+ const reparentChild = useTreeStore((state) => state.reparentChild);
4437
+ const addChild = useTreeStore((state) => state.addChild);
4438
+ const onAddComponent = (droppedItem) => {
4439
+ const { destination, draggableId } = droppedItem;
4440
+ if (!destination) {
4441
+ return;
4442
+ }
4443
+ /**
4444
+ * We only have the draggableId as information about the new component being dropped.
4445
+ * So we need to split it to get the blockId and the isAssembly flag.
4446
+ */
4447
+ const [blockId, isAssembly] = draggableId.split(':');
4448
+ const { nodeId: parentId, slotId } = parseZoneId(destination.droppableId);
4449
+ const droppingOnRoot = parentId === ROOT_ID;
4450
+ const isValidRootComponent = blockId === CONTENTFUL_COMPONENTS$1.container.id;
4451
+ let node = createTreeNode({ blockId, parentId, slotId });
4452
+ if (droppingOnRoot && !isValidRootComponent) {
4453
+ const wrappingContainer = createTreeNode({
4454
+ blockId: CONTENTFUL_COMPONENTS$1.container.id,
4455
+ parentId,
3817
4456
  });
4457
+ const childNode = createTreeNode({
4458
+ blockId,
4459
+ parentId: wrappingContainer.data.id,
4460
+ });
4461
+ node = wrappingContainer;
4462
+ node.children = [childNode];
3818
4463
  }
3819
- if (!registration) {
3820
- 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.`);
3821
- return undefined;
4464
+ /**
4465
+ * isAssembly comes from a string ID so we need to check if it's 'true' or 'false'
4466
+ * in string format.
4467
+ */
4468
+ if (isAssembly === 'false') {
4469
+ addChild(destination.index, parentId, node);
3822
4470
  }
3823
- return registration;
3824
- }, [node]);
4471
+ onDrop({
4472
+ data: tree,
4473
+ componentType: blockId,
4474
+ destinationIndex: destination.index,
4475
+ destinationZoneId: parentId,
4476
+ slotId,
4477
+ });
4478
+ };
4479
+ const onMoveComponent = (droppedItem) => {
4480
+ const { destination, source, draggableId } = droppedItem;
4481
+ if (!destination || !source) {
4482
+ return;
4483
+ }
4484
+ if (destination.droppableId === source.droppableId) {
4485
+ reorderChildren(destination.index, destination.droppableId, source.index);
4486
+ }
4487
+ if (destination.droppableId !== source.droppableId) {
4488
+ reparentChild(destination.index, destination.droppableId, source.index, source.droppableId);
4489
+ }
4490
+ onComponentMoved({
4491
+ nodeId: draggableId,
4492
+ destinationIndex: destination.index,
4493
+ destinationParentId: destination.droppableId,
4494
+ sourceIndex: source.index,
4495
+ sourceParentId: source.droppableId,
4496
+ });
4497
+ };
4498
+ return { onAddComponent, onMoveComponent };
4499
+ }
4500
+
4501
+ const TestDNDContainer = ({ onDragEnd, onBeforeDragStart, onDragStart, onDragUpdate, children, }) => {
4502
+ const handleDragStart = (event) => {
4503
+ const draggedItem = event.nativeEvent;
4504
+ const start = {
4505
+ mode: draggedItem.mode,
4506
+ draggableId: draggedItem.draggableId,
4507
+ type: draggedItem.type,
4508
+ source: draggedItem.source,
4509
+ };
4510
+ onBeforeDragStart(start);
4511
+ onDragStart(start, {});
4512
+ };
4513
+ const handleDrag = (event) => {
4514
+ const draggedItem = event.nativeEvent;
4515
+ const update = {
4516
+ mode: draggedItem.mode,
4517
+ draggableId: draggedItem.draggableId,
4518
+ type: draggedItem.type,
4519
+ source: draggedItem.source,
4520
+ destination: draggedItem.destination,
4521
+ combine: draggedItem.combine,
4522
+ };
4523
+ onDragUpdate(update, {});
4524
+ };
4525
+ const handleDragEnd = (event) => {
4526
+ const draggedItem = event.nativeEvent;
4527
+ const result = {
4528
+ mode: draggedItem.mode,
4529
+ draggableId: draggedItem.draggableId,
4530
+ type: draggedItem.type,
4531
+ source: draggedItem.source,
4532
+ destination: draggedItem.destination,
4533
+ combine: draggedItem.combine,
4534
+ reason: draggedItem.reason,
4535
+ };
4536
+ onDragEnd(result, {});
4537
+ };
4538
+ return (React.createElement("div", { "data-test-id": "dnd-context-substitute", onDragStart: handleDragStart, onDrag: handleDrag, onDragEnd: handleDragEnd }, children));
4539
+ };
4540
+
4541
+ const DNDProvider = ({ children }) => {
4542
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4543
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
4544
+ const setOnBeforeCaptureId = useDraggedItemStore((state) => state.setOnBeforeCaptureId);
4545
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4546
+ const updateItem = useDraggedItemStore((state) => state.updateItem);
4547
+ const { onAddComponent, onMoveComponent } = useCanvasInteractions();
4548
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4549
+ const prevSelectedNodeId = useRef(null);
4550
+ const isTestRun = typeof window !== 'undefined' && Object.prototype.hasOwnProperty.call(window, 'Cypress');
4551
+ const beforeDragStart = ({ source }) => {
4552
+ prevSelectedNodeId.current = selectedNodeId;
4553
+ // Unselect the current node when dragging and remove the outline
4554
+ setSelectedNodeId('');
4555
+ // Set dragging state here to make sure that DnD capture phase has completed
4556
+ setDraggingOnCanvas(true);
4557
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4558
+ nodeId: '',
4559
+ });
4560
+ if (source.droppableId !== COMPONENT_LIST_ID) {
4561
+ sendMessage(OUTGOING_EVENTS.ComponentMoveStarted, undefined);
4562
+ }
4563
+ };
4564
+ const beforeCapture = ({ draggableId }) => {
4565
+ setOnBeforeCaptureId(draggableId);
4566
+ };
4567
+ const dragStart = (start) => {
4568
+ updateItem(start);
4569
+ };
4570
+ const dragUpdate = (update) => {
4571
+ updateItem(update);
4572
+ };
4573
+ const dragEnd = (dropResult) => {
4574
+ setDraggingOnCanvas(false);
4575
+ setOnBeforeCaptureId('');
4576
+ updateItem();
4577
+ SimulateDnD$1.reset();
4578
+ // If the component is being dropped onto itself, do nothing
4579
+ // This can happen from an apparent race condition where the hovering zone gets set
4580
+ // to the component after its dropped.
4581
+ if (dropResult.destination?.droppableId === dropResult.draggableId) {
4582
+ return;
4583
+ }
4584
+ if (!dropResult.destination) {
4585
+ if (!draggedItem?.destination) {
4586
+ // User cancel drag
4587
+ sendMessage(OUTGOING_EVENTS.ComponentDragCanceled, undefined);
4588
+ //select the previously selected node if drag was canceled
4589
+ if (prevSelectedNodeId.current) {
4590
+ setSelectedNodeId(prevSelectedNodeId.current);
4591
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4592
+ nodeId: prevSelectedNodeId.current,
4593
+ });
4594
+ prevSelectedNodeId.current = null;
4595
+ }
4596
+ return;
4597
+ }
4598
+ // Use the destination from the draggedItem (when clicking the canvas)
4599
+ dropResult.destination = draggedItem.destination;
4600
+ }
4601
+ // New component added to canvas
4602
+ if (dropResult.source.droppableId.startsWith('component-list')) {
4603
+ onAddComponent(dropResult);
4604
+ }
4605
+ else {
4606
+ onMoveComponent(dropResult);
4607
+ }
4608
+ // If a node was previously selected prior to dragging, re-select it
4609
+ setSelectedNodeId(dropResult.draggableId);
4610
+ sendMessage(OUTGOING_EVENTS.ComponentMoveEnded, undefined);
4611
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4612
+ nodeId: dropResult.draggableId,
4613
+ });
4614
+ };
4615
+ 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)));
4616
+ };
4617
+
4618
+ /**
4619
+ * This hook gets the element co-ordinates of a specified element in the DOM
4620
+ * and sends the DOM Rect to the client app
4621
+ */
4622
+ const useSelectedInstanceCoordinates = ({ node }) => {
4623
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4624
+ useEffect(() => {
4625
+ if (selectedNodeId !== node.data.id) {
4626
+ return;
4627
+ }
4628
+ // Allows the drop animation to finish before
4629
+ // calculating the components coordinates
4630
+ setTimeout(() => {
4631
+ sendSelectedComponentCoordinates(node.data.id);
4632
+ }, 10);
4633
+ }, [node, selectedNodeId]);
4634
+ const selectedElement = node.data.id
4635
+ ? document.querySelector(`[data-cf-node-id="${selectedNodeId}"]`)
4636
+ : undefined;
4637
+ return selectedElement ? getElementCoordinates(selectedElement) : null;
3825
4638
  };
3826
4639
 
3827
4640
  /**
@@ -3869,13 +4682,15 @@ const getUnboundValues = ({ key, fallback, unboundValues, }) => {
3869
4682
  return get$1(unboundValues, lodashPath, fallback);
3870
4683
  };
3871
4684
 
3872
- const useComponentProps = ({ node, resolveDesignValue, definition, options, }) => {
4685
+ const useComponentProps = ({ node, areEntitiesFetched, resolveDesignValue, renderDropzone, definition, options, userIsDragging, requiresDragWrapper, }) => {
3873
4686
  const unboundValues = useEditorStore((state) => state.unboundValues);
3874
4687
  const hyperlinkPattern = useEditorStore((state) => state.hyperLinkPattern);
3875
4688
  const locale = useEditorStore((state) => state.locale);
3876
4689
  const dataSource = useEditorStore((state) => state.dataSource);
3877
4690
  const entityStore = useEntityStore((state) => state.entityStore);
3878
- const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
4691
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
4692
+ const nodeRect = useDraggedItemStore((state) => state.domRect);
4693
+ const isEmptyZone = !node.children.length;
3879
4694
  const props = useMemo(() => {
3880
4695
  const propsBase = {
3881
4696
  cfSsrClassName: node.data.props.cfSsrClassName
@@ -3965,126 +4780,1133 @@ const useComponentProps = ({ node, resolveDesignValue, definition, options, }) =
3965
4780
  return { ...acc };
3966
4781
  }
3967
4782
  }, {});
4783
+ const slotProps = {};
4784
+ if (definition.slots) {
4785
+ for (const slotId in definition.slots) {
4786
+ slotProps[slotId] = renderDropzone(node, {
4787
+ zoneId: [node.data.id, slotId].join('|'),
4788
+ });
4789
+ }
4790
+ }
3968
4791
  return {
3969
4792
  ...propsBase,
3970
4793
  ...extractedProps,
4794
+ ...slotProps,
3971
4795
  };
3972
4796
  }, [
3973
4797
  hyperlinkPattern,
3974
4798
  node,
3975
- locale,
3976
- definition,
3977
- resolveDesignValue,
3978
- dataSource,
4799
+ locale,
4800
+ definition,
4801
+ resolveDesignValue,
4802
+ dataSource,
4803
+ areEntitiesFetched,
4804
+ unboundValues,
4805
+ entityStore,
4806
+ renderDropzone,
4807
+ ]);
4808
+ const cfStyles = useMemo(() => buildCfStyles(props), [props]);
4809
+ const cfVisibility = props['cfVisibility'];
4810
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
4811
+ const isSingleColumn = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
4812
+ const isStructureComponent = isContentfulStructureComponent(node?.data.blockId);
4813
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
4814
+ const { overrideStyles, wrapperStyles } = useMemo(() => {
4815
+ // Move size styles to the wrapping div and override the component styles
4816
+ const overrideStyles = {};
4817
+ const wrapperStyles = { width: options?.wrapContainerWidth };
4818
+ if (requiresDragWrapper) {
4819
+ // when element is marked by user as not-visible, on that element the node `display: none !important`
4820
+ // will be set and it will disappear. However, when such a node has a wrapper div, the wrapper
4821
+ // should not have any css properties (at least not ones which force size), as such div should
4822
+ // simply be a zero height wrapper around element with `display: none !important`.
4823
+ // Hence we guard all wrapperStyles with `cfVisibility` check.
4824
+ if (cfVisibility && cfStyles.width)
4825
+ wrapperStyles.width = cfStyles.width;
4826
+ if (cfVisibility && cfStyles.height)
4827
+ wrapperStyles.height = cfStyles.height;
4828
+ if (cfVisibility && cfStyles.maxWidth)
4829
+ wrapperStyles.maxWidth = cfStyles.maxWidth;
4830
+ if (cfVisibility && cfStyles.margin)
4831
+ wrapperStyles.margin = cfStyles.margin;
4832
+ }
4833
+ // Override component styles to fill the wrapper
4834
+ if (wrapperStyles.width)
4835
+ overrideStyles.width = '100%';
4836
+ if (wrapperStyles.height)
4837
+ overrideStyles.height = '100%';
4838
+ if (wrapperStyles.margin)
4839
+ overrideStyles.margin = '0';
4840
+ if (wrapperStyles.maxWidth)
4841
+ overrideStyles.maxWidth = 'none';
4842
+ // Prevent the dragging element from changing sizes when it has a percentage width or height
4843
+ if (draggingId === node.data.id && nodeRect) {
4844
+ if (requiresDragWrapper) {
4845
+ if (isPercentValue(cfStyles.width))
4846
+ wrapperStyles.maxWidth = nodeRect.width;
4847
+ if (isPercentValue(cfStyles.height))
4848
+ wrapperStyles.maxHeight = nodeRect.height;
4849
+ }
4850
+ else {
4851
+ if (isPercentValue(cfStyles.width))
4852
+ overrideStyles.maxWidth = nodeRect.width;
4853
+ if (isPercentValue(cfStyles.height))
4854
+ overrideStyles.maxHeight = nodeRect.height;
4855
+ }
4856
+ }
4857
+ return { overrideStyles, wrapperStyles };
4858
+ }, [
4859
+ cfStyles,
4860
+ options?.wrapContainerWidth,
4861
+ requiresDragWrapper,
4862
+ node.data.id,
4863
+ draggingId,
4864
+ nodeRect,
4865
+ cfVisibility,
4866
+ ]);
4867
+ // Styles that will be applied to the component element
4868
+ // This has to be memoized to avoid recreating the styles in useEditorModeClassName on every render
4869
+ const componentStyles = useMemo(() => ({
4870
+ ...cfStyles,
4871
+ ...overrideStyles,
4872
+ ...(isEmptyZone &&
4873
+ isStructureWithRelativeHeight(node?.data.blockId, cfStyles.height) && {
4874
+ minHeight: EMPTY_CONTAINER_HEIGHT,
4875
+ }),
4876
+ ...(userIsDragging &&
4877
+ isStructureComponent &&
4878
+ !isSingleColumn &&
4879
+ !isAssemblyBlock && {
4880
+ padding: addExtraDropzonePadding(cfStyles.padding?.toString() || '0 0 0 0'),
4881
+ }),
4882
+ }), [
4883
+ cfStyles,
4884
+ isAssemblyBlock,
4885
+ isEmptyZone,
4886
+ isSingleColumn,
4887
+ isStructureComponent,
4888
+ node?.data.blockId,
4889
+ overrideStyles,
4890
+ userIsDragging,
4891
+ ]);
4892
+ const componentClass = useEditorModeClassName({
4893
+ styles: componentStyles,
4894
+ nodeId: node.data.id,
4895
+ });
4896
+ const sharedProps = {
4897
+ 'data-cf-node-id': node.data.id,
4898
+ 'data-cf-node-block-id': node.data.blockId,
4899
+ 'data-cf-node-block-type': node.type,
4900
+ className: props.cfSsrClassName ?? componentClass,
4901
+ ...(definition?.children ? { children: renderDropzone(node) } : {}),
4902
+ };
4903
+ const customComponentProps = {
4904
+ ...sharedProps,
4905
+ // Allows custom components to render differently in the editor. This needs to be activated
4906
+ // through options as the component has to be aware of this prop to not cause any React warnings.
4907
+ ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4908
+ ...sanitizeNodeProps(props),
4909
+ };
4910
+ const structuralOrPatternComponentProps = {
4911
+ ...sharedProps,
4912
+ editorMode: true,
4913
+ node,
4914
+ renderDropzone,
4915
+ };
4916
+ return {
4917
+ componentProps: isStructureComponent || isPatternNode
4918
+ ? structuralOrPatternComponentProps
4919
+ : customComponentProps,
4920
+ componentStyles,
4921
+ wrapperStyles,
4922
+ };
4923
+ };
4924
+ const addExtraDropzonePadding = (padding) => padding
4925
+ .split(' ')
4926
+ .map((value) => parseFloat(value) === 0 ? `${DRAG_PADDING}px` : `calc(${value} + ${DRAG_PADDING}px)`)
4927
+ .join(' ');
4928
+ const isPercentValue = (value) => typeof value === 'string' && value.endsWith('%');
4929
+
4930
+ class ImportedComponentError extends Error {
4931
+ constructor(message) {
4932
+ super(message);
4933
+ this.name = 'ImportedComponentError';
4934
+ }
4935
+ }
4936
+ class ExperienceSDKError extends Error {
4937
+ constructor(message) {
4938
+ super(message);
4939
+ this.name = 'ExperienceSDKError';
4940
+ }
4941
+ }
4942
+ class ImportedComponentErrorBoundary extends React.Component {
4943
+ componentDidCatch(error, _errorInfo) {
4944
+ if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
4945
+ // This error was already handled by a nested error boundary and should be passed upwards
4946
+ // We have to do this as we wrap every component on every layer with this error boundary and
4947
+ // thus an error deep in the tree bubbles through many layers of error boundaries.
4948
+ throw error;
4949
+ }
4950
+ // Differentiate between custom and SDK-provided components for error tracking
4951
+ const ErrorClass = isContentfulComponent(this.props.componentId)
4952
+ ? ExperienceSDKError
4953
+ : ImportedComponentError;
4954
+ const err = new ErrorClass(error.message);
4955
+ err.stack = error.stack;
4956
+ throw err;
4957
+ }
4958
+ render() {
4959
+ return this.props.children;
4960
+ }
4961
+ }
4962
+
4963
+ const MissingComponentPlaceholder = ({ blockId }) => {
4964
+ return (React.createElement("div", { style: {
4965
+ border: '1px solid red',
4966
+ width: '100%',
4967
+ height: '100%',
4968
+ } },
4969
+ "Missing component '",
4970
+ blockId,
4971
+ "'"));
4972
+ };
4973
+
4974
+ const CircularDependencyErrorPlaceholder = forwardRef(({ wrappingPatternIds, ...props }, ref) => {
4975
+ const entityStore = useEntityStore((state) => state.entityStore);
4976
+ return (React.createElement("div", { ...props,
4977
+ // Pass through ref to avoid DND errors being logged
4978
+ ref: ref, "data-cf-node-error": "circular-pattern-dependency", style: {
4979
+ border: '1px solid red',
4980
+ background: 'rgba(255, 0, 0, 0.1)',
4981
+ padding: '1rem 1rem 0 1rem',
4982
+ width: '100%',
4983
+ height: '100%',
4984
+ } },
4985
+ "Circular usage of patterns detected:",
4986
+ React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
4987
+ const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
4988
+ const entry = entityStore.getEntityFromLink(entryLink);
4989
+ const entryTitle = entry?.fields?.title;
4990
+ const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
4991
+ return React.createElement("li", { key: patternId }, text);
4992
+ }))));
4993
+ });
4994
+ CircularDependencyErrorPlaceholder.displayName = 'CircularDependencyErrorPlaceholder';
4995
+
4996
+ const useComponent = ({ node, resolveDesignValue, renderDropzone, userIsDragging, wrappingPatternIds, }) => {
4997
+ const areEntitiesFetched = useEntityStore((state) => state.areEntitiesFetched);
4998
+ const tree = useTreeStore((state) => state.tree);
4999
+ const componentRegistration = useMemo(() => {
5000
+ let registration = componentRegistry.get(node.data.blockId);
5001
+ if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
5002
+ registration = createAssemblyRegistration({
5003
+ definitionId: node.data.blockId,
5004
+ component: Assembly,
5005
+ });
5006
+ }
5007
+ if (!registration) {
5008
+ 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.`);
5009
+ return undefined;
5010
+ }
5011
+ return registration;
5012
+ }, [node]);
5013
+ const componentId = node.data.id;
5014
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
5015
+ const isPatternComponent = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5016
+ const parentComponentNode = getItem({ id: node.parentId }, tree);
5017
+ const isNestedPattern = isPatternNode &&
5018
+ [ASSEMBLY_BLOCK_NODE_TYPE, ASSEMBLY_NODE_TYPE].includes(parentComponentNode?.type ?? '');
5019
+ const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
5020
+ const requiresDragWrapper = !isPatternNode && !isStructureComponent && !componentRegistration?.options?.wrapComponent;
5021
+ const { componentProps, wrapperStyles } = useComponentProps({
5022
+ node,
3979
5023
  areEntitiesFetched,
3980
- unboundValues,
3981
- entityStore,
3982
- ]);
3983
- const cfStyles = useMemo(() => buildCfStyles(props), [props]);
3984
- // Styles that will be applied to the component element
3985
- const componentStyles = useMemo(() => ({
3986
- ...cfStyles,
3987
- ...(!node.children.length &&
3988
- isStructureWithRelativeHeight(node.data.blockId, cfStyles.height) && {
3989
- minHeight: EMPTY_CONTAINER_HEIGHT$1,
3990
- }),
3991
- }), [cfStyles, node.children.length, node.data.blockId]);
3992
- const cfCsrClassName = useEditorModeClassName({
3993
- styles: componentStyles,
3994
- nodeId: node.data.id,
5024
+ resolveDesignValue,
5025
+ renderDropzone,
5026
+ definition: componentRegistration?.definition,
5027
+ options: componentRegistration?.options,
5028
+ userIsDragging,
5029
+ requiresDragWrapper,
3995
5030
  });
3996
- const componentProps = useMemo(() => {
3997
- const sharedProps = {
3998
- 'data-cf-node-id': node.data.id,
3999
- 'data-cf-node-block-id': node.data.blockId,
4000
- 'data-cf-node-block-type': node.type,
4001
- className: props.cfSsrClassName ?? cfCsrClassName,
5031
+ const elementToRender = (props) => {
5032
+ const { dragProps = {} } = props || {};
5033
+ const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps;
5034
+ const { 'data-cf-node-block-id': dataCfNodeBlockId, 'data-cf-node-block-type': dataCfNodeBlockType, 'data-cf-node-id': dataCfNodeId, } = componentProps;
5035
+ const refCallback = (refNode) => {
5036
+ if (innerRef && refNode)
5037
+ innerRef(refNode);
4002
5038
  };
4003
- // Only pass `editorMode` and `node` to structure components and assembly root nodes.
4004
- const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
4005
- if (isStructureComponent) {
4006
- return {
4007
- ...sharedProps,
4008
- editorMode: true,
4009
- node,
4010
- };
5039
+ if (!componentRegistration) {
5040
+ return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
5041
+ }
5042
+ if (node.data.blockId && wrappingPatternIds.has(node.data.blockId)) {
5043
+ 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 }));
4011
5044
  }
5045
+ const element = React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, {
5046
+ ...componentProps,
5047
+ dragProps,
5048
+ }));
5049
+ if (!requiresDragWrapper) {
5050
+ return element;
5051
+ }
5052
+ 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 },
5053
+ ToolTipAndPlaceholder,
5054
+ element));
5055
+ };
5056
+ return {
5057
+ node,
5058
+ parentComponentNode,
5059
+ isAssembly: isPatternNode,
5060
+ isPatternNode,
5061
+ isPatternComponent,
5062
+ isNestedPattern,
5063
+ componentId,
5064
+ elementToRender,
5065
+ definition: componentRegistration?.definition,
5066
+ };
5067
+ };
5068
+
5069
+ const calcOffsetLeft = (parentElement, placeholderWidth, nodeWidth) => {
5070
+ if (!parentElement) {
5071
+ return 0;
5072
+ }
5073
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5074
+ if (alignItems === 'center') {
5075
+ return -(placeholderWidth - nodeWidth) / 2;
5076
+ }
5077
+ if (alignItems === 'end') {
5078
+ return -placeholderWidth + nodeWidth + 2;
5079
+ }
5080
+ return 0;
5081
+ };
5082
+ const calcOffsetTop = (parentElement, placeholderHeight, nodeHeight) => {
5083
+ if (!parentElement) {
5084
+ return 0;
5085
+ }
5086
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5087
+ if (alignItems === 'center') {
5088
+ return -(placeholderHeight - nodeHeight) / 2;
5089
+ }
5090
+ if (alignItems === 'end') {
5091
+ return -placeholderHeight + nodeHeight + 2;
5092
+ }
5093
+ return 0;
5094
+ };
5095
+ const getPaddingOffset = (element) => {
5096
+ const paddingLeft = parseFloat(window.getComputedStyle(element).paddingLeft);
5097
+ const paddingRight = parseFloat(window.getComputedStyle(element).paddingRight);
5098
+ const paddingTop = parseFloat(window.getComputedStyle(element).paddingTop);
5099
+ const paddingBottom = parseFloat(window.getComputedStyle(element).paddingBottom);
5100
+ const horizontalOffset = paddingLeft + paddingRight;
5101
+ const verticalOffset = paddingTop + paddingBottom;
5102
+ return [horizontalOffset, verticalOffset];
5103
+ };
5104
+ /**
5105
+ * Calculate the size and position of the dropzone indicator
5106
+ * when dragging a new component onto the canvas
5107
+ */
5108
+ const calcNewComponentStyles = (params) => {
5109
+ const { destinationIndex, elementIndex, dropzoneElementId, id, direction, totalIndexes } = params;
5110
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5111
+ const isHorizontal = direction === 'horizontal';
5112
+ const isRightAlign = isHorizontal && isEnd;
5113
+ const isBottomAlign = !isHorizontal && isEnd;
5114
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5115
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5116
+ if (!dropzone || !element) {
5117
+ return emptyStyles;
5118
+ }
5119
+ const elementSizes = element.getBoundingClientRect();
5120
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5121
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5122
+ const width = isHorizontal ? DRAGGABLE_WIDTH : dropzoneSizes.width - horizontalPadding;
5123
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : DRAGGABLE_HEIGHT;
5124
+ const top = isHorizontal
5125
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5126
+ : -height;
5127
+ const left = isHorizontal
5128
+ ? -width
5129
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5130
+ return {
5131
+ width,
5132
+ height,
5133
+ top: !isBottomAlign ? top : 'unset',
5134
+ right: isRightAlign ? -width : 'unset',
5135
+ bottom: isBottomAlign ? -height : 'unset',
5136
+ left: !isRightAlign ? left : 'unset',
5137
+ };
5138
+ };
5139
+ /**
5140
+ * Calculate the size and position of the dropzone indicator
5141
+ * when moving an existing component on the canvas
5142
+ */
5143
+ const calcMovementStyles = (params) => {
5144
+ const { destinationIndex, sourceIndex, destinationId, sourceId, elementIndex, dropzoneElementId, id, direction, totalIndexes, draggableId, } = params;
5145
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5146
+ const isHorizontal = direction === 'horizontal';
5147
+ const isSameZone = destinationId === sourceId;
5148
+ const isBelowSourceIndex = destinationIndex > sourceIndex;
5149
+ const isRightAlign = isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5150
+ const isBottomAlign = !isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5151
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5152
+ const draggable = document.querySelector(`[data-rfd-draggable-id="${draggableId}"]`);
5153
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5154
+ if (!dropzone || !element || !draggable) {
5155
+ return emptyStyles;
5156
+ }
5157
+ const elementSizes = element.getBoundingClientRect();
5158
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5159
+ const draggableSizes = draggable.getBoundingClientRect();
5160
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5161
+ const width = isHorizontal ? draggableSizes.width : dropzoneSizes.width - horizontalPadding;
5162
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : draggableSizes.height;
5163
+ const top = isHorizontal
5164
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5165
+ : -height;
5166
+ const left = isHorizontal
5167
+ ? -width
5168
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5169
+ return {
5170
+ width,
5171
+ height,
5172
+ top: !isBottomAlign ? top : 'unset',
5173
+ right: isRightAlign ? -width : 'unset',
5174
+ bottom: isBottomAlign ? -height : 'unset',
5175
+ left: !isRightAlign ? left : 'unset',
5176
+ };
5177
+ };
5178
+ const emptyStyles = { width: 0, height: 0 };
5179
+ const calcPlaceholderStyles = (params) => {
5180
+ const { isDraggingOver, sourceId } = params;
5181
+ if (!isDraggingOver) {
5182
+ return emptyStyles;
5183
+ }
5184
+ if (sourceId === COMPONENT_LIST_ID) {
5185
+ return calcNewComponentStyles(params);
5186
+ }
5187
+ return calcMovementStyles(params);
5188
+ };
5189
+ const Placeholder = (props) => {
5190
+ const sourceIndex = useDraggedItemStore((state) => state.draggedItem?.source.index) ?? -1;
5191
+ const draggableId = useDraggedItemStore((state) => state.draggedItem?.draggableId) ?? '';
5192
+ const sourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId) ?? '';
5193
+ const destinationIndex = useDraggedItemStore((state) => state.draggedItem?.destination?.index) ?? -1;
5194
+ const destinationId = useDraggedItemStore((state) => state.draggedItem?.destination?.droppableId) ?? '';
5195
+ const { elementIndex, totalIndexes, isDraggingOver } = props;
5196
+ const isActive = destinationIndex === elementIndex;
5197
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5198
+ const isVisible = isEnd || isActive;
5199
+ const isComponentList = destinationId === COMPONENT_LIST_ID;
5200
+ return (!isComponentList &&
5201
+ isDraggingOver &&
5202
+ isVisible && (React.createElement("div", { style: {
5203
+ ...calcPlaceholderStyles({
5204
+ ...props,
5205
+ sourceId,
5206
+ sourceIndex,
5207
+ destinationId,
5208
+ destinationIndex,
5209
+ draggableId,
5210
+ }),
5211
+ backgroundColor: 'rgba(var(--exp-builder-blue300-rgb), 0.5)',
5212
+ position: 'absolute',
5213
+ pointerEvents: 'none',
5214
+ } })));
5215
+ };
5216
+
5217
+ var css_248z$2 = ".styles-module_hitbox__i3wKV {\n position: fixed;\n pointer-events: all;\n}\n";
5218
+ var styles$2 = {"hitbox":"styles-module_hitbox__i3wKV"};
5219
+ styleInject(css_248z$2);
5220
+
5221
+ const useZoneStore = create()((set) => ({
5222
+ zones: {},
5223
+ hoveringZone: '',
5224
+ setHoveringZone(zoneId) {
5225
+ set({
5226
+ hoveringZone: zoneId,
5227
+ });
5228
+ },
5229
+ upsertZone(id, data) {
5230
+ set(produce((state) => {
5231
+ state.zones[id] = { ...(state.zones[id] || {}), ...data };
5232
+ }));
5233
+ },
5234
+ }));
5235
+
5236
+ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_HEIGHT, DEEP_ZONE } = HITBOX;
5237
+ const calcOffsetDepth = (depth) => {
5238
+ return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
5239
+ };
5240
+ const getHitboxStyles = ({ direction, zoneDepth, domRect, scrollY, offsetRect, }) => {
5241
+ if (!domRect) {
4012
5242
  return {
4013
- ...sharedProps,
4014
- // Allows custom components to render differently in the editor. This needs to be activated
4015
- // through options as the component has to be aware of this prop to not cause any React warnings.
4016
- ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4017
- ...sanitizeNodeProps(props),
5243
+ display: 'none',
4018
5244
  };
4019
- }, [cfCsrClassName, node, options?.enableCustomEditorView, props]);
4020
- return { componentProps };
5245
+ }
5246
+ const { width, height, top, left, bottom, right } = domRect;
5247
+ const { height: offsetHeight, width: offsetWidth } = offsetRect || { height: 0, width: 0 };
5248
+ const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
5249
+ const isDeepZone = zoneDepth > DEEP_ZONE;
5250
+ const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
5251
+ switch (direction) {
5252
+ case HitboxDirection.TOP:
5253
+ return {
5254
+ width,
5255
+ height: HEIGHT,
5256
+ top: top + offsetHeight - calcOffsetDepth(zoneDepth) - scrollY,
5257
+ left,
5258
+ zIndex: 100 + zoneDepth,
5259
+ };
5260
+ case HitboxDirection.BOTTOM:
5261
+ return {
5262
+ width,
5263
+ height: HEIGHT,
5264
+ top: bottom + offsetHeight + calcOffsetDepth(zoneDepth) - scrollY,
5265
+ left,
5266
+ zIndex: 100 + zoneDepth,
5267
+ };
5268
+ case HitboxDirection.LEFT:
5269
+ return {
5270
+ width: WIDTH,
5271
+ height: height - HEIGHT,
5272
+ left: left + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
5273
+ top: top + HEIGHT / 2 - scrollY,
5274
+ zIndex: 100 + zoneDepth,
5275
+ };
5276
+ case HitboxDirection.RIGHT:
5277
+ return {
5278
+ width: WIDTH,
5279
+ height: height - HEIGHT,
5280
+ left: right + offsetWidth + calcOffsetDepth(zoneDepth) - WIDTH / 2,
5281
+ top: top + HEIGHT / 2 - scrollY,
5282
+ zIndex: 100 + zoneDepth,
5283
+ };
5284
+ case HitboxDirection.SELF_VERTICAL: {
5285
+ if (isAboveMaxHeight && !isDeepZone) {
5286
+ return { display: 'none' };
5287
+ }
5288
+ const selfHeight = isDeepZone ? MIN_DEPTH_HEIGHT : MIN_HEIGHT;
5289
+ return {
5290
+ width,
5291
+ height: selfHeight,
5292
+ left,
5293
+ top: top + height / 2 - selfHeight / 2 - scrollY,
5294
+ zIndex: 1000 + zoneDepth,
5295
+ };
5296
+ }
5297
+ case HitboxDirection.SELF_HORIZONTAL: {
5298
+ if (width > DRAGGABLE_WIDTH) {
5299
+ return { display: 'none' };
5300
+ }
5301
+ return {
5302
+ width: width - DRAGGABLE_WIDTH * 2,
5303
+ height,
5304
+ left: left + DRAGGABLE_WIDTH,
5305
+ top: top - scrollY,
5306
+ zIndex: 1000 + zoneDepth,
5307
+ };
5308
+ }
5309
+ default:
5310
+ return {};
5311
+ }
4021
5312
  };
4022
5313
 
4023
- function EditorBlock({ node, resolveDesignValue, wrappingPatternIds: parentWrappingPatternIds = new Set(), }) {
4024
- const isRootAssemblyNode = node.type === ASSEMBLY_NODE_TYPE;
4025
- const wrappingPatternIds = useMemo(() => {
4026
- if (isRootAssemblyNode && node.data.blockId) {
4027
- return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5314
+ const Hitboxes = ({ zoneId, parentZoneId, isEmptyZone }) => {
5315
+ const tree = useTreeStore((state) => state.tree);
5316
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5317
+ const scrollY = useDraggedItemStore((state) => state.scrollY);
5318
+ const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
5319
+ const zones = useZoneStore((state) => state.zones);
5320
+ const hoveringZone = useZoneStore((state) => state.hoveringZone);
5321
+ const isHoveringZone = hoveringZone === zoneId;
5322
+ const hitboxContainer = useMemo(() => {
5323
+ return document.querySelector('[data-ctfl-hitboxes]');
5324
+ }, []);
5325
+ const domRect = useMemo(() => {
5326
+ if (!isDraggingOnCanvas)
5327
+ return;
5328
+ return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
5329
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5330
+ }, [zoneId, isDraggingOnCanvas]);
5331
+ // Use the size of the cloned dragging element to offset the position of the hitboxes
5332
+ // So that when dragging causes a dropzone to expand, the hitboxes will be in the correct position
5333
+ const offsetRect = useMemo(() => {
5334
+ if (!isDraggingOnCanvas || isEmptyZone || !isHoveringZone)
5335
+ return;
5336
+ return document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`)?.getBoundingClientRect();
5337
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5338
+ }, [isEmptyZone, isHoveringZone, isDraggingOnCanvas]);
5339
+ const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
5340
+ const isVertical = zoneDirection === 'vertical';
5341
+ const isRoot = parentZoneId === ROOT_ID;
5342
+ const { slotId: parentSlotId } = parseZoneId(parentZoneId);
5343
+ const getStyles = useCallback((direction) => getHitboxStyles({
5344
+ direction,
5345
+ zoneDepth,
5346
+ domRect,
5347
+ scrollY,
5348
+ offsetRect,
5349
+ }), [zoneDepth, domRect, scrollY, offsetRect]);
5350
+ const renderFinalRootHitbox = () => {
5351
+ if (!isRoot)
5352
+ return null;
5353
+ return (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) }));
5354
+ };
5355
+ const renderSurroundingHitboxes = () => {
5356
+ if (isRoot || parentSlotId)
5357
+ return null;
5358
+ return (React.createElement(React.Fragment, null,
5359
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
5360
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })));
5361
+ };
5362
+ const ActiveHitboxes = (React.createElement(React.Fragment, null,
5363
+ React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
5364
+ renderSurroundingHitboxes(),
5365
+ renderFinalRootHitbox()));
5366
+ if (!hitboxContainer) {
5367
+ return null;
5368
+ }
5369
+ return createPortal(ActiveHitboxes, hitboxContainer);
5370
+ };
5371
+
5372
+ const isRelativePreviewSize = (width) => {
5373
+ // For now, we solely allow 100% as relative value
5374
+ return width === '100%';
5375
+ };
5376
+ const getTooltipPositions = ({ previewSize, tooltipRect, coordinates, }) => {
5377
+ if (!coordinates || !tooltipRect) {
5378
+ return { display: 'none' };
5379
+ }
5380
+ /**
5381
+ * By default, the tooltip floats to the left of the element
5382
+ */
5383
+ const newTooltipStyles = { display: 'flex' };
5384
+ // If the preview size is relative, we don't change the floating direction
5385
+ if (!isRelativePreviewSize(previewSize)) {
5386
+ const previewSizeMatch = previewSize.match(/(\d{1,})px/);
5387
+ if (!previewSizeMatch) {
5388
+ return { display: 'none' };
4028
5389
  }
4029
- return parentWrappingPatternIds;
4030
- }, [isRootAssemblyNode, node, parentWrappingPatternIds]);
4031
- const componentRegistration = useComponentRegistration(node);
4032
- if (!componentRegistration) {
4033
- return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4034
- }
4035
- if (isRootAssemblyNode && node.data.blockId && parentWrappingPatternIds.has(node.data.blockId)) {
4036
- return (React.createElement(CircularDependencyErrorPlaceholder, { node: node, wrappingPatternIds: wrappingPatternIds }));
4037
- }
4038
- const slotNodes = {};
4039
- for (const slotId in componentRegistration.definition.slots) {
4040
- const nodes = node.children.filter((child) => child.data.slotId === slotId);
4041
- slotNodes[slotId] =
4042
- 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 })))));
4043
- }
4044
- const children = componentRegistration.definition.children
4045
- ? node.children
4046
- .filter((node) => node.data.slotId === undefined)
4047
- .map((childNode) => (React.createElement(EditorBlock, { key: childNode.data.id, node: childNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))
4048
- : null;
4049
- return (React.createElement(RegistrationComponent, { node: node, resolveDesignValue: resolveDesignValue, componentRegistration: componentRegistration, slotNodes: slotNodes }, children));
5390
+ const previewSizePx = parseInt(previewSizeMatch[1]);
5391
+ /**
5392
+ * If the element is at the right edge of the canvas, and the element isn't wide enough to fit the tooltip width,
5393
+ * we float the tooltip to the right of the element.
5394
+ */
5395
+ if (tooltipRect.width > previewSizePx - coordinates.right &&
5396
+ tooltipRect.width > coordinates.width) {
5397
+ newTooltipStyles['float'] = 'right';
5398
+ }
5399
+ }
5400
+ const tooltipHeight = tooltipRect.height === 0 ? 32 : tooltipRect.height;
5401
+ /**
5402
+ * For elements with small heights, we don't want the tooltip covering the content in the element,
5403
+ * so we show the tooltip at the top or bottom.
5404
+ */
5405
+ if (tooltipHeight * 2 > coordinates.height) {
5406
+ /**
5407
+ * If there's enough space for the tooltip at the top of the element, we show the tooltip at the top of the element,
5408
+ * else we show the tooltip at the bottom.
5409
+ */
5410
+ if (tooltipHeight < coordinates.top) {
5411
+ newTooltipStyles['bottom'] = coordinates.height;
5412
+ }
5413
+ else {
5414
+ newTooltipStyles['top'] = coordinates.height;
5415
+ }
5416
+ }
5417
+ /**
5418
+ * If the component draws outside of the borders of the canvas to the left we move the tooltip to the right
5419
+ * so that it is fully visible.
5420
+ */
5421
+ if (coordinates.left < 0) {
5422
+ newTooltipStyles['left'] = -coordinates.left;
5423
+ }
5424
+ /**
5425
+ * If for any reason, the element's top is negative, we show the tooltip at the bottom
5426
+ */
5427
+ if (coordinates.top < 0) {
5428
+ newTooltipStyles['top'] = coordinates.height;
5429
+ }
5430
+ return newTooltipStyles;
5431
+ };
5432
+
5433
+ 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";
5434
+ 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"};
5435
+ styleInject(css_248z$1);
5436
+
5437
+ const Tooltip = ({ coordinates, id, label, isAssemblyBlock, isContainer, isSelected, }) => {
5438
+ const tooltipRef = useRef(null);
5439
+ const previewSize = '100%'; // This should be based on breakpoints and added to usememo dependency array
5440
+ const tooltipStyles = useMemo(() => {
5441
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect();
5442
+ const draggableRect = document
5443
+ .querySelector(`[data-ctfl-draggable-id="${id}"]`)
5444
+ ?.getBoundingClientRect();
5445
+ const newTooltipStyles = getTooltipPositions({
5446
+ previewSize,
5447
+ tooltipRect,
5448
+ coordinates: draggableRect,
5449
+ });
5450
+ return newTooltipStyles;
5451
+ // 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
5452
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5453
+ }, [coordinates, id, tooltipRef.current]);
5454
+ if (isSelected) {
5455
+ return null;
5456
+ }
5457
+ return (React.createElement("div", { "data-tooltip": true, className: styles$1.tooltipWrapper },
5458
+ React.createElement("div", { "data-tooltip": true, ref: tooltipRef, style: tooltipStyles, className: classNames(styles$1.overlay, {
5459
+ [styles$1.overlayContainer]: isContainer,
5460
+ [styles$1.overlayAssembly]: isAssemblyBlock,
5461
+ }) }, label)));
5462
+ };
5463
+
5464
+ function useSingleColumn(node, resolveDesignValue) {
5465
+ const tree = useTreeStore((store) => store.tree);
5466
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5467
+ const isWrapped = useMemo(() => {
5468
+ if (!node.parentId || !isSingleColumn) {
5469
+ return false;
5470
+ }
5471
+ const parentNode = getItem({ id: node.parentId }, tree);
5472
+ if (!parentNode || parentNode.data.blockId !== CONTENTFUL_COMPONENTS$1.columns.id) {
5473
+ return false;
5474
+ }
5475
+ const { cfWrapColumns } = parentNode.data.props;
5476
+ if (cfWrapColumns.type !== 'DesignValue') {
5477
+ return false;
5478
+ }
5479
+ return resolveDesignValue(cfWrapColumns.valuesByBreakpoint);
5480
+ }, [tree, node, isSingleColumn, resolveDesignValue]);
5481
+ return {
5482
+ isSingleColumn,
5483
+ isWrapped,
5484
+ };
4050
5485
  }
4051
- const RegistrationComponent = ({ node, resolveDesignValue, componentRegistration, slotNodes, children, }) => {
4052
- const { componentProps } = useComponentProps({
4053
- node,
5486
+
5487
+ function getStyle$1(style, snapshot) {
5488
+ if (!snapshot.isDropAnimating) {
5489
+ return style;
5490
+ }
5491
+ return {
5492
+ ...style,
5493
+ // cannot be 0, but make it super tiny
5494
+ transitionDuration: `0.001s`,
5495
+ };
5496
+ }
5497
+ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, index, zoneId, userIsDragging, placeholder, wrappingPatternIds, }) => {
5498
+ const { slotId } = parseZoneId(zoneId);
5499
+ const ref = useRef(null);
5500
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
5501
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
5502
+ const { node, componentId, elementToRender, definition, isPatternNode, isPatternComponent, isNestedPattern, } = useComponent({
5503
+ node: rawNode,
4054
5504
  resolveDesignValue,
4055
- definition: componentRegistration.definition,
4056
- options: componentRegistration.options,
5505
+ renderDropzone,
5506
+ userIsDragging,
5507
+ wrappingPatternIds,
4057
5508
  });
4058
- return React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, { ...componentProps, ...slotNodes }, children));
5509
+ const { isSingleColumn, isWrapped } = useSingleColumn(node, resolveDesignValue);
5510
+ const setDomRect = useDraggedItemStore((state) => state.setDomRect);
5511
+ const isHoveredComponent = useDraggedItemStore((state) => state.hoveredComponentId === componentId);
5512
+ const coordinates = useSelectedInstanceCoordinates({ node });
5513
+ const displayName = node.data.displayName || rawNode.data.displayName || definition?.name;
5514
+ const testId = `draggable-${node.data.blockId ?? 'node'}`;
5515
+ const isSelected = node.data.id === selectedNodeId;
5516
+ const isContainer = node.data.blockId === CONTENTFUL_COMPONENTS$1.container.id;
5517
+ const isSlotComponent = Boolean(node.data.slotId);
5518
+ const isDragDisabled = isNestedPattern || isPatternComponent || (isSingleColumn && isWrapped) || isSlotComponent;
5519
+ const isEmptyZone = useMemo(() => {
5520
+ return !node.children.filter((node) => node.data.slotId === slotId).length;
5521
+ }, [node.children, slotId]);
5522
+ useDraggablePosition({
5523
+ draggableId: componentId,
5524
+ draggableRef: ref,
5525
+ position: DraggablePosition.MOUSE_POSITION,
5526
+ });
5527
+ const onClick = (e) => {
5528
+ e.stopPropagation();
5529
+ if (!userIsDragging) {
5530
+ setSelectedNodeId(node.data.id);
5531
+ // if it is the assembly directly we just want to select it as a normal component
5532
+ if (isPatternNode) {
5533
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5534
+ nodeId: node.data.id,
5535
+ });
5536
+ return;
5537
+ }
5538
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5539
+ assembly: node.data.assembly,
5540
+ nodeId: node.data.id,
5541
+ });
5542
+ }
5543
+ };
5544
+ const onMouseOver = (e) => {
5545
+ e.stopPropagation();
5546
+ if (userIsDragging)
5547
+ return;
5548
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {
5549
+ nodeId: componentId,
5550
+ });
5551
+ };
5552
+ const onMouseDown = (e) => {
5553
+ if (isDragDisabled) {
5554
+ return;
5555
+ }
5556
+ e.stopPropagation();
5557
+ setDomRect(e.currentTarget.getBoundingClientRect());
5558
+ };
5559
+ const ToolTipAndPlaceholder = (React.createElement(React.Fragment, null,
5560
+ React.createElement(Tooltip, { id: componentId, coordinates: coordinates, isAssemblyBlock: isPatternNode || isPatternComponent, isContainer: isContainer, isSelected: isSelected, label: displayName || 'No label specified' }),
5561
+ React.createElement(Placeholder, { ...placeholder, id: componentId }),
5562
+ userIsDragging && !isPatternComponent && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
5563
+ return (React.createElement(Draggable, { key: componentId, draggableId: componentId, index: index, isDragDisabled: isDragDisabled, disableInteractiveElementBlocking: true }, (provided, snapshot) => elementToRender({
5564
+ dragProps: {
5565
+ ...provided.draggableProps,
5566
+ ...provided.dragHandleProps,
5567
+ 'data-ctfl-draggable-id': componentId,
5568
+ 'data-test-id': testId,
5569
+ innerRef: (refNode) => {
5570
+ provided?.innerRef(refNode);
5571
+ ref.current = refNode;
5572
+ },
5573
+ className: classNames(styles$1.DraggableComponent, {
5574
+ [styles$1.isAssemblyBlock]: isPatternComponent || isPatternNode,
5575
+ [styles$1.isDragging]: snapshot?.isDragging || userIsDragging,
5576
+ [styles$1.isSelected]: isSelected,
5577
+ [styles$1.isHoveringComponent]: isHoveredComponent,
5578
+ }),
5579
+ style: getStyle$1(provided.draggableProps.style, snapshot),
5580
+ onMouseDown,
5581
+ onMouseOver,
5582
+ onClick,
5583
+ ToolTipAndPlaceholder,
5584
+ },
5585
+ })));
4059
5586
  };
4060
5587
 
4061
- 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";
4062
- 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"};
5588
+ 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";
5589
+ var styles = {"container":"EmptyContainer-module_container__XPH5b","highlight":"EmptyContainer-module_highlight__lcICy","icon":"EmptyContainer-module_icon__82-2O","label":"EmptyContainer-module_label__4TxRa"};
4063
5590
  styleInject(css_248z);
4064
5591
 
4065
- const EmptyCanvasMessage = () => {
4066
- return (React.createElement("div", { className: styles['empty-canvas-container'], "data-type": "empty-container" },
4067
- React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles['empty-canvas-icon'] },
5592
+ const EmptyContainer = ({ isDragging }) => {
5593
+ return (React.createElement("div", { className: classNames(styles.container, {
5594
+ [styles.highlight]: isDragging,
5595
+ }), "data-type": "empty-container" },
5596
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles.icon },
4068
5597
  React.createElement("rect", { width: "11.676", height: "11.676", x: "18.512", y: ".153", rx: "1.621", transform: "rotate(45 18.512 .153)" }),
4069
5598
  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)" }),
4070
5599
  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)" }),
4071
5600
  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)" }),
4072
5601
  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" })),
4073
- React.createElement("span", { className: styles['empty-canvas-label'] }, "Add components to begin")));
5602
+ React.createElement("span", { className: styles.label }, "Add components to begin")));
5603
+ };
5604
+
5605
+ const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
5606
+ const zone = useZoneStore((state) => state.zones);
5607
+ const upsertZone = useZoneStore((state) => state.upsertZone);
5608
+ useEffect(() => {
5609
+ function getDirection() {
5610
+ if (!node || !node.data.blockId) {
5611
+ return 'vertical';
5612
+ }
5613
+ if (!isContentfulStructureComponent(node.data.blockId)) {
5614
+ return 'vertical';
5615
+ }
5616
+ if (node.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id) {
5617
+ return 'horizontal';
5618
+ }
5619
+ const designValues = node.data.props['cfFlexDirection'];
5620
+ if (!designValues || !resolveDesignValue || designValues.type !== 'DesignValue') {
5621
+ return 'vertical';
5622
+ }
5623
+ const direction = resolveDesignValue(designValues.valuesByBreakpoint);
5624
+ if (direction === 'row') {
5625
+ return 'horizontal';
5626
+ }
5627
+ return 'vertical';
5628
+ }
5629
+ upsertZone(zoneId, { direction: getDirection() });
5630
+ }, [node, resolveDesignValue, zoneId, upsertZone]);
5631
+ return zone[zoneId]?.direction || 'vertical';
5632
+ };
5633
+
5634
+ function getStyle(style = {}, snapshot) {
5635
+ if (!snapshot?.isDropAnimating) {
5636
+ return style;
5637
+ }
5638
+ return {
5639
+ ...style,
5640
+ // cannot be 0, but make it super tiny
5641
+ transitionDuration: `0.001s`,
5642
+ };
5643
+ }
5644
+ const EditorBlockClone = ({ node: rawNode, resolveDesignValue, snapshot, provided, renderDropzone, wrappingPatternIds, }) => {
5645
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5646
+ const { node, elementToRender } = useComponent({
5647
+ node: rawNode,
5648
+ resolveDesignValue,
5649
+ renderDropzone,
5650
+ userIsDragging,
5651
+ wrappingPatternIds,
5652
+ });
5653
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5654
+ return elementToRender({
5655
+ dragProps: {
5656
+ ...provided?.draggableProps,
5657
+ ...provided?.dragHandleProps,
5658
+ 'data-ctfl-dragging-element': 'true',
5659
+ innerRef: provided?.innerRef,
5660
+ className: classNames(styles$1.DraggableComponent, styles$1.DraggableClone, {
5661
+ [styles$1.isAssemblyBlock]: isAssemblyBlock,
5662
+ [styles$1.isDragging]: snapshot?.isDragging,
5663
+ }),
5664
+ style: getStyle(provided?.draggableProps.style, snapshot),
5665
+ },
5666
+ });
4074
5667
  };
4075
5668
 
4076
- const RootRenderer = () => {
5669
+ const getHtmlDragProps = (dragProps) => {
5670
+ if (dragProps) {
5671
+ const { ToolTipAndPlaceholder, Tag, innerRef, wrapComponent, ...htmlDragProps } = dragProps;
5672
+ return htmlDragProps;
5673
+ }
5674
+ return {};
5675
+ };
5676
+ const getHtmlComponentProps = (props) => {
5677
+ if (props) {
5678
+ const { editorMode, renderDropzone, node, ...htmlProps } = props;
5679
+ return htmlProps;
5680
+ }
5681
+ return {};
5682
+ };
5683
+
5684
+ function DropzoneClone({ node, zoneId, resolveDesignValue, WrapperComponent = 'div', renderDropzone, dragProps, wrappingPatternIds, ...rest }) {
5685
+ const tree = useTreeStore((state) => state.tree);
5686
+ const content = node?.children || tree.root?.children || [];
5687
+ const { slotId } = parseZoneId(zoneId);
5688
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5689
+ const htmlProps = getHtmlComponentProps(rest);
5690
+ const isRootZone = zoneId === ROOT_ID;
5691
+ if (!resolveDesignValue) {
5692
+ return null;
5693
+ }
5694
+ return (React.createElement(WrapperComponent, { ...htmlDraggableProps, ...htmlProps, className: classNames(dragProps?.className, styles$1.Dropzone, styles$1.DropzoneClone, rest.className, {
5695
+ [styles$1.isRoot]: isRootZone,
5696
+ [styles$1.isEmptyZone]: !content.length,
5697
+ }), "data-ctfl-slot-id": slotId, ref: (refNode) => {
5698
+ if (dragProps?.innerRef) {
5699
+ dragProps.innerRef(refNode);
5700
+ }
5701
+ } }, content
5702
+ .filter((node) => node.data.slotId === slotId)
5703
+ .map((item) => {
5704
+ const componentId = item.data.id;
5705
+ return (React.createElement(EditorBlockClone, { key: componentId, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds }));
5706
+ })));
5707
+ }
5708
+
5709
+ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', dragProps, wrappingPatternIds: parentWrappingPatternIds = new Set(), ...rest }) {
5710
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5711
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
5712
+ const isDraggingNewComponent = useDraggedItemStore((state) => Boolean(state.componentId));
5713
+ const isHoveringZone = useZoneStore((state) => state.hoveringZone === zoneId);
5714
+ const tree = useTreeStore((state) => state.tree);
5715
+ const content = node?.children || tree.root?.children || [];
5716
+ const { slotId } = parseZoneId(zoneId);
5717
+ const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
5718
+ const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
5719
+ const draggedNode = useMemo(() => {
5720
+ if (!draggedItem)
5721
+ return;
5722
+ return getItem({ id: draggedItem.draggableId }, tree);
5723
+ }, [draggedItem, tree]);
5724
+ const isRootZone = zoneId === ROOT_ID;
5725
+ const isDestination = draggedDestinationId === zoneId;
5726
+ const isEmptyCanvas = isRootZone && !content.length;
5727
+ const isAssembly = ASSEMBLY_NODE_TYPES.includes(node?.type || '');
5728
+ const isRootAssembly = node?.type === ASSEMBLY_NODE_TYPE;
5729
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
5730
+ const htmlProps = getHtmlComponentProps(rest);
5731
+ const wrappingPatternIds = useMemo(() => {
5732
+ // On the top level, the node is not defined. If the root blockId is not the default string,
5733
+ // we assume that it is the entry ID of the experience/ pattern to properly detect circular dependencies
5734
+ if (!node && tree.root.data.blockId && tree.root.data.blockId !== ROOT_ID) {
5735
+ return new Set([tree.root.data.blockId, ...parentWrappingPatternIds]);
5736
+ }
5737
+ if (isRootAssembly && node?.data.blockId) {
5738
+ return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5739
+ }
5740
+ return parentWrappingPatternIds;
5741
+ }, [isRootAssembly, node, parentWrappingPatternIds, tree.root.data.blockId]);
5742
+ // To avoid a circular dependency, we create the recursive rendering function here and trickle it down
5743
+ const renderDropzone = useCallback((node, props) => {
5744
+ return (React.createElement(Dropzone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds, ...props }));
5745
+ }, [wrappingPatternIds, resolveDesignValue]);
5746
+ const renderClonedDropzone = useCallback((node, props) => {
5747
+ return (React.createElement(DropzoneClone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds, ...props }));
5748
+ }, [resolveDesignValue, wrappingPatternIds]);
5749
+ const isDropzoneEnabled = useMemo(() => {
5750
+ const isColumns = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
5751
+ const isDraggingSingleColumn = draggedNode?.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
5752
+ const isParentOfDraggedNode = node?.data.id === draggedNode?.parentId;
5753
+ // If dragging a single column, only enable the dropzone of the parent
5754
+ // columns component
5755
+ if (isDraggingSingleColumn && isColumns && isParentOfDraggedNode) {
5756
+ return true;
5757
+ }
5758
+ // If dragging a single column, disable dropzones for any component besides
5759
+ // the parent of the dragged single column
5760
+ if (isDraggingSingleColumn && !isParentOfDraggedNode) {
5761
+ return false;
5762
+ }
5763
+ // Disable dropzone for Columns component
5764
+ if (isColumns) {
5765
+ return false;
5766
+ }
5767
+ // Disable dropzone for Assembly
5768
+ if (isAssembly) {
5769
+ return false;
5770
+ }
5771
+ // Enable dropzone for the non-root hovered zones if component is not allowed on root
5772
+ if (!isDraggingNewComponent &&
5773
+ !isComponentAllowedOnRoot({ type: draggedNode?.type, componentId: draggedNode?.data.blockId })) {
5774
+ return isHoveringZone && !isRootZone;
5775
+ }
5776
+ // Enable dropzone for the hovered zone only
5777
+ return isHoveringZone;
5778
+ }, [isAssembly, isHoveringZone, isRootZone, isDraggingNewComponent, draggedNode, node]);
5779
+ if (!resolveDesignValue) {
5780
+ return null;
5781
+ }
5782
+ const isPatternWrapperComponentFullHeight = isRootAssembly
5783
+ ? node.children.length === 1 &&
5784
+ resolveDesignValue(node?.children[0]?.data.props.cfHeight?.valuesByBreakpoint ?? {}, 'cfHeight') === '100%'
5785
+ : false;
5786
+ const isPatternWrapperComponentFullWidth = isRootAssembly
5787
+ ? node.children.length === 1 &&
5788
+ resolveDesignValue(node?.children[0]?.data.props.cfWidth?.valuesByBreakpoint ?? {}, 'cfWidth') === '100%'
5789
+ : false;
5790
+ 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) => {
5791
+ return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ...htmlDraggableProps, ...htmlProps, ref: (refNode) => {
5792
+ if (dragProps?.innerRef) {
5793
+ dragProps.innerRef(refNode);
5794
+ }
5795
+ provided?.innerRef(refNode);
5796
+ }, id: zoneId, "data-ctfl-zone-id": zoneId, "data-ctfl-slot-id": slotId, className: classNames(dragProps?.className, styles$1.Dropzone, className, {
5797
+ [styles$1.isEmptyCanvas]: isEmptyCanvas,
5798
+ [styles$1.isDragging]: userIsDragging,
5799
+ [styles$1.isDestination]: isDestination && !isAssembly,
5800
+ [styles$1.isRoot]: isRootZone,
5801
+ [styles$1.isEmptyZone]: !content.length,
5802
+ [styles$1.isSlot]: Boolean(slotId),
5803
+ [styles$1.fullHeight]: isPatternWrapperComponentFullHeight,
5804
+ [styles$1.fullWidth]: isPatternWrapperComponentFullWidth,
5805
+ }) },
5806
+ isEmptyCanvas ? (React.createElement(EmptyContainer, { isDragging: isRootZone && userIsDragging })) : (content
5807
+ .filter((node) => node.data.slotId === slotId)
5808
+ .map((item, i) => (React.createElement(EditorBlock, { placeholder: {
5809
+ isDraggingOver: snapshot?.isDraggingOver,
5810
+ totalIndexes: content.length,
5811
+ elementIndex: i,
5812
+ dropzoneElementId: zoneId,
5813
+ direction,
5814
+ }, index: i, zoneId: zoneId, key: item.data.id, userIsDragging: userIsDragging, draggingNewComponent: isDraggingNewComponent, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds })))),
5815
+ provided?.placeholder,
5816
+ dragProps?.ToolTipAndPlaceholder));
5817
+ }));
5818
+ }
5819
+
5820
+ const RootRenderer = ({ onChange }) => {
4077
5821
  useEditorSubscriber();
5822
+ const dragItem = useDraggedItemStore((state) => state.componentId);
5823
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5824
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4078
5825
  const breakpoints = useTreeStore((state) => state.breakpoints);
5826
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4079
5827
  const containerRef = useRef(null);
4080
5828
  const { resolveDesignValue } = useBreakpoints(breakpoints);
5829
+ const [containerStyles, setContainerStyles] = useState({});
4081
5830
  const tree = useTreeStore((state) => state.tree);
4082
- // If the root blockId is defined but not the default string, it is the entry ID
4083
- // of the experience/ pattern to properly detect circular dependencies.
4084
- const rootBlockId = tree.root.data.blockId ?? ROOT_ID;
4085
- const wrappingPatternIds = rootBlockId !== ROOT_ID ? new Set([rootBlockId]) : new Set();
4086
- return (React.createElement(React.Fragment, null,
4087
- React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer, ref: containerRef }, !tree.root.children.length ? (React.createElement(EmptyCanvasMessage, null)) : (tree.root.children.map((topLevelChildNode) => (React.createElement(EditorBlock, { key: topLevelChildNode.data.id, node: topLevelChildNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))))));
5831
+ const handleMouseOver = useCallback(() => {
5832
+ // Remove hover state set by UI when mouse is over canvas
5833
+ setHoveredComponentId();
5834
+ // Remove hover styling from components in the layers tab
5835
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {});
5836
+ }, [setHoveredComponentId]);
5837
+ const handleClickOutside = useCallback((e) => {
5838
+ const element = e.target;
5839
+ const isRoot = element.getAttribute('data-ctfl-zone-id') === ROOT_ID;
5840
+ const clickedOnCanvas = element.closest(`[data-ctfl-root]`);
5841
+ if (clickedOnCanvas && !isRoot) {
5842
+ return;
5843
+ }
5844
+ sendMessage(OUTGOING_EVENTS.OutsideCanvasClick, {
5845
+ outsideCanvasClick: true,
5846
+ });
5847
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5848
+ nodeId: '',
5849
+ });
5850
+ setSelectedNodeId('');
5851
+ }, [setSelectedNodeId]);
5852
+ const handleResizeCanvas = useCallback(() => {
5853
+ const parentElement = containerRef.current?.parentElement;
5854
+ if (!parentElement) {
5855
+ return;
5856
+ }
5857
+ let siblingHeight = 0;
5858
+ for (const child of parentElement.children) {
5859
+ if (!child.hasAttribute('data-ctfl-root')) {
5860
+ siblingHeight += child.getBoundingClientRect().height;
5861
+ }
5862
+ }
5863
+ if (!siblingHeight) {
5864
+ /**
5865
+ * DRAGGABLE_HEIGHT is subtracted here due to an uninteded scrolling effect
5866
+ * when dragging a new component onto the canvas
5867
+ *
5868
+ * The DRAGGABLE_HEIGHT is then added as margin bottom to offset this value
5869
+ * so that visually there is no difference to the user.
5870
+ */
5871
+ setContainerStyles({
5872
+ minHeight: `${window.innerHeight - DRAGGABLE_HEIGHT}px`,
5873
+ });
5874
+ return;
5875
+ }
5876
+ setContainerStyles({
5877
+ minHeight: `${window.innerHeight - siblingHeight}px`,
5878
+ });
5879
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5880
+ }, [containerRef.current]);
5881
+ useEffect(() => {
5882
+ if (onChange)
5883
+ onChange(tree);
5884
+ }, [tree, onChange]);
5885
+ useEffect(() => {
5886
+ window.addEventListener('mouseover', handleMouseOver);
5887
+ return () => {
5888
+ window.removeEventListener('mouseover', handleMouseOver);
5889
+ };
5890
+ }, [handleMouseOver]);
5891
+ useEffect(() => {
5892
+ document.addEventListener('click', handleClickOutside);
5893
+ return () => {
5894
+ document.removeEventListener('click', handleClickOutside);
5895
+ };
5896
+ }, [handleClickOutside]);
5897
+ useEffect(() => {
5898
+ handleResizeCanvas();
5899
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5900
+ }, [containerRef.current]);
5901
+ return (React.createElement(DNDProvider, null,
5902
+ dragItem && React.createElement(DraggableContainer, { id: dragItem }),
5903
+ React.createElement("div", { "data-ctfl-root": true, className: styles$3.container, ref: containerRef, style: containerStyles },
5904
+ userIsDragging && React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitbox }),
5905
+ React.createElement(Dropzone, { zoneId: ROOT_ID, resolveDesignValue: resolveDesignValue }),
5906
+ userIsDragging && (React.createElement(React.Fragment, null,
5907
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitboxLower }),
5908
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.canvasBottomSpacer })))),
5909
+ React.createElement("div", { "data-ctfl-hitboxes": true })));
4088
5910
  };
4089
5911
 
4090
5912
  const useInitializeEditor = () => {
@@ -4128,11 +5950,38 @@ const useInitializeEditor = () => {
4128
5950
  const VisualEditorRoot = ({ experience }) => {
4129
5951
  const initialized = useInitializeEditor();
4130
5952
  const setHyperLinkPattern = useEditorStore((state) => state.setHyperLinkPattern);
5953
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
5954
+ const setHoveringZone = useZoneStore((state) => state.setHoveringZone);
4131
5955
  useEffect(() => {
4132
5956
  if (experience?.hyperlinkPattern) {
4133
5957
  setHyperLinkPattern(experience.hyperlinkPattern);
4134
5958
  }
4135
5959
  }, [experience?.hyperlinkPattern, setHyperLinkPattern]);
5960
+ useEffect(() => {
5961
+ const onMouseMove = (e) => {
5962
+ setMousePosition(e.clientX, e.clientY);
5963
+ const target = e.target;
5964
+ const zoneId = target.closest(`[${CTFL_ZONE_ID}]`)?.getAttribute(CTFL_ZONE_ID);
5965
+ if (zoneId) {
5966
+ setHoveringZone(zoneId);
5967
+ }
5968
+ if (!SimulateDnD$1.isDragging) {
5969
+ return;
5970
+ }
5971
+ if (target.id === NEW_COMPONENT_ID) {
5972
+ return;
5973
+ }
5974
+ SimulateDnD$1.updateDrag(e.clientX, e.clientY);
5975
+ sendMessage(OUTGOING_EVENTS.MouseMove, {
5976
+ clientX: e.pageX - window.scrollX,
5977
+ clientY: e.pageY - window.scrollY,
5978
+ });
5979
+ };
5980
+ document.addEventListener('mousemove', onMouseMove);
5981
+ return () => {
5982
+ document.removeEventListener('mousemove', onMouseMove);
5983
+ };
5984
+ }, [setHoveringZone, setMousePosition]);
4136
5985
  if (!initialized)
4137
5986
  return null;
4138
5987
  return React.createElement(RootRenderer, null);