@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/internal.cjs.js CHANGED
@@ -378,6 +378,8 @@ exports.EMPTY_CONTAINER_STYLE_REACT = _public.EMPTY_CONTAINER_STYLE_REACT;
378
378
  exports.END_CLASS = _public.END_CLASS;
379
379
  exports.PRODUCTION_MODE = _public.PRODUCTION_MODE;
380
380
  exports.START_CLASS = _public.START_CLASS;
381
+ exports.TEMP_EMPTY_CONTENTLET = _public.TEMP_EMPTY_CONTENTLET;
382
+ exports.TEMP_EMPTY_CONTENTLET_TYPE = _public.TEMP_EMPTY_CONTENTLET_TYPE;
381
383
  exports.__UVE_EVENTS__ = _public.__UVE_EVENTS__;
382
384
  exports.__UVE_EVENT_ERROR_FALLBACK__ = _public.__UVE_EVENT_ERROR_FALLBACK__;
383
385
  exports.combineClasses = _public.combineClasses;
@@ -396,7 +398,7 @@ exports.getDotContainerAttributes = _public.getDotContainerAttributes;
396
398
  exports.getDotContentletAttributes = _public.getDotContentletAttributes;
397
399
  exports.getUVEState = _public.getUVEState;
398
400
  exports.isValidBlocks = _public.isValidBlocks;
399
- exports.observeDocumentHeight = _public.observeDocumentHeight;
401
+ exports.readContentletDataset = _public.readContentletDataset;
400
402
  exports.setBounds = _public.setBounds;
401
403
  exports.__BASE_TINYMCE_CONFIG_WITH_NO_DEFAULT__ = __BASE_TINYMCE_CONFIG_WITH_NO_DEFAULT__;
402
404
  exports.__DEFAULT_TINYMCE_CONFIG__ = __DEFAULT_TINYMCE_CONFIG__;
package/internal.esm.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { g as getUVEState, s as sendMessageToUVE } from './public.esm.js';
2
- 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, B as isValidBlocks, F as observeDocumentHeight, G as setBounds } from './public.esm.js';
2
+ 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, T as TEMP_EMPTY_CONTENTLET, k as TEMP_EMPTY_CONTENTLET_TYPE, _ as __UVE_EVENTS__, l as __UVE_EVENT_ERROR_FALLBACK__, m as combineClasses, n as computeScrollIsInBottom, a as createUVESubscription, o as findDotCMSElement, p as findDotCMSVTLData, q as getClosestDotCMSContainerData, t as getColumnPositionClasses, v as getContainersData, w as getContentletsInContainer, x as getDotCMSContainerData, y as getDotCMSContentletsBound, z as getDotCMSPageBounds, A as getDotContainerAttributes, B as getDotContentletAttributes, F as isValidBlocks, G as readContentletDataset, H as setBounds } from './public.esm.js';
3
3
  import { UVE_MODE, DotCMSUVEAction } from '@dotcms/types';
4
4
  import '@dotcms/types/internal';
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotcms/uve",
3
- "version": "1.5.2",
3
+ "version": "1.5.4-next.33",
4
4
  "description": "Official JavaScript library for interacting with Universal Visual Editor (UVE)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,4 +50,4 @@
50
50
  "module": "./index.esm.js",
51
51
  "main": "./index.cjs.js",
52
52
  "types": "./index.d.ts"
53
- }
53
+ }
package/public.cjs.js CHANGED
@@ -3,6 +3,20 @@
3
3
  var types = require('@dotcms/types');
4
4
  var internal = require('@dotcms/types/internal');
5
5
 
6
+ /**
7
+ * Sentinel values for the placeholder contentlet used when the UVE represents
8
+ * an empty container (e.g. hover / selection without a real contentlet).
9
+ *
10
+ * @internal
11
+ */
12
+ const TEMP_EMPTY_CONTENTLET = 'TEMP_EMPTY_CONTENTLET';
13
+ /**
14
+ * Placeholder `contentType` for {@link TEMP_EMPTY_CONTENTLET}.
15
+ *
16
+ * @internal
17
+ */
18
+ const TEMP_EMPTY_CONTENTLET_TYPE = 'TEMP_EMPTY_CONTENTLET_TYPE';
19
+
6
20
  /**
7
21
  * Calculates the bounding information for each page element within the given containers.
8
22
  *
@@ -342,6 +356,27 @@ function getDotContainerAttributes({
342
356
  'data-dot-uuid': uuid
343
357
  };
344
358
  }
359
+ /**
360
+ * Read a contentlet's dataset attributes off a DOM element and return a
361
+ * normalized contentlet object. Mirrors the shape consumed by the editor's
362
+ * SET_BOUNDS and CONTENTLET_CLICKED events. Optionally parses the
363
+ * `dotStyleProperties` JSON when present.
364
+ */
365
+ function readContentletDataset(element) {
366
+ const dataset = element.dataset ?? {};
367
+ return {
368
+ identifier: dataset['dotIdentifier'],
369
+ title: dataset['dotTitle'],
370
+ inode: dataset['dotInode'],
371
+ contentType: dataset['dotType'],
372
+ baseType: dataset['dotBasetype'],
373
+ widgetTitle: dataset['dotWidgetTitle'],
374
+ onNumberOfPages: dataset['dotOnNumberOfPages'],
375
+ ...(dataset['dotStyleProperties'] && {
376
+ dotStyleProperties: JSON.parse(dataset['dotStyleProperties'])
377
+ })
378
+ };
379
+ }
345
380
 
346
381
  /**
347
382
  * Subscribes to content changes in the UVE editor
@@ -389,29 +424,124 @@ function onPageReload(callback) {
389
424
  event: types.UVEEventType.PAGE_RELOAD
390
425
  };
391
426
  }
427
+ const AUTO_BOUNDS_DEBOUNCE_MS = 100;
392
428
  /**
393
- * Subscribes to request bounds events in the UVE editor
429
+ * The single bounds-sync channel. Observes the iframe document and
430
+ * every `[data-dot-object="container"]` with a single ResizeObserver,
431
+ * debounces the trailing edge by {@link AUTO_BOUNDS_DEBOUNCE_MS}ms, and
432
+ * emits the full `getDotCMSPageBounds(...)` payload whenever the layout
433
+ * settles. Also listens on `scroll` (since scrolling moves contentlets
434
+ * without changing layout) and on `UVE_FLUSH_BOUNDS` (the editor's
435
+ * "give me bounds NOW, skip the debounce" message used during drag).
436
+ *
437
+ * Re-runs `querySelectorAll` and the observer wiring whenever a
438
+ * MutationObserver detects child changes that touch container nodes,
439
+ * so containers that mount/unmount after page-load are picked up
440
+ * automatically.
394
441
  *
395
- * @param {UVEEventHandler} callback - Function to be called when bounds are requested
396
- * @returns {Object} Object containing unsubscribe function and event type
397
- * @returns {Function} .unsubscribe - Function to remove the event listener
398
- * @returns {UVEEventType} .event - The event type being subscribed to
399
442
  * @internal
400
443
  */
401
- function onRequestBounds(callback) {
402
- const messageCallback = event => {
403
- if (event.data.name === internal.__DOTCMS_UVE_EVENT__.UVE_REQUEST_BOUNDS) {
404
- const containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
405
- const positionData = getDotCMSPageBounds(containers);
406
- callback(positionData);
444
+ function onAutoBounds(callback) {
445
+ let debounceTimer = null;
446
+ let observed = [];
447
+ const emit = () => {
448
+ const containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
449
+ callback(getDotCMSPageBounds(containers));
450
+ };
451
+ const scheduleEmit = () => {
452
+ if (debounceTimer !== null) {
453
+ clearTimeout(debounceTimer);
407
454
  }
455
+ debounceTimer = setTimeout(() => {
456
+ debounceTimer = null;
457
+ emit();
458
+ }, AUTO_BOUNDS_DEBOUNCE_MS);
408
459
  };
409
- window.addEventListener('message', messageCallback);
460
+ const resizeObserver = new ResizeObserver(() => {
461
+ scheduleEmit();
462
+ });
463
+ const observeAll = () => {
464
+ // Tear down previous observations before re-wiring.
465
+ for (const el of observed) {
466
+ resizeObserver.unobserve(el);
467
+ }
468
+ observed = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
469
+ resizeObserver.observe(document.documentElement);
470
+ for (const container of observed) {
471
+ resizeObserver.observe(container);
472
+ }
473
+ };
474
+ observeAll();
475
+ // Containers can mount/unmount after the page first paints (route
476
+ // changes in headless apps, lazy-loaded sections, etc.). Re-wire only
477
+ // when a node carrying [data-dot-object="container"] is added or
478
+ // removed — ignoring text/attribute churn keeps this observer cheap on
479
+ // busy pages.
480
+ const containsContainerNode = nodes => {
481
+ for (let i = 0; i < nodes.length; i++) {
482
+ const node = nodes[i];
483
+ if (node.nodeType !== Node.ELEMENT_NODE) {
484
+ continue;
485
+ }
486
+ const el = node;
487
+ if (el.matches?.('[data-dot-object="container"]') || el.querySelector?.('[data-dot-object="container"]')) {
488
+ return true;
489
+ }
490
+ }
491
+ return false;
492
+ };
493
+ const mutationObserver = new MutationObserver(mutations => {
494
+ for (const m of mutations) {
495
+ if (m.type !== 'childList') continue;
496
+ if (containsContainerNode(m.addedNodes) || containsContainerNode(m.removedNodes)) {
497
+ observeAll();
498
+ scheduleEmit();
499
+ return;
500
+ }
501
+ }
502
+ });
503
+ // The SDK script can run from <head> before <body> exists. Fall back to
504
+ // <html> in that case — childList+subtree on the documentElement still
505
+ // catches container nodes that mount once <body> arrives.
506
+ mutationObserver.observe(document.body ?? document.documentElement, {
507
+ childList: true,
508
+ subtree: true
509
+ });
510
+ // Scrolling inside the iframe doesn't change layout, so ResizeObserver
511
+ // doesn't fire, but every contentlet's viewport-relative position
512
+ // (getBoundingClientRect) does change. Re-emit bounds after each
513
+ // scroll burst settles so the editor's pinned selected overlay
514
+ // re-anchors to the on-screen position.
515
+ const onScroll = () => scheduleEmit();
516
+ window.addEventListener('scroll', onScroll, {
517
+ passive: true
518
+ });
519
+ // Flush channel: the editor occasionally needs an immediate snapshot
520
+ // of bounds (drag enter, where the dropzone has to know container
521
+ // rectangles before the user moves another pixel). Bypass the
522
+ // debounce timer and emit synchronously.
523
+ const onFlush = event => {
524
+ if (event?.data?.name !== internal.__DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS) return;
525
+ if (debounceTimer !== null) {
526
+ clearTimeout(debounceTimer);
527
+ debounceTimer = null;
528
+ }
529
+ emit();
530
+ };
531
+ window.addEventListener('message', onFlush);
410
532
  return {
411
533
  unsubscribe: () => {
412
- window.removeEventListener('message', messageCallback);
534
+ if (debounceTimer !== null) {
535
+ clearTimeout(debounceTimer);
536
+ debounceTimer = null;
537
+ }
538
+ resizeObserver.disconnect();
539
+ mutationObserver.disconnect();
540
+ window.removeEventListener('scroll', onScroll);
541
+ window.removeEventListener('message', onFlush);
542
+ observed = [];
413
543
  },
414
- event: types.UVEEventType.REQUEST_BOUNDS
544
+ event: types.UVEEventType.AUTO_BOUNDS
415
545
  };
416
546
  }
417
547
  /**
@@ -472,18 +602,34 @@ function onScrollToSection(callback) {
472
602
  };
473
603
  }
474
604
  /**
475
- * Subscribes to contentlet hover events in the UVE editor
605
+ * Subscribes to contentlet hover events in the UVE editor.
606
+ *
607
+ * The callback is invoked with a payload while the pointer is over a
608
+ * DotCMS element, and once with `null` when the pointer leaves the last
609
+ * reported element (transitions onto dead space). The editor uses the
610
+ * `null` signal to clear the hover overlay so it doesn't linger over
611
+ * areas that no longer have a contentlet under the pointer.
476
612
  *
477
- * @param {UVEEventHandler} callback - Function to be called when a contentlet is hovered
613
+ * @param {UVEEventHandler} callback - Function to be called when hover state changes
478
614
  * @returns {Object} Object containing unsubscribe function and event type
479
615
  * @returns {Function} .unsubscribe - Function to remove the event listener
480
616
  * @returns {UVEEventType} .event - The event type being subscribed to
481
617
  * @internal
482
618
  */
483
619
  function onContentletHovered(callback) {
620
+ let hasHover = false;
484
621
  const pointerMoveCallback = event => {
485
622
  const foundElement = findDotCMSElement(event.target);
486
- if (!foundElement) return;
623
+ if (!foundElement) {
624
+ // Transitioning from a hovered contentlet to dead space — emit
625
+ // a single null so the editor can clear its hover overlay.
626
+ // Subsequent moves over dead space are no-ops.
627
+ if (hasHover) {
628
+ hasHover = false;
629
+ callback(null);
630
+ }
631
+ return;
632
+ }
487
633
  const {
488
634
  x,
489
635
  y,
@@ -492,26 +638,15 @@ function onContentletHovered(callback) {
492
638
  } = foundElement.getBoundingClientRect();
493
639
  const isContainer = foundElement.dataset?.['dotObject'] === 'container';
494
640
  const contentletForEmptyContainer = {
495
- identifier: 'TEMP_EMPTY_CONTENTLET',
496
- title: 'TEMP_EMPTY_CONTENTLET',
497
- contentType: 'TEMP_EMPTY_CONTENTLET_TYPE',
641
+ identifier: TEMP_EMPTY_CONTENTLET,
642
+ title: TEMP_EMPTY_CONTENTLET,
643
+ contentType: TEMP_EMPTY_CONTENTLET_TYPE,
498
644
  inode: 'TEMPY_EMPTY_CONTENTLET_INODE',
499
- widgetTitle: 'TEMP_EMPTY_CONTENTLET',
500
- baseType: 'TEMP_EMPTY_CONTENTLET',
645
+ widgetTitle: TEMP_EMPTY_CONTENTLET,
646
+ baseType: TEMP_EMPTY_CONTENTLET,
501
647
  onNumberOfPages: 1
502
648
  };
503
- const contentlet = {
504
- identifier: foundElement.dataset?.['dotIdentifier'],
505
- title: foundElement.dataset?.['dotTitle'],
506
- inode: foundElement.dataset?.['dotInode'],
507
- contentType: foundElement.dataset?.['dotType'],
508
- baseType: foundElement.dataset?.['dotBasetype'],
509
- widgetTitle: foundElement.dataset?.['dotWidgetTitle'],
510
- onNumberOfPages: foundElement.dataset?.['dotOnNumberOfPages'],
511
- ...(foundElement.dataset?.['dotStyleProperties'] && {
512
- dotStyleProperties: JSON.parse(foundElement.dataset['dotStyleProperties'])
513
- })
514
- };
649
+ const contentlet = readContentletDataset(foundElement);
515
650
  const vtlFiles = findDotCMSVTLData(foundElement);
516
651
  const contentletPayload = {
517
652
  container:
@@ -528,8 +663,15 @@ function onContentletHovered(callback) {
528
663
  height,
529
664
  payload: contentletPayload
530
665
  };
666
+ hasHover = true;
531
667
  callback(contentletHoveredPayload);
532
668
  };
669
+ // We intentionally do not fire null on document `pointerleave`: the
670
+ // editor's hover toolbar lives in the parent window (outside the
671
+ // iframe), so leaving the iframe usually means the user is heading
672
+ // for the toolbar. Killing the overlay there would yank the toolbar
673
+ // away just as the user reaches for it. Dead-space-inside-iframe
674
+ // is already covered by the `pointermove` null branch above.
533
675
  document.addEventListener('pointermove', pointerMoveCallback);
534
676
  return {
535
677
  unsubscribe: () => {
@@ -538,6 +680,91 @@ function onContentletHovered(callback) {
538
680
  event: types.UVEEventType.CONTENTLET_HOVERED
539
681
  };
540
682
  }
683
+ /**
684
+ * Subscribes to contentlet click events in the UVE editor.
685
+ *
686
+ * The editor's hover overlay is `pointer-events: none` so wheel events pass
687
+ * through to the iframe. We detect the user's selection click here instead and
688
+ * post it back to the editor.
689
+ *
690
+ * @param {UVEEventHandler} callback - Function to be called when a contentlet is clicked
691
+ * @returns {Object} Object containing unsubscribe function and event type
692
+ * @internal
693
+ */
694
+ function onContentletClicked(callback) {
695
+ // Track the last selected contentlet so a second click on the same one
696
+ // lets the page's native click through (links, accordions, etc.). The
697
+ // first click is "select"; subsequent clicks on the selected contentlet
698
+ // are "interact with the page".
699
+ let lastSelectedInode;
700
+ const clickCallback = event => {
701
+ const foundElement = findDotCMSElement(event.target);
702
+ if (!foundElement) return;
703
+ const isContainer = foundElement.dataset?.['dotObject'] === 'container';
704
+ // Only emit for contentlet clicks; an empty container click is a no-op
705
+ // for selection purposes (there's nothing to select).
706
+ if (isContainer) return;
707
+ const inode = foundElement.dataset?.['dotInode'];
708
+ // If the user is clicking the already-selected contentlet, let the
709
+ // page handle the click natively (link navigation, button handlers,
710
+ // form submission). The editor selection toolbar already exposes the
711
+ // edit/delete/etc actions; the contentlet's own UI should still work.
712
+ if (inode && inode === lastSelectedInode) {
713
+ return;
714
+ }
715
+ // First click on this contentlet (or a different one) — select it in
716
+ // the editor and block the page's natural click. Capture phase +
717
+ // preventDefault + stopPropagation suppresses both the default action
718
+ // and any subscribers further down the tree.
719
+ event.preventDefault();
720
+ event.stopPropagation();
721
+ lastSelectedInode = inode;
722
+ const {
723
+ x,
724
+ y,
725
+ width,
726
+ height
727
+ } = foundElement.getBoundingClientRect();
728
+ const contentlet = readContentletDataset(foundElement);
729
+ const vtlFiles = findDotCMSVTLData(foundElement);
730
+ callback({
731
+ x,
732
+ y,
733
+ width,
734
+ height,
735
+ payload: {
736
+ container: foundElement.dataset?.['dotContainer'] ? JSON.parse(foundElement.dataset?.['dotContainer']) : getClosestDotCMSContainerData(foundElement),
737
+ contentlet,
738
+ vtlFiles
739
+ }
740
+ });
741
+ };
742
+ // The editor clears its selection on canvas resize / scroll. When that
743
+ // happens, our lastSelectedInode is stale: a click on what used to be the
744
+ // selected contentlet would be treated as a passthrough (page click) even
745
+ // though the editor no longer has it selected. Listen for the
746
+ // UVE_SELECTION_CLEARED message and reset the tracker.
747
+ const selectionClearedCallback = event => {
748
+ if (event?.data?.name === internal.__DOTCMS_UVE_EVENT__.UVE_SELECTION_CLEARED) {
749
+ lastSelectedInode = undefined;
750
+ }
751
+ };
752
+ // Capture phase so we run BEFORE the page's own click handlers and can
753
+ // preventDefault/stopPropagation effectively.
754
+ document.addEventListener('click', clickCallback, {
755
+ capture: true
756
+ });
757
+ window.addEventListener('message', selectionClearedCallback);
758
+ return {
759
+ unsubscribe: () => {
760
+ document.removeEventListener('click', clickCallback, {
761
+ capture: true
762
+ });
763
+ window.removeEventListener('message', selectionClearedCallback);
764
+ },
765
+ event: types.UVEEventType.CONTENTLET_CLICKED
766
+ };
767
+ }
541
768
 
542
769
  /**
543
770
  * Events that can be subscribed to in the UVE
@@ -552,17 +779,31 @@ const __UVE_EVENTS__ = {
552
779
  [types.UVEEventType.PAGE_RELOAD]: callback => {
553
780
  return onPageReload(callback);
554
781
  },
555
- [types.UVEEventType.REQUEST_BOUNDS]: callback => {
556
- return onRequestBounds(callback);
557
- },
558
782
  [types.UVEEventType.IFRAME_SCROLL]: callback => {
559
783
  return onIframeScroll(callback);
560
784
  },
561
785
  [types.UVEEventType.CONTENTLET_HOVERED]: callback => {
562
786
  return onContentletHovered(callback);
563
787
  },
788
+ [types.UVEEventType.CONTENTLET_CLICKED]: callback => {
789
+ return onContentletClicked(callback);
790
+ },
564
791
  [types.UVEEventType.SCROLL_TO_SECTION]: callback => {
565
792
  return onScrollToSection(callback);
793
+ },
794
+ // SELECTION_CLEARED is editor→SDK only. No public subscriber surface;
795
+ // onContentletClicked listens for the underlying postMessage internally
796
+ // to reset its lastSelectedInode tracker.
797
+ [types.UVEEventType.SELECTION_CLEARED]: _callback => {
798
+ return {
799
+ unsubscribe: () => {
800
+ /* no-op: SELECTION_CLEARED has no consumer-facing subscription */
801
+ },
802
+ event: types.UVEEventType.SELECTION_CLEARED
803
+ };
804
+ },
805
+ [types.UVEEventType.AUTO_BOUNDS]: callback => {
806
+ return onAutoBounds(callback);
566
807
  }
567
808
  };
568
809
  /**
@@ -730,97 +971,6 @@ function createUVESubscription(eventType, callback) {
730
971
  return eventCallback(callback);
731
972
  }
732
973
 
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
-
824
974
  /**
825
975
  * Sets the bounds of the containers in the editor.
826
976
  * Retrieves the containers from the DOM and sends their position data to the editor.
@@ -985,9 +1135,6 @@ function registerUVEEvents() {
985
1135
  const pageReloadSubscription = createUVESubscription(types.UVEEventType.PAGE_RELOAD, () => {
986
1136
  window.location.reload();
987
1137
  });
988
- const requestBoundsSubscription = createUVESubscription(types.UVEEventType.REQUEST_BOUNDS, bounds => {
989
- setBounds(bounds);
990
- });
991
1138
  const iframeScrollSubscription = createUVESubscription(types.UVEEventType.IFRAME_SCROLL, direction => {
992
1139
  if (window.scrollY === 0 && direction === 'up' || computeScrollIsInBottom() && direction === 'down') {
993
1140
  // If the iframe scroll is at the top or bottom, do not send anything.
@@ -1007,14 +1154,30 @@ function registerUVEEvents() {
1007
1154
  payload: contentletHovered
1008
1155
  });
1009
1156
  });
1157
+ const contentletClickedSubscription = createUVESubscription(types.UVEEventType.CONTENTLET_CLICKED, contentletClicked => {
1158
+ sendMessageToUVE({
1159
+ action: types.DotCMSUVEAction.SET_SELECTED_CONTENTLET,
1160
+ payload: contentletClicked
1161
+ });
1162
+ });
1010
1163
  const scrollToSectionSubscription = createUVESubscription(types.UVEEventType.SCROLL_TO_SECTION, payload => {
1011
1164
  sendMessageToUVE({
1012
1165
  action: types.DotCMSUVEAction.SECTION_OFFSET,
1013
1166
  payload
1014
1167
  });
1015
1168
  });
1169
+ // The single bounds-sync channel. The SDK observes layout changes
1170
+ // inside the iframe (media-query reflows, image/font load shifts,
1171
+ // container mount/unmount, scroll, etc.) and emits SET_BOUNDS on the
1172
+ // trailing edge of a debounce window. The editor can also send a
1173
+ // UVE_FLUSH_BOUNDS message to request an immediate synchronous emit
1174
+ // (used during drag/drop, where the dropzone needs current bounds
1175
+ // before the user moves another pixel).
1176
+ const autoBoundsSubscription = createUVESubscription(types.UVEEventType.AUTO_BOUNDS, bounds => {
1177
+ setBounds(bounds);
1178
+ });
1016
1179
  return {
1017
- subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription, scrollToSectionSubscription]
1180
+ subscriptions: [pageReloadSubscription, iframeScrollSubscription, contentletHoveredSubscription, contentletClickedSubscription, scrollToSectionSubscription, autoBoundsSubscription]
1018
1181
  };
1019
1182
  }
1020
1183
  /**
@@ -1057,60 +1220,6 @@ function listenBlockEditorInlineEvent() {
1057
1220
  }
1058
1221
  };
1059
1222
  }
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
- }
1114
1223
  const listenBlockEditorClick = () => {
1115
1224
  const editBlockEditorNodes = document.querySelectorAll('[data-block-editor-content]');
1116
1225
  if (!editBlockEditorNodes.length) {
@@ -1316,17 +1425,11 @@ function initUVE(config = {}) {
1316
1425
  const {
1317
1426
  destroyListenBlockEditorInlineEvent
1318
1427
  } = listenBlockEditorInlineEvent();
1319
- const {
1320
- destroyHeightReporter
1321
- } = shouldReportIframeHeightToParent() ? reportIframeHeight() : {
1322
- destroyHeightReporter: () => undefined
1323
- };
1324
1428
  return {
1325
1429
  destroyUVESubscriptions: () => {
1326
1430
  subscriptions.forEach(subscription => subscription.unsubscribe());
1327
1431
  destroyScrollHandler();
1328
1432
  destroyListenBlockEditorInlineEvent();
1329
- destroyHeightReporter();
1330
1433
  }
1331
1434
  };
1332
1435
  }
@@ -1339,6 +1442,8 @@ exports.EMPTY_CONTAINER_STYLE_REACT = EMPTY_CONTAINER_STYLE_REACT;
1339
1442
  exports.END_CLASS = END_CLASS;
1340
1443
  exports.PRODUCTION_MODE = PRODUCTION_MODE;
1341
1444
  exports.START_CLASS = START_CLASS;
1445
+ exports.TEMP_EMPTY_CONTENTLET = TEMP_EMPTY_CONTENTLET;
1446
+ exports.TEMP_EMPTY_CONTENTLET_TYPE = TEMP_EMPTY_CONTENTLET_TYPE;
1342
1447
  exports.__UVE_EVENTS__ = __UVE_EVENTS__;
1343
1448
  exports.__UVE_EVENT_ERROR_FALLBACK__ = __UVE_EVENT_ERROR_FALLBACK__;
1344
1449
  exports.combineClasses = combineClasses;
@@ -1362,7 +1467,7 @@ exports.getUVEState = getUVEState;
1362
1467
  exports.initInlineEditing = initInlineEditing;
1363
1468
  exports.initUVE = initUVE;
1364
1469
  exports.isValidBlocks = isValidBlocks;
1365
- exports.observeDocumentHeight = observeDocumentHeight;
1470
+ exports.readContentletDataset = readContentletDataset;
1366
1471
  exports.reorderMenu = reorderMenu;
1367
1472
  exports.sendMessageToUVE = sendMessageToUVE;
1368
1473
  exports.setBounds = setBounds;