@dotcms/uve 1.5.2 → 1.5.4-next.33

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/public.esm.js CHANGED
@@ -1,6 +1,20 @@
1
1
  import { UVEEventType, UVE_MODE, DotCMSUVEAction } from '@dotcms/types';
2
2
  import { __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal';
3
3
 
4
+ /**
5
+ * Sentinel values for the placeholder contentlet used when the UVE represents
6
+ * an empty container (e.g. hover / selection without a real contentlet).
7
+ *
8
+ * @internal
9
+ */
10
+ const TEMP_EMPTY_CONTENTLET = 'TEMP_EMPTY_CONTENTLET';
11
+ /**
12
+ * Placeholder `contentType` for {@link TEMP_EMPTY_CONTENTLET}.
13
+ *
14
+ * @internal
15
+ */
16
+ const TEMP_EMPTY_CONTENTLET_TYPE = 'TEMP_EMPTY_CONTENTLET_TYPE';
17
+
4
18
  /**
5
19
  * Calculates the bounding information for each page element within the given containers.
6
20
  *
@@ -340,6 +354,27 @@ function getDotContainerAttributes({
340
354
  'data-dot-uuid': uuid
341
355
  };
342
356
  }
357
+ /**
358
+ * Read a contentlet's dataset attributes off a DOM element and return a
359
+ * normalized contentlet object. Mirrors the shape consumed by the editor's
360
+ * SET_BOUNDS and CONTENTLET_CLICKED events. Optionally parses the
361
+ * `dotStyleProperties` JSON when present.
362
+ */
363
+ function readContentletDataset(element) {
364
+ const dataset = element.dataset ?? {};
365
+ return {
366
+ identifier: dataset['dotIdentifier'],
367
+ title: dataset['dotTitle'],
368
+ inode: dataset['dotInode'],
369
+ contentType: dataset['dotType'],
370
+ baseType: dataset['dotBasetype'],
371
+ widgetTitle: dataset['dotWidgetTitle'],
372
+ onNumberOfPages: dataset['dotOnNumberOfPages'],
373
+ ...(dataset['dotStyleProperties'] && {
374
+ dotStyleProperties: JSON.parse(dataset['dotStyleProperties'])
375
+ })
376
+ };
377
+ }
343
378
 
344
379
  /**
345
380
  * Subscribes to content changes in the UVE editor
@@ -387,29 +422,124 @@ function onPageReload(callback) {
387
422
  event: UVEEventType.PAGE_RELOAD
388
423
  };
389
424
  }
425
+ const AUTO_BOUNDS_DEBOUNCE_MS = 100;
390
426
  /**
391
- * Subscribes to request bounds events in the UVE editor
427
+ * The single bounds-sync channel. Observes the iframe document and
428
+ * every `[data-dot-object="container"]` with a single ResizeObserver,
429
+ * debounces the trailing edge by {@link AUTO_BOUNDS_DEBOUNCE_MS}ms, and
430
+ * emits the full `getDotCMSPageBounds(...)` payload whenever the layout
431
+ * settles. Also listens on `scroll` (since scrolling moves contentlets
432
+ * without changing layout) and on `UVE_FLUSH_BOUNDS` (the editor's
433
+ * "give me bounds NOW, skip the debounce" message used during drag).
434
+ *
435
+ * Re-runs `querySelectorAll` and the observer wiring whenever a
436
+ * MutationObserver detects child changes that touch container nodes,
437
+ * so containers that mount/unmount after page-load are picked up
438
+ * automatically.
392
439
  *
393
- * @param {UVEEventHandler} callback - Function to be called when bounds are requested
394
- * @returns {Object} Object containing unsubscribe function and event type
395
- * @returns {Function} .unsubscribe - Function to remove the event listener
396
- * @returns {UVEEventType} .event - The event type being subscribed to
397
440
  * @internal
398
441
  */
399
- function onRequestBounds(callback) {
400
- const messageCallback = event => {
401
- if (event.data.name === __DOTCMS_UVE_EVENT__.UVE_REQUEST_BOUNDS) {
402
- const containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
403
- const positionData = getDotCMSPageBounds(containers);
404
- callback(positionData);
442
+ function onAutoBounds(callback) {
443
+ let debounceTimer = null;
444
+ let observed = [];
445
+ const emit = () => {
446
+ const containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
447
+ callback(getDotCMSPageBounds(containers));
448
+ };
449
+ const scheduleEmit = () => {
450
+ if (debounceTimer !== null) {
451
+ clearTimeout(debounceTimer);
405
452
  }
453
+ debounceTimer = setTimeout(() => {
454
+ debounceTimer = null;
455
+ emit();
456
+ }, AUTO_BOUNDS_DEBOUNCE_MS);
406
457
  };
407
- window.addEventListener('message', messageCallback);
458
+ const resizeObserver = new ResizeObserver(() => {
459
+ scheduleEmit();
460
+ });
461
+ const observeAll = () => {
462
+ // Tear down previous observations before re-wiring.
463
+ for (const el of observed) {
464
+ resizeObserver.unobserve(el);
465
+ }
466
+ observed = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
467
+ resizeObserver.observe(document.documentElement);
468
+ for (const container of observed) {
469
+ resizeObserver.observe(container);
470
+ }
471
+ };
472
+ observeAll();
473
+ // Containers can mount/unmount after the page first paints (route
474
+ // changes in headless apps, lazy-loaded sections, etc.). Re-wire only
475
+ // when a node carrying [data-dot-object="container"] is added or
476
+ // removed — ignoring text/attribute churn keeps this observer cheap on
477
+ // busy pages.
478
+ const containsContainerNode = nodes => {
479
+ for (let i = 0; i < nodes.length; i++) {
480
+ const node = nodes[i];
481
+ if (node.nodeType !== Node.ELEMENT_NODE) {
482
+ continue;
483
+ }
484
+ const el = node;
485
+ if (el.matches?.('[data-dot-object="container"]') || el.querySelector?.('[data-dot-object="container"]')) {
486
+ return true;
487
+ }
488
+ }
489
+ return false;
490
+ };
491
+ const mutationObserver = new MutationObserver(mutations => {
492
+ for (const m of mutations) {
493
+ if (m.type !== 'childList') continue;
494
+ if (containsContainerNode(m.addedNodes) || containsContainerNode(m.removedNodes)) {
495
+ observeAll();
496
+ scheduleEmit();
497
+ return;
498
+ }
499
+ }
500
+ });
501
+ // The SDK script can run from <head> before <body> exists. Fall back to
502
+ // <html> in that case — childList+subtree on the documentElement still
503
+ // catches container nodes that mount once <body> arrives.
504
+ mutationObserver.observe(document.body ?? document.documentElement, {
505
+ childList: true,
506
+ subtree: true
507
+ });
508
+ // Scrolling inside the iframe doesn't change layout, so ResizeObserver
509
+ // doesn't fire, but every contentlet's viewport-relative position
510
+ // (getBoundingClientRect) does change. Re-emit bounds after each
511
+ // scroll burst settles so the editor's pinned selected overlay
512
+ // re-anchors to the on-screen position.
513
+ const onScroll = () => scheduleEmit();
514
+ window.addEventListener('scroll', onScroll, {
515
+ passive: true
516
+ });
517
+ // Flush channel: the editor occasionally needs an immediate snapshot
518
+ // of bounds (drag enter, where the dropzone has to know container
519
+ // rectangles before the user moves another pixel). Bypass the
520
+ // debounce timer and emit synchronously.
521
+ const onFlush = event => {
522
+ if (event?.data?.name !== __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS) return;
523
+ if (debounceTimer !== null) {
524
+ clearTimeout(debounceTimer);
525
+ debounceTimer = null;
526
+ }
527
+ emit();
528
+ };
529
+ window.addEventListener('message', onFlush);
408
530
  return {
409
531
  unsubscribe: () => {
410
- window.removeEventListener('message', messageCallback);
532
+ if (debounceTimer !== null) {
533
+ clearTimeout(debounceTimer);
534
+ debounceTimer = null;
535
+ }
536
+ resizeObserver.disconnect();
537
+ mutationObserver.disconnect();
538
+ window.removeEventListener('scroll', onScroll);
539
+ window.removeEventListener('message', onFlush);
540
+ observed = [];
411
541
  },
412
- event: UVEEventType.REQUEST_BOUNDS
542
+ event: UVEEventType.AUTO_BOUNDS
413
543
  };
414
544
  }
415
545
  /**
@@ -470,18 +600,34 @@ function onScrollToSection(callback) {
470
600
  };
471
601
  }
472
602
  /**
473
- * Subscribes to contentlet hover events in the UVE editor
603
+ * Subscribes to contentlet hover events in the UVE editor.
604
+ *
605
+ * The callback is invoked with a payload while the pointer is over a
606
+ * DotCMS element, and once with `null` when the pointer leaves the last
607
+ * reported element (transitions onto dead space). The editor uses the
608
+ * `null` signal to clear the hover overlay so it doesn't linger over
609
+ * areas that no longer have a contentlet under the pointer.
474
610
  *
475
- * @param {UVEEventHandler} callback - Function to be called when a contentlet is hovered
611
+ * @param {UVEEventHandler} callback - Function to be called when hover state changes
476
612
  * @returns {Object} Object containing unsubscribe function and event type
477
613
  * @returns {Function} .unsubscribe - Function to remove the event listener
478
614
  * @returns {UVEEventType} .event - The event type being subscribed to
479
615
  * @internal
480
616
  */
481
617
  function onContentletHovered(callback) {
618
+ let hasHover = false;
482
619
  const pointerMoveCallback = event => {
483
620
  const foundElement = findDotCMSElement(event.target);
484
- if (!foundElement) return;
621
+ if (!foundElement) {
622
+ // Transitioning from a hovered contentlet to dead space — emit
623
+ // a single null so the editor can clear its hover overlay.
624
+ // Subsequent moves over dead space are no-ops.
625
+ if (hasHover) {
626
+ hasHover = false;
627
+ callback(null);
628
+ }
629
+ return;
630
+ }
485
631
  const {
486
632
  x,
487
633
  y,
@@ -490,26 +636,15 @@ function onContentletHovered(callback) {
490
636
  } = foundElement.getBoundingClientRect();
491
637
  const isContainer = foundElement.dataset?.['dotObject'] === 'container';
492
638
  const contentletForEmptyContainer = {
493
- identifier: 'TEMP_EMPTY_CONTENTLET',
494
- title: 'TEMP_EMPTY_CONTENTLET',
495
- contentType: 'TEMP_EMPTY_CONTENTLET_TYPE',
639
+ identifier: TEMP_EMPTY_CONTENTLET,
640
+ title: TEMP_EMPTY_CONTENTLET,
641
+ contentType: TEMP_EMPTY_CONTENTLET_TYPE,
496
642
  inode: 'TEMPY_EMPTY_CONTENTLET_INODE',
497
- widgetTitle: 'TEMP_EMPTY_CONTENTLET',
498
- baseType: 'TEMP_EMPTY_CONTENTLET',
643
+ widgetTitle: TEMP_EMPTY_CONTENTLET,
644
+ baseType: TEMP_EMPTY_CONTENTLET,
499
645
  onNumberOfPages: 1
500
646
  };
501
- const contentlet = {
502
- identifier: foundElement.dataset?.['dotIdentifier'],
503
- title: foundElement.dataset?.['dotTitle'],
504
- inode: foundElement.dataset?.['dotInode'],
505
- contentType: foundElement.dataset?.['dotType'],
506
- baseType: foundElement.dataset?.['dotBasetype'],
507
- widgetTitle: foundElement.dataset?.['dotWidgetTitle'],
508
- onNumberOfPages: foundElement.dataset?.['dotOnNumberOfPages'],
509
- ...(foundElement.dataset?.['dotStyleProperties'] && {
510
- dotStyleProperties: JSON.parse(foundElement.dataset['dotStyleProperties'])
511
- })
512
- };
647
+ const contentlet = readContentletDataset(foundElement);
513
648
  const vtlFiles = findDotCMSVTLData(foundElement);
514
649
  const contentletPayload = {
515
650
  container:
@@ -526,8 +661,15 @@ function onContentletHovered(callback) {
526
661
  height,
527
662
  payload: contentletPayload
528
663
  };
664
+ hasHover = true;
529
665
  callback(contentletHoveredPayload);
530
666
  };
667
+ // We intentionally do not fire null on document `pointerleave`: the
668
+ // editor's hover toolbar lives in the parent window (outside the
669
+ // iframe), so leaving the iframe usually means the user is heading
670
+ // for the toolbar. Killing the overlay there would yank the toolbar
671
+ // away just as the user reaches for it. Dead-space-inside-iframe
672
+ // is already covered by the `pointermove` null branch above.
531
673
  document.addEventListener('pointermove', pointerMoveCallback);
532
674
  return {
533
675
  unsubscribe: () => {
@@ -536,6 +678,91 @@ function onContentletHovered(callback) {
536
678
  event: UVEEventType.CONTENTLET_HOVERED
537
679
  };
538
680
  }
681
+ /**
682
+ * Subscribes to contentlet click events in the UVE editor.
683
+ *
684
+ * The editor's hover overlay is `pointer-events: none` so wheel events pass
685
+ * through to the iframe. We detect the user's selection click here instead and
686
+ * post it back to the editor.
687
+ *
688
+ * @param {UVEEventHandler} callback - Function to be called when a contentlet is clicked
689
+ * @returns {Object} Object containing unsubscribe function and event type
690
+ * @internal
691
+ */
692
+ function onContentletClicked(callback) {
693
+ // Track the last selected contentlet so a second click on the same one
694
+ // lets the page's native click through (links, accordions, etc.). The
695
+ // first click is "select"; subsequent clicks on the selected contentlet
696
+ // are "interact with the page".
697
+ let lastSelectedInode;
698
+ const clickCallback = event => {
699
+ const foundElement = findDotCMSElement(event.target);
700
+ if (!foundElement) return;
701
+ const isContainer = foundElement.dataset?.['dotObject'] === 'container';
702
+ // Only emit for contentlet clicks; an empty container click is a no-op
703
+ // for selection purposes (there's nothing to select).
704
+ if (isContainer) return;
705
+ const inode = foundElement.dataset?.['dotInode'];
706
+ // If the user is clicking the already-selected contentlet, let the
707
+ // page handle the click natively (link navigation, button handlers,
708
+ // form submission). The editor selection toolbar already exposes the
709
+ // edit/delete/etc actions; the contentlet's own UI should still work.
710
+ if (inode && inode === lastSelectedInode) {
711
+ return;
712
+ }
713
+ // First click on this contentlet (or a different one) — select it in
714
+ // the editor and block the page's natural click. Capture phase +
715
+ // preventDefault + stopPropagation suppresses both the default action
716
+ // and any subscribers further down the tree.
717
+ event.preventDefault();
718
+ event.stopPropagation();
719
+ lastSelectedInode = inode;
720
+ const {
721
+ x,
722
+ y,
723
+ width,
724
+ height
725
+ } = foundElement.getBoundingClientRect();
726
+ const contentlet = readContentletDataset(foundElement);
727
+ const vtlFiles = findDotCMSVTLData(foundElement);
728
+ callback({
729
+ x,
730
+ y,
731
+ width,
732
+ height,
733
+ payload: {
734
+ container: foundElement.dataset?.['dotContainer'] ? JSON.parse(foundElement.dataset?.['dotContainer']) : getClosestDotCMSContainerData(foundElement),
735
+ contentlet,
736
+ vtlFiles
737
+ }
738
+ });
739
+ };
740
+ // The editor clears its selection on canvas resize / scroll. When that
741
+ // happens, our lastSelectedInode is stale: a click on what used to be the
742
+ // selected contentlet would be treated as a passthrough (page click) even
743
+ // though the editor no longer has it selected. Listen for the
744
+ // UVE_SELECTION_CLEARED message and reset the tracker.
745
+ const selectionClearedCallback = event => {
746
+ if (event?.data?.name === __DOTCMS_UVE_EVENT__.UVE_SELECTION_CLEARED) {
747
+ lastSelectedInode = undefined;
748
+ }
749
+ };
750
+ // Capture phase so we run BEFORE the page's own click handlers and can
751
+ // preventDefault/stopPropagation effectively.
752
+ document.addEventListener('click', clickCallback, {
753
+ capture: true
754
+ });
755
+ window.addEventListener('message', selectionClearedCallback);
756
+ return {
757
+ unsubscribe: () => {
758
+ document.removeEventListener('click', clickCallback, {
759
+ capture: true
760
+ });
761
+ window.removeEventListener('message', selectionClearedCallback);
762
+ },
763
+ event: UVEEventType.CONTENTLET_CLICKED
764
+ };
765
+ }
539
766
 
540
767
  /**
541
768
  * Events that can be subscribed to in the UVE
@@ -550,17 +777,31 @@ const __UVE_EVENTS__ = {
550
777
  [UVEEventType.PAGE_RELOAD]: callback => {
551
778
  return onPageReload(callback);
552
779
  },
553
- [UVEEventType.REQUEST_BOUNDS]: callback => {
554
- return onRequestBounds(callback);
555
- },
556
780
  [UVEEventType.IFRAME_SCROLL]: callback => {
557
781
  return onIframeScroll(callback);
558
782
  },
559
783
  [UVEEventType.CONTENTLET_HOVERED]: callback => {
560
784
  return onContentletHovered(callback);
561
785
  },
786
+ [UVEEventType.CONTENTLET_CLICKED]: callback => {
787
+ return onContentletClicked(callback);
788
+ },
562
789
  [UVEEventType.SCROLL_TO_SECTION]: callback => {
563
790
  return onScrollToSection(callback);
791
+ },
792
+ // SELECTION_CLEARED is editor→SDK only. No public subscriber surface;
793
+ // onContentletClicked listens for the underlying postMessage internally
794
+ // to reset its lastSelectedInode tracker.
795
+ [UVEEventType.SELECTION_CLEARED]: _callback => {
796
+ return {
797
+ unsubscribe: () => {
798
+ /* no-op: SELECTION_CLEARED has no consumer-facing subscription */
799
+ },
800
+ event: UVEEventType.SELECTION_CLEARED
801
+ };
802
+ },
803
+ [UVEEventType.AUTO_BOUNDS]: callback => {
804
+ return onAutoBounds(callback);
564
805
  }
565
806
  };
566
807
  /**
@@ -728,97 +969,6 @@ function createUVESubscription(eventType, callback) {
728
969
  return eventCallback(callback);
729
970
  }
730
971
 
731
- /**
732
- * Observes rendered document height changes and notifies the caller after layout settles.
733
- *
734
- * Uses ResizeObserver on <html> for layout/viewport-driven changes and MutationObserver
735
- * on <body> to catch DOM additions/removals that may shrink the page without a resize.
736
- * Measurement reads `body.offsetHeight`, which tracks actual content height and
737
- * decreases correctly after DOM removals, unaffected by CSS min-height on the html element.
738
- */
739
- function observeDocumentHeight({
740
- onHeightChange,
741
- documentRef = document,
742
- windowRef = window,
743
- debounceMs = 50
744
- }) {
745
- const html = documentRef.documentElement;
746
- const body = documentRef.body;
747
- let debounceTimer = null;
748
- let rafOuter = null;
749
- let rafInner = null;
750
- let lastHeight = null;
751
- let destroyed = false;
752
- const measureAndNotify = () => {
753
- const height = body.offsetHeight;
754
- if (!height || height === lastHeight) {
755
- return;
756
- }
757
- lastHeight = height;
758
- onHeightChange(height);
759
- };
760
- const scheduleNotify = () => {
761
- if (destroyed) {
762
- return;
763
- }
764
- if (debounceTimer !== null) {
765
- clearTimeout(debounceTimer);
766
- }
767
- debounceTimer = setTimeout(() => {
768
- debounceTimer = null;
769
- if (destroyed) {
770
- return;
771
- }
772
- rafOuter = windowRef.requestAnimationFrame(() => {
773
- rafOuter = null;
774
- if (destroyed) {
775
- return;
776
- }
777
- rafInner = windowRef.requestAnimationFrame(() => {
778
- rafInner = null;
779
- if (!destroyed) {
780
- measureAndNotify();
781
- }
782
- });
783
- });
784
- }, debounceMs);
785
- };
786
- const onLoad = () => scheduleNotify();
787
- if (documentRef.readyState === 'complete' || documentRef.readyState === 'interactive') {
788
- scheduleNotify();
789
- } else {
790
- windowRef.addEventListener('load', onLoad);
791
- }
792
- const resizeObserver = new ResizeObserver(() => scheduleNotify());
793
- resizeObserver.observe(html);
794
- const mutationObserver = new MutationObserver(() => scheduleNotify());
795
- mutationObserver.observe(documentRef.body ?? html, {
796
- childList: true,
797
- subtree: true
798
- });
799
- return {
800
- destroy: () => {
801
- destroyed = true;
802
- if (debounceTimer !== null) {
803
- clearTimeout(debounceTimer);
804
- debounceTimer = null;
805
- }
806
- if (rafOuter !== null) {
807
- windowRef.cancelAnimationFrame(rafOuter);
808
- rafOuter = null;
809
- }
810
- if (rafInner !== null) {
811
- windowRef.cancelAnimationFrame(rafInner);
812
- rafInner = null;
813
- }
814
- lastHeight = null;
815
- resizeObserver.disconnect();
816
- mutationObserver.disconnect();
817
- windowRef.removeEventListener('load', onLoad);
818
- }
819
- };
820
- }
821
-
822
972
  /**
823
973
  * Sets the bounds of the containers in the editor.
824
974
  * Retrieves the containers from the DOM and sends their position data to the editor.
@@ -983,9 +1133,6 @@ function registerUVEEvents() {
983
1133
  const pageReloadSubscription = createUVESubscription(UVEEventType.PAGE_RELOAD, () => {
984
1134
  window.location.reload();
985
1135
  });
986
- const requestBoundsSubscription = createUVESubscription(UVEEventType.REQUEST_BOUNDS, bounds => {
987
- setBounds(bounds);
988
- });
989
1136
  const iframeScrollSubscription = createUVESubscription(UVEEventType.IFRAME_SCROLL, direction => {
990
1137
  if (window.scrollY === 0 && direction === 'up' || computeScrollIsInBottom() && direction === 'down') {
991
1138
  // If the iframe scroll is at the top or bottom, do not send anything.
@@ -1005,14 +1152,30 @@ function registerUVEEvents() {
1005
1152
  payload: contentletHovered
1006
1153
  });
1007
1154
  });
1155
+ const contentletClickedSubscription = createUVESubscription(UVEEventType.CONTENTLET_CLICKED, contentletClicked => {
1156
+ sendMessageToUVE({
1157
+ action: DotCMSUVEAction.SET_SELECTED_CONTENTLET,
1158
+ payload: contentletClicked
1159
+ });
1160
+ });
1008
1161
  const scrollToSectionSubscription = createUVESubscription(UVEEventType.SCROLL_TO_SECTION, payload => {
1009
1162
  sendMessageToUVE({
1010
1163
  action: DotCMSUVEAction.SECTION_OFFSET,
1011
1164
  payload
1012
1165
  });
1013
1166
  });
1167
+ // The single bounds-sync channel. The SDK observes layout changes
1168
+ // inside the iframe (media-query reflows, image/font load shifts,
1169
+ // container mount/unmount, scroll, etc.) and emits SET_BOUNDS on the
1170
+ // trailing edge of a debounce window. The editor can also send a
1171
+ // UVE_FLUSH_BOUNDS message to request an immediate synchronous emit
1172
+ // (used during drag/drop, where the dropzone needs current bounds
1173
+ // before the user moves another pixel).
1174
+ const autoBoundsSubscription = createUVESubscription(UVEEventType.AUTO_BOUNDS, bounds => {
1175
+ setBounds(bounds);
1176
+ });
1014
1177
  return {
1015
- subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription, scrollToSectionSubscription]
1178
+ subscriptions: [pageReloadSubscription, iframeScrollSubscription, contentletHoveredSubscription, contentletClickedSubscription, scrollToSectionSubscription, autoBoundsSubscription]
1016
1179
  };
1017
1180
  }
1018
1181
  /**
@@ -1055,60 +1218,6 @@ function listenBlockEditorInlineEvent() {
1055
1218
  }
1056
1219
  };
1057
1220
  }
1058
- /**
1059
- * Returns whether iframe height must be synchronized via postMessage.
1060
- *
1061
- * Same-origin parents can measure iframe content directly, so they do not need
1062
- * child-driven height reporting. Cross-origin parents cannot access the iframe
1063
- * DOM, so they still need the reporter fallback.
1064
- */
1065
- function shouldReportIframeHeightToParent() {
1066
- if (window.parent === window) {
1067
- return false;
1068
- }
1069
- try {
1070
- const parentDocument = window.parent.document;
1071
- return !parentDocument;
1072
- } catch {
1073
- return true;
1074
- }
1075
- }
1076
- /**
1077
- * Reports the iframe document height to the parent UVE shell via postMessage.
1078
- *
1079
- * Uses ResizeObserver on <html> for viewport/font/image-driven size changes, and
1080
- * MutationObserver on <body> to catch DOM removals (e.g. contentlets deleted by
1081
- * the editor) that shrink the page without triggering a resize event.
1082
- *
1083
- * Measurement reads `document.documentElement.offsetHeight` — the actual rendered
1084
- * height of the <html> element after layout. `scrollHeight` is intentionally avoided
1085
- * because it does not reliably decrease when content is removed from the DOM.
1086
- *
1087
- * Height sends are coalesced to at most one per double-requestAnimationFrame pair
1088
- * so they always run after layout and paint have settled.
1089
- *
1090
- * @returns {{ destroyHeightReporter: () => void }} Cleanup function that removes
1091
- * all listeners and disconnects the observers.
1092
- */
1093
- function reportIframeHeight() {
1094
- const {
1095
- destroy
1096
- } = observeDocumentHeight({
1097
- onHeightChange: height => {
1098
- sendMessageToUVE({
1099
- action: DotCMSUVEAction.IFRAME_HEIGHT,
1100
- payload: {
1101
- height
1102
- }
1103
- });
1104
- }
1105
- });
1106
- return {
1107
- destroyHeightReporter: () => {
1108
- destroy();
1109
- }
1110
- };
1111
- }
1112
1221
  const listenBlockEditorClick = () => {
1113
1222
  const editBlockEditorNodes = document.querySelectorAll('[data-block-editor-content]');
1114
1223
  if (!editBlockEditorNodes.length) {
@@ -1314,19 +1423,13 @@ function initUVE(config = {}) {
1314
1423
  const {
1315
1424
  destroyListenBlockEditorInlineEvent
1316
1425
  } = listenBlockEditorInlineEvent();
1317
- const {
1318
- destroyHeightReporter
1319
- } = shouldReportIframeHeightToParent() ? reportIframeHeight() : {
1320
- destroyHeightReporter: () => undefined
1321
- };
1322
1426
  return {
1323
1427
  destroyUVESubscriptions: () => {
1324
1428
  subscriptions.forEach(subscription => subscription.unsubscribe());
1325
1429
  destroyScrollHandler();
1326
1430
  destroyListenBlockEditorInlineEvent();
1327
- destroyHeightReporter();
1328
1431
  }
1329
1432
  };
1330
1433
  }
1331
1434
 
1332
- export { getDotContentletAttributes as A, isValidBlocks as B, CUSTOM_NO_COMPONENT as C, DEVELOPMENT_MODE as D, EMPTY_CONTAINER_STYLE_ANGULAR as E, observeDocumentHeight as F, setBounds as G, PRODUCTION_MODE as P, START_CLASS as S, __UVE_EVENTS__ as _, createUVESubscription as a, enableBlockEditorInline as b, createContentlet as c, initUVE as d, editContentlet as e, DOT_SECTION_ID_PREFIX as f, getUVEState as g, EMPTY_CONTAINER_STYLE_REACT as h, initInlineEditing as i, END_CLASS as j, __UVE_EVENT_ERROR_FALLBACK__ as k, combineClasses as l, computeScrollIsInBottom as m, findDotCMSElement as n, findDotCMSVTLData as o, getClosestDotCMSContainerData as p, getColumnPositionClasses as q, reorderMenu as r, sendMessageToUVE as s, getContainersData as t, updateNavigation as u, getContentletsInContainer as v, getDotCMSContainerData as w, getDotCMSContentletsBound as x, getDotCMSPageBounds as y, getDotContainerAttributes as z };
1435
+ export { getDotContainerAttributes as A, getDotContentletAttributes as B, CUSTOM_NO_COMPONENT as C, DEVELOPMENT_MODE as D, EMPTY_CONTAINER_STYLE_ANGULAR as E, isValidBlocks as F, readContentletDataset as G, setBounds as H, PRODUCTION_MODE as P, START_CLASS as S, TEMP_EMPTY_CONTENTLET as T, __UVE_EVENTS__ as _, createUVESubscription as a, enableBlockEditorInline as b, createContentlet as c, initUVE as d, editContentlet as e, DOT_SECTION_ID_PREFIX as f, getUVEState as g, EMPTY_CONTAINER_STYLE_REACT as h, initInlineEditing as i, END_CLASS as j, TEMP_EMPTY_CONTENTLET_TYPE as k, __UVE_EVENT_ERROR_FALLBACK__ as l, combineClasses as m, computeScrollIsInBottom as n, findDotCMSElement as o, findDotCMSVTLData as p, getClosestDotCMSContainerData as q, reorderMenu as r, sendMessageToUVE as s, getColumnPositionClasses as t, updateNavigation as u, getContainersData as v, getContentletsInContainer as w, getDotCMSContainerData as x, getDotCMSContentletsBound as y, getDotCMSPageBounds as z };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Sentinel values for the placeholder contentlet used when the UVE represents
3
+ * an empty container (e.g. hover / selection without a real contentlet).
4
+ *
5
+ * @internal
6
+ */
7
+ export declare const TEMP_EMPTY_CONTENTLET: "TEMP_EMPTY_CONTENTLET";
8
+ /**
9
+ * Placeholder `contentType` for {@link TEMP_EMPTY_CONTENTLET}.
10
+ *
11
+ * @internal
12
+ */
13
+ export declare const TEMP_EMPTY_CONTENTLET_TYPE: "TEMP_EMPTY_CONTENTLET_TYPE";