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..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,31 +614,24 @@ export class EditEmaEditorComponent implements OnDestroy, AfterViewInit { return; } - this.dragDropService.setupDragEvents( - this.uveStore, - this.iframe, - this.customDragImage, - this.contentWindow, - this.host, - { - 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 { 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..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,6 +52,12 @@ export class DotUveDragDropService { return; } + // 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)); }); @@ -75,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; @@ -92,10 +92,7 @@ export class DotUveDragDropService { // Drag end fromEvent(this.window, 'dragend') - .pipe( - takeUntilDestroyed(this.destroyRef), - filter((event: DragEvent) => event.dataTransfer?.dropEffect === 'none') - ) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { handlers.onDragEnd(); }); @@ -145,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 8b6d9feb0510..b7c1c0c5c6a1 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,13 @@ export function onContentChanges(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder: survives the iframe rewrite 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 +57,13 @@ export function onPageReload(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder: survives the iframe rewrite 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 }; @@ -162,13 +167,13 @@ 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. + // 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(); - 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 +187,8 @@ export function onAutoBounds(callback: UVEEventHandler) { } emit(); }; - window.addEventListener('message', onFlush); + // Native binder (reused from `scroll` above): survives the iframe rewrite too. + nativeWindow.addEventListener('message', onFlush); return { unsubscribe: () => { @@ -192,8 +198,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 +224,13 @@ export function onIframeScroll(callback: UVEEventHandler) { } }; - window.addEventListener('message', messageCallback); + // Native binder: survives the iframe rewrite 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.IFRAME_SCROLL }; @@ -255,11 +263,13 @@ export function onScrollToSection(callback: UVEEventHandler) { callback({ sectionIndex, offsetTop: el.offsetTop }); }; - window.addEventListener('message', messageCallback); + // Native binder: survives the iframe rewrite 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 }; @@ -337,17 +347,19 @@ 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. - document.addEventListener('pointermove', pointerMoveCallback); + // 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` (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 { unsubscribe: () => { - document.removeEventListener('pointermove', pointerMoveCallback); + document.documentElement.removeEventListener('pointermove', pointerMoveCallback); }, event: UVEEventType.CONTENTLET_HOVERED }; @@ -431,15 +443,21 @@ 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 }); - window.addEventListener('message', selectionClearedCallback); + // 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: survives the iframe rewrite under Zone.js. See getNativeEventBinder. + const nativeWindow = getNativeEventBinder(window); + nativeWindow.addEventListener('message', selectionClearedCallback); return { unsubscribe: () => { - document.removeEventListener('click', clickCallback, { capture: true }); - window.removeEventListener('message', selectionClearedCallback); + document.documentElement.removeEventListener('click', clickCallback, { + capture: true + }); + 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 7ed5e5f01e65..00353d57b496 100644 --- a/core-web/libs/sdk/uve/src/script/utils.ts +++ b/core-web/libs/sdk/uve/src/script/utils.ts @@ -1,7 +1,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'; @@ -31,13 +31,18 @@ export function scrollHandler() { }); }; - window.addEventListener('scroll', scrollCallback); - window.addEventListener('scrollend', scrollEndCallback); + // 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); + addEventListener('scrollend', scrollEndCallback); return { destroyScrollHandler: () => { - window.removeEventListener('scroll', scrollCallback); - window.removeEventListener('scrollend', scrollEndCallback); + removeEventListener('scroll', scrollCallback); + removeEventListener('scrollend', scrollEndCallback); } }; } @@ -184,16 +189,18 @@ export function listenBlockEditorInlineEvent() { }; } - // If the page is not fully loaded, listen for the DOMContentLoaded event + // Native binder: `DOMContentLoaded` fires on `document` (not ``), so + // it survives the iframe rewrite under Zone.js. 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 3ac6531efff1..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 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 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",()=>{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: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()); })(); 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" +}