@contentful/experiences-visual-editor-react 1.39.0-alpha-20250528T1549-bd210e1.0 → 1.39.0-alpha-20250603T1549-6807858.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
@@ -2,7 +2,7 @@ import styleInject from 'style-inject';
2
2
  import React, { useState, useEffect, useCallback, forwardRef, useMemo, useLayoutEffect, useRef } from 'react';
3
3
  import { create } from 'zustand';
4
4
  import { produce } from 'immer';
5
- import { isEqual, omit, isArray, get as get$1 } from 'lodash-es';
5
+ import { isEqual, omit, isArray, get as get$1, debounce } from 'lodash-es';
6
6
  import { z } from 'zod';
7
7
  import md5 from 'md5';
8
8
  import { BLOCKS } from '@contentful/rich-text-types';
@@ -228,6 +228,7 @@ const OUTGOING_EVENTS = {
228
228
  OutsideCanvasClick: 'outsideCanvasClick',
229
229
  SDKFeatures: 'sdkFeatures',
230
230
  RequestEntities: 'REQUEST_ENTITIES',
231
+ CanvasGeometryUpdated: 'canvasGeometryUpdated',
231
232
  };
232
233
  const INCOMING_EVENTS$1 = {
233
234
  RequestEditorMode: 'requestEditorMode',
@@ -1398,6 +1399,51 @@ let DebugLogger$1 = class DebugLogger {
1398
1399
  DebugLogger$1.instance = null;
1399
1400
  DebugLogger$1.getInstance();
1400
1401
 
1402
+ const findOutermostCoordinates = (first, second) => {
1403
+ return {
1404
+ top: Math.min(first.top, second.top),
1405
+ right: Math.max(first.right, second.right),
1406
+ bottom: Math.max(first.bottom, second.bottom),
1407
+ left: Math.min(first.left, second.left),
1408
+ };
1409
+ };
1410
+ const getElementCoordinates = (element) => {
1411
+ const rect = element.getBoundingClientRect();
1412
+ /**
1413
+ * If element does not have children, or element has it's own width or height,
1414
+ * return the element's coordinates.
1415
+ */
1416
+ if (element.children.length === 0 || rect.width !== 0 || rect.height !== 0) {
1417
+ return rect;
1418
+ }
1419
+ const rects = [];
1420
+ /**
1421
+ * If element has children, or element does not have it's own width and height,
1422
+ * we find the cordinates of the children, and assume the outermost coordinates of the children
1423
+ * as the coordinate of the element.
1424
+ *
1425
+ * E.g child1 => {top: 2, bottom: 3, left: 4, right: 6} & child2 => {top: 1, bottom: 8, left: 12, right: 24}
1426
+ * The final assumed coordinates of the element would be => { top: 1, right: 24, bottom: 8, left: 4 }
1427
+ */
1428
+ for (const child of element.children) {
1429
+ const childRect = getElementCoordinates(child);
1430
+ if (childRect.width !== 0 || childRect.height !== 0) {
1431
+ const { top, right, bottom, left } = childRect;
1432
+ rects.push({ top, right, bottom, left });
1433
+ }
1434
+ }
1435
+ if (rects.length === 0) {
1436
+ return rect;
1437
+ }
1438
+ const { top, right, bottom, left } = rects.reduce(findOutermostCoordinates);
1439
+ return DOMRect.fromRect({
1440
+ x: left,
1441
+ y: top,
1442
+ height: bottom - top,
1443
+ width: right - left,
1444
+ });
1445
+ };
1446
+
1401
1447
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1402
1448
  const isLinkToAsset = (variable) => {
1403
1449
  if (!variable)
@@ -4073,18 +4119,115 @@ const EmptyCanvasMessage = () => {
4073
4119
  React.createElement("span", { className: styles['empty-canvas-label'] }, "Add components to begin")));
4074
4120
  };
4075
4121
 
4122
+ /**
4123
+ * This function gets the element co-ordinates of a specified component in the DOM and its parent
4124
+ * and sends the DOM Rect to the client app.
4125
+ */
4126
+ const sendCanvasGeometryUpdatedMessage = async (tree, sourceEvent) => {
4127
+ const nodeToCoordinatesMap = {};
4128
+ await waitForAllImagesToBeLoaded();
4129
+ collectNodeCoordinates(tree.root, nodeToCoordinatesMap);
4130
+ sendMessage(OUTGOING_EVENTS.CanvasGeometryUpdated, {
4131
+ size: {
4132
+ width: document.documentElement.offsetWidth,
4133
+ height: document.documentElement.offsetHeight,
4134
+ },
4135
+ nodes: nodeToCoordinatesMap,
4136
+ sourceEvent,
4137
+ });
4138
+ };
4139
+ const collectNodeCoordinates = (node, nodeToCoordinatesMap) => {
4140
+ const selectedElement = document.querySelector(`[data-cf-node-id="${node.data.id}"]`);
4141
+ if (selectedElement) {
4142
+ const rect = getElementCoordinates(selectedElement);
4143
+ nodeToCoordinatesMap[node.data.id] = {
4144
+ coordinates: {
4145
+ x: rect.x + window.scrollX,
4146
+ y: rect.y + window.scrollY,
4147
+ width: rect.width,
4148
+ height: rect.height,
4149
+ },
4150
+ };
4151
+ }
4152
+ node.children.forEach((child) => collectNodeCoordinates(child, nodeToCoordinatesMap));
4153
+ };
4154
+ const waitForAllImagesToBeLoaded = () => {
4155
+ // If the document contains an image, wait for this image to be loaded before collecting & sending all geometry data.
4156
+ const allImageNodes = document.querySelectorAll('img');
4157
+ return Promise.all(Array.from(allImageNodes).map((imageNode) => {
4158
+ if (imageNode.complete) {
4159
+ return Promise.resolve();
4160
+ }
4161
+ return new Promise((resolve, reject) => {
4162
+ const handleImageLoad = (event) => {
4163
+ imageNode.removeEventListener('load', handleImageLoad);
4164
+ imageNode.removeEventListener('error', handleImageLoad);
4165
+ if (event.type === 'error') {
4166
+ console.warn('Image failed to load:', imageNode);
4167
+ reject();
4168
+ }
4169
+ else {
4170
+ resolve();
4171
+ }
4172
+ };
4173
+ imageNode.addEventListener('load', handleImageLoad);
4174
+ imageNode.addEventListener('error', handleImageLoad);
4175
+ });
4176
+ }));
4177
+ };
4178
+
4179
+ const useCanvasGeometryUpdates = ({ tree, rootContainerRef, }) => {
4180
+ const debouncedUpdateGeometry = useMemo(() => debounce((tree, sourceEvent) => {
4181
+ // When the DOM changed, we still need to wait for the next frame to ensure that
4182
+ // rendering is complete (e.g. this is required when deleting a node).
4183
+ window.requestAnimationFrame(() => {
4184
+ sendCanvasGeometryUpdatedMessage(tree, sourceEvent);
4185
+ });
4186
+ }, 100, {
4187
+ leading: true,
4188
+ // To be sure, we recalculate it at the end of the frame again. Though, we couldn't
4189
+ // yet show the need for this. So we might be able to drop this later to boost performance.
4190
+ trailing: true,
4191
+ }), []);
4192
+ // Store tree in a ref to avoid the need to deactivate & reactivate the mutation observer
4193
+ // when the tree changes. This is important to avoid missing out on some mutation events.
4194
+ const treeRef = useRef(tree);
4195
+ useEffect(() => {
4196
+ treeRef.current = tree;
4197
+ }, [tree]);
4198
+ // Handling window resize events
4199
+ useEffect(() => {
4200
+ const resizeEventListener = () => debouncedUpdateGeometry(treeRef.current, 'resize');
4201
+ window.addEventListener('resize', resizeEventListener);
4202
+ return () => window.removeEventListener('resize', resizeEventListener);
4203
+ }, [debouncedUpdateGeometry]);
4204
+ // Handling DOM mutations
4205
+ useEffect(() => {
4206
+ if (!rootContainerRef.current)
4207
+ return;
4208
+ const observer = new MutationObserver(() => debouncedUpdateGeometry(treeRef.current, 'mutation'));
4209
+ observer.observe(rootContainerRef.current, {
4210
+ childList: true,
4211
+ subtree: true,
4212
+ attributes: true,
4213
+ });
4214
+ return () => observer.disconnect();
4215
+ }, [debouncedUpdateGeometry, rootContainerRef]);
4216
+ };
4217
+
4076
4218
  const RootRenderer = () => {
4219
+ const rootContainerRef = useRef(null);
4220
+ const tree = useTreeStore((state) => state.tree);
4221
+ useCanvasGeometryUpdates({ tree, rootContainerRef });
4077
4222
  useEditorSubscriber();
4078
4223
  const breakpoints = useTreeStore((state) => state.breakpoints);
4079
- const containerRef = useRef(null);
4080
4224
  const { resolveDesignValue } = useBreakpoints(breakpoints);
4081
- const tree = useTreeStore((state) => state.tree);
4082
4225
  // If the root blockId is defined but not the default string, it is the entry ID
4083
4226
  // of the experience/ pattern to properly detect circular dependencies.
4084
4227
  const rootBlockId = tree.root.data.blockId ?? ROOT_ID;
4085
4228
  const wrappingPatternIds = rootBlockId !== ROOT_ID ? new Set([rootBlockId]) : new Set();
4086
4229
  return (React.createElement(React.Fragment, null,
4087
- React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer, ref: containerRef }, !tree.root.children.length ? (React.createElement(EmptyCanvasMessage, null)) : (tree.root.children.map((topLevelChildNode) => (React.createElement(EditorBlock, { key: topLevelChildNode.data.id, node: topLevelChildNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))))));
4230
+ React.createElement("div", { "data-ctfl-root": true, className: styles$2.rootContainer, ref: rootContainerRef }, !tree.root.children.length ? (React.createElement(EmptyCanvasMessage, null)) : (tree.root.children.map((topLevelChildNode) => (React.createElement(EditorBlock, { key: topLevelChildNode.data.id, node: topLevelChildNode, resolveDesignValue: resolveDesignValue, wrappingPatternIds: wrappingPatternIds })))))));
4088
4231
  };
4089
4232
 
4090
4233
  const useInitializeEditor = () => {