@contentful/experiences-visual-editor-react 2.0.0-prerelease-20250704T1603-11d76eb.0 → 2.0.1-dev-20250708T1456-b79aa28.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,41 +1,35 @@
1
1
  import styleInject from 'style-inject';
2
- import React, { useState, useEffect, useCallback, forwardRef, useMemo, useLayoutEffect, useRef } from 'react';
2
+ import React, { useEffect, useRef, useState, useCallback, forwardRef, useLayoutEffect, useMemo } from 'react';
3
3
  import { z } from 'zod';
4
- import { omit, isArray, isEqual, get as get$2, debounce } from 'lodash-es';
4
+ import { omit, isArray, isEqual, get as get$2 } from 'lodash-es';
5
5
  import md5 from 'md5';
6
6
  import { BLOCKS } from '@contentful/rich-text-types';
7
7
  import { create, useStore } from 'zustand';
8
+ import { Droppable, Draggable, DragDropContext } from '@hello-pangea/dnd';
8
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
- /** @deprecated will be removed when dropping backward compatibility for old DND */
15
18
  const INCOMING_EVENTS$1 = {
16
19
  RequestEditorMode: 'requestEditorMode',
17
20
  RequestReadOnlyMode: 'requestReadOnlyMode',
18
21
  ExperienceUpdated: 'componentTreeUpdated',
19
- /** @deprecated will be removed when dropping backward compatibility for old DND */
20
22
  ComponentDraggingChanged: 'componentDraggingChanged',
21
- /** @deprecated will be removed when dropping backward compatibility for old DND */
22
23
  ComponentDragCanceled: 'componentDragCanceled',
23
- /** @deprecated will be removed when dropping backward compatibility for old DND */
24
24
  ComponentDragStarted: 'componentDragStarted',
25
- /** @deprecated will be removed when dropping backward compatibility for old DND */
26
25
  ComponentDragEnded: 'componentDragEnded',
27
- /** @deprecated will be removed when dropping backward compatibility for old DND */
28
26
  ComponentMoveEnded: 'componentMoveEnded',
29
- /** @deprecated will be removed when dropping backward compatibility for old DND */
30
27
  CanvasResized: 'canvasResized',
31
- /** @deprecated will be removed when dropping backward compatibility for old DND */
32
28
  SelectComponent: 'selectComponent',
33
- /** @deprecated will be removed when dropping backward compatibility for old DND */
34
29
  HoverComponent: 'hoverComponent',
35
30
  UpdatedEntity: 'updatedEntity',
36
31
  AssembliesAdded: 'assembliesAdded',
37
32
  AssembliesRegistered: 'assembliesRegistered',
38
- /** @deprecated will be removed when dropping backward compatibility for old DND */
39
33
  MouseMove: 'mouseMove',
40
34
  RequestedEntities: 'REQUESTED_ENTITIES',
41
35
  };
@@ -50,7 +44,7 @@ var StudioCanvasMode$3;
50
44
  StudioCanvasMode["EDITOR"] = "editorMode";
51
45
  StudioCanvasMode["NONE"] = "none";
52
46
  })(StudioCanvasMode$3 || (StudioCanvasMode$3 = {}));
53
- const CONTENTFUL_COMPONENTS$1 = {
47
+ const CONTENTFUL_COMPONENTS$2 = {
54
48
  section: {
55
49
  id: 'contentful-section',
56
50
  name: 'Section',
@@ -96,6 +90,9 @@ const CONTENTFUL_COMPONENTS$1 = {
96
90
  name: 'Carousel',
97
91
  },
98
92
  };
93
+ const ASSEMBLY_NODE_TYPE$1 = 'assembly';
94
+ const ASSEMBLY_DEFAULT_CATEGORY$1 = 'Assemblies';
95
+ const ASSEMBLY_BLOCK_NODE_TYPE$1 = 'assemblyBlock';
99
96
  const CF_STYLE_ATTRIBUTES = [
100
97
  'cfVisibility',
101
98
  'cfHorizontalAlignment',
@@ -130,7 +127,7 @@ const CF_STYLE_ATTRIBUTES = [
130
127
  'cfTextItalic',
131
128
  'cfTextUnderline',
132
129
  ];
133
- const EMPTY_CONTAINER_SIZE$1 = '80px';
130
+ const EMPTY_CONTAINER_HEIGHT$1 = '80px';
134
131
  const DEFAULT_IMAGE_WIDTH = '500px';
135
132
  var PostMessageMethods$3;
136
133
  (function (PostMessageMethods) {
@@ -140,14 +137,20 @@ var PostMessageMethods$3;
140
137
  const SUPPORTED_IMAGE_FORMATS = ['jpg', 'png', 'webp', 'gif', 'avif'];
141
138
 
142
139
  const structureComponentIds = new Set([
143
- CONTENTFUL_COMPONENTS$1.section.id,
144
- CONTENTFUL_COMPONENTS$1.columns.id,
145
- CONTENTFUL_COMPONENTS$1.container.id,
146
- CONTENTFUL_COMPONENTS$1.singleColumn.id,
140
+ CONTENTFUL_COMPONENTS$2.section.id,
141
+ CONTENTFUL_COMPONENTS$2.columns.id,
142
+ CONTENTFUL_COMPONENTS$2.container.id,
143
+ CONTENTFUL_COMPONENTS$2.singleColumn.id,
147
144
  ]);
148
- const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$1).map((component) => component.id));
145
+ const patternTypes = new Set([ASSEMBLY_NODE_TYPE$1, ASSEMBLY_BLOCK_NODE_TYPE$1]);
146
+ const allContentfulComponentIds = new Set(Object.values(CONTENTFUL_COMPONENTS$2).map((component) => component.id));
147
+ const isPatternComponent = (type) => patternTypes.has(type ?? '');
149
148
  const isContentfulStructureComponent = (componentId) => structureComponentIds.has((componentId ?? ''));
150
149
  const isContentfulComponent = (componentId) => allContentfulComponentIds.has((componentId ?? ''));
150
+ const isComponentAllowedOnRoot = ({ type, category, componentId }) => isPatternComponent(type) ||
151
+ category === ASSEMBLY_DEFAULT_CATEGORY$1 ||
152
+ isContentfulStructureComponent(componentId) ||
153
+ componentId === CONTENTFUL_COMPONENTS$2.divider.id;
151
154
  const isStructureWithRelativeHeight = (componentId, height) => {
152
155
  return isContentfulStructureComponent(componentId) && !height?.toString().endsWith('px');
153
156
  };
@@ -1190,10 +1193,6 @@ const findOutermostCoordinates = (first, second) => {
1190
1193
  left: Math.min(first.left, second.left),
1191
1194
  };
1192
1195
  };
1193
- const isElementHidden = (rect) => {
1194
- /** if the rect has no size and position, its element is not rendered in the DOM */
1195
- return rect.width === 0 && rect.height === 0 && rect.x === 0 && rect.y === 0;
1196
- };
1197
1196
  const getElementCoordinates = (element) => {
1198
1197
  const rect = element.getBoundingClientRect();
1199
1198
  /**
@@ -1433,17 +1432,21 @@ function buildTemplate({ template, context, }) {
1433
1432
 
1434
1433
  const stylesToKeep = ['cfImageAsset'];
1435
1434
  const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.includes(style));
1436
- // cfWrapColumns & cfWrapColumnsCount are no real style attributes as they are handled on the editor side
1437
- const propsToRemove = [
1438
- 'cfHyperlink',
1439
- 'cfOpenInNewTab',
1440
- 'cfSsrClassName',
1441
- 'cfWrapColumns',
1442
- 'cfWrapColumnsCount',
1443
- ];
1435
+ const propsToRemove = ['cfHyperlink', 'cfOpenInNewTab', 'cfSsrClassName'];
1444
1436
  const sanitizeNodeProps = (nodeProps) => {
1445
1437
  return omit(nodeProps, stylesToRemove, propsToRemove);
1446
1438
  };
1439
+
1440
+ /** Turn the visibility value into a style object that can be used for inline styles in React */
1441
+ const transformVisibility = (value) => {
1442
+ if (value === false) {
1443
+ return {
1444
+ display: 'none !important',
1445
+ };
1446
+ }
1447
+ // Don't explicitly set anything when visible to not overwrite values like `grid` or `flex`.
1448
+ return {};
1449
+ };
1447
1450
  const transformGridColumn = (span) => {
1448
1451
  if (!span) {
1449
1452
  return {};
@@ -1602,7 +1605,7 @@ const buildCfStyles = (values) => {
1602
1605
  };
1603
1606
  /**
1604
1607
  * Container/section default behavior:
1605
- * Default height => height: EMPTY_CONTAINER_SIZE
1608
+ * Default height => height: EMPTY_CONTAINER_HEIGHT
1606
1609
  * If a container component has children => height: 'fit-content'
1607
1610
  */
1608
1611
  const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
@@ -1612,7 +1615,7 @@ const calculateNodeDefaultHeight = ({ blockId, children, value, }) => {
1612
1615
  if (children.length) {
1613
1616
  return '100%';
1614
1617
  }
1615
- return EMPTY_CONTAINER_SIZE$1;
1618
+ return EMPTY_CONTAINER_HEIGHT$1;
1616
1619
  };
1617
1620
 
1618
1621
  function getOptimizedImageUrl(url, width, quality, format) {
@@ -2645,7 +2648,180 @@ function gatherDeepReferencesFromTree(startingNode, dataSource) {
2645
2648
  return deepReferences;
2646
2649
  }
2647
2650
 
2651
+ const useDraggedItemStore = create((set) => ({
2652
+ draggedItem: undefined,
2653
+ hoveredComponentId: undefined,
2654
+ domRect: undefined,
2655
+ componentId: '',
2656
+ isDraggingOnCanvas: false,
2657
+ onBeforeCaptureId: '',
2658
+ mouseX: 0,
2659
+ mouseY: 0,
2660
+ scrollY: 0,
2661
+ setComponentId(id) {
2662
+ set({ componentId: id });
2663
+ },
2664
+ setHoveredComponentId(id) {
2665
+ set({ hoveredComponentId: id });
2666
+ },
2667
+ updateItem: (item) => {
2668
+ set({ draggedItem: item });
2669
+ },
2670
+ setDraggingOnCanvas: (isDraggingOnCanvas) => {
2671
+ set({ isDraggingOnCanvas });
2672
+ },
2673
+ setOnBeforeCaptureId: (onBeforeCaptureId) => {
2674
+ set({ onBeforeCaptureId });
2675
+ },
2676
+ setMousePosition(x, y) {
2677
+ set({ mouseX: x, mouseY: y });
2678
+ },
2679
+ setDomRect(domRect) {
2680
+ set({ domRect });
2681
+ },
2682
+ setScrollY(y) {
2683
+ set({ scrollY: y });
2684
+ },
2685
+ }));
2686
+
2687
+ const SCROLL_STATES = {
2688
+ Start: 'scrollStart',
2689
+ IsScrolling: 'isScrolling',
2690
+ End: 'scrollEnd',
2691
+ };
2692
+ const OUTGOING_EVENTS = {
2693
+ Connected: 'connected',
2694
+ DesignTokens: 'registerDesignTokens',
2695
+ RegisteredBreakpoints: 'registeredBreakpoints',
2696
+ MouseMove: 'mouseMove',
2697
+ NewHoveredElement: 'newHoveredElement',
2698
+ ComponentSelected: 'componentSelected',
2699
+ RegisteredComponents: 'registeredComponents',
2700
+ RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
2701
+ ComponentDragCanceled: 'componentDragCanceled',
2702
+ ComponentDropped: 'componentDropped',
2703
+ ComponentMoved: 'componentMoved',
2704
+ CanvasReload: 'canvasReload',
2705
+ UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
2706
+ CanvasScroll: 'canvasScrolling',
2707
+ CanvasError: 'canvasError',
2708
+ ComponentMoveStarted: 'componentMoveStarted',
2709
+ ComponentMoveEnded: 'componentMoveEnded',
2710
+ OutsideCanvasClick: 'outsideCanvasClick',
2711
+ SDKFeatures: 'sdkFeatures',
2712
+ RequestEntities: 'REQUEST_ENTITIES',
2713
+ };
2714
+ const INCOMING_EVENTS = {
2715
+ RequestEditorMode: 'requestEditorMode',
2716
+ RequestReadOnlyMode: 'requestReadOnlyMode',
2717
+ ExperienceUpdated: 'componentTreeUpdated',
2718
+ ComponentDraggingChanged: 'componentDraggingChanged',
2719
+ ComponentDragCanceled: 'componentDragCanceled',
2720
+ ComponentDragStarted: 'componentDragStarted',
2721
+ ComponentDragEnded: 'componentDragEnded',
2722
+ ComponentMoveEnded: 'componentMoveEnded',
2723
+ CanvasResized: 'canvasResized',
2724
+ SelectComponent: 'selectComponent',
2725
+ HoverComponent: 'hoverComponent',
2726
+ UpdatedEntity: 'updatedEntity',
2727
+ AssembliesAdded: 'assembliesAdded',
2728
+ AssembliesRegistered: 'assembliesRegistered',
2729
+ MouseMove: 'mouseMove',
2730
+ RequestedEntities: 'REQUESTED_ENTITIES',
2731
+ };
2732
+ const INTERNAL_EVENTS = {
2733
+ ComponentsRegistered: 'cfComponentsRegistered',
2734
+ VisualEditorInitialize: 'cfVisualEditorInitialize',
2735
+ };
2736
+ const VISUAL_EDITOR_EVENTS = {
2737
+ Ready: 'cfVisualEditorReady',
2738
+ };
2739
+ /**
2740
+ * These modes are ONLY intended to be internally used within the context of
2741
+ * editing an experience inside of Contentful Studio. i.e. these modes
2742
+ * intentionally do not include preview/delivery modes.
2743
+ */
2744
+ var StudioCanvasMode$2;
2745
+ (function (StudioCanvasMode) {
2746
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
2747
+ StudioCanvasMode["EDITOR"] = "editorMode";
2748
+ StudioCanvasMode["NONE"] = "none";
2749
+ })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
2750
+ const CONTENTFUL_COMPONENTS$1 = {
2751
+ section: {
2752
+ id: 'contentful-section',
2753
+ name: 'Section',
2754
+ },
2755
+ container: {
2756
+ id: 'contentful-container',
2757
+ name: 'Container',
2758
+ },
2759
+ columns: {
2760
+ id: 'contentful-columns',
2761
+ name: 'Columns',
2762
+ },
2763
+ singleColumn: {
2764
+ id: 'contentful-single-column',
2765
+ name: 'Column',
2766
+ },
2767
+ button: {
2768
+ id: 'contentful-button',
2769
+ name: 'Button',
2770
+ },
2771
+ heading: {
2772
+ id: 'contentful-heading',
2773
+ name: 'Heading',
2774
+ },
2775
+ image: {
2776
+ id: 'contentful-image',
2777
+ name: 'Image',
2778
+ },
2779
+ richText: {
2780
+ id: 'contentful-richText',
2781
+ name: 'Rich Text',
2782
+ },
2783
+ text: {
2784
+ id: 'contentful-text',
2785
+ name: 'Text',
2786
+ },
2787
+ divider: {
2788
+ id: 'contentful-divider',
2789
+ name: 'Divider',
2790
+ },
2791
+ carousel: {
2792
+ id: 'contentful-carousel',
2793
+ name: 'Carousel',
2794
+ },
2795
+ };
2796
+ const ASSEMBLY_NODE_TYPE = 'assembly';
2797
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
2798
+ const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
2799
+ const ASSEMBLY_NODE_TYPES = [ASSEMBLY_NODE_TYPE, ASSEMBLY_BLOCK_NODE_TYPE];
2800
+ const EMPTY_CONTAINER_HEIGHT = '80px';
2801
+ const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
2802
+ var PostMessageMethods$2;
2803
+ (function (PostMessageMethods) {
2804
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2805
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2806
+ })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
2807
+
2808
+ const DRAGGABLE_HEIGHT = 30;
2809
+ const DRAGGABLE_WIDTH = 50;
2810
+ const DRAG_PADDING = 4;
2648
2811
  const ROOT_ID = 'root';
2812
+ const COMPONENT_LIST_ID = 'component-list';
2813
+ const NEW_COMPONENT_ID = 'ctfl-new-draggable';
2814
+ const CTFL_ZONE_ID = 'data-ctfl-zone-id';
2815
+ const CTFL_DRAGGING_ELEMENT = 'data-ctfl-dragging-element';
2816
+ const HITBOX = {
2817
+ WIDTH: 70,
2818
+ HEIGHT: 20,
2819
+ INITIAL_OFFSET: 10,
2820
+ OFFSET_INCREMENT: 8,
2821
+ MIN_HEIGHT: 45,
2822
+ MIN_DEPTH_HEIGHT: 20,
2823
+ DEEP_ZONE: 5,
2824
+ };
2649
2825
  var TreeAction;
2650
2826
  (function (TreeAction) {
2651
2827
  TreeAction[TreeAction["REMOVE_NODE"] = 0] = "REMOVE_NODE";
@@ -2655,6 +2831,139 @@ var TreeAction;
2655
2831
  TreeAction[TreeAction["REORDER_NODE"] = 4] = "REORDER_NODE";
2656
2832
  TreeAction[TreeAction["REPLACE_NODE"] = 5] = "REPLACE_NODE";
2657
2833
  })(TreeAction || (TreeAction = {}));
2834
+ var HitboxDirection;
2835
+ (function (HitboxDirection) {
2836
+ HitboxDirection[HitboxDirection["TOP"] = 0] = "TOP";
2837
+ HitboxDirection[HitboxDirection["LEFT"] = 1] = "LEFT";
2838
+ HitboxDirection[HitboxDirection["RIGHT"] = 2] = "RIGHT";
2839
+ HitboxDirection[HitboxDirection["BOTTOM"] = 3] = "BOTTOM";
2840
+ HitboxDirection[HitboxDirection["SELF_VERTICAL"] = 4] = "SELF_VERTICAL";
2841
+ HitboxDirection[HitboxDirection["SELF_HORIZONTAL"] = 5] = "SELF_HORIZONTAL";
2842
+ })(HitboxDirection || (HitboxDirection = {}));
2843
+ var DraggablePosition;
2844
+ (function (DraggablePosition) {
2845
+ DraggablePosition[DraggablePosition["CENTERED"] = 0] = "CENTERED";
2846
+ DraggablePosition[DraggablePosition["MOUSE_POSITION"] = 1] = "MOUSE_POSITION";
2847
+ })(DraggablePosition || (DraggablePosition = {}));
2848
+
2849
+ function useDraggablePosition({ draggableId, draggableRef, position }) {
2850
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
2851
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
2852
+ const preDragDomRect = useDraggedItemStore((state) => state.domRect);
2853
+ useEffect(() => {
2854
+ const el = draggableRef?.current ??
2855
+ document.querySelector(`[${CTFL_DRAGGING_ELEMENT}][data-cf-node-id="${draggableId}"]`);
2856
+ if (!isDraggingOnCanvas || draggingId !== draggableId || !el) {
2857
+ return;
2858
+ }
2859
+ const isCentered = position === DraggablePosition.CENTERED || !preDragDomRect;
2860
+ const domRect = isCentered ? el.getBoundingClientRect() : preDragDomRect;
2861
+ const { mouseX, mouseY } = useDraggedItemStore.getState();
2862
+ const top = isCentered ? mouseY - domRect.height / 2 : domRect.top;
2863
+ const left = isCentered ? mouseX - domRect.width / 2 : domRect.left;
2864
+ el.style.position = 'fixed';
2865
+ el.style.left = `${left}px`;
2866
+ el.style.top = `${top}px`;
2867
+ el.style.width = `${domRect.width}px`;
2868
+ el.style.height = `${domRect.height}px`;
2869
+ }, [draggableRef, draggableId, isDraggingOnCanvas, draggingId, position, preDragDomRect]);
2870
+ }
2871
+
2872
+ function getStyle$2(style, snapshot) {
2873
+ if (!snapshot.isDropAnimating) {
2874
+ return style;
2875
+ }
2876
+ return {
2877
+ ...style,
2878
+ // cannot be 0, but make it super tiny
2879
+ transitionDuration: `0.001s`,
2880
+ };
2881
+ }
2882
+ const DraggableContainer = ({ id }) => {
2883
+ const ref = useRef(null);
2884
+ useDraggablePosition({
2885
+ draggableId: id,
2886
+ draggableRef: ref,
2887
+ position: DraggablePosition.CENTERED,
2888
+ });
2889
+ return (React.createElement("div", { id: COMPONENT_LIST_ID, style: {
2890
+ position: 'absolute',
2891
+ top: 0,
2892
+ left: 0,
2893
+ pointerEvents: 'none',
2894
+ zIndex: -1,
2895
+ } },
2896
+ React.createElement(Droppable, { droppableId: COMPONENT_LIST_ID, isDropDisabled: true }, (provided) => (React.createElement("div", { ...provided.droppableProps, ref: provided.innerRef },
2897
+ 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) => {
2898
+ provided.innerRef(node);
2899
+ ref.current = node;
2900
+ }, ...provided.draggableProps, ...provided.dragHandleProps, style: {
2901
+ ...getStyle$2(provided.draggableProps.style, snapshot),
2902
+ width: DRAGGABLE_WIDTH,
2903
+ height: DRAGGABLE_HEIGHT,
2904
+ pointerEvents: 'none',
2905
+ } }))),
2906
+ provided.placeholder)))));
2907
+ };
2908
+
2909
+ function getItemFromTree(id, node) {
2910
+ // Check if the current node's id matches the search id
2911
+ if (node.data.id === id) {
2912
+ return node;
2913
+ }
2914
+ // Recursively search through each child
2915
+ for (const child of node.children) {
2916
+ const foundNode = getItemFromTree(id, child);
2917
+ if (foundNode) {
2918
+ // Node found in children
2919
+ return foundNode;
2920
+ }
2921
+ }
2922
+ // If the node is not found in this branch of the tree, return undefined
2923
+ return undefined;
2924
+ }
2925
+ function findDepthById(node, id, currentDepth = 1) {
2926
+ if (node.data.id === id) {
2927
+ return currentDepth;
2928
+ }
2929
+ // If the node has children, check each one
2930
+ for (const child of node.children) {
2931
+ const childDepth = findDepthById(child, id, currentDepth + 1);
2932
+ if (childDepth !== -1) {
2933
+ return childDepth; // Found the node in a child
2934
+ }
2935
+ }
2936
+ return -1; // Node not found in this branch
2937
+ }
2938
+ const getChildFromTree = (parentId, index, node) => {
2939
+ // Check if the current node's id matches the search id
2940
+ if (node.data.id === parentId) {
2941
+ return node.children[index];
2942
+ }
2943
+ // Recursively search through each child
2944
+ for (const child of node.children) {
2945
+ const foundNode = getChildFromTree(parentId, index, child);
2946
+ if (foundNode) {
2947
+ // Node found in children
2948
+ return foundNode;
2949
+ }
2950
+ }
2951
+ // If the node is not found in this branch of the tree, return undefined
2952
+ return undefined;
2953
+ };
2954
+ const getItem = (selector, tree) => {
2955
+ return getItemFromTree(selector.id, {
2956
+ type: 'block',
2957
+ data: {
2958
+ id: ROOT_ID,
2959
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2960
+ },
2961
+ children: tree.root.children,
2962
+ });
2963
+ };
2964
+ const getItemDepthFromNode = (selector, node) => {
2965
+ return findDepthById(node, selector.id);
2966
+ };
2658
2967
 
2659
2968
  function updateNode(nodeId, updatedNode, node) {
2660
2969
  if (node.data.id === nodeId) {
@@ -2693,33 +3002,24 @@ function addChildNode(indexToAdd, parentNodeId, nodeToAdd, node) {
2693
3002
  }
2694
3003
  node.children.forEach((childNode) => addChildNode(indexToAdd, parentNodeId, nodeToAdd, childNode));
2695
3004
  }
2696
-
2697
- function getItemFromTree(id, node) {
2698
- // Check if the current node's id matches the search id
2699
- if (node.data.id === id) {
2700
- return node;
3005
+ function reorderChildNode(oldIndex, newIndex, parentNodeId, node) {
3006
+ if (node.data.id === parentNodeId) {
3007
+ // Remove the child from the old position
3008
+ const [childToMove] = node.children.splice(oldIndex, 1);
3009
+ // Insert the child at the new position
3010
+ node.children.splice(newIndex, 0, childToMove);
3011
+ return;
2701
3012
  }
2702
- // Recursively search through each child
2703
- for (const child of node.children) {
2704
- const foundNode = getItemFromTree(id, child);
2705
- if (foundNode) {
2706
- // Node found in children
2707
- return foundNode;
2708
- }
3013
+ node.children.forEach((childNode) => reorderChildNode(oldIndex, newIndex, parentNodeId, childNode));
3014
+ }
3015
+ function reparentChildNode(oldIndex, newIndex, sourceNodeId, destinationNodeId, node) {
3016
+ const nodeToMove = getChildFromTree(sourceNodeId, oldIndex, node);
3017
+ if (!nodeToMove) {
3018
+ return;
2709
3019
  }
2710
- // If the node is not found in this branch of the tree, return undefined
2711
- return undefined;
3020
+ removeChildNode(oldIndex, nodeToMove.data.id, sourceNodeId, node);
3021
+ addChildNode(newIndex, destinationNodeId, nodeToMove, node);
2712
3022
  }
2713
- const getItem = (selector, tree) => {
2714
- return getItemFromTree(selector.id, {
2715
- type: 'block',
2716
- data: {
2717
- id: ROOT_ID,
2718
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2719
- },
2720
- children: tree.root.children,
2721
- });
2722
- };
2723
3023
 
2724
3024
  function missingNodeAction({ index, nodeAdded, child, tree, parentNodeId, currentNode, }) {
2725
3025
  if (nodeAdded) {
@@ -2841,85 +3141,6 @@ function getTreeDiffs(tree1, tree2, originalTree) {
2841
3141
  return differences.filter((diff) => diff);
2842
3142
  }
2843
3143
 
2844
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2845
- const OUTGOING_EVENTS = {
2846
- Connected: 'connected',
2847
- DesignTokens: 'registerDesignTokens',
2848
- RegisteredBreakpoints: 'registeredBreakpoints',
2849
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2850
- MouseMove: 'mouseMove',
2851
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2852
- ComponentSelected: 'componentSelected',
2853
- RegisteredComponents: 'registeredComponents',
2854
- RequestComponentTreeUpdate: 'requestComponentTreeUpdate',
2855
- CanvasReload: 'canvasReload',
2856
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2857
- UpdateSelectedComponentCoordinates: 'updateSelectedComponentCoordinates',
2858
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2859
- CanvasScroll: 'canvasScrolling',
2860
- CanvasError: 'canvasError',
2861
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2862
- OutsideCanvasClick: 'outsideCanvasClick',
2863
- SDKFeatures: 'sdkFeatures',
2864
- RequestEntities: 'REQUEST_ENTITIES',
2865
- CanvasGeometryUpdated: 'canvasGeometryUpdated',
2866
- };
2867
- const INCOMING_EVENTS = {
2868
- RequestEditorMode: 'requestEditorMode',
2869
- RequestReadOnlyMode: 'requestReadOnlyMode',
2870
- ExperienceUpdated: 'componentTreeUpdated',
2871
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2872
- ComponentDraggingChanged: 'componentDraggingChanged',
2873
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2874
- ComponentDragCanceled: 'componentDragCanceled',
2875
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2876
- ComponentDragStarted: 'componentDragStarted',
2877
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2878
- ComponentDragEnded: 'componentDragEnded',
2879
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2880
- ComponentMoveEnded: 'componentMoveEnded',
2881
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2882
- CanvasResized: 'canvasResized',
2883
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2884
- SelectComponent: 'selectComponent',
2885
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2886
- HoverComponent: 'hoverComponent',
2887
- UpdatedEntity: 'updatedEntity',
2888
- AssembliesAdded: 'assembliesAdded',
2889
- AssembliesRegistered: 'assembliesRegistered',
2890
- /** @deprecated will be removed when dropping backward compatibility for old DND */
2891
- MouseMove: 'mouseMove',
2892
- RequestedEntities: 'REQUESTED_ENTITIES',
2893
- };
2894
- const INTERNAL_EVENTS = {
2895
- ComponentsRegistered: 'cfComponentsRegistered',
2896
- VisualEditorInitialize: 'cfVisualEditorInitialize',
2897
- };
2898
- const VISUAL_EDITOR_EVENTS = {
2899
- Ready: 'cfVisualEditorReady',
2900
- };
2901
- /**
2902
- * These modes are ONLY intended to be internally used within the context of
2903
- * editing an experience inside of Contentful Studio. i.e. these modes
2904
- * intentionally do not include preview/delivery modes.
2905
- */
2906
- var StudioCanvasMode$2;
2907
- (function (StudioCanvasMode) {
2908
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
2909
- StudioCanvasMode["EDITOR"] = "editorMode";
2910
- StudioCanvasMode["NONE"] = "none";
2911
- })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
2912
- const ASSEMBLY_NODE_TYPE = 'assembly';
2913
- const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
2914
- const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
2915
- const EMPTY_CONTAINER_SIZE = '80px';
2916
- const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
2917
- var PostMessageMethods$2;
2918
- (function (PostMessageMethods) {
2919
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
2920
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
2921
- })(PostMessageMethods$2 || (PostMessageMethods$2 = {}));
2922
-
2923
3144
  const useTreeStore = create((set, get) => ({
2924
3145
  tree: {
2925
3146
  root: {
@@ -2951,6 +3172,20 @@ const useTreeStore = create((set, get) => ({
2951
3172
  });
2952
3173
  }));
2953
3174
  },
3175
+ /**
3176
+ * NOTE: this is for debugging purposes only as it causes ugly canvas flash.
3177
+ *
3178
+ * Force updates entire tree. Usually shouldn't be used as updateTree()
3179
+ * uses smart update algorithm based on diffs. But for troubleshooting
3180
+ * you may want to force update the tree so leaving this in.
3181
+ */
3182
+ updateTreeForced: (tree) => {
3183
+ set({
3184
+ tree,
3185
+ // Breakpoints must be updated, as we receive completely new tree with possibly new breakpoints
3186
+ breakpoints: tree?.root?.data?.breakpoints || [],
3187
+ });
3188
+ },
2954
3189
  updateTree: (tree) => {
2955
3190
  const currentTree = get().tree;
2956
3191
  /**
@@ -2996,6 +3231,21 @@ const useTreeStore = create((set, get) => ({
2996
3231
  state.breakpoints = tree?.root?.data?.breakpoints || [];
2997
3232
  }));
2998
3233
  },
3234
+ addChild: (index, parentId, node) => {
3235
+ set(produce((state) => {
3236
+ addChildNode(index, parentId, node, state.tree.root);
3237
+ }));
3238
+ },
3239
+ reorderChildren: (destinationIndex, destinationParentId, sourceIndex) => {
3240
+ set(produce((state) => {
3241
+ reorderChildNode(sourceIndex, destinationIndex, destinationParentId, state.tree.root);
3242
+ }));
3243
+ },
3244
+ reparentChild: (destinationIndex, destinationParentId, sourceIndex, sourceParentId) => {
3245
+ set(produce((state) => {
3246
+ reparentChildNode(sourceIndex, destinationIndex, sourceParentId, destinationParentId, state.tree.root);
3247
+ }));
3248
+ },
2999
3249
  // breadth first search
3000
3250
  findNodeById(nodeId) {
3001
3251
  if (!nodeId) {
@@ -3033,8 +3283,8 @@ const cloneDeepAsPOJO = (obj) => {
3033
3283
  return JSON.parse(JSON.stringify(obj));
3034
3284
  };
3035
3285
 
3036
- 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";
3037
- var styles$2 = {"rootContainer":"RootRenderer-module_rootContainer__9UawM"};
3286
+ 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";
3287
+ var styles$3 = {"hitbox":"render-module_hitbox__l4ysJ","hitboxLower":"render-module_hitboxLower__tgsA1","canvasBottomSpacer":"render-module_canvasBottomSpacer__JuxVh","container":"render-module_container__-C3d7"};
3038
3288
  styleInject(css_248z$a);
3039
3289
 
3040
3290
  // TODO: In order to support integrations without React, we should extract this heavy logic into simple
@@ -3073,6 +3323,66 @@ const useBreakpoints = (breakpoints) => {
3073
3323
  return { resolveDesignValue };
3074
3324
  };
3075
3325
 
3326
+ /**
3327
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
3328
+ * and sends the DOM Rect to the client app
3329
+ */
3330
+ const sendSelectedComponentCoordinates = (instanceId) => {
3331
+ const selection = getSelectionNodes(instanceId);
3332
+ if (selection?.target) {
3333
+ const sendUpdateSelectedComponentCoordinates = () => {
3334
+ sendMessage(OUTGOING_EVENTS.UpdateSelectedComponentCoordinates, {
3335
+ selectedNodeCoordinates: getElementCoordinates(selection.target),
3336
+ selectedAssemblyChildCoordinates: selection.patternChild
3337
+ ? getElementCoordinates(selection.patternChild)
3338
+ : undefined,
3339
+ parentCoordinates: selection.parent ? getElementCoordinates(selection.parent) : undefined,
3340
+ });
3341
+ };
3342
+ // If the target contains an image, wait for this image to be loaded before sending the coordinates
3343
+ const childImage = selection.target.querySelector('img');
3344
+ if (childImage) {
3345
+ const handleImageLoad = () => {
3346
+ sendUpdateSelectedComponentCoordinates();
3347
+ childImage.removeEventListener('load', handleImageLoad);
3348
+ };
3349
+ childImage.addEventListener('load', handleImageLoad);
3350
+ }
3351
+ sendUpdateSelectedComponentCoordinates();
3352
+ }
3353
+ };
3354
+ const getSelectionNodes = (instanceId) => {
3355
+ if (!instanceId)
3356
+ return;
3357
+ let selectedNode = document.querySelector(`[data-cf-node-id="${instanceId}"]`);
3358
+ let selectedPatternChild = null;
3359
+ let selectedParent = null;
3360
+ // Use RegEx instead of split to match the last occurrence of '---' in the instanceId instead of the first one
3361
+ const idMatch = instanceId.match(/(.*)---(.*)/);
3362
+ const rootNodeId = idMatch?.[1] ?? instanceId;
3363
+ const nodeLocation = idMatch?.[2];
3364
+ const isNestedPattern = nodeLocation && selectedNode?.dataset?.cfNodeBlockType === ASSEMBLY_NODE_TYPE;
3365
+ const isPatternChild = !isNestedPattern && nodeLocation;
3366
+ if (isPatternChild) {
3367
+ // For pattern child nodes, render the pattern itself as selected component
3368
+ selectedPatternChild = selectedNode;
3369
+ selectedNode = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3370
+ }
3371
+ else if (isNestedPattern) {
3372
+ // For nested patterns, return the upper pattern as parent
3373
+ selectedParent = document.querySelector(`[data-cf-node-id="${rootNodeId}"]`);
3374
+ }
3375
+ else {
3376
+ // Find the next valid parent of the selected element
3377
+ selectedParent = selectedNode?.parentElement ?? null;
3378
+ // Ensure that the selection parent is a VisualEditorBlock
3379
+ while (selectedParent && !selectedParent.dataset?.cfNodeId) {
3380
+ selectedParent = selectedParent?.parentElement;
3381
+ }
3382
+ }
3383
+ return { target: selectedNode, patternChild: selectedPatternChild, parent: selectedParent };
3384
+ };
3385
+
3076
3386
  // Note: During development, the hot reloading might empty this and it
3077
3387
  // stays empty leading to not rendering assemblies. Ideally, this is
3078
3388
  // integrated into the state machine to keep track of its state.
@@ -3106,10 +3416,14 @@ const useEditorStore = create((set, get) => ({
3106
3416
  dataSource: {},
3107
3417
  hyperLinkPattern: undefined,
3108
3418
  unboundValues: {},
3419
+ selectedNodeId: null,
3109
3420
  locale: null,
3110
3421
  setHyperLinkPattern: (pattern) => {
3111
3422
  set({ hyperLinkPattern: pattern });
3112
3423
  },
3424
+ setSelectedNodeId: (id) => {
3425
+ set({ selectedNodeId: id });
3426
+ },
3113
3427
  setDataSource(data) {
3114
3428
  const dataSource = get().dataSource;
3115
3429
  const newDataSource = { ...dataSource, ...data };
@@ -3141,7 +3455,6 @@ const useEditorStore = create((set, get) => ({
3141
3455
  var css_248z$8 = "@import url(https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;1,400;1,600&display=swap);:root{--cf-color-white:#fff;--cf-color-black:#000;--cf-color-gray100:#f7f9fa;--cf-color-gray400:#aec1cc;--cf-color-gray400-rgb:174,193,204;--cf-spacing-0:0rem;--cf-spacing-1:0.125rem;--cf-spacing-2:0.25rem;--cf-spacing-3:0.375rem;--cf-spacing-4:0.5rem;--cf-spacing-5:0.625rem;--cf-spacing-6:0.75rem;--cf-spacing-7:0.875rem;--cf-spacing-8:1rem;--cf-spacing-9:1.25rem;--cf-spacing-10:1.5rem;--cf-spacing-11:1.75rem;--cf-spacing-12:2rem;--cf-spacing-13:2.25rem;--cf-text-xs:0.75rem;--cf-text-sm:0.875rem;--cf-text-base:1rem;--cf-text-lg:1.125rem;--cf-text-xl:1.25rem;--cf-text-2xl:1.5rem;--cf-text-3xl:2rem;--cf-text-4xl:2.75rem;--cf-font-light:300;--cf-font-normal:400;--cf-font-medium:500;--cf-font-semibold:600;--cf-font-bold:700;--cf-font-extra-bold:800;--cf-font-black:900;--cf-border-radius-none:0px;--cf-border-radius-sm:0.125rem;--cf-border-radius:0.25rem;--cf-border-radius-md:0.375rem;--cf-border-radius-lg:0.5rem;--cf-border-radius-xl:0.75rem;--cf-border-radius-2xl:1rem;--cf-border-radius-3xl:1.5rem;--cf-border-radius-full:9999px;--cf-font-family-sans:Archivo,Helvetica,Arial,sans-serif;--cf-font-family-serif:Georgia,Cambria,Times New Roman,Times,serif;--cf-max-width-full:100%;--cf-button-bg:var(--cf-color-black);--cf-button-color:var(--cf-color-white);--cf-text-color:var(--cf-color-black)}*{box-sizing:border-box}";
3142
3456
  styleInject(css_248z$8);
3143
3457
 
3144
- /** @deprecated will be removed when dropping backward compatibility for old DND */
3145
3458
  /**
3146
3459
  * These modes are ONLY intended to be internally used within the context of
3147
3460
  * editing an experience inside of Contentful Studio. i.e. these modes
@@ -4146,8 +4459,8 @@ var VisualEditorMode;
4146
4459
  VisualEditorMode["InjectScript"] = "injectScript";
4147
4460
  })(VisualEditorMode || (VisualEditorMode = {}));
4148
4461
 
4149
- var css_248z$2 = ".contentful-container{display:flex;pointer-events:all;position:relative}.contentful-container::-webkit-scrollbar{display:none}.cf-container-wrapper{position:relative;width:100%}.contentful-container:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.contentful-section-label:after{content:\"Section\"}.contentful-container-label:after{content:\"Container\"}.contentful-container-link,.contentful-container-link:active,.contentful-container-link:focus-visible,.contentful-container-link:hover,.contentful-container-link:read-write,.contentful-container-link:visited{color:inherit;outline:unset;text-decoration:unset}";
4150
- styleInject(css_248z$2);
4462
+ var css_248z$2$1 = ".contentful-container{display:flex;pointer-events:all;position:relative}.contentful-container::-webkit-scrollbar{display:none}.cf-single-column-wrapper{position:relative}.cf-container-wrapper{position:relative;width:100%}.contentful-container:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.contentful-section-label:after{content:\"Section\"}.contentful-container-label:after{content:\"Container\"}.contentful-container-link,.contentful-container-link:active,.contentful-container-link:focus-visible,.contentful-container-link:hover,.contentful-container-link:read-write,.contentful-container-link:visited{color:inherit;outline:unset;text-decoration:unset}";
4463
+ styleInject(css_248z$2$1);
4151
4464
 
4152
4465
  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) => {
4153
4466
  return (React.createElement("div", { id: id, ref: ref, style: {
@@ -4171,24 +4484,113 @@ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave,
4171
4484
  });
4172
4485
  Flex.displayName = 'Flex';
4173
4486
 
4174
- var css_248z$1$1 = ".cf-divider{display:contents;height:100%;position:relative;width:100%}.cf-divider hr{border:none}";
4487
+ var css_248z$1$1 = ".cf-divider{display:contents;height:100%;position:relative;width:100%}.cf-divider hr{border:none}[data-ctfl-zone-id=root] .cf-divider:before{bottom:-5px;content:\"\";left:-5px;pointer-events:all;position:absolute;right:-5px;top:-5px}";
4175
4488
  styleInject(css_248z$1$1);
4176
4489
 
4177
- var css_248z$9 = ".cf-columns{display:flex;flex-direction:column;gap:24px;grid-template-columns:repeat(12,1fr);min-height:0;min-width:0}@media (min-width:768px){.cf-columns{display:grid}}.cf-single-column-wrapper{position:relative}.cf-single-column-wrapper:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.cf-single-column-label:after{content:\"Column\"}";
4490
+ var css_248z$9 = ".cf-columns{display:flex;flex-direction:column;gap:24px;grid-template-columns:repeat(12,1fr);min-height:0;min-width:0}@media (min-width:768px){.cf-columns{display:grid}}.cf-single-column-wrapper{display:flex;position:relative}.cf-single-column{pointer-events:all}.cf-single-column-wrapper:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.cf-single-column-label:after{content:\"Column\"}";
4178
4491
  styleInject(css_248z$9);
4179
4492
 
4493
+ const ColumnWrapper = forwardRef((props, ref) => {
4494
+ return (React.createElement("div", { ref: ref, ...props, style: {
4495
+ ...(props.style || {}),
4496
+ display: 'grid',
4497
+ gridTemplateColumns: 'repeat(12, [col-start] 1fr)',
4498
+ } }, props.children));
4499
+ });
4500
+ ColumnWrapper.displayName = 'ColumnWrapper';
4501
+
4180
4502
  const assemblyStyle = { display: 'contents' };
4503
+ // Feel free to do any magic as regards variable definitions for assemblies
4504
+ // Or if this isn't necessary by the time we figure that part out, we can bid this part farewell
4181
4505
  const Assembly = (props) => {
4506
+ if (props.editorMode) {
4507
+ const { node, dragProps, ...editorModeProps } = props;
4508
+ return props.renderDropzone(node, {
4509
+ ...editorModeProps,
4510
+ ['data-test-id']: 'contentful-assembly',
4511
+ className: props.className,
4512
+ dragProps,
4513
+ });
4514
+ }
4182
4515
  // Using a display contents so assembly content/children
4183
4516
  // can appear as if they are direct children of the div wrapper's parent
4184
4517
  return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
4185
4518
  };
4186
4519
 
4187
- function useEditorSubscriber(inMemoryEntitiesStore) {
4188
- const entityStore = inMemoryEntitiesStore((state) => state.entityStore);
4189
- const areEntitiesFetched = inMemoryEntitiesStore((state) => state.areEntitiesFetched);
4190
- const setEntitiesFetched = inMemoryEntitiesStore((state) => state.setEntitiesFetched);
4191
- const resetEntityStore = inMemoryEntitiesStore((state) => state.resetEntityStore);
4520
+ class DragState {
4521
+ constructor() {
4522
+ this.isDragStartedOnParent = false;
4523
+ this.isDraggingItem = false;
4524
+ }
4525
+ get isDragging() {
4526
+ return this.isDraggingItem;
4527
+ }
4528
+ get isDraggingOnParent() {
4529
+ return this.isDragStartedOnParent;
4530
+ }
4531
+ updateIsDragging(isDraggingItem) {
4532
+ this.isDraggingItem = isDraggingItem;
4533
+ }
4534
+ updateIsDragStartedOnParent(isDragStartedOnParent) {
4535
+ this.isDragStartedOnParent = isDragStartedOnParent;
4536
+ }
4537
+ resetState() {
4538
+ this.isDraggingItem = false;
4539
+ this.isDragStartedOnParent = false;
4540
+ }
4541
+ }
4542
+
4543
+ class SimulateDnD extends DragState {
4544
+ constructor() {
4545
+ super();
4546
+ this.draggingElement = null;
4547
+ }
4548
+ setupDrag() {
4549
+ this.updateIsDragStartedOnParent(true);
4550
+ }
4551
+ startDrag(coordX, coordY) {
4552
+ this.draggingElement = document.getElementById(NEW_COMPONENT_ID);
4553
+ this.updateIsDragging(true);
4554
+ this.simulateMouseEvent(coordX, coordY, 'mousedown');
4555
+ }
4556
+ updateDrag(coordX, coordY) {
4557
+ if (!this.draggingElement) {
4558
+ this.draggingElement = document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`);
4559
+ }
4560
+ this.simulateMouseEvent(coordX, coordY);
4561
+ }
4562
+ endDrag(coordX, coordY) {
4563
+ this.simulateMouseEvent(coordX, coordY, 'mouseup');
4564
+ this.reset();
4565
+ }
4566
+ reset() {
4567
+ this.draggingElement = null;
4568
+ this.resetState();
4569
+ }
4570
+ simulateMouseEvent(coordX, coordY, eventName = 'mousemove') {
4571
+ if (!this.draggingElement) {
4572
+ return;
4573
+ }
4574
+ const options = {
4575
+ bubbles: true,
4576
+ cancelable: true,
4577
+ view: window,
4578
+ pageX: 0,
4579
+ pageY: 0,
4580
+ clientX: coordX,
4581
+ clientY: coordY,
4582
+ };
4583
+ const event = new MouseEvent(eventName, options);
4584
+ this.draggingElement.dispatchEvent(event);
4585
+ }
4586
+ }
4587
+ var SimulateDnD$1 = new SimulateDnD();
4588
+
4589
+ function useEditorSubscriber(entityCache) {
4590
+ const entityStore = entityCache((state) => state.entityStore);
4591
+ const areEntitiesFetched = entityCache((state) => state.areEntitiesFetched);
4592
+ const setEntitiesFetched = entityCache((state) => state.setEntitiesFetched);
4593
+ const resetEntityStore = entityCache((state) => state.resetEntityStore);
4192
4594
  const { updateTree, updateNodesByUpdatedEntity } = useTreeStore((state) => ({
4193
4595
  updateTree: state.updateTree,
4194
4596
  updateNodesByUpdatedEntity: state.updateNodesByUpdatedEntity,
@@ -4198,6 +4600,13 @@ function useEditorSubscriber(inMemoryEntitiesStore) {
4198
4600
  const setLocale = useEditorStore((state) => state.setLocale);
4199
4601
  const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
4200
4602
  const setDataSource = useEditorStore((state) => state.setDataSource);
4603
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
4604
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
4605
+ const setComponentId = useDraggedItemStore((state) => state.setComponentId);
4606
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
4607
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
4608
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
4609
+ const setScrollY = useDraggedItemStore((state) => state.setScrollY);
4201
4610
  const reloadApp = () => {
4202
4611
  sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
4203
4612
  // Wait a moment to ensure that the message was sent
@@ -4375,6 +4784,27 @@ function useEditorSubscriber(inMemoryEntitiesStore) {
4375
4784
  }
4376
4785
  break;
4377
4786
  }
4787
+ case INCOMING_EVENTS.CanvasResized: {
4788
+ const { selectedNodeId } = eventData.payload;
4789
+ if (selectedNodeId) {
4790
+ sendSelectedComponentCoordinates(selectedNodeId);
4791
+ }
4792
+ break;
4793
+ }
4794
+ case INCOMING_EVENTS.HoverComponent: {
4795
+ const { hoveredNodeId } = eventData.payload;
4796
+ setHoveredComponentId(hoveredNodeId);
4797
+ break;
4798
+ }
4799
+ case INCOMING_EVENTS.ComponentDraggingChanged: {
4800
+ const { isDragging } = eventData.payload;
4801
+ if (!isDragging) {
4802
+ setComponentId('');
4803
+ setDraggingOnCanvas(false);
4804
+ SimulateDnD$1.reset();
4805
+ }
4806
+ break;
4807
+ }
4378
4808
  case INCOMING_EVENTS.UpdatedEntity: {
4379
4809
  const { entity: updatedEntity, shouldRerender } = eventData.payload;
4380
4810
  if (updatedEntity) {
@@ -4391,6 +4821,51 @@ function useEditorSubscriber(inMemoryEntitiesStore) {
4391
4821
  case INCOMING_EVENTS.RequestEditorMode: {
4392
4822
  break;
4393
4823
  }
4824
+ case INCOMING_EVENTS.ComponentDragCanceled: {
4825
+ if (SimulateDnD$1.isDragging) {
4826
+ //simulate a mouseup event to cancel the drag
4827
+ SimulateDnD$1.endDrag(0, 0);
4828
+ }
4829
+ break;
4830
+ }
4831
+ case INCOMING_EVENTS.ComponentDragStarted: {
4832
+ const { id, isAssembly } = eventData.payload;
4833
+ SimulateDnD$1.setupDrag();
4834
+ setComponentId(`${id}:${isAssembly}` || '');
4835
+ setDraggingOnCanvas(true);
4836
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
4837
+ nodeId: '',
4838
+ });
4839
+ break;
4840
+ }
4841
+ case INCOMING_EVENTS.ComponentDragEnded: {
4842
+ SimulateDnD$1.reset();
4843
+ setComponentId('');
4844
+ setDraggingOnCanvas(false);
4845
+ break;
4846
+ }
4847
+ case INCOMING_EVENTS.SelectComponent: {
4848
+ const { selectedNodeId: nodeId } = eventData.payload;
4849
+ setSelectedNodeId(nodeId);
4850
+ sendSelectedComponentCoordinates(nodeId);
4851
+ break;
4852
+ }
4853
+ case INCOMING_EVENTS.MouseMove: {
4854
+ const { mouseX, mouseY } = eventData.payload;
4855
+ setMousePosition(mouseX, mouseY);
4856
+ if (SimulateDnD$1.isDraggingOnParent && !SimulateDnD$1.isDragging) {
4857
+ SimulateDnD$1.startDrag(mouseX, mouseY);
4858
+ }
4859
+ else {
4860
+ SimulateDnD$1.updateDrag(mouseX, mouseY);
4861
+ }
4862
+ break;
4863
+ }
4864
+ case INCOMING_EVENTS.ComponentMoveEnded: {
4865
+ const { mouseX, mouseY } = eventData.payload;
4866
+ SimulateDnD$1.endDrag(mouseX, mouseY);
4867
+ break;
4868
+ }
4394
4869
  default:
4395
4870
  console.error(`[experiences-sdk-react::onMessage] Logic error, unsupported eventType: [${eventData.eventType}]`);
4396
4871
  }
@@ -4401,8 +4876,11 @@ function useEditorSubscriber(inMemoryEntitiesStore) {
4401
4876
  };
4402
4877
  }, [
4403
4878
  entityStore,
4879
+ setComponentId,
4880
+ setDraggingOnCanvas,
4404
4881
  setDataSource,
4405
4882
  setLocale,
4883
+ setSelectedNodeId,
4406
4884
  dataSource,
4407
4885
  areEntitiesFetched,
4408
4886
  fetchMissingEntities,
@@ -4410,125 +4888,362 @@ function useEditorSubscriber(inMemoryEntitiesStore) {
4410
4888
  unboundValues,
4411
4889
  updateTree,
4412
4890
  updateNodesByUpdatedEntity,
4891
+ setMousePosition,
4413
4892
  resetEntityStore,
4893
+ setHoveredComponentId,
4414
4894
  ]);
4895
+ /*
4896
+ * Handles on scroll business
4897
+ */
4898
+ useEffect(() => {
4899
+ let timeoutId = 0;
4900
+ let isScrolling = false;
4901
+ const onScroll = () => {
4902
+ setScrollY(window.scrollY);
4903
+ if (isScrolling === false) {
4904
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.Start);
4905
+ }
4906
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.IsScrolling);
4907
+ isScrolling = true;
4908
+ clearTimeout(timeoutId);
4909
+ timeoutId = window.setTimeout(() => {
4910
+ if (isScrolling === false) {
4911
+ return;
4912
+ }
4913
+ isScrolling = false;
4914
+ sendMessage(OUTGOING_EVENTS.CanvasScroll, SCROLL_STATES.End);
4915
+ /**
4916
+ * On scroll end, send new co-ordinates of selected node
4917
+ */
4918
+ if (selectedNodeId) {
4919
+ sendSelectedComponentCoordinates(selectedNodeId);
4920
+ }
4921
+ }, 150);
4922
+ };
4923
+ window.addEventListener('scroll', onScroll, { capture: true, passive: true });
4924
+ return () => {
4925
+ window.removeEventListener('scroll', onScroll, { capture: true });
4926
+ clearTimeout(timeoutId);
4927
+ };
4928
+ }, [selectedNodeId, setScrollY]);
4415
4929
  }
4416
4930
 
4417
- const CircularDependencyErrorPlaceholder = ({ wrappingPatternIds, ...props }) => {
4418
- return (React.createElement("div", { ...props, "data-cf-node-error": "circular-pattern-dependency", style: {
4419
- border: '1px solid red',
4420
- background: 'rgba(255, 0, 0, 0.1)',
4421
- padding: '1rem 1rem 0 1rem',
4422
- width: '100%',
4423
- height: '100%',
4424
- } },
4425
- "Circular usage of patterns detected:",
4426
- React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
4427
- const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
4428
- const entry = inMemoryEntities.maybeResolveLink(entryLink);
4429
- const entryTitle = entry?.fields?.title;
4430
- const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
4431
- return React.createElement("li", { key: patternId }, text);
4432
- }))));
4931
+ const onComponentMoved = (options) => {
4932
+ sendMessage(OUTGOING_EVENTS.ComponentMoved, options);
4433
4933
  };
4434
4934
 
4435
- class ImportedComponentError extends Error {
4436
- constructor(message) {
4437
- super(message);
4438
- this.name = 'ImportedComponentError';
4439
- }
4440
- }
4441
- class ExperienceSDKError extends Error {
4442
- constructor(message) {
4443
- super(message);
4444
- this.name = 'ExperienceSDKError';
4445
- }
4446
- }
4447
- class ImportedComponentErrorBoundary extends React.Component {
4448
- componentDidCatch(error, _errorInfo) {
4449
- if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
4450
- // This error was already handled by a nested error boundary and should be passed upwards
4451
- // We have to do this as we wrap every component on every layer with this error boundary and
4452
- // thus an error deep in the tree bubbles through many layers of error boundaries.
4453
- throw error;
4454
- }
4455
- // Differentiate between custom and SDK-provided components for error tracking
4456
- const ErrorClass = isContentfulComponent(this.props.componentId)
4457
- ? ExperienceSDKError
4458
- : ImportedComponentError;
4459
- const err = new ErrorClass(error.message);
4460
- err.stack = error.stack;
4461
- throw err;
4462
- }
4463
- render() {
4464
- return this.props.children;
4465
- }
4466
- }
4935
+ const generateId = (type) => `${type}-${v4()}`;
4467
4936
 
4468
- const MissingComponentPlaceholder = ({ blockId }) => {
4469
- return (React.createElement("div", { style: {
4470
- border: '1px solid red',
4471
- width: '100%',
4472
- height: '100%',
4473
- } },
4474
- "Missing component '",
4475
- blockId,
4476
- "'"));
4937
+ const createTreeNode = ({ blockId, parentId, slotId }) => {
4938
+ const node = {
4939
+ type: 'block',
4940
+ data: {
4941
+ id: generateId(blockId),
4942
+ blockId,
4943
+ slotId,
4944
+ props: {},
4945
+ dataSource: {},
4946
+ breakpoints: [],
4947
+ unboundValues: {},
4948
+ },
4949
+ parentId,
4950
+ children: [],
4951
+ };
4952
+ return node;
4477
4953
  };
4478
4954
 
4479
- var css_248z$1 = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
4480
- var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
4481
- styleInject(css_248z$1);
4955
+ const onComponentDropped = ({ node, index, parentBlockId, parentType, parentId, }) => {
4956
+ sendMessage(OUTGOING_EVENTS.ComponentDropped, {
4957
+ node,
4958
+ index: index ?? node.children.length,
4959
+ parentNode: {
4960
+ type: parentType,
4961
+ data: {
4962
+ blockId: parentBlockId,
4963
+ id: parentId,
4964
+ },
4965
+ },
4966
+ });
4967
+ };
4482
4968
 
4483
- const useComponentRegistration = (node) => {
4484
- return useMemo(() => {
4485
- let registration = componentRegistry.get(node.data.blockId);
4486
- if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
4487
- registration = createAssemblyRegistration({
4488
- definitionId: node.data.blockId,
4489
- component: Assembly,
4490
- });
4491
- }
4492
- if (!registration) {
4493
- 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.`);
4494
- return undefined;
4495
- }
4496
- return registration;
4497
- }, [node]);
4969
+ const onDrop = ({ destinationIndex, componentType, destinationZoneId, data, slotId, }) => {
4970
+ const parentId = destinationZoneId;
4971
+ const parentNode = getItem({ id: parentId }, data);
4972
+ const parentIsRoot = parentId === ROOT_ID;
4973
+ const emptyComponentData = {
4974
+ type: 'block',
4975
+ parentId,
4976
+ children: [],
4977
+ data: {
4978
+ blockId: componentType,
4979
+ id: generateId(componentType),
4980
+ slotId,
4981
+ breakpoints: [],
4982
+ dataSource: {},
4983
+ props: {},
4984
+ unboundValues: {},
4985
+ },
4986
+ };
4987
+ onComponentDropped({
4988
+ node: emptyComponentData,
4989
+ index: destinationIndex,
4990
+ parentType: parentIsRoot ? 'root' : parentNode?.type,
4991
+ parentBlockId: parentNode?.data.blockId,
4992
+ parentId: parentIsRoot ? 'root' : parentId,
4993
+ });
4498
4994
  };
4499
4995
 
4500
4996
  /**
4501
- * @param styles: the list of styles to apply
4502
- * @param nodeId: the id of node that these styles will be applied to
4503
- * @returns className: the className that was used
4504
- * Builds and adds a style tag in the document. Returns the className to be attached to the element.
4505
- * In editor mode the nodeId is used as the identifier in order to avoid creating endless tags as the styles are tweeked
4997
+ * Parses a droppable zone ID into a node ID and slot ID.
4998
+ *
4999
+ * The slot ID is optional and only present if the component implements multiple drop zones.
5000
+ *
5001
+ * @param zoneId - Expected formats are `nodeId` or `nodeId|slotId`.
4506
5002
  */
4507
- const useEditorModeClassName = ({ styles, nodeId, }) => {
4508
- const [className, setClassName] = useState('');
4509
- useLayoutEffect(() => {
4510
- if (Object.keys(styles).length === 0) {
5003
+ const parseZoneId = (zoneId) => {
5004
+ const [nodeId, slotId] = zoneId.includes('|') ? zoneId.split('|') : [zoneId, undefined];
5005
+ return { nodeId, slotId };
5006
+ };
5007
+
5008
+ function useCanvasInteractions() {
5009
+ const tree = useTreeStore((state) => state.tree);
5010
+ const reorderChildren = useTreeStore((state) => state.reorderChildren);
5011
+ const reparentChild = useTreeStore((state) => state.reparentChild);
5012
+ const addChild = useTreeStore((state) => state.addChild);
5013
+ const onAddComponent = (droppedItem) => {
5014
+ const { destination, draggableId } = droppedItem;
5015
+ if (!destination) {
4511
5016
  return;
4512
5017
  }
4513
- const [newClassName, styleRules] = buildStyleTag({ styles, nodeId });
4514
- addStylesTag(newClassName, styleRules);
4515
- if (className !== newClassName) {
4516
- setClassName(newClassName);
4517
- // Clean up: remove outdated styles from DOM
4518
- removeStylesTag(className);
5018
+ /**
5019
+ * We only have the draggableId as information about the new component being dropped.
5020
+ * So we need to split it to get the blockId and the isAssembly flag.
5021
+ */
5022
+ const [blockId, isAssembly] = draggableId.split(':');
5023
+ const { nodeId: parentId, slotId } = parseZoneId(destination.droppableId);
5024
+ const droppingOnRoot = parentId === ROOT_ID;
5025
+ const isValidRootComponent = blockId === CONTENTFUL_COMPONENTS$1.container.id;
5026
+ let node = createTreeNode({ blockId, parentId, slotId });
5027
+ if (droppingOnRoot && !isValidRootComponent) {
5028
+ const wrappingContainer = createTreeNode({
5029
+ blockId: CONTENTFUL_COMPONENTS$1.container.id,
5030
+ parentId,
5031
+ });
5032
+ const childNode = createTreeNode({
5033
+ blockId,
5034
+ parentId: wrappingContainer.data.id,
5035
+ });
5036
+ node = wrappingContainer;
5037
+ node.children = [childNode];
4519
5038
  }
4520
- }, [styles, nodeId, className]);
4521
- return className;
4522
- };
4523
- const removeStylesTag = (className) => {
4524
- const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
4525
- if (existingTag) {
4526
- document.head.removeChild(existingTag);
4527
- }
5039
+ /**
5040
+ * isAssembly comes from a string ID so we need to check if it's 'true' or 'false'
5041
+ * in string format.
5042
+ */
5043
+ if (isAssembly === 'false') {
5044
+ addChild(destination.index, parentId, node);
5045
+ }
5046
+ onDrop({
5047
+ data: tree,
5048
+ componentType: blockId,
5049
+ destinationIndex: destination.index,
5050
+ destinationZoneId: parentId,
5051
+ slotId,
5052
+ });
5053
+ };
5054
+ const onMoveComponent = (droppedItem) => {
5055
+ const { destination, source, draggableId } = droppedItem;
5056
+ if (!destination || !source) {
5057
+ return;
5058
+ }
5059
+ if (destination.droppableId === source.droppableId) {
5060
+ reorderChildren(destination.index, destination.droppableId, source.index);
5061
+ }
5062
+ if (destination.droppableId !== source.droppableId) {
5063
+ reparentChild(destination.index, destination.droppableId, source.index, source.droppableId);
5064
+ }
5065
+ onComponentMoved({
5066
+ nodeId: draggableId,
5067
+ destinationIndex: destination.index,
5068
+ destinationParentId: destination.droppableId,
5069
+ sourceIndex: source.index,
5070
+ sourceParentId: source.droppableId,
5071
+ });
5072
+ };
5073
+ return { onAddComponent, onMoveComponent };
5074
+ }
5075
+
5076
+ const TestDNDContainer = ({ onDragEnd, onBeforeDragStart, onDragStart, onDragUpdate, children, }) => {
5077
+ const handleDragStart = (event) => {
5078
+ const draggedItem = event.nativeEvent;
5079
+ const start = {
5080
+ mode: draggedItem.mode,
5081
+ draggableId: draggedItem.draggableId,
5082
+ type: draggedItem.type,
5083
+ source: draggedItem.source,
5084
+ };
5085
+ onBeforeDragStart(start);
5086
+ onDragStart(start, {});
5087
+ };
5088
+ const handleDrag = (event) => {
5089
+ const draggedItem = event.nativeEvent;
5090
+ const update = {
5091
+ mode: draggedItem.mode,
5092
+ draggableId: draggedItem.draggableId,
5093
+ type: draggedItem.type,
5094
+ source: draggedItem.source,
5095
+ destination: draggedItem.destination,
5096
+ combine: draggedItem.combine,
5097
+ };
5098
+ onDragUpdate(update, {});
5099
+ };
5100
+ const handleDragEnd = (event) => {
5101
+ const draggedItem = event.nativeEvent;
5102
+ const result = {
5103
+ mode: draggedItem.mode,
5104
+ draggableId: draggedItem.draggableId,
5105
+ type: draggedItem.type,
5106
+ source: draggedItem.source,
5107
+ destination: draggedItem.destination,
5108
+ combine: draggedItem.combine,
5109
+ reason: draggedItem.reason,
5110
+ };
5111
+ onDragEnd(result, {});
5112
+ };
5113
+ return (React.createElement("div", { "data-test-id": "dnd-context-substitute", onDragStart: handleDragStart, onDrag: handleDrag, onDragEnd: handleDragEnd }, children));
4528
5114
  };
4529
- const addStylesTag = (className, styleRules) => {
4530
- const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
4531
- if (existingTag) {
5115
+
5116
+ const DNDProvider = ({ children }) => {
5117
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
5118
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
5119
+ const setOnBeforeCaptureId = useDraggedItemStore((state) => state.setOnBeforeCaptureId);
5120
+ const setDraggingOnCanvas = useDraggedItemStore((state) => state.setDraggingOnCanvas);
5121
+ const updateItem = useDraggedItemStore((state) => state.updateItem);
5122
+ const { onAddComponent, onMoveComponent } = useCanvasInteractions();
5123
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
5124
+ const prevSelectedNodeId = useRef(null);
5125
+ const isTestRun = typeof window !== 'undefined' && Object.prototype.hasOwnProperty.call(window, 'Cypress');
5126
+ const beforeDragStart = ({ source }) => {
5127
+ prevSelectedNodeId.current = selectedNodeId;
5128
+ // Unselect the current node when dragging and remove the outline
5129
+ setSelectedNodeId('');
5130
+ // Set dragging state here to make sure that DnD capture phase has completed
5131
+ setDraggingOnCanvas(true);
5132
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5133
+ nodeId: '',
5134
+ });
5135
+ if (source.droppableId !== COMPONENT_LIST_ID) {
5136
+ sendMessage(OUTGOING_EVENTS.ComponentMoveStarted, undefined);
5137
+ }
5138
+ };
5139
+ const beforeCapture = ({ draggableId }) => {
5140
+ setOnBeforeCaptureId(draggableId);
5141
+ };
5142
+ const dragStart = (start) => {
5143
+ updateItem(start);
5144
+ };
5145
+ const dragUpdate = (update) => {
5146
+ updateItem(update);
5147
+ };
5148
+ const dragEnd = (dropResult) => {
5149
+ setDraggingOnCanvas(false);
5150
+ setOnBeforeCaptureId('');
5151
+ updateItem();
5152
+ SimulateDnD$1.reset();
5153
+ // If the component is being dropped onto itself, do nothing
5154
+ // This can happen from an apparent race condition where the hovering zone gets set
5155
+ // to the component after its dropped.
5156
+ if (dropResult.destination?.droppableId === dropResult.draggableId) {
5157
+ return;
5158
+ }
5159
+ if (!dropResult.destination) {
5160
+ if (!draggedItem?.destination) {
5161
+ // User cancel drag
5162
+ sendMessage(OUTGOING_EVENTS.ComponentDragCanceled, undefined);
5163
+ //select the previously selected node if drag was canceled
5164
+ if (prevSelectedNodeId.current) {
5165
+ setSelectedNodeId(prevSelectedNodeId.current);
5166
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5167
+ nodeId: prevSelectedNodeId.current,
5168
+ });
5169
+ prevSelectedNodeId.current = null;
5170
+ }
5171
+ return;
5172
+ }
5173
+ // Use the destination from the draggedItem (when clicking the canvas)
5174
+ dropResult.destination = draggedItem.destination;
5175
+ }
5176
+ // New component added to canvas
5177
+ if (dropResult.source.droppableId.startsWith('component-list')) {
5178
+ onAddComponent(dropResult);
5179
+ }
5180
+ else {
5181
+ onMoveComponent(dropResult);
5182
+ }
5183
+ // If a node was previously selected prior to dragging, re-select it
5184
+ setSelectedNodeId(dropResult.draggableId);
5185
+ sendMessage(OUTGOING_EVENTS.ComponentMoveEnded, undefined);
5186
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
5187
+ nodeId: dropResult.draggableId,
5188
+ });
5189
+ };
5190
+ 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)));
5191
+ };
5192
+
5193
+ /**
5194
+ * This hook gets the element co-ordinates of a specified element in the DOM
5195
+ * and sends the DOM Rect to the client app
5196
+ */
5197
+ const useSelectedInstanceCoordinates = ({ node }) => {
5198
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
5199
+ useEffect(() => {
5200
+ if (selectedNodeId !== node.data.id) {
5201
+ return;
5202
+ }
5203
+ // Allows the drop animation to finish before
5204
+ // calculating the components coordinates
5205
+ setTimeout(() => {
5206
+ sendSelectedComponentCoordinates(node.data.id);
5207
+ }, 10);
5208
+ }, [node, selectedNodeId]);
5209
+ const selectedElement = node.data.id
5210
+ ? document.querySelector(`[data-cf-node-id="${selectedNodeId}"]`)
5211
+ : undefined;
5212
+ return selectedElement ? getElementCoordinates(selectedElement) : null;
5213
+ };
5214
+
5215
+ /**
5216
+ * @param styles: the list of styles to apply
5217
+ * @param nodeId: the id of node that these styles will be applied to
5218
+ * @returns className: the className that was used
5219
+ * Builds and adds a style tag in the document. Returns the className to be attached to the element.
5220
+ * In editor mode the nodeId is used as the identifier in order to avoid creating endless tags as the styles are tweeked
5221
+ */
5222
+ const useEditorModeClassName = ({ styles, nodeId, }) => {
5223
+ const [className, setClassName] = useState('');
5224
+ useLayoutEffect(() => {
5225
+ if (Object.keys(styles).length === 0) {
5226
+ return;
5227
+ }
5228
+ const [newClassName, styleRules] = buildStyleTag({ styles, nodeId });
5229
+ addStylesTag(newClassName, styleRules);
5230
+ if (className !== newClassName) {
5231
+ setClassName(newClassName);
5232
+ // Clean up: remove outdated styles from DOM
5233
+ removeStylesTag(className);
5234
+ }
5235
+ }, [styles, nodeId, className]);
5236
+ return className;
5237
+ };
5238
+ const removeStylesTag = (className) => {
5239
+ const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
5240
+ if (existingTag) {
5241
+ document.head.removeChild(existingTag);
5242
+ }
5243
+ };
5244
+ const addStylesTag = (className, styleRules) => {
5245
+ const existingTag = document.querySelector(`[data-cf-styles="${className}"]`);
5246
+ if (existingTag) {
4532
5247
  existingTag.innerHTML = styleRules;
4533
5248
  return;
4534
5249
  }
@@ -4574,12 +5289,15 @@ const maybeMergePatternDefaultDesignValues = ({ variableName, variableMapping, n
4574
5289
  return variableMapping.valuesByBreakpoint;
4575
5290
  };
4576
5291
 
4577
- const useComponentProps = ({ node, entityStore, areEntitiesFetched, resolveDesignValue, definition, options, }) => {
5292
+ const useComponentProps = ({ node, entityStore, areEntitiesFetched, resolveDesignValue, renderDropzone, definition, options, userIsDragging, requiresDragWrapper, }) => {
4578
5293
  const unboundValues = useEditorStore((state) => state.unboundValues);
4579
5294
  const hyperlinkPattern = useEditorStore((state) => state.hyperLinkPattern);
4580
5295
  const locale = useEditorStore((state) => state.locale);
4581
5296
  const dataSource = useEditorStore((state) => state.dataSource);
5297
+ const draggingId = useDraggedItemStore((state) => state.onBeforeCaptureId);
5298
+ const nodeRect = useDraggedItemStore((state) => state.domRect);
4582
5299
  const findNodeById = useTreeStore((state) => state.findNodeById);
5300
+ const isEmptyZone = !node.children.length;
4583
5301
  const props = useMemo(() => {
4584
5302
  const propsBase = {
4585
5303
  cfSsrClassName: node.data.props.cfSsrClassName
@@ -4675,9 +5393,18 @@ const useComponentProps = ({ node, entityStore, areEntitiesFetched, resolveDesig
4675
5393
  return { ...acc };
4676
5394
  }
4677
5395
  }, {});
5396
+ const slotProps = {};
5397
+ if (definition.slots) {
5398
+ for (const slotId in definition.slots) {
5399
+ slotProps[slotId] = renderDropzone(node, {
5400
+ zoneId: [node.data.id, slotId].join('|'),
5401
+ });
5402
+ }
5403
+ }
4678
5404
  return {
4679
5405
  ...propsBase,
4680
5406
  ...extractedProps,
5407
+ ...slotProps,
4681
5408
  };
4682
5409
  }, [
4683
5410
  hyperlinkPattern,
@@ -4689,224 +5416,1120 @@ const useComponentProps = ({ node, entityStore, areEntitiesFetched, resolveDesig
4689
5416
  areEntitiesFetched,
4690
5417
  unboundValues,
4691
5418
  entityStore,
5419
+ renderDropzone,
4692
5420
  findNodeById,
4693
5421
  ]);
4694
- const cfStyles = useMemo(() => buildCfStyles(props), [props]);
4695
- const shouldRenderEmptySpaceWithMinSize = useMemo(() => {
4696
- if (node.children.length)
4697
- return false;
4698
- // Render with minimum height and with in those two scenarios:
4699
- if (isStructureWithRelativeHeight(node.data.blockId, cfStyles.height))
4700
- return true;
4701
- if (definition?.children)
4702
- return true;
4703
- return false;
4704
- }, [cfStyles.height, definition?.children, node.children.length, node.data.blockId]);
5422
+ const cfStyles = useMemo(() => ({
5423
+ ...buildCfStyles(props),
5424
+ // This is not handled by buildCfStyles as it requires separate disjunct media queries in preview mode
5425
+ ...transformVisibility(props.cfVisibility),
5426
+ }), [props]);
5427
+ const cfVisibility = props['cfVisibility'];
5428
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5429
+ const isSingleColumn = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
5430
+ const isStructureComponent = isContentfulStructureComponent(node?.data.blockId);
5431
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
5432
+ const { overrideStyles, wrapperStyles } = useMemo(() => {
5433
+ // Move size styles to the wrapping div and override the component styles
5434
+ const overrideStyles = {};
5435
+ const wrapperStyles = { width: options?.wrapContainerWidth };
5436
+ if (requiresDragWrapper) {
5437
+ // when element is marked by user as not-visible, on that element the node `display: none !important`
5438
+ // will be set and it will disappear. However, when such a node has a wrapper div, the wrapper
5439
+ // should not have any css properties (at least not ones which force size), as such div should
5440
+ // simply be a zero height wrapper around element with `display: none !important`.
5441
+ // Hence we guard all wrapperStyles with `cfVisibility` check.
5442
+ if (cfVisibility && cfStyles.width)
5443
+ wrapperStyles.width = cfStyles.width;
5444
+ if (cfVisibility && cfStyles.height)
5445
+ wrapperStyles.height = cfStyles.height;
5446
+ if (cfVisibility && cfStyles.maxWidth)
5447
+ wrapperStyles.maxWidth = cfStyles.maxWidth;
5448
+ if (cfVisibility && cfStyles.margin)
5449
+ wrapperStyles.margin = cfStyles.margin;
5450
+ }
5451
+ // Override component styles to fill the wrapper
5452
+ if (wrapperStyles.width)
5453
+ overrideStyles.width = '100%';
5454
+ if (wrapperStyles.height)
5455
+ overrideStyles.height = '100%';
5456
+ if (wrapperStyles.margin)
5457
+ overrideStyles.margin = '0';
5458
+ if (wrapperStyles.maxWidth)
5459
+ overrideStyles.maxWidth = 'none';
5460
+ // Prevent the dragging element from changing sizes when it has a percentage width or height
5461
+ if (draggingId === node.data.id && nodeRect) {
5462
+ if (requiresDragWrapper) {
5463
+ if (isPercentValue(cfStyles.width))
5464
+ wrapperStyles.maxWidth = nodeRect.width;
5465
+ if (isPercentValue(cfStyles.height))
5466
+ wrapperStyles.maxHeight = nodeRect.height;
5467
+ }
5468
+ else {
5469
+ if (isPercentValue(cfStyles.width))
5470
+ overrideStyles.maxWidth = nodeRect.width;
5471
+ if (isPercentValue(cfStyles.height))
5472
+ overrideStyles.maxHeight = nodeRect.height;
5473
+ }
5474
+ }
5475
+ return { overrideStyles, wrapperStyles };
5476
+ }, [
5477
+ cfStyles,
5478
+ options?.wrapContainerWidth,
5479
+ requiresDragWrapper,
5480
+ node.data.id,
5481
+ draggingId,
5482
+ nodeRect,
5483
+ cfVisibility,
5484
+ ]);
4705
5485
  // Styles that will be applied to the component element
5486
+ // This has to be memoized to avoid recreating the styles in useEditorModeClassName on every render
4706
5487
  const componentStyles = useMemo(() => ({
4707
5488
  ...cfStyles,
4708
- ...(shouldRenderEmptySpaceWithMinSize && {
4709
- minHeight: EMPTY_CONTAINER_SIZE,
4710
- minWidth: EMPTY_CONTAINER_SIZE,
5489
+ ...overrideStyles,
5490
+ ...(isEmptyZone &&
5491
+ isStructureWithRelativeHeight(node?.data.blockId, cfStyles.height) && {
5492
+ minHeight: EMPTY_CONTAINER_HEIGHT,
5493
+ }),
5494
+ ...(userIsDragging &&
5495
+ isStructureComponent &&
5496
+ !isSingleColumn &&
5497
+ !isAssemblyBlock && {
5498
+ padding: addExtraDropzonePadding(cfStyles.padding?.toString() || '0 0 0 0'),
4711
5499
  }),
4712
- }), [cfStyles, shouldRenderEmptySpaceWithMinSize]);
4713
- const cfCsrClassName = useEditorModeClassName({
5500
+ }), [
5501
+ cfStyles,
5502
+ isAssemblyBlock,
5503
+ isEmptyZone,
5504
+ isSingleColumn,
5505
+ isStructureComponent,
5506
+ node?.data.blockId,
5507
+ overrideStyles,
5508
+ userIsDragging,
5509
+ ]);
5510
+ const componentClass = useEditorModeClassName({
4714
5511
  styles: componentStyles,
4715
5512
  nodeId: node.data.id,
4716
5513
  });
4717
- const componentProps = useMemo(() => {
4718
- const sharedProps = {
4719
- 'data-cf-node-id': node.data.id,
4720
- 'data-cf-node-block-id': node.data.blockId,
4721
- 'data-cf-node-block-type': node.type,
4722
- className: props.cfSsrClassName ?? cfCsrClassName,
5514
+ const sharedProps = {
5515
+ 'data-cf-node-id': node.data.id,
5516
+ 'data-cf-node-block-id': node.data.blockId,
5517
+ 'data-cf-node-block-type': node.type,
5518
+ className: props.cfSsrClassName ?? componentClass,
5519
+ ...(definition?.children ? { children: renderDropzone(node) } : {}),
5520
+ };
5521
+ const customComponentProps = {
5522
+ ...sharedProps,
5523
+ // Allows custom components to render differently in the editor. This needs to be activated
5524
+ // through options as the component has to be aware of this prop to not cause any React warnings.
5525
+ ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
5526
+ ...sanitizeNodeProps(props),
5527
+ };
5528
+ const structuralOrPatternComponentProps = {
5529
+ ...sharedProps,
5530
+ editorMode: true,
5531
+ node,
5532
+ renderDropzone,
5533
+ };
5534
+ return {
5535
+ componentProps: isStructureComponent || isPatternNode
5536
+ ? structuralOrPatternComponentProps
5537
+ : customComponentProps,
5538
+ componentStyles,
5539
+ wrapperStyles,
5540
+ };
5541
+ };
5542
+ const addExtraDropzonePadding = (padding) => padding
5543
+ .split(' ')
5544
+ .map((value) => parseFloat(value) === 0 ? `${DRAG_PADDING}px` : `calc(${value} + ${DRAG_PADDING}px)`)
5545
+ .join(' ');
5546
+ const isPercentValue = (value) => typeof value === 'string' && value.endsWith('%');
5547
+
5548
+ class ImportedComponentError extends Error {
5549
+ constructor(message) {
5550
+ super(message);
5551
+ this.name = 'ImportedComponentError';
5552
+ }
5553
+ }
5554
+ class ExperienceSDKError extends Error {
5555
+ constructor(message) {
5556
+ super(message);
5557
+ this.name = 'ExperienceSDKError';
5558
+ }
5559
+ }
5560
+ class ImportedComponentErrorBoundary extends React.Component {
5561
+ componentDidCatch(error, _errorInfo) {
5562
+ if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
5563
+ // This error was already handled by a nested error boundary and should be passed upwards
5564
+ // We have to do this as we wrap every component on every layer with this error boundary and
5565
+ // thus an error deep in the tree bubbles through many layers of error boundaries.
5566
+ throw error;
5567
+ }
5568
+ // Differentiate between custom and SDK-provided components for error tracking
5569
+ const ErrorClass = isContentfulComponent(this.props.componentId)
5570
+ ? ExperienceSDKError
5571
+ : ImportedComponentError;
5572
+ const err = new ErrorClass(error.message);
5573
+ err.stack = error.stack;
5574
+ throw err;
5575
+ }
5576
+ render() {
5577
+ return this.props.children;
5578
+ }
5579
+ }
5580
+
5581
+ const MissingComponentPlaceholder = ({ blockId }) => {
5582
+ return (React.createElement("div", { style: {
5583
+ border: '1px solid red',
5584
+ width: '100%',
5585
+ height: '100%',
5586
+ } },
5587
+ "Missing component '",
5588
+ blockId,
5589
+ "'"));
5590
+ };
5591
+
5592
+ const CircularDependencyErrorPlaceholder = forwardRef(({ wrappingPatternIds, ...props }, ref) => {
5593
+ return (React.createElement("div", { ...props,
5594
+ // Pass through ref to avoid DND errors being logged
5595
+ ref: ref, "data-cf-node-error": "circular-pattern-dependency", style: {
5596
+ border: '1px solid red',
5597
+ background: 'rgba(255, 0, 0, 0.1)',
5598
+ padding: '1rem 1rem 0 1rem',
5599
+ width: '100%',
5600
+ height: '100%',
5601
+ } },
5602
+ "Circular usage of patterns detected:",
5603
+ React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
5604
+ const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
5605
+ const entry = inMemoryEntities.maybeResolveLink(entryLink);
5606
+ const entryTitle = entry?.fields?.title;
5607
+ const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
5608
+ return React.createElement("li", { key: patternId }, text);
5609
+ }))));
5610
+ });
5611
+ CircularDependencyErrorPlaceholder.displayName = 'CircularDependencyErrorPlaceholder';
5612
+
5613
+ const useComponent = ({ node, entityStore, areEntitiesFetched, resolveDesignValue, renderDropzone, userIsDragging, wrappingPatternIds, }) => {
5614
+ const tree = useTreeStore((state) => state.tree);
5615
+ const componentRegistration = useMemo(() => {
5616
+ let registration = componentRegistry.get(node.data.blockId);
5617
+ if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
5618
+ registration = createAssemblyRegistration({
5619
+ definitionId: node.data.blockId,
5620
+ component: Assembly,
5621
+ });
5622
+ }
5623
+ if (!registration) {
5624
+ 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.`);
5625
+ return undefined;
5626
+ }
5627
+ return registration;
5628
+ }, [node]);
5629
+ const componentId = node.data.id;
5630
+ const isPatternNode = node.type === ASSEMBLY_NODE_TYPE;
5631
+ const isPatternComponent = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
5632
+ const parentComponentNode = getItem({ id: node.parentId }, tree);
5633
+ const isNestedPattern = isPatternNode &&
5634
+ [ASSEMBLY_BLOCK_NODE_TYPE, ASSEMBLY_NODE_TYPE].includes(parentComponentNode?.type ?? '');
5635
+ const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
5636
+ const requiresDragWrapper = !isPatternNode && !isStructureComponent && !componentRegistration?.options?.wrapComponent;
5637
+ const { componentProps, wrapperStyles } = useComponentProps({
5638
+ node,
5639
+ entityStore,
5640
+ areEntitiesFetched,
5641
+ resolveDesignValue,
5642
+ renderDropzone,
5643
+ definition: componentRegistration?.definition,
5644
+ options: componentRegistration?.options,
5645
+ userIsDragging,
5646
+ requiresDragWrapper,
5647
+ });
5648
+ const elementToRender = (props) => {
5649
+ const { dragProps = {} } = props || {};
5650
+ const { children, innerRef, Tag = 'div', ToolTipAndPlaceholder, style, ...rest } = dragProps;
5651
+ const { 'data-cf-node-block-id': dataCfNodeBlockId, 'data-cf-node-block-type': dataCfNodeBlockType, 'data-cf-node-id': dataCfNodeId, } = componentProps;
5652
+ const refCallback = (refNode) => {
5653
+ if (innerRef && refNode)
5654
+ innerRef(refNode);
4723
5655
  };
4724
- // Only pass `editorMode` and `node` to structure components and assembly root nodes.
4725
- const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
4726
- if (isStructureComponent) {
4727
- return {
4728
- ...sharedProps,
4729
- editorMode: true,
4730
- node,
4731
- };
5656
+ if (!componentRegistration) {
5657
+ return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
5658
+ }
5659
+ if (node.data.blockId && wrappingPatternIds.has(node.data.blockId)) {
5660
+ 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 }));
4732
5661
  }
5662
+ const element = React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, {
5663
+ ...componentProps,
5664
+ dragProps,
5665
+ }));
5666
+ if (!requiresDragWrapper) {
5667
+ return element;
5668
+ }
5669
+ 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 },
5670
+ ToolTipAndPlaceholder,
5671
+ element));
5672
+ };
5673
+ return {
5674
+ node,
5675
+ parentComponentNode,
5676
+ isAssembly: isPatternNode,
5677
+ isPatternNode,
5678
+ isPatternComponent,
5679
+ isNestedPattern,
5680
+ componentId,
5681
+ elementToRender,
5682
+ definition: componentRegistration?.definition,
5683
+ };
5684
+ };
5685
+
5686
+ const calcOffsetLeft = (parentElement, placeholderWidth, nodeWidth) => {
5687
+ if (!parentElement) {
5688
+ return 0;
5689
+ }
5690
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5691
+ if (alignItems === 'center') {
5692
+ return -(placeholderWidth - nodeWidth) / 2;
5693
+ }
5694
+ if (alignItems === 'end') {
5695
+ return -placeholderWidth + nodeWidth + 2;
5696
+ }
5697
+ return 0;
5698
+ };
5699
+ const calcOffsetTop = (parentElement, placeholderHeight, nodeHeight) => {
5700
+ if (!parentElement) {
5701
+ return 0;
5702
+ }
5703
+ const alignItems = window.getComputedStyle(parentElement).alignItems;
5704
+ if (alignItems === 'center') {
5705
+ return -(placeholderHeight - nodeHeight) / 2;
5706
+ }
5707
+ if (alignItems === 'end') {
5708
+ return -placeholderHeight + nodeHeight + 2;
5709
+ }
5710
+ return 0;
5711
+ };
5712
+ const getPaddingOffset = (element) => {
5713
+ const paddingLeft = parseFloat(window.getComputedStyle(element).paddingLeft);
5714
+ const paddingRight = parseFloat(window.getComputedStyle(element).paddingRight);
5715
+ const paddingTop = parseFloat(window.getComputedStyle(element).paddingTop);
5716
+ const paddingBottom = parseFloat(window.getComputedStyle(element).paddingBottom);
5717
+ const horizontalOffset = paddingLeft + paddingRight;
5718
+ const verticalOffset = paddingTop + paddingBottom;
5719
+ return [horizontalOffset, verticalOffset];
5720
+ };
5721
+ /**
5722
+ * Calculate the size and position of the dropzone indicator
5723
+ * when dragging a new component onto the canvas
5724
+ */
5725
+ const calcNewComponentStyles = (params) => {
5726
+ const { destinationIndex, elementIndex, dropzoneElementId, id, direction, totalIndexes } = params;
5727
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5728
+ const isHorizontal = direction === 'horizontal';
5729
+ const isRightAlign = isHorizontal && isEnd;
5730
+ const isBottomAlign = !isHorizontal && isEnd;
5731
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5732
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5733
+ if (!dropzone || !element) {
5734
+ return emptyStyles;
5735
+ }
5736
+ const elementSizes = element.getBoundingClientRect();
5737
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5738
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5739
+ const width = isHorizontal ? DRAGGABLE_WIDTH : dropzoneSizes.width - horizontalPadding;
5740
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : DRAGGABLE_HEIGHT;
5741
+ const top = isHorizontal
5742
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5743
+ : -height;
5744
+ const left = isHorizontal
5745
+ ? -width
5746
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5747
+ return {
5748
+ width,
5749
+ height,
5750
+ top: !isBottomAlign ? top : 'unset',
5751
+ right: isRightAlign ? -width : 'unset',
5752
+ bottom: isBottomAlign ? -height : 'unset',
5753
+ left: !isRightAlign ? left : 'unset',
5754
+ };
5755
+ };
5756
+ /**
5757
+ * Calculate the size and position of the dropzone indicator
5758
+ * when moving an existing component on the canvas
5759
+ */
5760
+ const calcMovementStyles = (params) => {
5761
+ const { destinationIndex, sourceIndex, destinationId, sourceId, elementIndex, dropzoneElementId, id, direction, totalIndexes, draggableId, } = params;
5762
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5763
+ const isHorizontal = direction === 'horizontal';
5764
+ const isSameZone = destinationId === sourceId;
5765
+ const isBelowSourceIndex = destinationIndex > sourceIndex;
5766
+ const isRightAlign = isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5767
+ const isBottomAlign = !isHorizontal && (isEnd || (isSameZone && isBelowSourceIndex));
5768
+ const dropzone = document.querySelector(`[data-rfd-droppable-id="${dropzoneElementId}"]`);
5769
+ const draggable = document.querySelector(`[data-rfd-draggable-id="${draggableId}"]`);
5770
+ const element = document.querySelector(`[data-ctfl-draggable-id="${id}"]`);
5771
+ if (!dropzone || !element || !draggable) {
5772
+ return emptyStyles;
5773
+ }
5774
+ const elementSizes = element.getBoundingClientRect();
5775
+ const dropzoneSizes = dropzone.getBoundingClientRect();
5776
+ const draggableSizes = draggable.getBoundingClientRect();
5777
+ const [horizontalPadding, verticalPadding] = getPaddingOffset(dropzone);
5778
+ const width = isHorizontal ? draggableSizes.width : dropzoneSizes.width - horizontalPadding;
5779
+ const height = isHorizontal ? dropzoneSizes.height - verticalPadding : draggableSizes.height;
5780
+ const top = isHorizontal
5781
+ ? calcOffsetTop(element.parentElement, height, elementSizes.height)
5782
+ : -height;
5783
+ const left = isHorizontal
5784
+ ? -width
5785
+ : calcOffsetLeft(element.parentElement, width, elementSizes.width);
5786
+ return {
5787
+ width,
5788
+ height,
5789
+ top: !isBottomAlign ? top : 'unset',
5790
+ right: isRightAlign ? -width : 'unset',
5791
+ bottom: isBottomAlign ? -height : 'unset',
5792
+ left: !isRightAlign ? left : 'unset',
5793
+ };
5794
+ };
5795
+ const emptyStyles = { width: 0, height: 0 };
5796
+ const calcPlaceholderStyles = (params) => {
5797
+ const { isDraggingOver, sourceId } = params;
5798
+ if (!isDraggingOver) {
5799
+ return emptyStyles;
5800
+ }
5801
+ if (sourceId === COMPONENT_LIST_ID) {
5802
+ return calcNewComponentStyles(params);
5803
+ }
5804
+ return calcMovementStyles(params);
5805
+ };
5806
+ const Placeholder = (props) => {
5807
+ const sourceIndex = useDraggedItemStore((state) => state.draggedItem?.source.index) ?? -1;
5808
+ const draggableId = useDraggedItemStore((state) => state.draggedItem?.draggableId) ?? '';
5809
+ const sourceId = useDraggedItemStore((state) => state.draggedItem?.source.droppableId) ?? '';
5810
+ const destinationIndex = useDraggedItemStore((state) => state.draggedItem?.destination?.index) ?? -1;
5811
+ const destinationId = useDraggedItemStore((state) => state.draggedItem?.destination?.droppableId) ?? '';
5812
+ const { elementIndex, totalIndexes, isDraggingOver } = props;
5813
+ const isActive = destinationIndex === elementIndex;
5814
+ const isEnd = destinationIndex === totalIndexes && elementIndex === totalIndexes - 1;
5815
+ const isVisible = isEnd || isActive;
5816
+ const isComponentList = destinationId === COMPONENT_LIST_ID;
5817
+ return (!isComponentList &&
5818
+ isDraggingOver &&
5819
+ isVisible && (React.createElement("div", { style: {
5820
+ ...calcPlaceholderStyles({
5821
+ ...props,
5822
+ sourceId,
5823
+ sourceIndex,
5824
+ destinationId,
5825
+ destinationIndex,
5826
+ draggableId,
5827
+ }),
5828
+ backgroundColor: 'rgba(var(--exp-builder-blue300-rgb), 0.5)',
5829
+ position: 'absolute',
5830
+ pointerEvents: 'none',
5831
+ } })));
5832
+ };
5833
+
5834
+ var css_248z$2 = ".styles-module_hitbox__i3wKV {\n position: fixed;\n pointer-events: all;\n}\n";
5835
+ var styles$2 = {"hitbox":"styles-module_hitbox__i3wKV"};
5836
+ styleInject(css_248z$2);
5837
+
5838
+ const useZoneStore = create()((set) => ({
5839
+ zones: {},
5840
+ hoveringZone: '',
5841
+ setHoveringZone(zoneId) {
5842
+ set({
5843
+ hoveringZone: zoneId,
5844
+ });
5845
+ },
5846
+ upsertZone(id, data) {
5847
+ set(produce((state) => {
5848
+ state.zones[id] = { ...(state.zones[id] || {}), ...data };
5849
+ }));
5850
+ },
5851
+ }));
5852
+
5853
+ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_HEIGHT, DEEP_ZONE } = HITBOX;
5854
+ const calcOffsetDepth = (depth) => {
5855
+ return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
5856
+ };
5857
+ const getHitboxStyles = ({ direction, zoneDepth, domRect, scrollY, offsetRect, }) => {
5858
+ if (!domRect) {
4733
5859
  return {
4734
- ...sharedProps,
4735
- // Allows custom components to render differently in the editor. This needs to be activated
4736
- // through options as the component has to be aware of this prop to not cause any React warnings.
4737
- ...(options?.enableCustomEditorView ? { isInExpEditorMode: true } : {}),
4738
- ...sanitizeNodeProps(props),
5860
+ display: 'none',
4739
5861
  };
4740
- }, [cfCsrClassName, node, options?.enableCustomEditorView, props]);
4741
- return { componentProps };
5862
+ }
5863
+ const { width, height, top, left, bottom, right } = domRect;
5864
+ const { height: offsetHeight, width: offsetWidth } = offsetRect || { height: 0, width: 0 };
5865
+ const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
5866
+ const isDeepZone = zoneDepth > DEEP_ZONE;
5867
+ const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
5868
+ switch (direction) {
5869
+ case HitboxDirection.TOP:
5870
+ return {
5871
+ width,
5872
+ height: HEIGHT,
5873
+ top: top + offsetHeight - calcOffsetDepth(zoneDepth) - scrollY,
5874
+ left,
5875
+ zIndex: 100 + zoneDepth,
5876
+ };
5877
+ case HitboxDirection.BOTTOM:
5878
+ return {
5879
+ width,
5880
+ height: HEIGHT,
5881
+ top: bottom + offsetHeight + calcOffsetDepth(zoneDepth) - scrollY,
5882
+ left,
5883
+ zIndex: 100 + zoneDepth,
5884
+ };
5885
+ case HitboxDirection.LEFT:
5886
+ return {
5887
+ width: WIDTH,
5888
+ height: height - HEIGHT,
5889
+ left: left + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
5890
+ top: top + HEIGHT / 2 - scrollY,
5891
+ zIndex: 100 + zoneDepth,
5892
+ };
5893
+ case HitboxDirection.RIGHT:
5894
+ return {
5895
+ width: WIDTH,
5896
+ height: height - HEIGHT,
5897
+ left: right + offsetWidth + calcOffsetDepth(zoneDepth) - WIDTH / 2,
5898
+ top: top + HEIGHT / 2 - scrollY,
5899
+ zIndex: 100 + zoneDepth,
5900
+ };
5901
+ case HitboxDirection.SELF_VERTICAL: {
5902
+ if (isAboveMaxHeight && !isDeepZone) {
5903
+ return { display: 'none' };
5904
+ }
5905
+ const selfHeight = isDeepZone ? MIN_DEPTH_HEIGHT : MIN_HEIGHT;
5906
+ return {
5907
+ width,
5908
+ height: selfHeight,
5909
+ left,
5910
+ top: top + height / 2 - selfHeight / 2 - scrollY,
5911
+ zIndex: 1000 + zoneDepth,
5912
+ };
5913
+ }
5914
+ case HitboxDirection.SELF_HORIZONTAL: {
5915
+ if (width > DRAGGABLE_WIDTH) {
5916
+ return { display: 'none' };
5917
+ }
5918
+ return {
5919
+ width: width - DRAGGABLE_WIDTH * 2,
5920
+ height,
5921
+ left: left + DRAGGABLE_WIDTH,
5922
+ top: top - scrollY,
5923
+ zIndex: 1000 + zoneDepth,
5924
+ };
5925
+ }
5926
+ default:
5927
+ return {};
5928
+ }
4742
5929
  };
4743
5930
 
4744
- function EditorBlock({ node, resolveDesignValue, wrappingPatternIds: parentWrappingPatternIds = new Set(), entityStore, areEntitiesFetched, }) {
4745
- const isRootAssemblyNode = node.type === ASSEMBLY_NODE_TYPE;
4746
- const wrappingPatternIds = useMemo(() => {
4747
- if (isRootAssemblyNode && node.data.blockId) {
4748
- return new Set([node.data.blockId, ...parentWrappingPatternIds]);
5931
+ const Hitboxes = ({ zoneId, parentZoneId, isEmptyZone }) => {
5932
+ const tree = useTreeStore((state) => state.tree);
5933
+ const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
5934
+ const scrollY = useDraggedItemStore((state) => state.scrollY);
5935
+ const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
5936
+ const zones = useZoneStore((state) => state.zones);
5937
+ const hoveringZone = useZoneStore((state) => state.hoveringZone);
5938
+ const isHoveringZone = hoveringZone === zoneId;
5939
+ const hitboxContainer = useMemo(() => {
5940
+ return document.querySelector('[data-ctfl-hitboxes]');
5941
+ }, []);
5942
+ const domRect = useMemo(() => {
5943
+ if (!isDraggingOnCanvas)
5944
+ return;
5945
+ return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
5946
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5947
+ }, [zoneId, isDraggingOnCanvas]);
5948
+ // Use the size of the cloned dragging element to offset the position of the hitboxes
5949
+ // So that when dragging causes a dropzone to expand, the hitboxes will be in the correct position
5950
+ const offsetRect = useMemo(() => {
5951
+ if (!isDraggingOnCanvas || isEmptyZone || !isHoveringZone)
5952
+ return;
5953
+ return document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`)?.getBoundingClientRect();
5954
+ // eslint-disable-next-line react-hooks/exhaustive-deps
5955
+ }, [isEmptyZone, isHoveringZone, isDraggingOnCanvas]);
5956
+ const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
5957
+ const isVertical = zoneDirection === 'vertical';
5958
+ const isRoot = parentZoneId === ROOT_ID;
5959
+ const { slotId: parentSlotId } = parseZoneId(parentZoneId);
5960
+ const getStyles = useCallback((direction) => getHitboxStyles({
5961
+ direction,
5962
+ zoneDepth,
5963
+ domRect,
5964
+ scrollY,
5965
+ offsetRect,
5966
+ }), [zoneDepth, domRect, scrollY, offsetRect]);
5967
+ const renderFinalRootHitbox = () => {
5968
+ if (!isRoot)
5969
+ return null;
5970
+ return (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) }));
5971
+ };
5972
+ const renderSurroundingHitboxes = () => {
5973
+ if (isRoot || parentSlotId)
5974
+ return null;
5975
+ return (React.createElement(React.Fragment, null,
5976
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
5977
+ React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })));
5978
+ };
5979
+ const ActiveHitboxes = (React.createElement(React.Fragment, null,
5980
+ React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
5981
+ renderSurroundingHitboxes(),
5982
+ renderFinalRootHitbox()));
5983
+ if (!hitboxContainer) {
5984
+ return null;
5985
+ }
5986
+ return createPortal(ActiveHitboxes, hitboxContainer);
5987
+ };
5988
+
5989
+ const isRelativePreviewSize = (width) => {
5990
+ // For now, we solely allow 100% as relative value
5991
+ return width === '100%';
5992
+ };
5993
+ const getTooltipPositions = ({ previewSize, tooltipRect, coordinates, }) => {
5994
+ if (!coordinates || !tooltipRect) {
5995
+ return { display: 'none' };
5996
+ }
5997
+ /**
5998
+ * By default, the tooltip floats to the left of the element
5999
+ */
6000
+ const newTooltipStyles = { display: 'flex' };
6001
+ // If the preview size is relative, we don't change the floating direction
6002
+ if (!isRelativePreviewSize(previewSize)) {
6003
+ const previewSizeMatch = previewSize.match(/(\d{1,})px/);
6004
+ if (!previewSizeMatch) {
6005
+ return { display: 'none' };
6006
+ }
6007
+ const previewSizePx = parseInt(previewSizeMatch[1]);
6008
+ /**
6009
+ * If the element is at the right edge of the canvas, and the element isn't wide enough to fit the tooltip width,
6010
+ * we float the tooltip to the right of the element.
6011
+ */
6012
+ if (tooltipRect.width > previewSizePx - coordinates.right &&
6013
+ tooltipRect.width > coordinates.width) {
6014
+ newTooltipStyles['float'] = 'right';
4749
6015
  }
4750
- return parentWrappingPatternIds;
4751
- }, [isRootAssemblyNode, node, parentWrappingPatternIds]);
4752
- const componentRegistration = useComponentRegistration(node);
4753
- if (!componentRegistration) {
4754
- return React.createElement(MissingComponentPlaceholder, { blockId: node.data.blockId });
4755
- }
4756
- if (isRootAssemblyNode && node.data.blockId && parentWrappingPatternIds.has(node.data.blockId)) {
4757
- return React.createElement(CircularDependencyErrorPlaceholder, { wrappingPatternIds: wrappingPatternIds });
4758
- }
4759
- const slotNodes = {};
4760
- for (const slotId in componentRegistration.definition.slots) {
4761
- const nodes = node.children.filter((child) => child.data.slotId === slotId);
4762
- slotNodes[slotId] =
4763
- 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, entityStore: entityStore, areEntitiesFetched: areEntitiesFetched })))));
4764
- }
4765
- const children = componentRegistration.definition.children
4766
- ? node.children
4767
- .filter((node) => node.data.slotId === undefined)
4768
- .map((childNode) => (React.createElement(EditorBlock, { key: childNode.data.id, node: childNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds, entityStore: entityStore, areEntitiesFetched: areEntitiesFetched })))
4769
- : null;
4770
- return (React.createElement(RegistrationComponent, { node: node, resolveDesignValue: resolveDesignValue, componentRegistration: componentRegistration, slotNodes: slotNodes, entityStore: entityStore, areEntitiesFetched: areEntitiesFetched }, children));
6016
+ }
6017
+ const tooltipHeight = tooltipRect.height === 0 ? 32 : tooltipRect.height;
6018
+ /**
6019
+ * For elements with small heights, we don't want the tooltip covering the content in the element,
6020
+ * so we show the tooltip at the top or bottom.
6021
+ */
6022
+ if (tooltipHeight * 2 > coordinates.height) {
6023
+ /**
6024
+ * If there's enough space for the tooltip at the top of the element, we show the tooltip at the top of the element,
6025
+ * else we show the tooltip at the bottom.
6026
+ */
6027
+ if (tooltipHeight < coordinates.top) {
6028
+ newTooltipStyles['bottom'] = coordinates.height;
6029
+ }
6030
+ else {
6031
+ newTooltipStyles['top'] = coordinates.height;
6032
+ }
6033
+ }
6034
+ /**
6035
+ * If the component draws outside of the borders of the canvas to the left we move the tooltip to the right
6036
+ * so that it is fully visible.
6037
+ */
6038
+ if (coordinates.left < 0) {
6039
+ newTooltipStyles['left'] = -coordinates.left;
6040
+ }
6041
+ /**
6042
+ * If for any reason, the element's top is negative, we show the tooltip at the bottom
6043
+ */
6044
+ if (coordinates.top < 0) {
6045
+ newTooltipStyles['top'] = coordinates.height;
6046
+ }
6047
+ return newTooltipStyles;
6048
+ };
6049
+
6050
+ 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";
6051
+ 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"};
6052
+ styleInject(css_248z$1);
6053
+
6054
+ const Tooltip = ({ coordinates, id, label, isAssemblyBlock, isContainer, isSelected, }) => {
6055
+ const tooltipRef = useRef(null);
6056
+ const previewSize = '100%'; // This should be based on breakpoints and added to usememo dependency array
6057
+ const tooltipStyles = useMemo(() => {
6058
+ const tooltipRect = tooltipRef.current?.getBoundingClientRect();
6059
+ const draggableRect = document
6060
+ .querySelector(`[data-ctfl-draggable-id="${id}"]`)
6061
+ ?.getBoundingClientRect();
6062
+ const newTooltipStyles = getTooltipPositions({
6063
+ previewSize,
6064
+ tooltipRect,
6065
+ coordinates: draggableRect,
6066
+ });
6067
+ return newTooltipStyles;
6068
+ // 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
6069
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6070
+ }, [coordinates, id, tooltipRef.current]);
6071
+ if (isSelected) {
6072
+ return null;
6073
+ }
6074
+ return (React.createElement("div", { "data-tooltip": true, className: styles$1.tooltipWrapper },
6075
+ React.createElement("div", { "data-tooltip": true, ref: tooltipRef, style: tooltipStyles, className: classNames(styles$1.overlay, {
6076
+ [styles$1.overlayContainer]: isContainer,
6077
+ [styles$1.overlayAssembly]: isAssemblyBlock,
6078
+ }) }, label)));
6079
+ };
6080
+
6081
+ function useSingleColumn(node, resolveDesignValue) {
6082
+ const tree = useTreeStore((store) => store.tree);
6083
+ const isSingleColumn = node.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
6084
+ const isWrapped = useMemo(() => {
6085
+ if (!node.parentId || !isSingleColumn) {
6086
+ return false;
6087
+ }
6088
+ const parentNode = getItem({ id: node.parentId }, tree);
6089
+ if (!parentNode || parentNode.data.blockId !== CONTENTFUL_COMPONENTS$1.columns.id) {
6090
+ return false;
6091
+ }
6092
+ const { cfWrapColumns } = parentNode.data.props;
6093
+ if (cfWrapColumns?.type !== 'DesignValue') {
6094
+ return false;
6095
+ }
6096
+ return resolveDesignValue(cfWrapColumns.valuesByBreakpoint);
6097
+ }, [tree, node, isSingleColumn, resolveDesignValue]);
6098
+ return {
6099
+ isSingleColumn,
6100
+ isWrapped,
6101
+ };
4771
6102
  }
4772
- const RegistrationComponent = ({ node, resolveDesignValue, componentRegistration, slotNodes, children, entityStore, areEntitiesFetched, }) => {
4773
- const { componentProps } = useComponentProps({
4774
- node,
4775
- resolveDesignValue,
4776
- definition: componentRegistration.definition,
4777
- options: componentRegistration.options,
6103
+
6104
+ function getStyle$1(style, snapshot) {
6105
+ if (!snapshot.isDropAnimating) {
6106
+ return style;
6107
+ }
6108
+ return {
6109
+ ...style,
6110
+ // cannot be 0, but make it super tiny
6111
+ transitionDuration: `0.001s`,
6112
+ };
6113
+ }
6114
+ const EditorBlock = ({ node: rawNode, entityStore, areEntitiesFetched, resolveDesignValue, renderDropzone, index, zoneId, userIsDragging, placeholder, wrappingPatternIds, }) => {
6115
+ const { slotId } = parseZoneId(zoneId);
6116
+ const ref = useRef(null);
6117
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
6118
+ const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
6119
+ const { node, componentId, elementToRender, definition, isPatternNode, isPatternComponent, isNestedPattern, } = useComponent({
6120
+ node: rawNode,
4778
6121
  entityStore,
4779
6122
  areEntitiesFetched,
6123
+ resolveDesignValue,
6124
+ renderDropzone,
6125
+ userIsDragging,
6126
+ wrappingPatternIds,
6127
+ });
6128
+ const { isSingleColumn, isWrapped } = useSingleColumn(node, resolveDesignValue);
6129
+ const setDomRect = useDraggedItemStore((state) => state.setDomRect);
6130
+ const isHoveredComponent = useDraggedItemStore((state) => state.hoveredComponentId === componentId);
6131
+ const coordinates = useSelectedInstanceCoordinates({ node });
6132
+ const displayName = node.data.displayName || rawNode.data.displayName || definition?.name;
6133
+ const testId = `draggable-${node.data.blockId ?? 'node'}`;
6134
+ const isSelected = node.data.id === selectedNodeId;
6135
+ const isContainer = node.data.blockId === CONTENTFUL_COMPONENTS$1.container.id;
6136
+ const isSlotComponent = Boolean(node.data.slotId);
6137
+ const isDragDisabled = isNestedPattern || isPatternComponent || (isSingleColumn && isWrapped) || isSlotComponent;
6138
+ const isEmptyZone = useMemo(() => {
6139
+ return !node.children.filter((node) => node.data.slotId === slotId).length;
6140
+ }, [node.children, slotId]);
6141
+ useDraggablePosition({
6142
+ draggableId: componentId,
6143
+ draggableRef: ref,
6144
+ position: DraggablePosition.MOUSE_POSITION,
4780
6145
  });
4781
- return React.createElement(ImportedComponentErrorBoundary, { componentId: node.data.blockId }, React.createElement(componentRegistration.component, { ...componentProps, ...slotNodes }, children));
6146
+ const onClick = (e) => {
6147
+ e.stopPropagation();
6148
+ if (!userIsDragging) {
6149
+ setSelectedNodeId(node.data.id);
6150
+ // if it is the assembly directly we just want to select it as a normal component
6151
+ if (isPatternNode) {
6152
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
6153
+ nodeId: node.data.id,
6154
+ });
6155
+ return;
6156
+ }
6157
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
6158
+ assembly: node.data.assembly,
6159
+ nodeId: node.data.id,
6160
+ });
6161
+ }
6162
+ };
6163
+ const onMouseOver = (e) => {
6164
+ e.stopPropagation();
6165
+ if (userIsDragging)
6166
+ return;
6167
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {
6168
+ nodeId: componentId,
6169
+ });
6170
+ };
6171
+ const onMouseDown = (e) => {
6172
+ if (isDragDisabled) {
6173
+ return;
6174
+ }
6175
+ e.stopPropagation();
6176
+ setDomRect(e.currentTarget.getBoundingClientRect());
6177
+ };
6178
+ const ToolTipAndPlaceholder = (React.createElement(React.Fragment, null,
6179
+ React.createElement(Tooltip, { id: componentId, coordinates: coordinates, isAssemblyBlock: isPatternNode || isPatternComponent, isContainer: isContainer, isSelected: isSelected, label: displayName || 'No label specified' }),
6180
+ React.createElement(Placeholder, { ...placeholder, id: componentId }),
6181
+ userIsDragging && !isPatternComponent && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
6182
+ return (React.createElement(Draggable, { key: componentId, draggableId: componentId, index: index, isDragDisabled: isDragDisabled, disableInteractiveElementBlocking: true }, (provided, snapshot) => elementToRender({
6183
+ dragProps: {
6184
+ ...provided.draggableProps,
6185
+ ...provided.dragHandleProps,
6186
+ 'data-ctfl-draggable-id': componentId,
6187
+ 'data-test-id': testId,
6188
+ innerRef: (refNode) => {
6189
+ provided?.innerRef(refNode);
6190
+ ref.current = refNode;
6191
+ },
6192
+ className: classNames(styles$1.DraggableComponent, {
6193
+ [styles$1.isAssemblyBlock]: isPatternComponent || isPatternNode,
6194
+ [styles$1.isDragging]: snapshot?.isDragging || userIsDragging,
6195
+ [styles$1.isSelected]: isSelected,
6196
+ [styles$1.isHoveringComponent]: isHoveredComponent,
6197
+ }),
6198
+ style: getStyle$1(provided.draggableProps.style, snapshot),
6199
+ onMouseDown,
6200
+ onMouseOver,
6201
+ onClick,
6202
+ ToolTipAndPlaceholder,
6203
+ },
6204
+ })));
4782
6205
  };
4783
6206
 
4784
- 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";
4785
- 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"};
6207
+ 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";
6208
+ var styles = {"container":"EmptyContainer-module_container__XPH5b","highlight":"EmptyContainer-module_highlight__lcICy","icon":"EmptyContainer-module_icon__82-2O","label":"EmptyContainer-module_label__4TxRa"};
4786
6209
  styleInject(css_248z);
4787
6210
 
4788
- const EmptyCanvasMessage = () => {
4789
- return (React.createElement("div", { className: styles['empty-canvas-container'], "data-type": "empty-container" },
4790
- React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles['empty-canvas-icon'] },
6211
+ const EmptyContainer = ({ isDragging }) => {
6212
+ return (React.createElement("div", { className: classNames(styles.container, {
6213
+ [styles.highlight]: isDragging,
6214
+ }), "data-type": "empty-container" },
6215
+ React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", width: "37", height: "36", fill: "none", className: styles.icon },
4791
6216
  React.createElement("rect", { width: "11.676", height: "11.676", x: "18.512", y: ".153", rx: "1.621", transform: "rotate(45 18.512 .153)" }),
4792
6217
  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)" }),
4793
6218
  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)" }),
4794
6219
  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)" }),
4795
6220
  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" })),
4796
- React.createElement("span", { className: styles['empty-canvas-label'] }, "Add components to begin")));
6221
+ React.createElement("span", { className: styles.label }, "Add components to begin")));
4797
6222
  };
4798
6223
 
4799
- /**
4800
- * This function gets the element co-ordinates of a specified component in the DOM and its parent
4801
- * and sends the DOM Rect to the client app.
4802
- */
4803
- const sendCanvasGeometryUpdatedMessage = async (tree, sourceEvent) => {
4804
- const nodeToCoordinatesMap = {};
4805
- await waitForAllImagesToBeLoaded();
4806
- collectNodeCoordinates(tree.root, nodeToCoordinatesMap);
4807
- sendMessage(OUTGOING_EVENTS.CanvasGeometryUpdated, {
4808
- size: {
4809
- width: document.documentElement.offsetWidth,
4810
- height: document.documentElement.offsetHeight,
6224
+ const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
6225
+ const zone = useZoneStore((state) => state.zones);
6226
+ const upsertZone = useZoneStore((state) => state.upsertZone);
6227
+ useEffect(() => {
6228
+ function getDirection() {
6229
+ if (!node || !node.data.blockId) {
6230
+ return 'vertical';
6231
+ }
6232
+ if (!isContentfulStructureComponent(node.data.blockId)) {
6233
+ return 'vertical';
6234
+ }
6235
+ if (node.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id) {
6236
+ return 'horizontal';
6237
+ }
6238
+ const designValues = node.data.props['cfFlexDirection'];
6239
+ if (!designValues || !resolveDesignValue || designValues.type !== 'DesignValue') {
6240
+ return 'vertical';
6241
+ }
6242
+ const direction = resolveDesignValue(designValues.valuesByBreakpoint);
6243
+ if (direction === 'row') {
6244
+ return 'horizontal';
6245
+ }
6246
+ return 'vertical';
6247
+ }
6248
+ upsertZone(zoneId, { direction: getDirection() });
6249
+ }, [node, resolveDesignValue, zoneId, upsertZone]);
6250
+ return zone[zoneId]?.direction || 'vertical';
6251
+ };
6252
+
6253
+ function getStyle(style = {}, snapshot) {
6254
+ if (!snapshot?.isDropAnimating) {
6255
+ return style;
6256
+ }
6257
+ return {
6258
+ ...style,
6259
+ // cannot be 0, but make it super tiny
6260
+ transitionDuration: `0.001s`,
6261
+ };
6262
+ }
6263
+ const EditorBlockClone = ({ node: rawNode, entityStore, areEntitiesFetched, resolveDesignValue, snapshot, provided, renderDropzone, wrappingPatternIds, }) => {
6264
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
6265
+ const { node, elementToRender } = useComponent({
6266
+ node: rawNode,
6267
+ entityStore,
6268
+ areEntitiesFetched,
6269
+ resolveDesignValue,
6270
+ renderDropzone,
6271
+ userIsDragging,
6272
+ wrappingPatternIds,
6273
+ });
6274
+ const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
6275
+ return elementToRender({
6276
+ dragProps: {
6277
+ ...provided?.draggableProps,
6278
+ ...provided?.dragHandleProps,
6279
+ 'data-ctfl-dragging-element': 'true',
6280
+ innerRef: provided?.innerRef,
6281
+ className: classNames(styles$1.DraggableComponent, styles$1.DraggableClone, {
6282
+ [styles$1.isAssemblyBlock]: isAssemblyBlock,
6283
+ [styles$1.isDragging]: snapshot?.isDragging,
6284
+ }),
6285
+ style: getStyle(provided?.draggableProps.style, snapshot),
4811
6286
  },
4812
- nodes: nodeToCoordinatesMap,
4813
- sourceEvent,
4814
6287
  });
4815
6288
  };
4816
- const collectNodeCoordinates = (node, nodeToCoordinatesMap) => {
4817
- const selectedElement = document.querySelector(`[data-cf-node-id="${node.data.id}"]`);
4818
- if (selectedElement) {
4819
- const rect = getElementCoordinates(selectedElement);
4820
- if (isElementHidden(rect)) {
6289
+
6290
+ const getHtmlDragProps = (dragProps) => {
6291
+ if (dragProps) {
6292
+ const { ToolTipAndPlaceholder, Tag, innerRef, wrapComponent, ...htmlDragProps } = dragProps;
6293
+ return htmlDragProps;
6294
+ }
6295
+ return {};
6296
+ };
6297
+ const getHtmlComponentProps = (props) => {
6298
+ if (props) {
6299
+ const { editorMode, renderDropzone, node, ...htmlProps } = props;
6300
+ return htmlProps;
6301
+ }
6302
+ return {};
6303
+ };
6304
+
6305
+ function DropzoneClone({ node, entityStore, zoneId, resolveDesignValue, WrapperComponent = 'div', renderDropzone, dragProps, wrappingPatternIds, areEntitiesFetched, ...rest }) {
6306
+ const tree = useTreeStore((state) => state.tree);
6307
+ const content = node?.children || tree.root?.children || [];
6308
+ const { slotId } = parseZoneId(zoneId);
6309
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
6310
+ const htmlProps = getHtmlComponentProps(rest);
6311
+ const isRootZone = zoneId === ROOT_ID;
6312
+ if (!resolveDesignValue) {
6313
+ return null;
6314
+ }
6315
+ return (React.createElement(WrapperComponent, { ...htmlDraggableProps, ...htmlProps, className: classNames(dragProps?.className, styles$1.Dropzone, styles$1.DropzoneClone, rest.className, {
6316
+ [styles$1.isRoot]: isRootZone,
6317
+ [styles$1.isEmptyZone]: !content.length,
6318
+ }), "data-ctfl-slot-id": slotId, ref: (refNode) => {
6319
+ if (dragProps?.innerRef) {
6320
+ dragProps.innerRef(refNode);
6321
+ }
6322
+ } }, content
6323
+ .filter((node) => node.data.slotId === slotId)
6324
+ .map((item) => {
6325
+ const componentId = item.data.id;
6326
+ return (React.createElement(EditorBlockClone, { entityStore: entityStore, areEntitiesFetched: areEntitiesFetched, key: componentId, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds }));
6327
+ })));
6328
+ }
6329
+
6330
+ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponent = 'div', dragProps, entityStore, areEntitiesFetched, wrappingPatternIds: parentWrappingPatternIds = new Set(), ...rest }) {
6331
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
6332
+ const draggedItem = useDraggedItemStore((state) => state.draggedItem);
6333
+ const isDraggingNewComponent = useDraggedItemStore((state) => Boolean(state.componentId));
6334
+ const isHoveringZone = useZoneStore((state) => state.hoveringZone === zoneId);
6335
+ const tree = useTreeStore((state) => state.tree);
6336
+ const content = node?.children || tree.root?.children || [];
6337
+ const { slotId } = parseZoneId(zoneId);
6338
+ const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
6339
+ const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
6340
+ const draggedNode = useMemo(() => {
6341
+ if (!draggedItem)
4821
6342
  return;
6343
+ return getItem({ id: draggedItem.draggableId }, tree);
6344
+ }, [draggedItem, tree]);
6345
+ const isRootZone = zoneId === ROOT_ID;
6346
+ const isDestination = draggedDestinationId === zoneId;
6347
+ const isEmptyCanvas = isRootZone && !content.length;
6348
+ const isAssembly = ASSEMBLY_NODE_TYPES.includes(node?.type || '');
6349
+ const isRootAssembly = node?.type === ASSEMBLY_NODE_TYPE;
6350
+ const htmlDraggableProps = getHtmlDragProps(dragProps);
6351
+ const htmlProps = getHtmlComponentProps(rest);
6352
+ const wrappingPatternIds = useMemo(() => {
6353
+ // On the top level, the node is not defined. If the root blockId is not the default string,
6354
+ // we assume that it is the entry ID of the experience/ pattern to properly detect circular dependencies
6355
+ if (!node && tree.root.data.blockId && tree.root.data.blockId !== ROOT_ID) {
6356
+ return new Set([tree.root.data.blockId, ...parentWrappingPatternIds]);
4822
6357
  }
4823
- nodeToCoordinatesMap[node.data.id] = {
4824
- coordinates: {
4825
- x: rect.x + window.scrollX,
4826
- y: rect.y + window.scrollY,
4827
- width: rect.width,
4828
- height: rect.height,
4829
- },
4830
- };
6358
+ if (isRootAssembly && node?.data.blockId) {
6359
+ return new Set([node.data.blockId, ...parentWrappingPatternIds]);
6360
+ }
6361
+ return parentWrappingPatternIds;
6362
+ }, [isRootAssembly, node, parentWrappingPatternIds, tree.root.data.blockId]);
6363
+ // To avoid a circular dependency, we create the recursive rendering function here and trickle it down
6364
+ const renderDropzone = useCallback((node, props) => {
6365
+ return (React.createElement(Dropzone, { zoneId: node.data.id, entityStore: entityStore, node: node, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds, areEntitiesFetched: areEntitiesFetched, ...props }));
6366
+ }, [wrappingPatternIds, resolveDesignValue, entityStore, areEntitiesFetched]);
6367
+ const renderClonedDropzone = useCallback((node, props) => {
6368
+ return (React.createElement(DropzoneClone, { entityStore: entityStore, areEntitiesFetched: areEntitiesFetched, zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds, ...props }));
6369
+ }, [resolveDesignValue, wrappingPatternIds, entityStore, areEntitiesFetched]);
6370
+ const isDropzoneEnabled = useMemo(() => {
6371
+ const isColumns = node?.data.blockId === CONTENTFUL_COMPONENTS$1.columns.id;
6372
+ const isDraggingSingleColumn = draggedNode?.data.blockId === CONTENTFUL_COMPONENTS$1.singleColumn.id;
6373
+ const isParentOfDraggedNode = node?.data.id === draggedNode?.parentId;
6374
+ // If dragging a single column, only enable the dropzone of the parent
6375
+ // columns component
6376
+ if (isDraggingSingleColumn && isColumns && isParentOfDraggedNode) {
6377
+ return true;
6378
+ }
6379
+ // If dragging a single column, disable dropzones for any component besides
6380
+ // the parent of the dragged single column
6381
+ if (isDraggingSingleColumn && !isParentOfDraggedNode) {
6382
+ return false;
6383
+ }
6384
+ // Disable dropzone for Columns component
6385
+ if (isColumns) {
6386
+ return false;
6387
+ }
6388
+ // Disable dropzone for Assembly
6389
+ if (isAssembly) {
6390
+ return false;
6391
+ }
6392
+ // Enable dropzone for the non-root hovered zones if component is not allowed on root
6393
+ if (!isDraggingNewComponent &&
6394
+ !isComponentAllowedOnRoot({ type: draggedNode?.type, componentId: draggedNode?.data.blockId })) {
6395
+ return isHoveringZone && !isRootZone;
6396
+ }
6397
+ // Enable dropzone for the hovered zone only
6398
+ return isHoveringZone;
6399
+ }, [isAssembly, isHoveringZone, isRootZone, isDraggingNewComponent, draggedNode, node]);
6400
+ if (!resolveDesignValue) {
6401
+ return null;
4831
6402
  }
4832
- node.children.forEach((child) => collectNodeCoordinates(child, nodeToCoordinatesMap));
4833
- };
4834
- const waitForAllImagesToBeLoaded = () => {
4835
- // If the document contains an image, wait for this image to be loaded before collecting & sending all geometry data.
4836
- const allImageNodes = document.querySelectorAll('img');
4837
- return Promise.all(Array.from(allImageNodes).map((imageNode) => {
4838
- if (imageNode.complete) {
4839
- return Promise.resolve();
4840
- }
4841
- return new Promise((resolve, reject) => {
4842
- const handleImageLoad = (event) => {
4843
- imageNode.removeEventListener('load', handleImageLoad);
4844
- imageNode.removeEventListener('error', handleImageLoad);
4845
- if (event.type === 'error') {
4846
- console.warn('Image failed to load:', imageNode);
4847
- reject();
6403
+ const isPatternWrapperComponentFullHeight = isRootAssembly
6404
+ ? node.children.length === 1 &&
6405
+ resolveDesignValue(node?.children[0]?.data.props.cfHeight?.valuesByBreakpoint ?? {}, 'cfHeight') === '100%'
6406
+ : false;
6407
+ const isPatternWrapperComponentFullWidth = isRootAssembly
6408
+ ? node.children.length === 1 &&
6409
+ resolveDesignValue(node?.children[0]?.data.props.cfWidth?.valuesByBreakpoint ?? {}, 'cfWidth') === '100%'
6410
+ : false;
6411
+ return (React.createElement(Droppable, { droppableId: zoneId, direction: direction, isDropDisabled: !isDropzoneEnabled, renderClone: (provided, snapshot, rubic) => (React.createElement(EditorBlockClone, { entityStore: entityStore, areEntitiesFetched: areEntitiesFetched, node: content[rubic.source.index], resolveDesignValue: resolveDesignValue, provided: provided, snapshot: snapshot, renderDropzone: renderClonedDropzone, wrappingPatternIds: wrappingPatternIds })) }, (provided, snapshot) => {
6412
+ return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ...htmlDraggableProps, ...htmlProps, ref: (refNode) => {
6413
+ if (dragProps?.innerRef) {
6414
+ dragProps.innerRef(refNode);
4848
6415
  }
4849
- else {
4850
- resolve();
4851
- }
4852
- };
4853
- imageNode.addEventListener('load', handleImageLoad);
4854
- imageNode.addEventListener('error', handleImageLoad);
4855
- });
6416
+ provided?.innerRef(refNode);
6417
+ }, id: zoneId, "data-ctfl-zone-id": zoneId, "data-ctfl-slot-id": slotId, className: classNames(dragProps?.className, styles$1.Dropzone, className, {
6418
+ [styles$1.isEmptyCanvas]: isEmptyCanvas,
6419
+ [styles$1.isDragging]: userIsDragging,
6420
+ [styles$1.isDestination]: isDestination && !isAssembly,
6421
+ [styles$1.isRoot]: isRootZone,
6422
+ [styles$1.isEmptyZone]: !content.length,
6423
+ [styles$1.isSlot]: Boolean(slotId),
6424
+ [styles$1.fullHeight]: isPatternWrapperComponentFullHeight,
6425
+ [styles$1.fullWidth]: isPatternWrapperComponentFullWidth,
6426
+ }) },
6427
+ isEmptyCanvas ? (React.createElement(EmptyContainer, { isDragging: isRootZone && userIsDragging })) : (content
6428
+ .filter((node) => node.data.slotId === slotId)
6429
+ .map((item, i) => (React.createElement(EditorBlock, { entityStore: entityStore, areEntitiesFetched: areEntitiesFetched, placeholder: {
6430
+ isDraggingOver: snapshot?.isDraggingOver,
6431
+ totalIndexes: content.length,
6432
+ elementIndex: i,
6433
+ dropzoneElementId: zoneId,
6434
+ direction,
6435
+ }, index: i, zoneId: zoneId, key: item.data.id, userIsDragging: userIsDragging, draggingNewComponent: isDraggingNewComponent, node: item, resolveDesignValue: resolveDesignValue, renderDropzone: renderDropzone, wrappingPatternIds: wrappingPatternIds })))),
6436
+ provided?.placeholder,
6437
+ dragProps?.ToolTipAndPlaceholder));
4856
6438
  }));
4857
- };
6439
+ }
4858
6440
 
4859
- const useCanvasGeometryUpdates = ({ tree }) => {
4860
- const debouncedUpdateGeometry = useMemo(() => debounce((tree, sourceEvent) => {
4861
- // When the DOM changed, we still need to wait for the next frame to ensure that
4862
- // rendering is complete (e.g. this is required when deleting a node).
4863
- window.requestAnimationFrame(() => {
4864
- sendCanvasGeometryUpdatedMessage(tree, sourceEvent);
6441
+ const RootRenderer = ({ onChange, inMemoryEntitiesStore, }) => {
6442
+ useEditorSubscriber(inMemoryEntitiesStore);
6443
+ const dragItem = useDraggedItemStore((state) => state.componentId);
6444
+ const userIsDragging = useDraggedItemStore((state) => state.isDraggingOnCanvas);
6445
+ const setHoveredComponentId = useDraggedItemStore((state) => state.setHoveredComponentId);
6446
+ const breakpoints = useTreeStore((state) => state.breakpoints);
6447
+ const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
6448
+ const containerRef = useRef(null);
6449
+ const { resolveDesignValue } = useBreakpoints(breakpoints);
6450
+ const [containerStyles, setContainerStyles] = useState({});
6451
+ const tree = useTreeStore((state) => state.tree);
6452
+ const handleMouseOver = useCallback(() => {
6453
+ // Remove hover state set by UI when mouse is over canvas
6454
+ setHoveredComponentId();
6455
+ // Remove hover styling from components in the layers tab
6456
+ sendMessage(OUTGOING_EVENTS.NewHoveredElement, {});
6457
+ }, [setHoveredComponentId]);
6458
+ const handleClickOutside = useCallback((e) => {
6459
+ const element = e.target;
6460
+ const isRoot = element.getAttribute('data-ctfl-zone-id') === ROOT_ID;
6461
+ const clickedOnCanvas = element.closest(`[data-ctfl-root]`);
6462
+ if (clickedOnCanvas && !isRoot) {
6463
+ return;
6464
+ }
6465
+ sendMessage(OUTGOING_EVENTS.OutsideCanvasClick, {
6466
+ outsideCanvasClick: true,
6467
+ });
6468
+ sendMessage(OUTGOING_EVENTS.ComponentSelected, {
6469
+ nodeId: '',
4865
6470
  });
4866
- }, 100, {
4867
- leading: true,
4868
- // To be sure, we recalculate it at the end of the frame again. Though, we couldn't
4869
- // yet show the need for this. So we might be able to drop this later to boost performance.
4870
- trailing: true,
4871
- }), []);
4872
- // Store tree in a ref to avoid the need to deactivate & reactivate the mutation observer
4873
- // when the tree changes. This is important to avoid missing out on some mutation events.
4874
- const treeRef = useRef(tree);
6471
+ setSelectedNodeId('');
6472
+ }, [setSelectedNodeId]);
6473
+ const handleResizeCanvas = useCallback(() => {
6474
+ const parentElement = containerRef.current?.parentElement;
6475
+ if (!parentElement) {
6476
+ return;
6477
+ }
6478
+ let siblingHeight = 0;
6479
+ for (const child of parentElement.children) {
6480
+ if (!child.hasAttribute('data-ctfl-root')) {
6481
+ siblingHeight += child.getBoundingClientRect().height;
6482
+ }
6483
+ }
6484
+ if (!siblingHeight) {
6485
+ /**
6486
+ * DRAGGABLE_HEIGHT is subtracted here due to an uninteded scrolling effect
6487
+ * when dragging a new component onto the canvas
6488
+ *
6489
+ * The DRAGGABLE_HEIGHT is then added as margin bottom to offset this value
6490
+ * so that visually there is no difference to the user.
6491
+ */
6492
+ setContainerStyles({
6493
+ minHeight: `${window.innerHeight - DRAGGABLE_HEIGHT}px`,
6494
+ });
6495
+ return;
6496
+ }
6497
+ setContainerStyles({
6498
+ minHeight: `${window.innerHeight - siblingHeight}px`,
6499
+ });
6500
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6501
+ }, [containerRef.current]);
4875
6502
  useEffect(() => {
4876
- treeRef.current = tree;
4877
- }, [tree]);
4878
- // Handling window resize events
6503
+ if (onChange)
6504
+ onChange(tree);
6505
+ }, [tree, onChange]);
4879
6506
  useEffect(() => {
4880
- const resizeEventListener = () => debouncedUpdateGeometry(treeRef.current, 'resize');
4881
- window.addEventListener('resize', resizeEventListener);
4882
- return () => window.removeEventListener('resize', resizeEventListener);
4883
- }, [debouncedUpdateGeometry]);
4884
- // Handling DOM mutations
6507
+ window.addEventListener('mouseover', handleMouseOver);
6508
+ return () => {
6509
+ window.removeEventListener('mouseover', handleMouseOver);
6510
+ };
6511
+ }, [handleMouseOver]);
4885
6512
  useEffect(() => {
4886
- const observer = new MutationObserver(() => debouncedUpdateGeometry(treeRef.current, 'mutation'));
4887
- observer.observe(document.documentElement, {
4888
- childList: true,
4889
- subtree: true,
4890
- attributes: true,
4891
- });
4892
- return () => observer.disconnect();
4893
- }, [debouncedUpdateGeometry]);
4894
- };
4895
-
4896
- const RootRenderer = ({ inMemoryEntitiesStore }) => {
4897
- useEditorSubscriber(inMemoryEntitiesStore);
4898
- const tree = useTreeStore((state) => state.tree);
4899
- useCanvasGeometryUpdates({ tree });
4900
- const breakpoints = useTreeStore((state) => state.breakpoints);
4901
- const { resolveDesignValue } = useBreakpoints(breakpoints);
4902
- // If the root blockId is defined but not the default string, it is the entry ID
4903
- // of the experience/ pattern to properly detect circular dependencies.
4904
- const rootBlockId = tree.root.data.blockId ?? ROOT_ID;
4905
- const wrappingPatternIds = rootBlockId !== ROOT_ID ? new Set([rootBlockId]) : new Set();
6513
+ document.addEventListener('click', handleClickOutside);
6514
+ return () => {
6515
+ document.removeEventListener('click', handleClickOutside);
6516
+ };
6517
+ }, [handleClickOutside]);
6518
+ useEffect(() => {
6519
+ handleResizeCanvas();
6520
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6521
+ }, [containerRef.current]);
4906
6522
  const entityStore = inMemoryEntitiesStore((state) => state.entityStore);
4907
6523
  const areEntitiesFetched = inMemoryEntitiesStore((state) => state.areEntitiesFetched);
4908
- return (React.createElement(React.Fragment, null,
4909
- React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer }, !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, entityStore: entityStore, areEntitiesFetched: areEntitiesFetched })))))));
6524
+ return (React.createElement(DNDProvider, null,
6525
+ dragItem && React.createElement(DraggableContainer, { id: dragItem }),
6526
+ React.createElement("div", { "data-ctfl-root": true, className: styles$3.container, ref: containerRef, style: containerStyles },
6527
+ userIsDragging && React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitbox }),
6528
+ React.createElement(Dropzone, { zoneId: ROOT_ID, resolveDesignValue: resolveDesignValue, entityStore: entityStore, areEntitiesFetched: areEntitiesFetched }),
6529
+ userIsDragging && (React.createElement(React.Fragment, null,
6530
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.hitboxLower }),
6531
+ React.createElement("div", { "data-ctfl-zone-id": ROOT_ID, className: styles$3.canvasBottomSpacer })))),
6532
+ React.createElement("div", { "data-ctfl-hitboxes": true })));
4910
6533
  };
4911
6534
 
4912
6535
  const useInitializeEditor = (inMemoryEntitiesStore) => {
@@ -4948,11 +6571,38 @@ const useInitializeEditor = (inMemoryEntitiesStore) => {
4948
6571
  const VisualEditorRoot = ({ experience, inMemoryEntitiesStore: inMemoryEntitiesStore$1 = inMemoryEntitiesStore, }) => {
4949
6572
  const initialized = useInitializeEditor(inMemoryEntitiesStore$1);
4950
6573
  const setHyperLinkPattern = useEditorStore((state) => state.setHyperLinkPattern);
6574
+ const setMousePosition = useDraggedItemStore((state) => state.setMousePosition);
6575
+ const setHoveringZone = useZoneStore((state) => state.setHoveringZone);
4951
6576
  useEffect(() => {
4952
6577
  if (experience?.hyperlinkPattern) {
4953
6578
  setHyperLinkPattern(experience.hyperlinkPattern);
4954
6579
  }
4955
6580
  }, [experience?.hyperlinkPattern, setHyperLinkPattern]);
6581
+ useEffect(() => {
6582
+ const onMouseMove = (e) => {
6583
+ setMousePosition(e.clientX, e.clientY);
6584
+ const target = e.target;
6585
+ const zoneId = target.closest(`[${CTFL_ZONE_ID}]`)?.getAttribute(CTFL_ZONE_ID);
6586
+ if (zoneId) {
6587
+ setHoveringZone(zoneId);
6588
+ }
6589
+ if (!SimulateDnD$1.isDragging) {
6590
+ return;
6591
+ }
6592
+ if (target.id === NEW_COMPONENT_ID) {
6593
+ return;
6594
+ }
6595
+ SimulateDnD$1.updateDrag(e.clientX, e.clientY);
6596
+ sendMessage(OUTGOING_EVENTS.MouseMove, {
6597
+ clientX: e.pageX - window.scrollX,
6598
+ clientY: e.pageY - window.scrollY,
6599
+ });
6600
+ };
6601
+ document.addEventListener('mousemove', onMouseMove);
6602
+ return () => {
6603
+ document.removeEventListener('mousemove', onMouseMove);
6604
+ };
6605
+ }, [setHoveringZone, setMousePosition]);
4956
6606
  if (!initialized)
4957
6607
  return null;
4958
6608
  return React.createElement(RootRenderer, { inMemoryEntitiesStore: inMemoryEntitiesStore$1 });