@contentful/experiences-visual-editor-react 1.0.2 → 1.0.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,7 @@
1
- # visual-editor
1
+ # @contentful/experiences-visual-editor-react
2
+
3
+ This package provides a visual editor for the [Experiences SDK](https://www.contentful.com/developers/docs/experiences/set-up-experiences-sdk/). It implements drag-and-drop functionality into the canvas, making the creation and editing of experiences more intuitive and user-friendly.
4
+
5
+ ## Documentation
6
+
7
+ Please refer to our [Documentation](https://www.contentful.com/developers/docs/experiences/) to learn more about it.
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ const structureComponents = new Set([
89
89
  CONTENTFUL_COMPONENTS$1.singleColumn.id,
90
90
  ]);
91
91
  const isContentfulStructureComponent = (componentId) => structureComponents.has(componentId ?? '');
92
+ const isComponentAllowedOnRoot = (componentId) => isContentfulStructureComponent(componentId) || componentId === CONTENTFUL_COMPONENTS$1.divider.id;
92
93
  const isEmptyStructureWithRelativeHeight = (children, componentId, height) => {
93
94
  return (children === 0 &&
94
95
  isContentfulStructureComponent(componentId) &&
@@ -2763,7 +2764,7 @@ const DraggableChildComponent = (props) => {
2763
2764
  })));
2764
2765
  };
2765
2766
 
2766
- var css_248z$2 = ".styles-module_container__te-1H {\n margin-left: auto;\n margin-right: auto;\n position: relative;\n height: 100%;\n width: 100%;\n background-color: transparent;\n transition: background-color 0.2s;\n pointer-events: all !important;\n}\n\n.styles-module_container__te-1H:not(.styles-module_isRoot__5cn-i):before {\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n outline-offset: -1px;\n outline: 2px solid transparent;\n z-index: 1;\n transition: outline 0.2s;\n pointer-events: none;\n}\n\n.styles-module_isRoot__5cn-i,\n.styles-module_isEmptyCanvas__0XHZR {\n flex: 1;\n}\n\n.styles-module_isEmptyZone__zVpnZ {\n min-height: 80px;\n}\n\n.styles-module_isDragging__Gm8v5:not(.styles-module_isRoot__5cn-i):before {\n outline: 2px dashed var(--exp-builder-gray300);\n}\n\n.styles-module_isDestination__5sCQx:not(.styles-module_isRoot__5cn-i):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_hitbox__YQ-1Z {\n position: fixed;\n pointer-events: all !important;\n}\n\n.styles-module_hitbox__YQ-1Z {\n position: fixed;\n pointer-events: all !important;\n}\n";
2767
+ var css_248z$2 = ".styles-module_container__te-1H {\n margin-left: auto;\n margin-right: auto;\n position: relative;\n height: 100%;\n width: 100%;\n background-color: transparent;\n transition: background-color 0.2s;\n pointer-events: all;\n}\n\n.styles-module_container__te-1H:not(.styles-module_isRoot__5cn-i):before {\n content: '';\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n outline-offset: -1px;\n outline: 2px solid transparent;\n z-index: 1;\n transition: outline 0.2s;\n pointer-events: none;\n}\n\n.styles-module_isRoot__5cn-i,\n.styles-module_isEmptyCanvas__0XHZR {\n flex: 1;\n}\n\n.styles-module_isEmptyZone__zVpnZ {\n min-height: 80px;\n}\n\n.styles-module_isDragging__Gm8v5:not(.styles-module_isRoot__5cn-i):before {\n outline: 2px dashed var(--exp-builder-gray300);\n}\n\n.styles-module_isDestination__5sCQx:not(.styles-module_isRoot__5cn-i):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_hitbox__YQ-1Z {\n position: fixed;\n pointer-events: all;\n}\n";
2767
2768
  var styles$2 = {"container":"styles-module_container__te-1H","isRoot":"styles-module_isRoot__5cn-i","isEmptyCanvas":"styles-module_isEmptyCanvas__0XHZR","isEmptyZone":"styles-module_isEmptyZone__zVpnZ","isDragging":"styles-module_isDragging__Gm8v5","isDestination":"styles-module_isDestination__5sCQx","hitbox":"styles-module_hitbox__YQ-1Z"};
2768
2769
  styleInject(css_248z$2);
2769
2770
 
@@ -3155,13 +3156,14 @@ const { WIDTH, HEIGHT, INITIAL_OFFSET, OFFSET_INCREMENT, MIN_HEIGHT, MIN_DEPTH_H
3155
3156
  const calcOffsetDepth = (depth) => {
3156
3157
  return INITIAL_OFFSET - OFFSET_INCREMENT * depth;
3157
3158
  };
3158
- const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3159
+ const getHitboxStyles = ({ direction, zoneDepth, domRect, scrollY, offsetRect, }) => {
3159
3160
  if (!domRect) {
3160
3161
  return {
3161
3162
  display: 'none',
3162
3163
  };
3163
3164
  }
3164
3165
  const { width, height, top, left, bottom, right } = domRect;
3166
+ const { height: offsetHeight, width: offsetWidth } = offsetRect || { height: 0, width: 0 };
3165
3167
  const MAX_SELF_HEIGHT = DRAGGABLE_HEIGHT * 2;
3166
3168
  const isDeepZone = zoneDepth > DEEP_ZONE;
3167
3169
  const isAboveMaxHeight = height > MAX_SELF_HEIGHT;
@@ -3170,7 +3172,7 @@ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3170
3172
  return {
3171
3173
  width,
3172
3174
  height: HEIGHT,
3173
- top: top - calcOffsetDepth(zoneDepth) - scrollY,
3175
+ top: top + offsetHeight - calcOffsetDepth(zoneDepth) - scrollY,
3174
3176
  left,
3175
3177
  zIndex: 100 + zoneDepth,
3176
3178
  };
@@ -3178,7 +3180,7 @@ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3178
3180
  return {
3179
3181
  width,
3180
3182
  height: HEIGHT,
3181
- top: bottom + calcOffsetDepth(zoneDepth) - scrollY,
3183
+ top: bottom + offsetHeight + calcOffsetDepth(zoneDepth) - scrollY,
3182
3184
  left,
3183
3185
  zIndex: 100 + zoneDepth,
3184
3186
  };
@@ -3186,7 +3188,7 @@ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3186
3188
  return {
3187
3189
  width: WIDTH,
3188
3190
  height: height - HEIGHT,
3189
- left: left - calcOffsetDepth(zoneDepth) - WIDTH / 2,
3191
+ left: left + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
3190
3192
  top: top + HEIGHT / 2 - scrollY,
3191
3193
  zIndex: 100 + zoneDepth,
3192
3194
  };
@@ -3194,7 +3196,7 @@ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3194
3196
  return {
3195
3197
  width: WIDTH,
3196
3198
  height: height - HEIGHT,
3197
- left: right - calcOffsetDepth(zoneDepth) - WIDTH / 2,
3199
+ left: right + offsetWidth - calcOffsetDepth(zoneDepth) - WIDTH / 2,
3198
3200
  top: top + HEIGHT / 2 - scrollY,
3199
3201
  zIndex: 100 + zoneDepth,
3200
3202
  };
@@ -3228,12 +3230,14 @@ const getHitboxStyles = ({ direction, zoneDepth, domRect }) => {
3228
3230
  }
3229
3231
  };
3230
3232
 
3231
- const Hitboxes = ({ zoneId, parentZoneId, enableRootHitboxes }) => {
3233
+ const Hitboxes = ({ zoneId, parentZoneId, isEmptyZone }) => {
3232
3234
  const tree = useTreeStore((state) => state.tree);
3233
3235
  const isDraggingOnCanvas = useDraggedItemStore((state) => state.isDraggingOnCanvas);
3234
3236
  const scrollY = useDraggedItemStore((state) => state.scrollY);
3235
3237
  const zoneDepth = useMemo(() => getItemDepthFromNode({ id: parentZoneId }, tree.root), [tree, parentZoneId]);
3236
3238
  const [fetchDomRect, setFetchDomRect] = useState(Date.now());
3239
+ const { zones, hoveringZone } = useZoneStore();
3240
+ const isHoveringZone = hoveringZone === zoneId;
3237
3241
  useEffect(() => {
3238
3242
  /**
3239
3243
  * A bit hacky but we need to wait a very small amount
@@ -3252,16 +3256,27 @@ const Hitboxes = ({ zoneId, parentZoneId, enableRootHitboxes }) => {
3252
3256
  return document.querySelector(`[${CTFL_ZONE_ID}="${zoneId}"]`)?.getBoundingClientRect();
3253
3257
  // eslint-disable-next-line react-hooks/exhaustive-deps
3254
3258
  }, [zoneId, fetchDomRect]);
3255
- const zones = useZoneStore((state) => state.zones);
3259
+ // Use the size of the cloned dragging element to offset the position of the hitboxes
3260
+ // So that when dragging causes a dropzone to expand, the hitboxes will be in the correct position
3261
+ const offsetRect = useMemo(() => {
3262
+ if (isEmptyZone || !isHoveringZone)
3263
+ return;
3264
+ return document.querySelector(`[${CTFL_DRAGGING_ELEMENT}]`)?.getBoundingClientRect();
3265
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3266
+ }, [isEmptyZone, isHoveringZone, fetchDomRect]);
3256
3267
  const zoneDirection = zones[parentZoneId]?.direction || 'vertical';
3257
3268
  const isVertical = zoneDirection === 'vertical';
3258
3269
  const isRoot = parentZoneId === ROOT_ID;
3259
- const showRootHitboxes = isRoot && enableRootHitboxes;
3260
- const getStyles = useCallback((direction) => getHitboxStyles({ direction, zoneDepth, domRect, scrollY }), [zoneDepth, domRect, scrollY]);
3270
+ const getStyles = useCallback((direction) => getHitboxStyles({
3271
+ direction,
3272
+ zoneDepth,
3273
+ domRect,
3274
+ scrollY,
3275
+ offsetRect,
3276
+ }), [zoneDepth, domRect, scrollY, offsetRect]);
3261
3277
  const ActiveHitboxes = (React.createElement(React.Fragment, null,
3262
3278
  React.createElement("div", { "data-ctfl-zone-id": zoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.SELF_VERTICAL : HitboxDirection.SELF_HORIZONTAL) }),
3263
- showRootHitboxes && (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) })),
3264
- !isRoot && (React.createElement(React.Fragment, null,
3279
+ isRoot ? (React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(HitboxDirection.BOTTOM) })) : (React.createElement(React.Fragment, null,
3265
3280
  React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.TOP : HitboxDirection.LEFT) }),
3266
3281
  React.createElement("div", { "data-ctfl-zone-id": parentZoneId, className: styles$2.hitbox, style: getStyles(isVertical ? HitboxDirection.BOTTOM : HitboxDirection.RIGHT) })))));
3267
3282
  if (!hitboxContainer) {
@@ -3270,7 +3285,7 @@ const Hitboxes = ({ zoneId, parentZoneId, enableRootHitboxes }) => {
3270
3285
  return createPortal(ActiveHitboxes, hitboxContainer);
3271
3286
  };
3272
3287
 
3273
- const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, draggingNewComponent, index, zoneId, userIsDragging, placeholder, }) => {
3288
+ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, index, zoneId, userIsDragging, placeholder, }) => {
3274
3289
  const setSelectedNodeId = useEditorStore((state) => state.setSelectedNodeId);
3275
3290
  const selectedNodeId = useEditorStore((state) => state.selectedNodeId);
3276
3291
  const { node, componentId, wrapperProps, definition, elementToRender } = useComponent({
@@ -3285,8 +3300,7 @@ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, draggi
3285
3300
  const isAssemblyBlock = node.type === ASSEMBLY_BLOCK_NODE_TYPE;
3286
3301
  const isAssembly = node.type === ASSEMBLY_NODE_TYPE;
3287
3302
  const isStructureComponent = isContentfulStructureComponent(node.data.blockId);
3288
- const isRootComponent = zoneId === ROOT_ID;
3289
- const enableRootHitboxes = isRootComponent && !!draggingNewComponent;
3303
+ const isEmptyZone = !node.children.length;
3290
3304
  const onClick = (e) => {
3291
3305
  e.stopPropagation();
3292
3306
  if (!userIsDragging) {
@@ -3307,11 +3321,11 @@ const EditorBlock = ({ node: rawNode, resolveDesignValue, renderDropzone, draggi
3307
3321
  if (node.data.blockId === CONTENTFUL_COMPONENTS.singleColumn.id) {
3308
3322
  return (React.createElement(React.Fragment, null,
3309
3323
  React.createElement(DraggableChildComponent, { elementToRender: elementToRender, id: componentId, index: index, isAssemblyBlock: isAssemblyBlock, isDragDisabled: isSingleColumn, isSelected: selectedNodeId === componentId, userIsDragging: userIsDragging, isContainer: isContainer, blockId: node.data.blockId, coordinates: coordinates, wrapperProps: wrapperProps, onClick: onClick, definition: definition }),
3310
- isStructureComponent && !isSingleColumn && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, enableRootHitboxes: enableRootHitboxes }))));
3324
+ isStructureComponent && !isSingleColumn && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
3311
3325
  }
3312
3326
  return (React.createElement(DraggableComponent, { placeholder: placeholder, definition: definition, id: componentId, index: index, isAssemblyBlock: isAssemblyBlock, isDragDisabled: isAssemblyBlock, isSelected: selectedNodeId === componentId, userIsDragging: userIsDragging, isContainer: isContainer, blockId: node.data.blockId, coordinates: coordinates, wrapperProps: wrapperProps, onClick: onClick },
3313
3327
  elementToRender(),
3314
- isStructureComponent && !isSingleColumn && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, enableRootHitboxes: enableRootHitboxes }))));
3328
+ isStructureComponent && !isSingleColumn && userIsDragging && (React.createElement(Hitboxes, { parentZoneId: zoneId, zoneId: componentId, isEmptyZone: isEmptyZone }))));
3315
3329
  };
3316
3330
 
3317
3331
  var css_248z$1 = ".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";
@@ -3331,27 +3345,6 @@ const EmptyContainer = ({ isDragging }) => {
3331
3345
  React.createElement("span", { className: styles$1.label }, "Add components to begin")));
3332
3346
  };
3333
3347
 
3334
- const getZoneParents = (zoneId) => {
3335
- const element = document.querySelector(`[data-rfd-droppable-id='${zoneId}']`);
3336
- if (!element) {
3337
- return [];
3338
- }
3339
- function getZonesToRoot(element, parentIds = []) {
3340
- if (!element) {
3341
- return parentIds;
3342
- }
3343
- const attribute = element.getAttribute('data-rfd-droppable-id');
3344
- if (attribute === ROOT_ID) {
3345
- return parentIds;
3346
- }
3347
- if (attribute) {
3348
- parentIds.push(attribute);
3349
- }
3350
- return getZonesToRoot(element.parentElement, parentIds);
3351
- }
3352
- return getZonesToRoot(element);
3353
- };
3354
-
3355
3348
  const useDropzoneDirection = ({ resolveDesignValue, node, zoneId }) => {
3356
3349
  const zone = useZoneStore((state) => state.zones);
3357
3350
  const upsertZone = useZoneStore((state) => state.upsertZone);
@@ -3434,8 +3427,12 @@ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponen
3434
3427
  const tree = useTreeStore((state) => state.tree);
3435
3428
  const content = node?.children || tree.root?.children || [];
3436
3429
  const direction = useDropzoneDirection({ resolveDesignValue, node, zoneId });
3437
- const draggedSourceId = draggedItem && draggedItem.source.droppableId;
3438
3430
  const draggedDestinationId = draggedItem && draggedItem.destination?.droppableId;
3431
+ const draggedBlockId = useMemo(() => {
3432
+ if (!draggedItem)
3433
+ return;
3434
+ return getItem({ id: draggedItem.draggableId }, tree)?.data.blockId;
3435
+ }, [draggedItem, tree]);
3439
3436
  const isDraggingNewComponent = !!newComponentId;
3440
3437
  const isHoveringZone = hoveringZone === zoneId;
3441
3438
  const isRootZone = zoneId === ROOT_ID;
@@ -3449,43 +3446,33 @@ function Dropzone({ node, zoneId, resolveDesignValue, className, WrapperComponen
3449
3446
  const renderClonedDropzone = useCallback((node, props) => {
3450
3447
  return (React.createElement(DropzoneClone, { zoneId: node.data.id, node: node, resolveDesignValue: resolveDesignValue, renderDropzone: renderClonedDropzone, ...props }));
3451
3448
  }, [resolveDesignValue]);
3452
- if (!resolveDesignValue) {
3453
- return null;
3454
- }
3455
- /**
3456
- * The Rules of Dropzones
3457
- *
3458
- * 1. A dropzone is disabled unless the mouse is hovering over it
3459
- *
3460
- * 2. Dragging a new component onto the canvas has no addtional rules
3461
- * besides rule #1
3462
- *
3463
- * 3. Dragging a component that is a direct descendant of the root
3464
- * (parentId === ROOT_ID) then only the Root Dropzone is enabled
3465
- *
3466
- * 4. Dragging a nested component (parentId !== ROOT_ID) then the Root
3467
- * Dropzone is disabled, all other Dropzones follow rule #1
3468
- *
3469
- * 5. Assemblies and the SingleColumn component are always disabled
3470
- *
3471
- */
3472
- const isDropzoneEnabled = () => {
3449
+ const isDropzoneEnabled = useMemo(() => {
3450
+ // Disable dropzone for Columns component
3473
3451
  if (node?.data.blockId === CONTENTFUL_COMPONENTS.columns.id) {
3474
3452
  return false;
3475
3453
  }
3454
+ // Disable dropzone for Assembly
3476
3455
  if (isAssembly) {
3477
3456
  return false;
3478
3457
  }
3479
- if (isDraggingNewComponent) {
3480
- return isHoveringZone;
3458
+ // Enable dropzone for the non-root hovered zones if component is not allowed on root
3459
+ if (!isDraggingNewComponent && !isComponentAllowedOnRoot(draggedBlockId)) {
3460
+ return isHoveringZone && !isRootZone;
3481
3461
  }
3482
- const draggingParentIds = getZoneParents(draggedSourceId || '');
3483
- if (!draggingParentIds.length) {
3484
- return isRootZone;
3485
- }
3486
- return isHoveringZone && !isRootZone;
3487
- };
3488
- return (React.createElement(Droppable, { droppableId: zoneId, direction: direction, isDropDisabled: !isDropzoneEnabled(), renderClone: (provided, snapshot, rubic) => (React.createElement(EditorBlockClone, { node: content[rubic.source.index], resolveDesignValue: resolveDesignValue, provided: provided, snapshot: snapshot, renderDropzone: renderClonedDropzone })) }, (provided, snapshot) => {
3462
+ // Enable dropzone for the hovered zone only
3463
+ return isHoveringZone;
3464
+ }, [
3465
+ node?.data.blockId,
3466
+ isAssembly,
3467
+ isHoveringZone,
3468
+ isRootZone,
3469
+ isDraggingNewComponent,
3470
+ draggedBlockId,
3471
+ ]);
3472
+ if (!resolveDesignValue) {
3473
+ return null;
3474
+ }
3475
+ return (React.createElement(Droppable, { droppableId: zoneId, direction: direction, isDropDisabled: !isDropzoneEnabled, renderClone: (provided, snapshot, rubic) => (React.createElement(EditorBlockClone, { node: content[rubic.source.index], resolveDesignValue: resolveDesignValue, provided: provided, snapshot: snapshot, renderDropzone: renderClonedDropzone })) }, (provided, snapshot) => {
3489
3476
  return (React.createElement(WrapperComponent, { ...(provided || { droppableProps: {} }).droppableProps, ref: provided?.innerRef, id: zoneId, "data-ctfl-zone-id": zoneId, className: classNames(styles$2.container, {
3490
3477
  [styles$2.isEmptyCanvas]: isEmptyCanvas,
3491
3478
  [styles$2.isDragging]: userIsDragging && !isAssembly,