@dotcms/uve 1.4.0-next.2 → 1.4.0-next.3

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,8 +1,8 @@
1
1
  # dotCMS UVE SDK
2
2
 
3
- The `@dotcms/uve` SDK adds live editing to your JavaScript app using the dotCMS Universal Visual Editor (UVE). It provides low-level tools that power our framework-specific SDKs, such as [`@dotcms/react`](https://github.com/dotCMS/core/blob/main/core-web/libs/sdk/react/README.md) and [`@dotcms/angular`](https://github.com/dotCMS/core/blob/main/core-web/libs/sdk/angular/README.md).
3
+ The `@dotcms/uve` SDK adds live editing to your JavaScript app using the dotCMS Universal Visual Editor (UVE). It provides low-level tools that power our framework-specific SDKs, such as [@dotcms/react](https://github.com/dotCMS/core/blob/main/core-web/libs/sdk/react/README.md) and [@dotcms/angular](https://github.com/dotCMS/core/blob/main/core-web/libs/sdk/angular/README.md).
4
4
 
5
- > ⚠️ We **do not recommend using this SDK directly** for most use cases, you should use a [framework SDK that handles setup](#Getting Started: Recommended Examples), rendering, and event wiring for you.
5
+ > ⚠️ We **do not recommend using this SDK directly** for most use cases, you should use a [framework SDK that handles setup](#getting-started-recommended-examples), rendering, and event wiring for you.
6
6
 
7
7
  With `@dotcms/uve`, framework SDKs are able to:
8
8
  - Make pages and contentlets editable
package/internal.cjs.js CHANGED
@@ -76,6 +76,7 @@ const __TINYMCE_PATH_ON_DOTCMS__ = '/ext/tinymcev7/tinymce.min.js';
76
76
 
77
77
  exports.CUSTOM_NO_COMPONENT = _public.CUSTOM_NO_COMPONENT;
78
78
  exports.DEVELOPMENT_MODE = _public.DEVELOPMENT_MODE;
79
+ exports.DOT_SECTION_ID_PREFIX = _public.DOT_SECTION_ID_PREFIX;
79
80
  exports.EMPTY_CONTAINER_STYLE_ANGULAR = _public.EMPTY_CONTAINER_STYLE_ANGULAR;
80
81
  exports.EMPTY_CONTAINER_STYLE_REACT = _public.EMPTY_CONTAINER_STYLE_REACT;
81
82
  exports.END_CLASS = _public.END_CLASS;
@@ -99,6 +100,7 @@ exports.getDotContainerAttributes = _public.getDotContainerAttributes;
99
100
  exports.getDotContentletAttributes = _public.getDotContentletAttributes;
100
101
  exports.getUVEState = _public.getUVEState;
101
102
  exports.isValidBlocks = _public.isValidBlocks;
103
+ exports.observeDocumentHeight = _public.observeDocumentHeight;
102
104
  exports.setBounds = _public.setBounds;
103
105
  exports.__BASE_TINYMCE_CONFIG_WITH_NO_DEFAULT__ = __BASE_TINYMCE_CONFIG_WITH_NO_DEFAULT__;
104
106
  exports.__DEFAULT_TINYMCE_CONFIG__ = __DEFAULT_TINYMCE_CONFIG__;
package/internal.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- export { C as CUSTOM_NO_COMPONENT, D as DEVELOPMENT_MODE, E as EMPTY_CONTAINER_STYLE_ANGULAR, f as EMPTY_CONTAINER_STYLE_REACT, h as END_CLASS, P as PRODUCTION_MODE, S as START_CLASS, _ as __UVE_EVENTS__, j as __UVE_EVENT_ERROR_FALLBACK__, k as combineClasses, l as computeScrollIsInBottom, a as createUVESubscription, m as findDotCMSElement, n as findDotCMSVTLData, o as getClosestDotCMSContainerData, p as getColumnPositionClasses, q as getContainersData, t as getContentletsInContainer, v as getDotCMSContainerData, w as getDotCMSContentletsBound, x as getDotCMSPageBounds, y as getDotContainerAttributes, z as getDotContentletAttributes, g as getUVEState, A as isValidBlocks, B as setBounds } from './public.esm.js';
1
+ export { C as CUSTOM_NO_COMPONENT, D as DEVELOPMENT_MODE, f as DOT_SECTION_ID_PREFIX, E as EMPTY_CONTAINER_STYLE_ANGULAR, h as EMPTY_CONTAINER_STYLE_REACT, j as END_CLASS, P as PRODUCTION_MODE, S as START_CLASS, _ as __UVE_EVENTS__, k as __UVE_EVENT_ERROR_FALLBACK__, l as combineClasses, m as computeScrollIsInBottom, a as createUVESubscription, n as findDotCMSElement, o as findDotCMSVTLData, p as getClosestDotCMSContainerData, q as getColumnPositionClasses, t as getContainersData, v as getContentletsInContainer, w as getDotCMSContainerData, x as getDotCMSContentletsBound, y as getDotCMSPageBounds, z as getDotContainerAttributes, A as getDotContentletAttributes, g as getUVEState, B as isValidBlocks, F as observeDocumentHeight, G as setBounds } from './public.esm.js';
2
2
  import '@dotcms/types';
3
3
  import '@dotcms/types/internal';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotcms/uve",
3
- "version": "1.4.0-next.2",
3
+ "version": "1.4.0-next.3",
4
4
  "description": "Official JavaScript library for interacting with Universal Visual Editor (UVE)",
5
5
  "repository": {
6
6
  "type": "git",
package/public.cjs.js CHANGED
@@ -438,6 +438,39 @@ function onIframeScroll(callback) {
438
438
  event: types.UVEEventType.IFRAME_SCROLL
439
439
  };
440
440
  }
441
+ /**
442
+ * Listens for scroll-to-section requests from the UVE editor.
443
+ *
444
+ * Queries `#dot-section-{n}` first, then falls back to `#section-{n}`.
445
+ * If the element is found, calls the callback with `{ sectionIndex, offsetTop }`.
446
+ * If not found, the callback is not invoked.
447
+ *
448
+ * @param {UVEEventHandler} callback - Receives `{ sectionIndex: number; offsetTop: number }`.
449
+ * @internal
450
+ */
451
+ function onScrollToSection(callback) {
452
+ const messageCallback = event => {
453
+ if (event.data.name !== internal.__DOTCMS_UVE_EVENT__.UVE_SCROLL_TO_SECTION) {
454
+ return;
455
+ }
456
+ const sectionIndex = event.data.sectionIndex;
457
+ const el = document.querySelector(`#${DOT_SECTION_ID_PREFIX}${sectionIndex}`) ?? document.querySelector(`#section-${sectionIndex}`);
458
+ if (!el) {
459
+ return;
460
+ }
461
+ callback({
462
+ sectionIndex,
463
+ offsetTop: el.offsetTop
464
+ });
465
+ };
466
+ window.addEventListener('message', messageCallback);
467
+ return {
468
+ unsubscribe: () => {
469
+ window.removeEventListener('message', messageCallback);
470
+ },
471
+ event: types.UVEEventType.SCROLL_TO_SECTION
472
+ };
473
+ }
441
474
  /**
442
475
  * Subscribes to contentlet hover events in the UVE editor
443
476
  *
@@ -527,6 +560,9 @@ const __UVE_EVENTS__ = {
527
560
  },
528
561
  [types.UVEEventType.CONTENTLET_HOVERED]: callback => {
529
562
  return onContentletHovered(callback);
563
+ },
564
+ [types.UVEEventType.SCROLL_TO_SECTION]: callback => {
565
+ return onScrollToSection(callback);
530
566
  }
531
567
  };
532
568
  /**
@@ -601,6 +637,13 @@ const EMPTY_CONTAINER_STYLE_ANGULAR = {
601
637
  * @internal
602
638
  */
603
639
  const CUSTOM_NO_COMPONENT = 'CustomNoComponent';
640
+ /**
641
+ * ID prefix applied to page section wrappers for editor scroll-to-section support.
642
+ * Used by SDK row components and the UVE scroll event handler.
643
+ *
644
+ * @internal
645
+ */
646
+ const DOT_SECTION_ID_PREFIX = 'dot-section-';
604
647
 
605
648
  /**
606
649
  * Gets the current state of the Universal Visual Editor (UVE).
@@ -687,6 +730,97 @@ function createUVESubscription(eventType, callback) {
687
730
  return eventCallback(callback);
688
731
  }
689
732
 
733
+ /**
734
+ * Observes rendered document height changes and notifies the caller after layout settles.
735
+ *
736
+ * Uses ResizeObserver on <html> for layout/viewport-driven changes and MutationObserver
737
+ * on <body> to catch DOM additions/removals that may shrink the page without a resize.
738
+ * Measurement reads `body.offsetHeight`, which tracks actual content height and
739
+ * decreases correctly after DOM removals, unaffected by CSS min-height on the html element.
740
+ */
741
+ function observeDocumentHeight({
742
+ onHeightChange,
743
+ documentRef = document,
744
+ windowRef = window,
745
+ debounceMs = 50
746
+ }) {
747
+ const html = documentRef.documentElement;
748
+ const body = documentRef.body;
749
+ let debounceTimer = null;
750
+ let rafOuter = null;
751
+ let rafInner = null;
752
+ let lastHeight = null;
753
+ let destroyed = false;
754
+ const measureAndNotify = () => {
755
+ const height = body.offsetHeight;
756
+ if (!height || height === lastHeight) {
757
+ return;
758
+ }
759
+ lastHeight = height;
760
+ onHeightChange(height);
761
+ };
762
+ const scheduleNotify = () => {
763
+ if (destroyed) {
764
+ return;
765
+ }
766
+ if (debounceTimer !== null) {
767
+ clearTimeout(debounceTimer);
768
+ }
769
+ debounceTimer = setTimeout(() => {
770
+ debounceTimer = null;
771
+ if (destroyed) {
772
+ return;
773
+ }
774
+ rafOuter = windowRef.requestAnimationFrame(() => {
775
+ rafOuter = null;
776
+ if (destroyed) {
777
+ return;
778
+ }
779
+ rafInner = windowRef.requestAnimationFrame(() => {
780
+ rafInner = null;
781
+ if (!destroyed) {
782
+ measureAndNotify();
783
+ }
784
+ });
785
+ });
786
+ }, debounceMs);
787
+ };
788
+ const onLoad = () => scheduleNotify();
789
+ if (documentRef.readyState === 'complete' || documentRef.readyState === 'interactive') {
790
+ scheduleNotify();
791
+ } else {
792
+ windowRef.addEventListener('load', onLoad);
793
+ }
794
+ const resizeObserver = new ResizeObserver(() => scheduleNotify());
795
+ resizeObserver.observe(html);
796
+ const mutationObserver = new MutationObserver(() => scheduleNotify());
797
+ mutationObserver.observe(documentRef.body ?? html, {
798
+ childList: true,
799
+ subtree: true
800
+ });
801
+ return {
802
+ destroy: () => {
803
+ destroyed = true;
804
+ if (debounceTimer !== null) {
805
+ clearTimeout(debounceTimer);
806
+ debounceTimer = null;
807
+ }
808
+ if (rafOuter !== null) {
809
+ windowRef.cancelAnimationFrame(rafOuter);
810
+ rafOuter = null;
811
+ }
812
+ if (rafInner !== null) {
813
+ windowRef.cancelAnimationFrame(rafInner);
814
+ rafInner = null;
815
+ }
816
+ lastHeight = null;
817
+ resizeObserver.disconnect();
818
+ mutationObserver.disconnect();
819
+ windowRef.removeEventListener('load', onLoad);
820
+ }
821
+ };
822
+ }
823
+
690
824
  /**
691
825
  * Sets the bounds of the containers in the editor.
692
826
  * Retrieves the containers from the DOM and sends their position data to the editor.
@@ -873,8 +1007,14 @@ function registerUVEEvents() {
873
1007
  payload: contentletHovered
874
1008
  });
875
1009
  });
1010
+ const scrollToSectionSubscription = createUVESubscription(types.UVEEventType.SCROLL_TO_SECTION, payload => {
1011
+ sendMessageToUVE({
1012
+ action: types.DotCMSUVEAction.SECTION_OFFSET,
1013
+ payload
1014
+ });
1015
+ });
876
1016
  return {
877
- subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription]
1017
+ subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription, scrollToSectionSubscription]
878
1018
  };
879
1019
  }
880
1020
  /**
@@ -917,6 +1057,60 @@ function listenBlockEditorInlineEvent() {
917
1057
  }
918
1058
  };
919
1059
  }
1060
+ /**
1061
+ * Returns whether iframe height must be synchronized via postMessage.
1062
+ *
1063
+ * Same-origin parents can measure iframe content directly, so they do not need
1064
+ * child-driven height reporting. Cross-origin parents cannot access the iframe
1065
+ * DOM, so they still need the reporter fallback.
1066
+ */
1067
+ function shouldReportIframeHeightToParent() {
1068
+ if (window.parent === window) {
1069
+ return false;
1070
+ }
1071
+ try {
1072
+ const parentDocument = window.parent.document;
1073
+ return !parentDocument;
1074
+ } catch {
1075
+ return true;
1076
+ }
1077
+ }
1078
+ /**
1079
+ * Reports the iframe document height to the parent UVE shell via postMessage.
1080
+ *
1081
+ * Uses ResizeObserver on <html> for viewport/font/image-driven size changes, and
1082
+ * MutationObserver on <body> to catch DOM removals (e.g. contentlets deleted by
1083
+ * the editor) that shrink the page without triggering a resize event.
1084
+ *
1085
+ * Measurement reads `document.documentElement.offsetHeight` — the actual rendered
1086
+ * height of the <html> element after layout. `scrollHeight` is intentionally avoided
1087
+ * because it does not reliably decrease when content is removed from the DOM.
1088
+ *
1089
+ * Height sends are coalesced to at most one per double-requestAnimationFrame pair
1090
+ * so they always run after layout and paint have settled.
1091
+ *
1092
+ * @returns {{ destroyHeightReporter: () => void }} Cleanup function that removes
1093
+ * all listeners and disconnects the observers.
1094
+ */
1095
+ function reportIframeHeight() {
1096
+ const {
1097
+ destroy
1098
+ } = observeDocumentHeight({
1099
+ onHeightChange: height => {
1100
+ sendMessageToUVE({
1101
+ action: types.DotCMSUVEAction.IFRAME_HEIGHT,
1102
+ payload: {
1103
+ height
1104
+ }
1105
+ });
1106
+ }
1107
+ });
1108
+ return {
1109
+ destroyHeightReporter: () => {
1110
+ destroy();
1111
+ }
1112
+ };
1113
+ }
920
1114
  const listenBlockEditorClick = () => {
921
1115
  const editBlockEditorNodes = document.querySelectorAll('[data-block-editor-content]');
922
1116
  if (!editBlockEditorNodes.length) {
@@ -1122,17 +1316,24 @@ function initUVE(config = {}) {
1122
1316
  const {
1123
1317
  destroyListenBlockEditorInlineEvent
1124
1318
  } = listenBlockEditorInlineEvent();
1319
+ const {
1320
+ destroyHeightReporter
1321
+ } = shouldReportIframeHeightToParent() ? reportIframeHeight() : {
1322
+ destroyHeightReporter: () => undefined
1323
+ };
1125
1324
  return {
1126
1325
  destroyUVESubscriptions: () => {
1127
1326
  subscriptions.forEach(subscription => subscription.unsubscribe());
1128
1327
  destroyScrollHandler();
1129
1328
  destroyListenBlockEditorInlineEvent();
1329
+ destroyHeightReporter();
1130
1330
  }
1131
1331
  };
1132
1332
  }
1133
1333
 
1134
1334
  exports.CUSTOM_NO_COMPONENT = CUSTOM_NO_COMPONENT;
1135
1335
  exports.DEVELOPMENT_MODE = DEVELOPMENT_MODE;
1336
+ exports.DOT_SECTION_ID_PREFIX = DOT_SECTION_ID_PREFIX;
1136
1337
  exports.EMPTY_CONTAINER_STYLE_ANGULAR = EMPTY_CONTAINER_STYLE_ANGULAR;
1137
1338
  exports.EMPTY_CONTAINER_STYLE_REACT = EMPTY_CONTAINER_STYLE_REACT;
1138
1339
  exports.END_CLASS = END_CLASS;
@@ -1161,6 +1362,7 @@ exports.getUVEState = getUVEState;
1161
1362
  exports.initInlineEditing = initInlineEditing;
1162
1363
  exports.initUVE = initUVE;
1163
1364
  exports.isValidBlocks = isValidBlocks;
1365
+ exports.observeDocumentHeight = observeDocumentHeight;
1164
1366
  exports.reorderMenu = reorderMenu;
1165
1367
  exports.sendMessageToUVE = sendMessageToUVE;
1166
1368
  exports.setBounds = setBounds;
package/public.esm.js CHANGED
@@ -436,6 +436,39 @@ function onIframeScroll(callback) {
436
436
  event: UVEEventType.IFRAME_SCROLL
437
437
  };
438
438
  }
439
+ /**
440
+ * Listens for scroll-to-section requests from the UVE editor.
441
+ *
442
+ * Queries `#dot-section-{n}` first, then falls back to `#section-{n}`.
443
+ * If the element is found, calls the callback with `{ sectionIndex, offsetTop }`.
444
+ * If not found, the callback is not invoked.
445
+ *
446
+ * @param {UVEEventHandler} callback - Receives `{ sectionIndex: number; offsetTop: number }`.
447
+ * @internal
448
+ */
449
+ function onScrollToSection(callback) {
450
+ const messageCallback = event => {
451
+ if (event.data.name !== __DOTCMS_UVE_EVENT__.UVE_SCROLL_TO_SECTION) {
452
+ return;
453
+ }
454
+ const sectionIndex = event.data.sectionIndex;
455
+ const el = document.querySelector(`#${DOT_SECTION_ID_PREFIX}${sectionIndex}`) ?? document.querySelector(`#section-${sectionIndex}`);
456
+ if (!el) {
457
+ return;
458
+ }
459
+ callback({
460
+ sectionIndex,
461
+ offsetTop: el.offsetTop
462
+ });
463
+ };
464
+ window.addEventListener('message', messageCallback);
465
+ return {
466
+ unsubscribe: () => {
467
+ window.removeEventListener('message', messageCallback);
468
+ },
469
+ event: UVEEventType.SCROLL_TO_SECTION
470
+ };
471
+ }
439
472
  /**
440
473
  * Subscribes to contentlet hover events in the UVE editor
441
474
  *
@@ -525,6 +558,9 @@ const __UVE_EVENTS__ = {
525
558
  },
526
559
  [UVEEventType.CONTENTLET_HOVERED]: callback => {
527
560
  return onContentletHovered(callback);
561
+ },
562
+ [UVEEventType.SCROLL_TO_SECTION]: callback => {
563
+ return onScrollToSection(callback);
528
564
  }
529
565
  };
530
566
  /**
@@ -599,6 +635,13 @@ const EMPTY_CONTAINER_STYLE_ANGULAR = {
599
635
  * @internal
600
636
  */
601
637
  const CUSTOM_NO_COMPONENT = 'CustomNoComponent';
638
+ /**
639
+ * ID prefix applied to page section wrappers for editor scroll-to-section support.
640
+ * Used by SDK row components and the UVE scroll event handler.
641
+ *
642
+ * @internal
643
+ */
644
+ const DOT_SECTION_ID_PREFIX = 'dot-section-';
602
645
 
603
646
  /**
604
647
  * Gets the current state of the Universal Visual Editor (UVE).
@@ -685,6 +728,97 @@ function createUVESubscription(eventType, callback) {
685
728
  return eventCallback(callback);
686
729
  }
687
730
 
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
+
688
822
  /**
689
823
  * Sets the bounds of the containers in the editor.
690
824
  * Retrieves the containers from the DOM and sends their position data to the editor.
@@ -871,8 +1005,14 @@ function registerUVEEvents() {
871
1005
  payload: contentletHovered
872
1006
  });
873
1007
  });
1008
+ const scrollToSectionSubscription = createUVESubscription(UVEEventType.SCROLL_TO_SECTION, payload => {
1009
+ sendMessageToUVE({
1010
+ action: DotCMSUVEAction.SECTION_OFFSET,
1011
+ payload
1012
+ });
1013
+ });
874
1014
  return {
875
- subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription]
1015
+ subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription, scrollToSectionSubscription]
876
1016
  };
877
1017
  }
878
1018
  /**
@@ -915,6 +1055,60 @@ function listenBlockEditorInlineEvent() {
915
1055
  }
916
1056
  };
917
1057
  }
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
+ }
918
1112
  const listenBlockEditorClick = () => {
919
1113
  const editBlockEditorNodes = document.querySelectorAll('[data-block-editor-content]');
920
1114
  if (!editBlockEditorNodes.length) {
@@ -1120,13 +1314,19 @@ function initUVE(config = {}) {
1120
1314
  const {
1121
1315
  destroyListenBlockEditorInlineEvent
1122
1316
  } = listenBlockEditorInlineEvent();
1317
+ const {
1318
+ destroyHeightReporter
1319
+ } = shouldReportIframeHeightToParent() ? reportIframeHeight() : {
1320
+ destroyHeightReporter: () => undefined
1321
+ };
1123
1322
  return {
1124
1323
  destroyUVESubscriptions: () => {
1125
1324
  subscriptions.forEach(subscription => subscription.unsubscribe());
1126
1325
  destroyScrollHandler();
1127
1326
  destroyListenBlockEditorInlineEvent();
1327
+ destroyHeightReporter();
1128
1328
  }
1129
1329
  };
1130
1330
  }
1131
1331
 
1132
- export { isValidBlocks as A, setBounds as B, CUSTOM_NO_COMPONENT as C, DEVELOPMENT_MODE as D, EMPTY_CONTAINER_STYLE_ANGULAR as E, 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, EMPTY_CONTAINER_STYLE_REACT as f, getUVEState as g, END_CLASS as h, initInlineEditing as i, __UVE_EVENT_ERROR_FALLBACK__ as j, combineClasses as k, computeScrollIsInBottom as l, findDotCMSElement as m, findDotCMSVTLData as n, getClosestDotCMSContainerData as o, getColumnPositionClasses as p, getContainersData as q, reorderMenu as r, sendMessageToUVE as s, getContentletsInContainer as t, updateNavigation as u, getDotCMSContainerData as v, getDotCMSContentletsBound as w, getDotCMSPageBounds as x, getDotContainerAttributes as y, getDotContentletAttributes as z };
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 };
@@ -74,3 +74,10 @@ export declare const EMPTY_CONTAINER_STYLE_ANGULAR: {
74
74
  * @internal
75
75
  */
76
76
  export declare const CUSTOM_NO_COMPONENT = "CustomNoComponent";
77
+ /**
78
+ * ID prefix applied to page section wrappers for editor scroll-to-section support.
79
+ * Used by SDK row components and the UVE scroll event handler.
80
+ *
81
+ * @internal
82
+ */
83
+ export declare const DOT_SECTION_ID_PREFIX = "dot-section-";
@@ -51,6 +51,20 @@ export declare function onIframeScroll(callback: UVEEventHandler): {
51
51
  unsubscribe: () => void;
52
52
  event: UVEEventType;
53
53
  };
54
+ /**
55
+ * Listens for scroll-to-section requests from the UVE editor.
56
+ *
57
+ * Queries `#dot-section-{n}` first, then falls back to `#section-{n}`.
58
+ * If the element is found, calls the callback with `{ sectionIndex, offsetTop }`.
59
+ * If not found, the callback is not invoked.
60
+ *
61
+ * @param {UVEEventHandler} callback - Receives `{ sectionIndex: number; offsetTop: number }`.
62
+ * @internal
63
+ */
64
+ export declare function onScrollToSection(callback: UVEEventHandler): {
65
+ unsubscribe: () => void;
66
+ event: UVEEventType;
67
+ };
54
68
  /**
55
69
  * Subscribes to contentlet hover events in the UVE editor
56
70
  *
package/src/internal.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './internal/index';
2
2
  export * from './lib/core/core.utils';
3
+ export * from './lib/dom/document-height-observer';
3
4
  export * from './lib/dom/dom.utils';
4
5
  export * from './lib/editor/internal';
@@ -0,0 +1,18 @@
1
+ export interface ObserveDocumentHeightOptions {
2
+ onHeightChange: (height: number) => void;
3
+ documentRef?: Document;
4
+ windowRef?: Window;
5
+ debounceMs?: number;
6
+ }
7
+ export interface DocumentHeightObserverHandle {
8
+ destroy: () => void;
9
+ }
10
+ /**
11
+ * Observes rendered document height changes and notifies the caller after layout settles.
12
+ *
13
+ * Uses ResizeObserver on <html> for layout/viewport-driven changes and MutationObserver
14
+ * on <body> to catch DOM additions/removals that may shrink the page without a resize.
15
+ * Measurement reads `body.offsetHeight`, which tracks actual content height and
16
+ * decreases correctly after DOM removals, unaffected by CSS min-height on the html element.
17
+ */
18
+ export declare function observeDocumentHeight({ onHeightChange, documentRef, windowRef, debounceMs }: ObserveDocumentHeightOptions): DocumentHeightObserverHandle;
@@ -51,3 +51,40 @@ export declare function setClientIsReady(config?: DotCMSPageResponse): void;
51
51
  export declare function listenBlockEditorInlineEvent(): {
52
52
  destroyListenBlockEditorInlineEvent: () => void;
53
53
  };
54
+ /**
55
+ * Returns whether iframe height must be synchronized via postMessage.
56
+ *
57
+ * Same-origin parents can measure iframe content directly, so they do not need
58
+ * child-driven height reporting. Cross-origin parents cannot access the iframe
59
+ * DOM, so they still need the reporter fallback.
60
+ */
61
+ export declare function shouldReportIframeHeightToParent(): boolean;
62
+ /**
63
+ * Reports the iframe document height to the parent UVE shell via postMessage.
64
+ *
65
+ * Uses ResizeObserver on <html> for viewport/font/image-driven size changes, and
66
+ * MutationObserver on <body> to catch DOM removals (e.g. contentlets deleted by
67
+ * the editor) that shrink the page without triggering a resize event.
68
+ *
69
+ * Measurement reads `document.documentElement.offsetHeight` — the actual rendered
70
+ * height of the <html> element after layout. `scrollHeight` is intentionally avoided
71
+ * because it does not reliably decrease when content is removed from the DOM.
72
+ *
73
+ * Height sends are coalesced to at most one per double-requestAnimationFrame pair
74
+ * so they always run after layout and paint have settled.
75
+ *
76
+ * @returns {{ destroyHeightReporter: () => void }} Cleanup function that removes
77
+ * all listeners and disconnects the observers.
78
+ */
79
+ export declare function reportIframeHeight(): {
80
+ destroyHeightReporter: () => void;
81
+ };
82
+ /**
83
+ * Injects UVE editor styles for empty containers and contentlets into the page.
84
+ * Provides visual placeholders so editors can identify and interact with empty areas.
85
+ *
86
+ * The empty-container label is read from the dotCMS i18n cache in localStorage
87
+ * (`dotMessagesKeys`). Falls back to 'Empty container' if the cache is unavailable
88
+ * (e.g. headless pages on a different origin).
89
+ */
90
+ export declare function injectEmptyStateStyles(): void;