From 82c938fa1889164e4dc04fb6fd84e1f74c0cb93b Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Fri, 26 Jun 2026 13:17:56 -0400 Subject: [PATCH 1/6] fix(uve): bind hover/selection listeners to documentElement to survive Zone.js iframe rewrites (#36167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On traditional (server-rendered) pages, UVE reuses a single iframe and rewrites it via `doc.open()/write()/close()` on every in-editor navigation. When the edited page loads Zone.js inside that iframe (e.g. an embedded Angular Elements / web-component widget bundle), Zone runs in global-events mode: one native gateway listener plus a JS-level task list stored on the node. `document.open()` tears down the native gateway on the persistent `document`/`window` nodes, but the JS-level Zone task list survives on them. On re-init Zone sees "already registered" and skips re-binding the native gateway, so the listener silently never fires again. The UVE SDK attached its hover (`pointermove`) and selection (`click`) listeners to `document`, so hover-to-edit and click-to-select broke after the first in-editor re-render and only came back after a hard browser reload. Fix: attach those listeners to `document.documentElement` (the `` node), which is recreated fresh on every iframe rewrite and therefore carries no stale Zone task list. Both events bubble to ``, and the click listener keeps capture phase, so behavior is identical on pages that do not load Zone.js — no regression. Window-level listeners (`message`, `scroll`) were left on `window`: the live diagnostic showed they still fire after a rewrite, so only the confirmed-dead `document` listeners were moved. Co-Authored-By: Claude Opus 4.8 (1M context) --- core-web/libs/sdk/uve/src/internal/events.ts | 32 +++++++++++++++++--- dotCMS/src/main/webapp/ext/uve/dot-uve.js | 2 +- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/core-web/libs/sdk/uve/src/internal/events.ts b/core-web/libs/sdk/uve/src/internal/events.ts index 8b6d9feb0510..388ba5d91ff7 100644 --- a/core-web/libs/sdk/uve/src/internal/events.ts +++ b/core-web/libs/sdk/uve/src/internal/events.ts @@ -343,11 +343,24 @@ export function onContentletHovered(callback: UVEEventHandler) { // for the toolbar. Killing the overlay there would yank the toolbar // away just as the user reaches for it. Dead-space-inside-iframe // is already covered by the `pointermove` null branch above. - document.addEventListener('pointermove', pointerMoveCallback); + // + // Bind to `document.documentElement` (the node) rather than + // `document`. UVE renders traditional pages by reusing one iframe and + // rewriting it via `doc.open()/write()/close()` on each in-editor + // navigation. When Zone.js is loaded inside that iframe it runs in + // global-events mode (one native gateway listener + a JS-level task + // list stored on the node); `document.open()` tears down the native + // gateway but the task list survives on the persistent `document` + // node, so Zone sees "already registered" on re-init and skips + // re-binding the native listener — pointermove silently goes dead. + // `documentElement` is a fresh node after each `write()`, so it + // carries no stale task list and re-binds cleanly. `pointermove` + // bubbles to , so behavior is identical on no-Zone pages. + document.documentElement.addEventListener('pointermove', pointerMoveCallback); return { unsubscribe: () => { - document.removeEventListener('pointermove', pointerMoveCallback); + document.documentElement.removeEventListener('pointermove', pointerMoveCallback); }, event: UVEEventType.CONTENTLET_HOVERED }; @@ -433,12 +446,23 @@ export function onContentletClicked(callback: UVEEventHandler) { // Capture phase so we run BEFORE the page's own click handlers and can // preventDefault/stopPropagation effectively. - document.addEventListener('click', clickCallback, { capture: true }); + // + // Bind to `document.documentElement` rather than `document` for the same + // reason as the hover listener above: on Zone.js-loading traditional + // pages the persistent `document` node keeps a stale Zone task list + // across `doc.open()/write()/close()` iframe rewrites, so re-binding on + // it is silently skipped after the first in-editor navigation. The + // node is recreated on each rewrite and re-binds cleanly. Capture + // phase on still runs before the page's own handlers, so + // preventDefault/stopPropagation behave identically. + document.documentElement.addEventListener('click', clickCallback, { capture: true }); window.addEventListener('message', selectionClearedCallback); return { unsubscribe: () => { - document.removeEventListener('click', clickCallback, { capture: true }); + document.documentElement.removeEventListener('click', clickCallback, { + capture: true + }); window.removeEventListener('message', selectionClearedCallback); }, event: UVEEventType.CONTENTLET_CLICKED diff --git a/dotCMS/src/main/webapp/ext/uve/dot-uve.js b/dotCMS/src/main/webapp/ext/uve/dot-uve.js index 3ac6531efff1..93717d816e73 100644 --- a/dotCMS/src/main/webapp/ext/uve/dot-uve.js +++ b/dotCMS/src/main/webapp/ext/uve/dot-uve.js @@ -1,5 +1,5 @@ (function(){ -var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN="UNKNOWN",o))(C||{});var S="TEMP_EMPTY_CONTENTLET",R="TEMP_EMPTY_CONTENTLET_TYPE";function w(e){return e.map(t=>{let n=t.getBoundingClientRect(),r=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:P(t)}),contentlets:ee(n,r)}})}function ee(e,t){return t.map(n=>{let r=n.getBoundingClientRect();return{x:0,y:r.y-e.y,width:r.width,height:r.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):b(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function P(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function b(e){let t=e.closest('[data-dot-object="container"]');return t?P(t):(console.warn("No container found for the contentlet"),null)}function f(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:f(e?.parentElement)}function D(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function _(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function h(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function U(e){let t=n=>{n.data.name==="uve-set-page-data"&&e(n.data.payload)};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"changes"}}function V(e){let t=n=>{n.data.name==="uve-reload-page"&&e()};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"page-reload"}}var te=100;function k(e){let t=null,n=[],r=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(w(a))},o=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,r()},te)},i=new ResizeObserver(()=>{o()}),s=()=>{for(let a of n)i.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),i.observe(document.documentElement);for(let a of n)i.observe(a)};s();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){s(),o();return}});u.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let g=()=>o();window.addEventListener("scroll",g,{passive:!0});let E=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),r())};return window.addEventListener("message",E),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),i.disconnect(),u.disconnect(),window.removeEventListener("scroll",g),window.removeEventListener("message",E),n=[]},event:"auto-bounds"}}function H(e){let t=n=>{if(n.data.name==="uve-scroll-inside-iframe"){let r=n.data.direction;e(r)}};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"iframe-scroll"}}function B(e){let t=n=>{if(n.data.name!=="uve-scroll-to-section")return;let r=n.data.sectionIndex,o=document.querySelector(`#${q}${r}`)??document.querySelector(`#section-${r}`);o&&e({sectionIndex:r,offsetTop:o.offsetTop})};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"scroll-to-section"}}function F(e){let t=!1,n=r=>{let o=f(r.target);if(!o){t&&(t=!1,e(null));return}let{x:i,y:s,width:l,height:u}=o.getBoundingClientRect(),g=o.dataset?.dotObject==="container",E={identifier:S,title:S,contentType:R,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:S,baseType:S,onNumberOfPages:1},a=h(o),c=D(o),m={container:o.dataset?.dotContainer?JSON.parse(o.dataset?.dotContainer):b(o),contentlet:g?E:a,vtlFiles:c},T={x:i,y:s,width:l,height:u,payload:m};t=!0,e(T)};return document.addEventListener("pointermove",n),{unsubscribe:()=>{document.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function G(e){let t,n=o=>{let i=f(o.target);if(!i||i.dataset?.dotObject==="container")return;let l=i.dataset?.dotInode;if(l&&l===t)return;o.preventDefault(),o.stopPropagation(),t=l;let{x:u,y:g,width:E,height:a}=i.getBoundingClientRect(),c=h(i),m=D(i);e({x:u,y:g,width:E,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):b(i),contentlet:c,vtlFiles:m}})},r=o=>{o?.data?.name==="uve-selection-cleared"&&(t=void 0)};return document.addEventListener("click",n,{capture:!0}),window.addEventListener("message",r),{unsubscribe:()=>{document.removeEventListener("click",n,{capture:!0}),window.removeEventListener("message",r)},event:"contentlet-clicked"}}var j={changes:e=>U(e),"page-reload":e=>V(e),"iframe-scroll":e=>H(e),"contentlet-hovered":e=>F(e),"contentlet-clicked":e=>G(e),"scroll-to-section":e=>B(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>k(e)},M=e=>({unsubscribe:()=>{},event:e});var q="dot-section-";function y(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(C),n=e.searchParams.get("mode")??"EDIT_MODE",r=e.searchParams.get("language_id"),o=e.searchParams.get("personaId"),i=e.searchParams.get("variantName"),s=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),u=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:r,persona:o,variantName:i,experimentId:s,publishDate:l,dotCMSHost:u}}function p(e,t){if(!y())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=j[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function $(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function Y(e){d({action:"edit-contentlet",payload:e})}function Q(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function J(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function K(e){d({action:"create-contentlet",payload:{contentType:e}})}function W(e){d({action:"set-bounds",payload:e})}function ne(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function O(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})};return window.addEventListener("scroll",e),window.addEventListener("scrollend",t),{destroyScrollHandler:()=>{window.removeEventListener("scroll",e),window.removeEventListener("scrollend",t)}}}function L(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function N(){let e=p("page-reload",()=>{window.location.reload()}),t=p("iframe-scroll",s=>{if(window.scrollY===0&&s==="up"||_()&&s==="down")return;let l=s==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=p("contentlet-hovered",s=>{d({action:"set-contentlet",payload:s})}),r=p("contentlet-clicked",s=>{d({action:"set-selected-contentlet",payload:s})}),o=p("scroll-to-section",s=>{d({action:"section-offset",payload:s})}),i=p("auto-bounds",s=>{W(s)});return{subscriptions:[e,t,n,r,o,i]}}function v(e){d({action:"client-ready",payload:e})}function x(){if(document.readyState==="complete")return z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{z()};return document.addEventListener("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{document.removeEventListener("DOMContentLoaded",e)}}}function X(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=ne(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` +var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN="UNKNOWN",o))(C||{});var S="TEMP_EMPTY_CONTENTLET",R="TEMP_EMPTY_CONTENTLET_TYPE";function w(e){return e.map(t=>{let n=t.getBoundingClientRect(),r=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:P(t)}),contentlets:ee(n,r)}})}function ee(e,t){return t.map(n=>{let r=n.getBoundingClientRect();return{x:0,y:r.y-e.y,width:r.width,height:r.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):b(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function P(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function b(e){let t=e.closest('[data-dot-object="container"]');return t?P(t):(console.warn("No container found for the contentlet"),null)}function f(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:f(e?.parentElement)}function D(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function _(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function h(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function U(e){let t=n=>{n.data.name==="uve-set-page-data"&&e(n.data.payload)};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"changes"}}function V(e){let t=n=>{n.data.name==="uve-reload-page"&&e()};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"page-reload"}}var te=100;function k(e){let t=null,n=[],r=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(w(a))},o=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,r()},te)},i=new ResizeObserver(()=>{o()}),s=()=>{for(let a of n)i.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),i.observe(document.documentElement);for(let a of n)i.observe(a)};s();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){s(),o();return}});u.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let g=()=>o();window.addEventListener("scroll",g,{passive:!0});let E=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),r())};return window.addEventListener("message",E),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),i.disconnect(),u.disconnect(),window.removeEventListener("scroll",g),window.removeEventListener("message",E),n=[]},event:"auto-bounds"}}function H(e){let t=n=>{if(n.data.name==="uve-scroll-inside-iframe"){let r=n.data.direction;e(r)}};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"iframe-scroll"}}function B(e){let t=n=>{if(n.data.name!=="uve-scroll-to-section")return;let r=n.data.sectionIndex,o=document.querySelector(`#${q}${r}`)??document.querySelector(`#section-${r}`);o&&e({sectionIndex:r,offsetTop:o.offsetTop})};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"scroll-to-section"}}function F(e){let t=!1,n=r=>{let o=f(r.target);if(!o){t&&(t=!1,e(null));return}let{x:i,y:s,width:l,height:u}=o.getBoundingClientRect(),g=o.dataset?.dotObject==="container",E={identifier:S,title:S,contentType:R,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:S,baseType:S,onNumberOfPages:1},a=h(o),c=D(o),m={container:o.dataset?.dotContainer?JSON.parse(o.dataset?.dotContainer):b(o),contentlet:g?E:a,vtlFiles:c},T={x:i,y:s,width:l,height:u,payload:m};t=!0,e(T)};return document.documentElement.addEventListener("pointermove",n),{unsubscribe:()=>{document.documentElement.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function G(e){let t,n=o=>{let i=f(o.target);if(!i||i.dataset?.dotObject==="container")return;let l=i.dataset?.dotInode;if(l&&l===t)return;o.preventDefault(),o.stopPropagation(),t=l;let{x:u,y:g,width:E,height:a}=i.getBoundingClientRect(),c=h(i),m=D(i);e({x:u,y:g,width:E,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):b(i),contentlet:c,vtlFiles:m}})},r=o=>{o?.data?.name==="uve-selection-cleared"&&(t=void 0)};return document.documentElement.addEventListener("click",n,{capture:!0}),window.addEventListener("message",r),{unsubscribe:()=>{document.documentElement.removeEventListener("click",n,{capture:!0}),window.removeEventListener("message",r)},event:"contentlet-clicked"}}var j={changes:e=>U(e),"page-reload":e=>V(e),"iframe-scroll":e=>H(e),"contentlet-hovered":e=>F(e),"contentlet-clicked":e=>G(e),"scroll-to-section":e=>B(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>k(e)},M=e=>({unsubscribe:()=>{},event:e});var q="dot-section-";function y(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(C),n=e.searchParams.get("mode")??"EDIT_MODE",r=e.searchParams.get("language_id"),o=e.searchParams.get("personaId"),i=e.searchParams.get("variantName"),s=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),u=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:r,persona:o,variantName:i,experimentId:s,publishDate:l,dotCMSHost:u}}function p(e,t){if(!y())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=j[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function $(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function Y(e){d({action:"edit-contentlet",payload:e})}function Q(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function J(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function K(e){d({action:"create-contentlet",payload:{contentType:e}})}function W(e){d({action:"set-bounds",payload:e})}function ne(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function O(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})};return window.addEventListener("scroll",e),window.addEventListener("scrollend",t),{destroyScrollHandler:()=>{window.removeEventListener("scroll",e),window.removeEventListener("scrollend",t)}}}function L(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function N(){let e=p("page-reload",()=>{window.location.reload()}),t=p("iframe-scroll",s=>{if(window.scrollY===0&&s==="up"||_()&&s==="down")return;let l=s==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=p("contentlet-hovered",s=>{d({action:"set-contentlet",payload:s})}),r=p("contentlet-clicked",s=>{d({action:"set-selected-contentlet",payload:s})}),o=p("scroll-to-section",s=>{d({action:"section-offset",payload:s})}),i=p("auto-bounds",s=>{W(s)});return{subscriptions:[e,t,n,r,o,i]}}function v(e){d({action:"client-ready",payload:e})}function x(){if(document.readyState==="complete")return z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{z()};return document.addEventListener("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{document.removeEventListener("DOMContentLoaded",e)}}}function X(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=ne(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` [data-dot-object="container"]:empty { width: 100%; background-color: #ECF0FD; From c8c0e485e6ac7ced11b816470dd3555f3e0bc841 Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Fri, 26 Jun 2026 13:53:21 -0400 Subject: [PATCH 2/6] fix(uve): flush container bounds on dragstart so dropzone targets always render (#36167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragging a new contentlet from the palette sometimes showed no dropzone drop targets even though the drag was clearly active. The dropzone renders only when `editorState === DRAGGING` AND `editorBounds` is populated. `dragstart` defers `setEditorDragItem` to the next animation frame (so the browser can snapshot the native drag image before Angular re-renders). The `dragenter` handler — the only place that posted `UVE_FLUSH_BOUNDS` to refresh bounds — gates on `editorDragItem` already being set and bails otherwise. Because of the `!fromElement` filter, only the first `dragenter` ever runs, so when it fires before the deferred frame it bails before flushing. `dragover` then flips the editor into DRAGGING but never flushes bounds, leaving the dropzone with stale/empty `editorBounds` (which get cleared to `[]` on scroll/device-switch) and no visible drop targets. The failure is intermittent because it depends on whether `dragenter` beats the animation frame and whether bounds happened to be fresh. Fix: post `UVE_FLUSH_BOUNDS` synchronously at drag start, independent of the race, so fresh bounds are requested the moment a drag begins and have arrived by the time `dragover` engages DRAGGING. The `requestAnimationFrame` defer for the drag item is kept so the drag-image snapshot is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dot-uve-drag-drop/dot-uve-drag-drop.service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts index 39ef67c7e1ac..bb987508e0ca 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts @@ -53,6 +53,19 @@ export class DotUveDragDropService { return; } + // Request fresh container bounds the instant the drag begins. + // `setEditorDragItem` is deferred to the next animation frame + // (so the browser can snapshot the drag image before Angular + // re-renders), which means the `dragenter` handler — the only + // other place that flushes bounds — can fire first, see no + // drag item yet, and bail before posting UVE_FLUSH_BOUNDS. When + // that race happens the dropzone is left with stale/empty + // `editorBounds` and renders no drop targets. Flushing here, at + // drag start, guarantees bounds are requested regardless of the + // race so they have arrived by the time `dragover` flips the + // editor into DRAGGING. + contentWindow?.postMessage({ name: __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS }, host); + requestAnimationFrame(() => uveStore.setEditorDragItem(data)); }); From 2260148f3e89bd78c5a45b893eb5735f28c511f4 Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Fri, 26 Jun 2026 14:42:03 -0400 Subject: [PATCH 3/6] fix(uve): reset editor state on every dragend so consecutive drags work (#36167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a successful drop, starting a new drag sometimes showed no dropzone targets. The dropzone's bounds are flushed by `$handleIsDraggingEffect`, an Angular effect that only re-fires on an IDLE→DRAGGING transition. The dragend handler only reset editor state when `dropEffect === 'none'` (cancelled drops); successful drops relied on the async save→reload to reset. That left a window where `editorState` was still DRAGGING when the next drag began, so there was no clean IDLE→DRAGGING transition, the flush effect never re-fired, and the dropzone was left without bounds. Reset editor UI state on every dragend instead. `dragend` always fires when the gesture ends, and `handleDrop` has already consumed the drag item synchronously (drop fires before dragend), so resetting here is safe and guarantees a clean IDLE state for the next drag. The save→reload still re-renders content independently. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../dot-uve-drag-drop.service.ts | 17 ++++++--- dotCMS/src/main/webapp/ext/uve/dot-uve.js | 4 +-- dotCMS/src/main/webapp/ext/uve/package.json | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 dotCMS/src/main/webapp/ext/uve/package.json diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts index bb987508e0ca..633a2c2d5c46 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts @@ -103,12 +103,19 @@ export class DotUveDragDropService { handlers.onDragEnter(event); }); - // Drag end + // Drag end — reset editor UI state after EVERY drag gesture, not just + // cancelled ones. Previously this was filtered to `dropEffect === 'none'` + // (cancelled drops only), leaving successful drops to be reset by the + // async save→reload. That left a window where `editorState` stayed + // DRAGGING after a successful drop: the next drag then had no clean + // IDLE→DRAGGING transition, so `$handleIsDraggingEffect` (which flushes + // container bounds on that transition) never re-fired and the dropzone + // showed no targets. `dragend` always fires when the gesture ends and + // `handleDrop` has already consumed the drag item synchronously (drop + // fires before dragend), so resetting here is safe and guarantees a + // clean IDLE state for the next drag. fromEvent(this.window, 'dragend') - .pipe( - takeUntilDestroyed(this.destroyRef), - filter((event: DragEvent) => event.dataTransfer?.dropEffect === 'none') - ) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { handlers.onDragEnd(); }); diff --git a/dotCMS/src/main/webapp/ext/uve/dot-uve.js b/dotCMS/src/main/webapp/ext/uve/dot-uve.js index 93717d816e73..0007217d5b64 100644 --- a/dotCMS/src/main/webapp/ext/uve/dot-uve.js +++ b/dotCMS/src/main/webapp/ext/uve/dot-uve.js @@ -1,5 +1,5 @@ (function(){ -var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN="UNKNOWN",o))(C||{});var S="TEMP_EMPTY_CONTENTLET",R="TEMP_EMPTY_CONTENTLET_TYPE";function w(e){return e.map(t=>{let n=t.getBoundingClientRect(),r=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:P(t)}),contentlets:ee(n,r)}})}function ee(e,t){return t.map(n=>{let r=n.getBoundingClientRect();return{x:0,y:r.y-e.y,width:r.width,height:r.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):b(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function P(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function b(e){let t=e.closest('[data-dot-object="container"]');return t?P(t):(console.warn("No container found for the contentlet"),null)}function f(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:f(e?.parentElement)}function D(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function _(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function h(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function U(e){let t=n=>{n.data.name==="uve-set-page-data"&&e(n.data.payload)};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"changes"}}function V(e){let t=n=>{n.data.name==="uve-reload-page"&&e()};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"page-reload"}}var te=100;function k(e){let t=null,n=[],r=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(w(a))},o=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,r()},te)},i=new ResizeObserver(()=>{o()}),s=()=>{for(let a of n)i.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),i.observe(document.documentElement);for(let a of n)i.observe(a)};s();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){s(),o();return}});u.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let g=()=>o();window.addEventListener("scroll",g,{passive:!0});let E=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),r())};return window.addEventListener("message",E),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),i.disconnect(),u.disconnect(),window.removeEventListener("scroll",g),window.removeEventListener("message",E),n=[]},event:"auto-bounds"}}function H(e){let t=n=>{if(n.data.name==="uve-scroll-inside-iframe"){let r=n.data.direction;e(r)}};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"iframe-scroll"}}function B(e){let t=n=>{if(n.data.name!=="uve-scroll-to-section")return;let r=n.data.sectionIndex,o=document.querySelector(`#${q}${r}`)??document.querySelector(`#section-${r}`);o&&e({sectionIndex:r,offsetTop:o.offsetTop})};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"scroll-to-section"}}function F(e){let t=!1,n=r=>{let o=f(r.target);if(!o){t&&(t=!1,e(null));return}let{x:i,y:s,width:l,height:u}=o.getBoundingClientRect(),g=o.dataset?.dotObject==="container",E={identifier:S,title:S,contentType:R,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:S,baseType:S,onNumberOfPages:1},a=h(o),c=D(o),m={container:o.dataset?.dotContainer?JSON.parse(o.dataset?.dotContainer):b(o),contentlet:g?E:a,vtlFiles:c},T={x:i,y:s,width:l,height:u,payload:m};t=!0,e(T)};return document.documentElement.addEventListener("pointermove",n),{unsubscribe:()=>{document.documentElement.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function G(e){let t,n=o=>{let i=f(o.target);if(!i||i.dataset?.dotObject==="container")return;let l=i.dataset?.dotInode;if(l&&l===t)return;o.preventDefault(),o.stopPropagation(),t=l;let{x:u,y:g,width:E,height:a}=i.getBoundingClientRect(),c=h(i),m=D(i);e({x:u,y:g,width:E,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):b(i),contentlet:c,vtlFiles:m}})},r=o=>{o?.data?.name==="uve-selection-cleared"&&(t=void 0)};return document.documentElement.addEventListener("click",n,{capture:!0}),window.addEventListener("message",r),{unsubscribe:()=>{document.documentElement.removeEventListener("click",n,{capture:!0}),window.removeEventListener("message",r)},event:"contentlet-clicked"}}var j={changes:e=>U(e),"page-reload":e=>V(e),"iframe-scroll":e=>H(e),"contentlet-hovered":e=>F(e),"contentlet-clicked":e=>G(e),"scroll-to-section":e=>B(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>k(e)},M=e=>({unsubscribe:()=>{},event:e});var q="dot-section-";function y(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(C),n=e.searchParams.get("mode")??"EDIT_MODE",r=e.searchParams.get("language_id"),o=e.searchParams.get("personaId"),i=e.searchParams.get("variantName"),s=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),u=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:r,persona:o,variantName:i,experimentId:s,publishDate:l,dotCMSHost:u}}function p(e,t){if(!y())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=j[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function $(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function Y(e){d({action:"edit-contentlet",payload:e})}function Q(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function J(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function K(e){d({action:"create-contentlet",payload:{contentType:e}})}function W(e){d({action:"set-bounds",payload:e})}function ne(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function O(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})};return window.addEventListener("scroll",e),window.addEventListener("scrollend",t),{destroyScrollHandler:()=>{window.removeEventListener("scroll",e),window.removeEventListener("scrollend",t)}}}function L(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function N(){let e=p("page-reload",()=>{window.location.reload()}),t=p("iframe-scroll",s=>{if(window.scrollY===0&&s==="up"||_()&&s==="down")return;let l=s==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=p("contentlet-hovered",s=>{d({action:"set-contentlet",payload:s})}),r=p("contentlet-clicked",s=>{d({action:"set-selected-contentlet",payload:s})}),o=p("scroll-to-section",s=>{d({action:"section-offset",payload:s})}),i=p("auto-bounds",s=>{W(s)});return{subscriptions:[e,t,n,r,o,i]}}function v(e){d({action:"client-ready",payload:e})}function x(){if(document.readyState==="complete")return z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{z()};return document.addEventListener("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{document.removeEventListener("DOMContentLoaded",e)}}}function X(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=ne(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` +var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN="UNKNOWN",o))(C||{});var S="TEMP_EMPTY_CONTENTLET",R="TEMP_EMPTY_CONTENTLET_TYPE";function _(e){return e.map(t=>{let n=t.getBoundingClientRect(),r=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:w(t)}),contentlets:ee(n,r)}})}function ee(e,t){return t.map(n=>{let r=n.getBoundingClientRect();return{x:0,y:r.y-e.y,width:r.width,height:r.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):b(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function w(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function b(e){let t=e.closest('[data-dot-object="container"]');return t?w(t):(console.warn("No container found for the contentlet"),null)}function f(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:f(e?.parentElement)}function D(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function A(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function h(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function U(e){let t=n=>{n.data.name==="uve-set-page-data"&&e(n.data.payload)};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"changes"}}function V(e){let t=n=>{n.data.name==="uve-reload-page"&&e()};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"page-reload"}}var te=100;function k(e){let t=null,n=[],r=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(_(a))},o=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,r()},te)},i=new ResizeObserver(()=>{o()}),s=()=>{for(let a of n)i.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),i.observe(document.documentElement);for(let a of n)i.observe(a)};s();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){s(),o();return}});u.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let g=()=>o();window.addEventListener("scroll",g,{passive:!0});let E=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),r())};return window.addEventListener("message",E),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),i.disconnect(),u.disconnect(),window.removeEventListener("scroll",g),window.removeEventListener("message",E),n=[]},event:"auto-bounds"}}function H(e){let t=n=>{if(n.data.name==="uve-scroll-inside-iframe"){let r=n.data.direction;e(r)}};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"iframe-scroll"}}function B(e){let t=n=>{if(n.data.name!=="uve-scroll-to-section")return;let r=n.data.sectionIndex,o=document.querySelector(`#${q}${r}`)??document.querySelector(`#section-${r}`);o&&e({sectionIndex:r,offsetTop:o.offsetTop})};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"scroll-to-section"}}function F(e){let t=!1,n=r=>{let o=f(r.target);if(!o){t&&(t=!1,e(null));return}let{x:i,y:s,width:l,height:u}=o.getBoundingClientRect(),g=o.dataset?.dotObject==="container",E={identifier:S,title:S,contentType:R,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:S,baseType:S,onNumberOfPages:1},a=h(o),c=D(o),m={container:o.dataset?.dotContainer?JSON.parse(o.dataset?.dotContainer):b(o),contentlet:g?E:a,vtlFiles:c},T={x:i,y:s,width:l,height:u,payload:m};t=!0,e(T)};return document.documentElement.addEventListener("pointermove",n),{unsubscribe:()=>{document.documentElement.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function G(e){let t,n=o=>{let i=f(o.target);if(!i||i.dataset?.dotObject==="container")return;let l=i.dataset?.dotInode;if(l&&l===t)return;o.preventDefault(),o.stopPropagation(),t=l;let{x:u,y:g,width:E,height:a}=i.getBoundingClientRect(),c=h(i),m=D(i);e({x:u,y:g,width:E,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):b(i),contentlet:c,vtlFiles:m}})},r=o=>{o?.data?.name==="uve-selection-cleared"&&(t=void 0)};return document.documentElement.addEventListener("click",n,{capture:!0}),window.addEventListener("message",r),{unsubscribe:()=>{document.documentElement.removeEventListener("click",n,{capture:!0}),window.removeEventListener("message",r)},event:"contentlet-clicked"}}var j={changes:e=>U(e),"page-reload":e=>V(e),"iframe-scroll":e=>H(e),"contentlet-hovered":e=>F(e),"contentlet-clicked":e=>G(e),"scroll-to-section":e=>B(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>k(e)},M=e=>({unsubscribe:()=>{},event:e});var q="dot-section-";function y(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(C),n=e.searchParams.get("mode")??"EDIT_MODE",r=e.searchParams.get("language_id"),o=e.searchParams.get("personaId"),i=e.searchParams.get("variantName"),s=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),u=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:r,persona:o,variantName:i,experimentId:s,publishDate:l,dotCMSHost:u}}function p(e,t){if(!y())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=j[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function Y(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function $(e){d({action:"edit-contentlet",payload:e})}function K(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function Q(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function W(e){d({action:"create-contentlet",payload:{contentType:e}})}function J(e){d({action:"set-bounds",payload:e})}function ne(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function N(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})};return window.addEventListener("scroll",e),window.addEventListener("scrollend",t),{destroyScrollHandler:()=>{window.removeEventListener("scroll",e),window.removeEventListener("scrollend",t)}}}function O(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function L(){let e=p("page-reload",()=>{window.location.reload()}),t=p("iframe-scroll",s=>{if(window.scrollY===0&&s==="up"||A()&&s==="down")return;let l=s==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=p("contentlet-hovered",s=>{d({action:"set-contentlet",payload:s})}),r=p("contentlet-clicked",s=>{d({action:"set-selected-contentlet",payload:s})}),o=p("scroll-to-section",s=>{d({action:"section-offset",payload:s})}),i=p("auto-bounds",s=>{J(s)});return{subscriptions:[e,t,n,r,o,i]}}function v(e){d({action:"client-ready",payload:e})}function x(){if(document.readyState==="complete")return z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{z()};return document.addEventListener("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{document.removeEventListener("DOMContentLoaded",e)}}}function X(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=ne(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` [data-dot-object="container"]:empty { width: 100%; background-color: #ECF0FD; @@ -18,5 +18,5 @@ var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN=" [data-dot-object="container"]:empty::after { content: '${t}'; } - `,document.head?.appendChild(n)}var z=()=>{let e=document.querySelectorAll("[data-block-editor-content]");e.length&&e.forEach(t=>{let{inode:n,language:r="1",contentType:o,fieldName:i,blockEditorContent:s}=t.dataset,l=JSON.parse(s||"");if(!n||!r||!o||!i){console.error("Missing data attributes for block editor inline editing."),console.warn("inode, language, contentType and fieldName are required.");return}t.classList.add("dotcms__inline-edit-field"),t.addEventListener("click",()=>{J("BLOCK_EDITOR",{inode:n,content:l,language:parseInt(r),fieldName:i,contentType:o})})})};function Z(e){let{mode:t}=y()||{};if(!t||t!=="EDIT_MODE")return;let n=e.filter((r,o)=>r.contentType?!0:(console.warn(`[registerStyleEditorSchemas] Skipping schema with index [${o}] for not having a contentType`),!1));d({action:"register-style-schemas",payload:{schemas:n}})}var oe={createSubscription:p,createContentlet:K,editContentlet:Y,reorderMenu:Q,updateNavigation:$,registerStyleEditorSchemas:Z};window.dotUVE=oe;var re=y();re?.mode==="EDIT_MODE"&&(N(),O(),L(),v(),x(),X()); + `,document.head?.appendChild(n)}var z=()=>{let e=document.querySelectorAll("[data-block-editor-content]");e.length&&e.forEach(t=>{let{inode:n,language:r="1",contentType:o,fieldName:i,blockEditorContent:s}=t.dataset,l=JSON.parse(s||"");if(!n||!r||!o||!i){console.error("Missing data attributes for block editor inline editing."),console.warn("inode, language, contentType and fieldName are required.");return}t.classList.add("dotcms__inline-edit-field"),t.addEventListener("click",()=>{Q("BLOCK_EDITOR",{inode:n,content:l,language:parseInt(r),fieldName:i,contentType:o})})})};function Z(e){let{mode:t}=y()||{};if(!t||t!=="EDIT_MODE")return;let n=e.filter((r,o)=>r.contentType?!0:(console.warn(`[registerStyleEditorSchemas] Skipping schema with index [${o}] for not having a contentType`),!1));d({action:"register-style-schemas",payload:{schemas:n}})}var oe={createSubscription:p,createContentlet:W,editContentlet:$,reorderMenu:K,updateNavigation:Y,registerStyleEditorSchemas:Z};window.dotUVE=oe;var re=y();re?.mode==="EDIT_MODE"&&(L(),N(),O(),v(),x(),X()); })(); diff --git a/dotCMS/src/main/webapp/ext/uve/package.json b/dotCMS/src/main/webapp/ext/uve/package.json new file mode 100644 index 000000000000..69c4f94eb43d --- /dev/null +++ b/dotCMS/src/main/webapp/ext/uve/package.json @@ -0,0 +1,36 @@ +{ + "name": "@dotcms/uve", + "version": "1.1.1", + "description": "Official JavaScript library for interacting with Universal Visual Editor (UVE)", + "repository": { + "type": "git", + "url": "git+https://github.com/dotCMS/core.git#main" + }, + "devDependencies": { + "@dotcms/types": "latest" + }, + "keywords": [ + "dotCMS", + "CMS", + "Content Management", + "UVE", + "Universal Visual Editor" + ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./internal": "./src/internal.ts" + }, + "typesVersions": { + "*": { + ".": ["./src/index.d.ts"], + "internal": ["./src/internal.d.ts"] + } + }, + "author": "dotcms ", + "license": "MIT", + "bugs": { + "url": "https://github.com/dotCMS/core/issues" + }, + "homepage": "https://github.com/dotCMS/core/tree/main/core-web/libs/sdk/uve/README.md" +} From 6eecc895b93349bb65aafbbcd4a8f95ba6c0581e Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Thu, 2 Jul 2026 14:15:42 -0400 Subject: [PATCH 4/6] fix(uve): implement native event binding for iframe listeners to ensure reliability across Zone.js rewrites --- core-web/libs/sdk/uve/src/internal/events.ts | 53 ++++++++++++++----- .../libs/sdk/uve/src/lib/dom/dom.utils.ts | 42 +++++++++++++++ .../libs/sdk/uve/src/script/utils.spec.ts | 34 ++++++++++++ core-web/libs/sdk/uve/src/script/utils.ts | 30 ++++++++--- dotCMS/src/main/webapp/ext/uve/dot-uve.js | 4 +- 5 files changed, 139 insertions(+), 24 deletions(-) diff --git a/core-web/libs/sdk/uve/src/internal/events.ts b/core-web/libs/sdk/uve/src/internal/events.ts index 388ba5d91ff7..a338e50dc727 100644 --- a/core-web/libs/sdk/uve/src/internal/events.ts +++ b/core-web/libs/sdk/uve/src/internal/events.ts @@ -9,6 +9,7 @@ import { findDotCMSVTLData, getClosestDotCMSContainerData, getDotCMSPageBounds, + getNativeEventBinder, readContentletDataset } from '../lib/dom/dom.utils'; @@ -28,11 +29,14 @@ export function onContentChanges(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder so this parent→iframe receiver survives the iframe's + // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', messageCallback); return { unsubscribe: () => { - window.removeEventListener('message', messageCallback); + nativeWindow.removeEventListener('message', messageCallback); }, event: UVEEventType.CONTENT_CHANGES }; @@ -54,11 +58,14 @@ export function onPageReload(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder so this parent→iframe receiver survives the iframe's + // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', messageCallback); return { unsubscribe: () => { - window.removeEventListener('message', messageCallback); + nativeWindow.removeEventListener('message', messageCallback); }, event: UVEEventType.PAGE_RELOAD }; @@ -167,8 +174,13 @@ export function onAutoBounds(callback: UVEEventHandler) { // (getBoundingClientRect) does change. Re-emit bounds after each // scroll burst settles so the editor's pinned selected overlay // re-anchors to the on-screen position. + // Bind through Zone.js's native (untracked) listener so this scroll-driven + // re-anchor survives the iframe's document.open()/write()/close() rewrites. + // Without it, controls hide on scroll but the SET_BOUNDS that brings them + // back never fires under Zone.js. See getNativeEventBinder for the rationale. + const nativeWindow = getNativeEventBinder(window); const onScroll = () => scheduleEmit(); - window.addEventListener('scroll', onScroll, { passive: true }); + nativeWindow.addEventListener('scroll', onScroll, { passive: true }); // Flush channel: the editor occasionally needs an immediate snapshot // of bounds (drag enter, where the dropzone has to know container @@ -182,7 +194,10 @@ export function onAutoBounds(callback: UVEEventHandler) { } emit(); }; - window.addEventListener('message', onFlush); + // Reuse the native binder (declared above for `scroll`) so this + // parent→iframe flush receiver survives the iframe rewrites too — this is + // the channel that supplies the dropzone's bounds during drag-and-drop. + nativeWindow.addEventListener('message', onFlush); return { unsubscribe: () => { @@ -192,8 +207,8 @@ export function onAutoBounds(callback: UVEEventHandler) { } resizeObserver.disconnect(); mutationObserver.disconnect(); - window.removeEventListener('scroll', onScroll); - window.removeEventListener('message', onFlush); + nativeWindow.removeEventListener('scroll', onScroll); + nativeWindow.removeEventListener('message', onFlush); observed = []; }, event: UVEEventType.AUTO_BOUNDS @@ -218,11 +233,15 @@ export function onIframeScroll(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder so this parent→iframe receiver survives the iframe's + // document.open()/write()/close() rewrites under Zone.js — this drives the + // edge auto-scroll while dragging near the iframe top/bottom. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', messageCallback); return { unsubscribe: () => { - window.removeEventListener('message', messageCallback); + nativeWindow.removeEventListener('message', messageCallback); }, event: UVEEventType.IFRAME_SCROLL }; @@ -255,11 +274,14 @@ export function onScrollToSection(callback: UVEEventHandler) { callback({ sectionIndex, offsetTop: el.offsetTop }); }; - window.addEventListener('message', messageCallback); + // Native binder so this parent→iframe receiver survives the iframe's + // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', messageCallback); return { unsubscribe: () => { - window.removeEventListener('message', messageCallback); + nativeWindow.removeEventListener('message', messageCallback); }, event: UVEEventType.SCROLL_TO_SECTION }; @@ -456,14 +478,17 @@ export function onContentletClicked(callback: UVEEventHandler) { // phase on still runs before the page's own handlers, so // preventDefault/stopPropagation behave identically. document.documentElement.addEventListener('click', clickCallback, { capture: true }); - window.addEventListener('message', selectionClearedCallback); + // Native binder so this parent→iframe receiver survives the iframe's + // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', selectionClearedCallback); return { unsubscribe: () => { document.documentElement.removeEventListener('click', clickCallback, { capture: true }); - window.removeEventListener('message', selectionClearedCallback); + nativeWindow.removeEventListener('message', selectionClearedCallback); }, event: UVEEventType.CONTENTLET_CLICKED }; diff --git a/core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts b/core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts index 8ace29b0b6ca..f83cfa1b31a5 100644 --- a/core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts +++ b/core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts @@ -455,3 +455,45 @@ export function readContentletDataset(element: HTMLElement) { }) }; } + +/** + * Returns Zone.js's *unpatched* native `addEventListener` / `removeEventListener` + * bound to `target`, falling back to the standard methods when Zone.js is absent. + * + * UVE reuses a single iframe and rewrites it with `document.open()/write()/close()` + * on every in-editor navigation. When Zone.js is loaded inside that iframe it runs + * in global-events mode: one native "gateway" listener per (target, eventType) + * plus a JS-level task list stored on the target node. `document.open()` tears down + * the native gateway on the persistent `window`/`document` nodes, but the task list + * survives on them, so Zone sees "already registered" on re-init and skips + * re-installing the native gateway — the listener silently goes dead after the + * first navigation. + * + * Hover/click dodge this by binding to `document.documentElement`, a node that + * `write()` recreates fresh. `scroll`/`message` (on `window`) and + * `DOMContentLoaded` (on `document`) can't: none of them fire on ``, so + * there is no fresh node to rebind to. Going through Zone's native (untracked) + * methods sidesteps the dedup entirely, so the listener rebinds cleanly after + * every rewrite. + */ +export function getNativeEventBinder( + target: T +): Pick { + const zone = (globalThis as { Zone?: { __symbol__?: (name: string) => string } }).Zone; + const symbolFor = (name: string): string => + typeof zone?.__symbol__ === 'function' ? zone.__symbol__(name) : `__zone_symbol__${name}`; + + const source = target as unknown as Record; + const base = target as EventTarget; + const nativeAdd = source[symbolFor('addEventListener')] as + | EventTarget['addEventListener'] + | undefined; + const nativeRemove = source[symbolFor('removeEventListener')] as + | EventTarget['removeEventListener'] + | undefined; + + return { + addEventListener: (nativeAdd ?? base.addEventListener).bind(base), + removeEventListener: (nativeRemove ?? base.removeEventListener).bind(base) + } as Pick; +} diff --git a/core-web/libs/sdk/uve/src/script/utils.spec.ts b/core-web/libs/sdk/uve/src/script/utils.spec.ts index 1a8ccd38af78..a6e587536d06 100644 --- a/core-web/libs/sdk/uve/src/script/utils.spec.ts +++ b/core-web/libs/sdk/uve/src/script/utils.spec.ts @@ -51,6 +51,40 @@ describe('scrollHandler', () => { expect(postMessageSpy).not.toHaveBeenCalled(); }); + + it('binds through Zone.js native listeners when Zone is present, not the patched ones', () => { + // Simulate a Zone.js-loaded page: expose the unpatched native methods + // under the __zone_symbol__ keys the way Zone.js stashes them. + const nativeAdd = jest.fn(); + const nativeRemove = jest.fn(); + const win = window as unknown as Record; + + try { + win['__zone_symbol__addEventListener'] = nativeAdd; + win['__zone_symbol__removeEventListener'] = nativeRemove; + (globalThis as unknown as { Zone: unknown }).Zone = { + __symbol__: (name: string) => `__zone_symbol__${name}` + }; + const patchedAddSpy = jest.spyOn(window, 'addEventListener'); + + ({ destroyScrollHandler } = scrollHandler()); + + expect(nativeAdd).toHaveBeenCalledWith('scroll', expect.any(Function)); + expect(nativeAdd).toHaveBeenCalledWith('scrollend', expect.any(Function)); + // Must NOT register on the Zone-patched window.addEventListener, which + // goes dead after the iframe's document.open()/write()/close() rewrites. + expect(patchedAddSpy).not.toHaveBeenCalledWith('scroll', expect.any(Function)); + + destroyScrollHandler(); + expect(nativeRemove).toHaveBeenCalledWith('scroll', expect.any(Function)); + expect(nativeRemove).toHaveBeenCalledWith('scrollend', expect.any(Function)); + } finally { + // Always clean up so Zone never leaks into other tests/suites. + delete win['__zone_symbol__addEventListener']; + delete win['__zone_symbol__removeEventListener']; + delete (globalThis as unknown as { Zone?: unknown }).Zone; + } + }); }); describe('addClassToEmptyContentlets', () => { diff --git a/core-web/libs/sdk/uve/src/script/utils.ts b/core-web/libs/sdk/uve/src/script/utils.ts index 28e6ddea08a3..b02ba7f9724f 100644 --- a/core-web/libs/sdk/uve/src/script/utils.ts +++ b/core-web/libs/sdk/uve/src/script/utils.ts @@ -2,7 +2,7 @@ import { DotCMSPageResponse, DotCMSUVEAction, UVEEventType } from '@dotcms/types'; import { createUVESubscription } from '../lib/core/core.utils'; -import { computeScrollIsInBottom } from '../lib/dom/dom.utils'; +import { computeScrollIsInBottom, getNativeEventBinder } from '../lib/dom/dom.utils'; import { setBounds } from '../lib/editor/internal'; import { initInlineEditing, sendMessageToUVE } from '../lib/editor/public'; @@ -32,13 +32,20 @@ export function scrollHandler() { }); }; - window.addEventListener('scroll', scrollCallback); - window.addEventListener('scrollend', scrollEndCallback); + // Bind through Zone.js's native (untracked) listeners so scroll survives the + // iframe's document.open()/write()/close() rewrites. Zone kills listeners + // rebound on the persistent `window` node after the first navigation, and + // viewport scroll can't be moved to a fresh `documentElement` the way + // hover/click were. See getNativeEventBinder for the full rationale. + const { addEventListener, removeEventListener } = getNativeEventBinder(window); + + addEventListener('scroll', scrollCallback); + addEventListener('scrollend', scrollEndCallback); return { destroyScrollHandler: () => { - window.removeEventListener('scroll', scrollCallback); - window.removeEventListener('scrollend', scrollEndCallback); + removeEventListener('scroll', scrollCallback); + removeEventListener('scrollend', scrollEndCallback); } }; } @@ -185,16 +192,23 @@ export function listenBlockEditorInlineEvent() { }; } - // If the page is not fully loaded, listen for the DOMContentLoaded event + // If the page is not fully loaded, listen for the DOMContentLoaded event. + // Bind through Zone's native listener so it survives the iframe's + // document.open()/write()/close() rewrites — without it, inline block-editor + // editing stops wiring up after the first in-editor navigation on a Zone.js + // page. `DOMContentLoaded` fires on `document`, not ``, so the + // documentElement trick used for hover/click doesn't apply here. See + // getNativeEventBinder. const handleDOMContentLoaded = () => { listenBlockEditorClick(); }; - document.addEventListener('DOMContentLoaded', handleDOMContentLoaded); + const { addEventListener, removeEventListener } = getNativeEventBinder(document); + addEventListener('DOMContentLoaded', handleDOMContentLoaded); return { destroyListenBlockEditorInlineEvent: () => { - document.removeEventListener('DOMContentLoaded', handleDOMContentLoaded); + removeEventListener('DOMContentLoaded', handleDOMContentLoaded); } }; } diff --git a/dotCMS/src/main/webapp/ext/uve/dot-uve.js b/dotCMS/src/main/webapp/ext/uve/dot-uve.js index 0007217d5b64..7d5877209f0b 100644 --- a/dotCMS/src/main/webapp/ext/uve/dot-uve.js +++ b/dotCMS/src/main/webapp/ext/uve/dot-uve.js @@ -1,5 +1,5 @@ (function(){ -var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN="UNKNOWN",o))(C||{});var S="TEMP_EMPTY_CONTENTLET",R="TEMP_EMPTY_CONTENTLET_TYPE";function _(e){return e.map(t=>{let n=t.getBoundingClientRect(),r=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:w(t)}),contentlets:ee(n,r)}})}function ee(e,t){return t.map(n=>{let r=n.getBoundingClientRect();return{x:0,y:r.y-e.y,width:r.width,height:r.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):b(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function w(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function b(e){let t=e.closest('[data-dot-object="container"]');return t?w(t):(console.warn("No container found for the contentlet"),null)}function f(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:f(e?.parentElement)}function D(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function A(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function h(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function U(e){let t=n=>{n.data.name==="uve-set-page-data"&&e(n.data.payload)};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"changes"}}function V(e){let t=n=>{n.data.name==="uve-reload-page"&&e()};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"page-reload"}}var te=100;function k(e){let t=null,n=[],r=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(_(a))},o=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,r()},te)},i=new ResizeObserver(()=>{o()}),s=()=>{for(let a of n)i.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),i.observe(document.documentElement);for(let a of n)i.observe(a)};s();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){s(),o();return}});u.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let g=()=>o();window.addEventListener("scroll",g,{passive:!0});let E=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),r())};return window.addEventListener("message",E),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),i.disconnect(),u.disconnect(),window.removeEventListener("scroll",g),window.removeEventListener("message",E),n=[]},event:"auto-bounds"}}function H(e){let t=n=>{if(n.data.name==="uve-scroll-inside-iframe"){let r=n.data.direction;e(r)}};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"iframe-scroll"}}function B(e){let t=n=>{if(n.data.name!=="uve-scroll-to-section")return;let r=n.data.sectionIndex,o=document.querySelector(`#${q}${r}`)??document.querySelector(`#section-${r}`);o&&e({sectionIndex:r,offsetTop:o.offsetTop})};return window.addEventListener("message",t),{unsubscribe:()=>{window.removeEventListener("message",t)},event:"scroll-to-section"}}function F(e){let t=!1,n=r=>{let o=f(r.target);if(!o){t&&(t=!1,e(null));return}let{x:i,y:s,width:l,height:u}=o.getBoundingClientRect(),g=o.dataset?.dotObject==="container",E={identifier:S,title:S,contentType:R,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:S,baseType:S,onNumberOfPages:1},a=h(o),c=D(o),m={container:o.dataset?.dotContainer?JSON.parse(o.dataset?.dotContainer):b(o),contentlet:g?E:a,vtlFiles:c},T={x:i,y:s,width:l,height:u,payload:m};t=!0,e(T)};return document.documentElement.addEventListener("pointermove",n),{unsubscribe:()=>{document.documentElement.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function G(e){let t,n=o=>{let i=f(o.target);if(!i||i.dataset?.dotObject==="container")return;let l=i.dataset?.dotInode;if(l&&l===t)return;o.preventDefault(),o.stopPropagation(),t=l;let{x:u,y:g,width:E,height:a}=i.getBoundingClientRect(),c=h(i),m=D(i);e({x:u,y:g,width:E,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):b(i),contentlet:c,vtlFiles:m}})},r=o=>{o?.data?.name==="uve-selection-cleared"&&(t=void 0)};return document.documentElement.addEventListener("click",n,{capture:!0}),window.addEventListener("message",r),{unsubscribe:()=>{document.documentElement.removeEventListener("click",n,{capture:!0}),window.removeEventListener("message",r)},event:"contentlet-clicked"}}var j={changes:e=>U(e),"page-reload":e=>V(e),"iframe-scroll":e=>H(e),"contentlet-hovered":e=>F(e),"contentlet-clicked":e=>G(e),"scroll-to-section":e=>B(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>k(e)},M=e=>({unsubscribe:()=>{},event:e});var q="dot-section-";function y(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(C),n=e.searchParams.get("mode")??"EDIT_MODE",r=e.searchParams.get("language_id"),o=e.searchParams.get("personaId"),i=e.searchParams.get("variantName"),s=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),u=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:r,persona:o,variantName:i,experimentId:s,publishDate:l,dotCMSHost:u}}function p(e,t){if(!y())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=j[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function Y(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function $(e){d({action:"edit-contentlet",payload:e})}function K(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function Q(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function W(e){d({action:"create-contentlet",payload:{contentType:e}})}function J(e){d({action:"set-bounds",payload:e})}function ne(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function N(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})};return window.addEventListener("scroll",e),window.addEventListener("scrollend",t),{destroyScrollHandler:()=>{window.removeEventListener("scroll",e),window.removeEventListener("scrollend",t)}}}function O(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function L(){let e=p("page-reload",()=>{window.location.reload()}),t=p("iframe-scroll",s=>{if(window.scrollY===0&&s==="up"||A()&&s==="down")return;let l=s==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=p("contentlet-hovered",s=>{d({action:"set-contentlet",payload:s})}),r=p("contentlet-clicked",s=>{d({action:"set-selected-contentlet",payload:s})}),o=p("scroll-to-section",s=>{d({action:"section-offset",payload:s})}),i=p("auto-bounds",s=>{J(s)});return{subscriptions:[e,t,n,r,o,i]}}function v(e){d({action:"client-ready",payload:e})}function x(){if(document.readyState==="complete")return z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{z()};return document.addEventListener("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{document.removeEventListener("DOMContentLoaded",e)}}}function X(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=ne(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` +var y=(r=>(r.EDIT="EDIT_MODE",r.PREVIEW="PREVIEW_MODE",r.LIVE="LIVE",r.UNKNOWN="UNKNOWN",r))(y||{});var T="TEMP_EMPTY_CONTENTLET",A="TEMP_EMPTY_CONTENTLET_TYPE";function P(e){return e.map(t=>{let n=t.getBoundingClientRect(),o=Array.from(t.querySelectorAll('[data-dot-object="contentlet"]'));return{x:n.x,y:n.y,width:n.width,height:n.height,payload:JSON.stringify({container:w(t)}),contentlets:ne(n,o)}})}function ne(e,t){return t.map(n=>{let o=n.getBoundingClientRect();return{x:0,y:o.y-e.y,width:o.width,height:o.height,payload:JSON.stringify({container:n.dataset?.dotContainer?JSON.parse(n.dataset?.dotContainer):f(n),contentlet:{identifier:n.dataset?.dotIdentifier,title:n.dataset?.dotTitle,inode:n.dataset?.dotInode,contentType:n.dataset?.dotType}})}})}function w(e){return{acceptTypes:e.dataset?.dotAcceptTypes||"",identifier:e.dataset?.dotIdentifier||"",maxContentlets:e.dataset?.maxContentlets||"",uuid:e.dataset?.dotUuid||""}}function f(e){let t=e.closest('[data-dot-object="container"]');return t?w(t):(console.warn("No container found for the contentlet"),null)}function D(e){if(!e)return null;let t=e.querySelector('[data-dot-object="empty-content"]');return e?.dataset?.dotObject==="contentlet"||e?.dataset?.dotObject==="container"&&t||e?.dataset?.dotObject==="container"&&e.children.length===0?e:D(e?.parentElement)}function h(e){let t=e.querySelectorAll('[data-dot-object="vtl-file"]');return t.length?Array.from(t).map(n=>({inode:n.dataset?.dotInode,name:n.dataset?.dotUrl})):null}function U(){let e=document.documentElement.scrollHeight,t=window.innerHeight;return window.scrollY+t>=e}function v(e){let t=e.dataset??{};return{identifier:t.dotIdentifier,title:t.dotTitle,inode:t.dotInode,contentType:t.dotType,baseType:t.dotBasetype,widgetTitle:t.dotWidgetTitle,onNumberOfPages:t.dotOnNumberOfPages,...t.dotStyleProperties&&{dotStyleProperties:JSON.parse(t.dotStyleProperties)}}}function u(e){let t=globalThis.Zone,n=l=>typeof t?.__symbol__=="function"?t.__symbol__(l):`__zone_symbol__${l}`,o=e,r=e,s=o[n("addEventListener")],i=o[n("removeEventListener")];return{addEventListener:(s??r.addEventListener).bind(r),removeEventListener:(i??r.removeEventListener).bind(r)}}function k(e){let t=o=>{o.data.name==="uve-set-page-data"&&e(o.data.payload)},n=u(window);return n.addEventListener("message",t),{unsubscribe:()=>{n.removeEventListener("message",t)},event:"changes"}}function H(e){let t=o=>{o.data.name==="uve-reload-page"&&e()},n=u(window);return n.addEventListener("message",t),{unsubscribe:()=>{n.removeEventListener("message",t)},event:"page-reload"}}var oe=100;function B(e){let t=null,n=[],o=()=>{let a=Array.from(document.querySelectorAll('[data-dot-object="container"]'));e(P(a))},r=()=>{t!==null&&clearTimeout(t),t=setTimeout(()=>{t=null,o()},oe)},s=new ResizeObserver(()=>{r()}),i=()=>{for(let a of n)s.unobserve(a);n=Array.from(document.querySelectorAll('[data-dot-object="container"]')),s.observe(document.documentElement);for(let a of n)s.observe(a)};i();let l=a=>{for(let c=0;c{for(let c of a)if(c.type==="childList"&&(l(c.addedNodes)||l(c.removedNodes))){i(),r();return}});p.observe(document.body??document.documentElement,{childList:!0,subtree:!0});let E=u(window),m=()=>r();E.addEventListener("scroll",m,{passive:!0});let C=a=>{a?.data?.name==="uve-flush-bounds"&&(t!==null&&(clearTimeout(t),t=null),o())};return E.addEventListener("message",C),{unsubscribe:()=>{t!==null&&(clearTimeout(t),t=null),s.disconnect(),p.disconnect(),E.removeEventListener("scroll",m),E.removeEventListener("message",C),n=[]},event:"auto-bounds"}}function F(e){let t=o=>{if(o.data.name==="uve-scroll-inside-iframe"){let r=o.data.direction;e(r)}},n=u(window);return n.addEventListener("message",t),{unsubscribe:()=>{n.removeEventListener("message",t)},event:"iframe-scroll"}}function G(e){let t=o=>{if(o.data.name!=="uve-scroll-to-section")return;let r=o.data.sectionIndex,s=document.querySelector(`#${Y}${r}`)??document.querySelector(`#section-${r}`);s&&e({sectionIndex:r,offsetTop:s.offsetTop})},n=u(window);return n.addEventListener("message",t),{unsubscribe:()=>{n.removeEventListener("message",t)},event:"scroll-to-section"}}function q(e){let t=!1,n=o=>{let r=D(o.target);if(!r){t&&(t=!1,e(null));return}let{x:s,y:i,width:l,height:p}=r.getBoundingClientRect(),E=r.dataset?.dotObject==="container",m={identifier:T,title:T,contentType:A,inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:T,baseType:T,onNumberOfPages:1},C=v(r),a=h(r),c={container:r.dataset?.dotContainer?JSON.parse(r.dataset?.dotContainer):f(r),contentlet:E?m:C,vtlFiles:a},S={x:s,y:i,width:l,height:p,payload:c};t=!0,e(S)};return document.documentElement.addEventListener("pointermove",n),{unsubscribe:()=>{document.documentElement.removeEventListener("pointermove",n)},event:"contentlet-hovered"}}function j(e){let t,n=s=>{let i=D(s.target);if(!i||i.dataset?.dotObject==="container")return;let p=i.dataset?.dotInode;if(p&&p===t)return;s.preventDefault(),s.stopPropagation(),t=p;let{x:E,y:m,width:C,height:a}=i.getBoundingClientRect(),c=v(i),S=h(i);e({x:E,y:m,width:C,height:a,payload:{container:i.dataset?.dotContainer?JSON.parse(i.dataset?.dotContainer):f(i),contentlet:c,vtlFiles:S}})},o=s=>{s?.data?.name==="uve-selection-cleared"&&(t=void 0)};document.documentElement.addEventListener("click",n,{capture:!0});let r=u(window);return r.addEventListener("message",o),{unsubscribe:()=>{document.documentElement.removeEventListener("click",n,{capture:!0}),r.removeEventListener("message",o)},event:"contentlet-clicked"}}var $={changes:e=>k(e),"page-reload":e=>H(e),"iframe-scroll":e=>F(e),"contentlet-hovered":e=>q(e),"contentlet-clicked":e=>j(e),"scroll-to-section":e=>G(e),"selection-cleared":e=>({unsubscribe:()=>{},event:"selection-cleared"}),"auto-bounds":e=>B(e)},M=e=>({unsubscribe:()=>{},event:e});var Y="dot-section-";function b(){if(typeof window>"u"||window.parent===window||!window.location)return;let e=new URL(window.location.href),t=Object.values(y),n=e.searchParams.get("mode")??"EDIT_MODE",o=e.searchParams.get("language_id"),r=e.searchParams.get("personaId"),s=e.searchParams.get("variantName"),i=e.searchParams.get("experimentId"),l=e.searchParams.get("publishDate"),p=e.searchParams.get("dotCMSHost");return t.includes(n)||(n="EDIT_MODE"),{mode:n,languageId:o,persona:r,variantName:s,experimentId:i,publishDate:l,dotCMSHost:p}}function g(e,t){if(!b())return console.warn("UVE Subscription: Not running inside UVE"),M(e);let n=$[e];return n?n(t):(console.error(`UVE Subscription: Event ${e} not found`),M(e))}function W(e){d({action:"set-url",payload:{url:e||"/"}})}function d(e){window.parent.postMessage(e,"*")}function K(e){d({action:"edit-contentlet",payload:e})}function Q(e){let{startLevel:t=1,depth:n=2}=e||{};d({action:"reorder-menu",payload:{startLevel:t,depth:n}})}function J(e,t){d({action:"init-inline-editing",payload:{type:e,data:t}})}function z(e){d({action:"create-contentlet",payload:{contentType:e}})}function X(e){d({action:"set-bounds",payload:e})}function re(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r\n|\r|\n/g,"\\a ").replace(/\f/g," ")}function L(){let e=()=>{d({action:"scroll"})},t=()=>{d({action:"scroll-end"})},{addEventListener:n,removeEventListener:o}=u(window);return n("scroll",e),n("scrollend",t),{destroyScrollHandler:()=>{o("scroll",e),o("scrollend",t)}}}function N(){document.querySelectorAll('[data-dot-object="contentlet"]').forEach(t=>{t.clientHeight||t.classList.add("empty-contentlet")})}function O(){let e=g("page-reload",()=>{window.location.reload()}),t=g("iframe-scroll",i=>{if(window.scrollY===0&&i==="up"||U()&&i==="down")return;let l=i==="up"?-120:120;window.scrollBy({left:0,top:l,behavior:"smooth"})}),n=g("contentlet-hovered",i=>{d({action:"set-contentlet",payload:i})}),o=g("contentlet-clicked",i=>{d({action:"set-selected-contentlet",payload:i})}),r=g("scroll-to-section",i=>{d({action:"section-offset",payload:i})}),s=g("auto-bounds",i=>{X(i)});return{subscriptions:[e,t,n,o,r,s]}}function x(e){d({action:"client-ready",payload:e})}function _(){if(document.readyState==="complete")return Z(),{destroyListenBlockEditorInlineEvent:()=>{}};let e=()=>{Z()},{addEventListener:t,removeEventListener:n}=u(document);return t("DOMContentLoaded",e),{destroyListenBlockEditorInlineEvent:()=>{n("DOMContentLoaded",e)}}}function ee(){let e="Empty container";try{e=JSON.parse(localStorage.getItem("dotMessagesKeys")??"{}")["editpage.container.is.empty"]??e}catch{}let t=re(e),n=document.createElement("style");n.dataset.dotStyles="uve-empty-state",n.textContent=` [data-dot-object="container"]:empty { width: 100%; background-color: #ECF0FD; @@ -18,5 +18,5 @@ var C=(o=>(o.EDIT="EDIT_MODE",o.PREVIEW="PREVIEW_MODE",o.LIVE="LIVE",o.UNKNOWN=" [data-dot-object="container"]:empty::after { content: '${t}'; } - `,document.head?.appendChild(n)}var z=()=>{let e=document.querySelectorAll("[data-block-editor-content]");e.length&&e.forEach(t=>{let{inode:n,language:r="1",contentType:o,fieldName:i,blockEditorContent:s}=t.dataset,l=JSON.parse(s||"");if(!n||!r||!o||!i){console.error("Missing data attributes for block editor inline editing."),console.warn("inode, language, contentType and fieldName are required.");return}t.classList.add("dotcms__inline-edit-field"),t.addEventListener("click",()=>{Q("BLOCK_EDITOR",{inode:n,content:l,language:parseInt(r),fieldName:i,contentType:o})})})};function Z(e){let{mode:t}=y()||{};if(!t||t!=="EDIT_MODE")return;let n=e.filter((r,o)=>r.contentType?!0:(console.warn(`[registerStyleEditorSchemas] Skipping schema with index [${o}] for not having a contentType`),!1));d({action:"register-style-schemas",payload:{schemas:n}})}var oe={createSubscription:p,createContentlet:W,editContentlet:$,reorderMenu:K,updateNavigation:Y,registerStyleEditorSchemas:Z};window.dotUVE=oe;var re=y();re?.mode==="EDIT_MODE"&&(L(),N(),O(),v(),x(),X()); + `,document.head?.appendChild(n)}var Z=()=>{let e=document.querySelectorAll("[data-block-editor-content]");e.length&&e.forEach(t=>{let{inode:n,language:o="1",contentType:r,fieldName:s,blockEditorContent:i}=t.dataset,l=JSON.parse(i||"");if(!n||!o||!r||!s){console.error("Missing data attributes for block editor inline editing."),console.warn("inode, language, contentType and fieldName are required.");return}t.classList.add("dotcms__inline-edit-field"),t.addEventListener("click",()=>{J("BLOCK_EDITOR",{inode:n,content:l,language:parseInt(o),fieldName:s,contentType:r})})})};function te(e){let{mode:t}=b()||{};if(!t||t!=="EDIT_MODE")return;let n=e.filter((o,r)=>o.contentType?!0:(console.warn(`[registerStyleEditorSchemas] Skipping schema with index [${r}] for not having a contentType`),!1));d({action:"register-style-schemas",payload:{schemas:n}})}var ie={createSubscription:g,createContentlet:z,editContentlet:K,reorderMenu:Q,updateNavigation:W,registerStyleEditorSchemas:te};window.dotUVE=ie;var se=b();se?.mode==="EDIT_MODE"&&(O(),L(),N(),x(),_(),ee()); })(); From f8759a4c10b1874be6a165e50fdacf89135aaac0 Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Thu, 2 Jul 2026 14:28:23 -0400 Subject: [PATCH 5/6] refactor(uve): streamline drag-and-drop service by integrating iframe messaging and removing unused parameters --- .../edit-ema-editor.component.ts | 2 - .../dot-uve-drag-drop.service.ts | 48 +++--------- core-web/libs/sdk/uve/src/internal/events.ts | 73 ++++++------------- core-web/libs/sdk/uve/src/script/utils.ts | 17 ++--- 4 files changed, 37 insertions(+), 103 deletions(-) diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts index ea3aeba2aebe..57a2fd57f671 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts @@ -618,8 +618,6 @@ export class EditEmaEditorComponent implements OnDestroy, AfterViewInit { this.uveStore, this.iframe, this.customDragImage, - this.contentWindow, - this.host, { onDrop: (event) => this.handleDrop(event), onDragEnter: () => { diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts index 633a2c2d5c46..aee6a97459de 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts @@ -5,13 +5,13 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { filter } from 'rxjs/operators'; -import { __DOTCMS_UVE_EVENT__ } from '@dotcms/types/internal'; import { WINDOW } from '@dotcms/utils'; import { IFRAME_SCROLL_ZONE } from '../../shared/consts'; import { EDITOR_STATE } from '../../shared/enums'; import { UVEStore } from '../../store/dot-uve.store'; import { TEMPORAL_DRAG_ITEM, getDragItemData } from '../../utils'; +import { UveIframeMessengerService } from '../iframe-messenger/uve-iframe-messenger.service'; export interface DragDropHandlers { onDrop: (event: DragEvent) => void; @@ -26,13 +26,12 @@ export interface DragDropHandlers { export class DotUveDragDropService { private readonly window = inject(WINDOW); private readonly destroyRef = inject(DestroyRef); + private readonly iframeMessenger = inject(UveIframeMessengerService); setupDragEvents( uveStore: InstanceType, iframe: ElementRef, customDragImage: ElementRef, - contentWindow: Window | null, - host: string, handlers: DragDropHandlers ): void { // Drag start @@ -53,19 +52,12 @@ export class DotUveDragDropService { return; } - // Request fresh container bounds the instant the drag begins. - // `setEditorDragItem` is deferred to the next animation frame - // (so the browser can snapshot the drag image before Angular - // re-renders), which means the `dragenter` handler — the only - // other place that flushes bounds — can fire first, see no - // drag item yet, and bail before posting UVE_FLUSH_BOUNDS. When - // that race happens the dropzone is left with stale/empty - // `editorBounds` and renders no drop targets. Flushing here, at - // drag start, guarantees bounds are requested regardless of the - // race so they have arrived by the time `dragover` flips the - // editor into DRAGGING. - contentWindow?.postMessage({ name: __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS }, host); - + // Flush bounds at drag start. `setEditorDragItem` is deferred to + // the next frame (to snapshot the drag image first), so the + // `dragenter` handler can fire before it and bail without + // flushing — leaving the dropzone with empty bounds and no + // targets. Flushing here guarantees bounds regardless of that race. + this.iframeMessenger.flushBounds(); requestAnimationFrame(() => uveStore.setEditorDragItem(data)); }); @@ -88,12 +80,7 @@ export class DotUveDragDropService { uveStore.setEditorState(EDITOR_STATE.DRAGGING); // Drag enter: dropzone needs current bounds before any // pixel of movement, so flush past the auto-bounds debounce. - contentWindow?.postMessage( - { - name: __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS - }, - host - ); + this.iframeMessenger.flushBounds(); if (dragItem) { return; @@ -103,17 +90,7 @@ export class DotUveDragDropService { handlers.onDragEnter(event); }); - // Drag end — reset editor UI state after EVERY drag gesture, not just - // cancelled ones. Previously this was filtered to `dropEffect === 'none'` - // (cancelled drops only), leaving successful drops to be reset by the - // async save→reload. That left a window where `editorState` stayed - // DRAGGING after a successful drop: the next drag then had no clean - // IDLE→DRAGGING transition, so `$handleIsDraggingEffect` (which flushes - // container bounds on that transition) never re-fired and the dropzone - // showed no targets. `dragend` always fires when the gesture ends and - // `handleDrop` has already consumed the drag item synchronously (drop - // fires before dragend), so resetting here is safe and guarantees a - // clean IDLE state for the next drag. + // Drag end fromEvent(this.window, 'dragend') .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { @@ -165,10 +142,7 @@ export class DotUveDragDropService { uveStore.updateEditorScrollDragState(); - contentWindow?.postMessage( - { name: __DOTCMS_UVE_EVENT__.UVE_SCROLL_INSIDE_IFRAME, direction }, - host - ); + this.iframeMessenger.scrollInsideIframe(direction); handlers.onDragOver(event); }); diff --git a/core-web/libs/sdk/uve/src/internal/events.ts b/core-web/libs/sdk/uve/src/internal/events.ts index a338e50dc727..b7c1c0c5c6a1 100644 --- a/core-web/libs/sdk/uve/src/internal/events.ts +++ b/core-web/libs/sdk/uve/src/internal/events.ts @@ -29,8 +29,7 @@ export function onContentChanges(callback: UVEEventHandler) { } }; - // Native binder so this parent→iframe receiver survives the iframe's - // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + // Native binder: survives the iframe rewrite under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); nativeWindow.addEventListener('message', messageCallback); @@ -58,8 +57,7 @@ export function onPageReload(callback: UVEEventHandler) { } }; - // Native binder so this parent→iframe receiver survives the iframe's - // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + // Native binder: survives the iframe rewrite under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); nativeWindow.addEventListener('message', messageCallback); @@ -169,15 +167,10 @@ export function onAutoBounds(callback: UVEEventHandler) { subtree: true }); - // Scrolling inside the iframe doesn't change layout, so ResizeObserver - // doesn't fire, but every contentlet's viewport-relative position - // (getBoundingClientRect) does change. Re-emit bounds after each - // scroll burst settles so the editor's pinned selected overlay - // re-anchors to the on-screen position. - // Bind through Zone.js's native (untracked) listener so this scroll-driven - // re-anchor survives the iframe's document.open()/write()/close() rewrites. - // Without it, controls hide on scroll but the SET_BOUNDS that brings them - // back never fires under Zone.js. See getNativeEventBinder for the rationale. + // Scrolling doesn't change layout (ResizeObserver won't fire) but moves + // every contentlet, so re-emit bounds after the scroll settles to re-anchor + // the selected overlay. Native binder so it survives the iframe rewrite + // under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); const onScroll = () => scheduleEmit(); nativeWindow.addEventListener('scroll', onScroll, { passive: true }); @@ -194,9 +187,7 @@ export function onAutoBounds(callback: UVEEventHandler) { } emit(); }; - // Reuse the native binder (declared above for `scroll`) so this - // parent→iframe flush receiver survives the iframe rewrites too — this is - // the channel that supplies the dropzone's bounds during drag-and-drop. + // Native binder (reused from `scroll` above): survives the iframe rewrite too. nativeWindow.addEventListener('message', onFlush); return { @@ -233,9 +224,7 @@ export function onIframeScroll(callback: UVEEventHandler) { } }; - // Native binder so this parent→iframe receiver survives the iframe's - // document.open()/write()/close() rewrites under Zone.js — this drives the - // edge auto-scroll while dragging near the iframe top/bottom. + // Native binder: survives the iframe rewrite under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); nativeWindow.addEventListener('message', messageCallback); @@ -274,8 +263,7 @@ export function onScrollToSection(callback: UVEEventHandler) { callback({ sectionIndex, offsetTop: el.offsetTop }); }; - // Native binder so this parent→iframe receiver survives the iframe's - // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + // Native binder: survives the iframe rewrite under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); nativeWindow.addEventListener('message', messageCallback); @@ -359,25 +347,14 @@ export function onContentletHovered(callback: UVEEventHandler) { callback(contentletHoveredPayload); }; - // We intentionally do not fire null on document `pointerleave`: the - // editor's hover toolbar lives in the parent window (outside the - // iframe), so leaving the iframe usually means the user is heading - // for the toolbar. Killing the overlay there would yank the toolbar - // away just as the user reaches for it. Dead-space-inside-iframe - // is already covered by the `pointermove` null branch above. + // We intentionally do not fire null on `pointerleave`: the editor's hover + // toolbar lives in the parent window, so leaving the iframe usually means + // the user is reaching for it — killing the overlay would yank it away. // - // Bind to `document.documentElement` (the node) rather than - // `document`. UVE renders traditional pages by reusing one iframe and - // rewriting it via `doc.open()/write()/close()` on each in-editor - // navigation. When Zone.js is loaded inside that iframe it runs in - // global-events mode (one native gateway listener + a JS-level task - // list stored on the node); `document.open()` tears down the native - // gateway but the task list survives on the persistent `document` - // node, so Zone sees "already registered" on re-init and skips - // re-binding the native listener — pointermove silently goes dead. - // `documentElement` is a fresh node after each `write()`, so it - // carries no stale task list and re-binds cleanly. `pointermove` - // bubbles to , so behavior is identical on no-Zone pages. + // Bind to `document.documentElement` (a fresh node on each iframe rewrite) + // rather than `document`, which goes dead under Zone.js after a rewrite. + // `pointermove` bubbles to , so no-Zone behavior is identical. See + // getNativeEventBinder for the full rationale. document.documentElement.addEventListener('pointermove', pointerMoveCallback); return { @@ -466,20 +443,12 @@ export function onContentletClicked(callback: UVEEventHandler) { } }; - // Capture phase so we run BEFORE the page's own click handlers and can - // preventDefault/stopPropagation effectively. - // - // Bind to `document.documentElement` rather than `document` for the same - // reason as the hover listener above: on Zone.js-loading traditional - // pages the persistent `document` node keeps a stale Zone task list - // across `doc.open()/write()/close()` iframe rewrites, so re-binding on - // it is silently skipped after the first in-editor navigation. The - // node is recreated on each rewrite and re-binds cleanly. Capture - // phase on still runs before the page's own handlers, so - // preventDefault/stopPropagation behave identically. + // Capture phase so we run BEFORE the page's own click handlers. Bound to + // `document.documentElement` (a fresh node on each iframe rewrite) rather + // than `document`, which goes dead under Zone.js after a rewrite; capture on + // still runs first. See getNativeEventBinder for the full rationale. document.documentElement.addEventListener('click', clickCallback, { capture: true }); - // Native binder so this parent→iframe receiver survives the iframe's - // document.open()/write()/close() rewrites under Zone.js. See getNativeEventBinder. + // Native binder: survives the iframe rewrite under Zone.js. See getNativeEventBinder. const nativeWindow = getNativeEventBinder(window); nativeWindow.addEventListener('message', selectionClearedCallback); diff --git a/core-web/libs/sdk/uve/src/script/utils.ts b/core-web/libs/sdk/uve/src/script/utils.ts index b02ba7f9724f..1f5a5a80083a 100644 --- a/core-web/libs/sdk/uve/src/script/utils.ts +++ b/core-web/libs/sdk/uve/src/script/utils.ts @@ -32,11 +32,9 @@ export function scrollHandler() { }); }; - // Bind through Zone.js's native (untracked) listeners so scroll survives the - // iframe's document.open()/write()/close() rewrites. Zone kills listeners - // rebound on the persistent `window` node after the first navigation, and - // viewport scroll can't be moved to a fresh `documentElement` the way - // hover/click were. See getNativeEventBinder for the full rationale. + // Native binder: scroll survives the iframe rewrite under Zone.js. It can't + // move to a fresh `documentElement` like hover/click (viewport scroll only + // fires on `window`). See getNativeEventBinder. const { addEventListener, removeEventListener } = getNativeEventBinder(window); addEventListener('scroll', scrollCallback); @@ -192,13 +190,8 @@ export function listenBlockEditorInlineEvent() { }; } - // If the page is not fully loaded, listen for the DOMContentLoaded event. - // Bind through Zone's native listener so it survives the iframe's - // document.open()/write()/close() rewrites — without it, inline block-editor - // editing stops wiring up after the first in-editor navigation on a Zone.js - // page. `DOMContentLoaded` fires on `document`, not ``, so the - // documentElement trick used for hover/click doesn't apply here. See - // getNativeEventBinder. + // Native binder: `DOMContentLoaded` fires on `document` (not ``), so + // it survives the iframe rewrite under Zone.js. See getNativeEventBinder. const handleDOMContentLoaded = () => { listenBlockEditorClick(); }; From cd3bfd7e57553dd9602349a488136d767eea41f6 Mon Sep 17 00:00:00 2001 From: rjvelazco Date: Fri, 3 Jul 2026 10:42:18 -0400 Subject: [PATCH 6/6] style(uve): run prettier on setupDragEvents call site The drag-drop refactor removed two args from setupDragEvents but left the call site multi-line; prettier collapses it to one line. Fixes the format-test (nx format:check) failure in the PR build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../edit-ema-editor.component.ts | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts index 57a2fd57f671..e98d41b289f8 100644 --- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts +++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts @@ -614,29 +614,24 @@ export class EditEmaEditorComponent implements OnDestroy, AfterViewInit { return; } - this.dragDropService.setupDragEvents( - this.uveStore, - this.iframe, - this.customDragImage, - { - onDrop: (event) => this.handleDrop(event), - onDragEnter: () => { - // Handled in dragDropService - }, - onDragOver: () => { - // Handled in dragDropService - }, - onDragLeave: () => { - this.uveStore.resetEditorProperties(); - }, - onDragEnd: () => { - this.uveStore.resetEditorProperties(); - }, - onDragStart: () => { - // Handled in dragDropService - } + this.dragDropService.setupDragEvents(this.uveStore, this.iframe, this.customDragImage, { + onDrop: (event) => this.handleDrop(event), + onDragEnter: () => { + // Handled in dragDropService + }, + onDragOver: () => { + // Handled in dragDropService + }, + onDragLeave: () => { + this.uveStore.resetEditorProperties(); + }, + onDragEnd: () => { + this.uveStore.resetEditorProperties(); + }, + onDragStart: () => { + // Handled in dragDropService } - ); + }); } private handleUveMessage(message: PostMessage): void {