Skip to content

fix(uve): keep hover/selection/drag working on Zone.js traditional pages#36333

Open
rjvelazco wants to merge 9 commits into
mainfrom
issue-36167-uve-zonejs-iframe-listeners
Open

fix(uve): keep hover/selection/drag working on Zone.js traditional pages#36333
rjvelazco wants to merge 9 commits into
mainfrom
issue-36167-uve-zonejs-iframe-listeners

Conversation

@rjvelazco

@rjvelazco rjvelazco commented Jun 26, 2026

Copy link
Copy Markdown
Member

Important

Summary

Why these bugs happen. UVE reuses one iframe and rewrites the whole document (document.open()/write()/close()) on every in-editor navigation/reload. When Zone.js is in the page it keeps its own list of event listeners on the window/document nodes; the rewrite wipes the real (native) listeners but not Zone's list, so when the SDK re-attaches, Zone thinks they're "already there", skips re-binding, and the listeners silently go dead.

The fix we actually need is to stop rewriting the entire iframe on every change — the SDK should patch only the elements that changed, like the headless React/Angular SDKs do. That removes the teardown that kills the listeners, and the workarounds here become unnecessary.

This PR is that workaround until then: re-bind the affected listeners to nodes that survive the rewrite (documentElement), or via Zone's native, untracked addEventListener.


Files changed

  • core-web/libs/sdk/uve/src/internal/events.ts — Fix 1 (documentElement for hover/click) + Fix 3 (native binder for inbound message + auto-bounds scroll).
  • core-web/libs/sdk/uve/src/script/utils.ts — Fix 3 (native binder for scroll/scrollend + DOMContentLoaded).
  • core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts — Fix 3 (getNativeEventBinder helper).
  • core-web/libs/sdk/uve/src/script/utils.spec.ts — Fix 3 test.
  • dotCMS/src/main/webapp/ext/uve/dot-uve.js — regenerated SDK bundle (Fix 1 + Fix 3).
  • core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts — Fix 2.

Verification

  • Hover / selection / drag / scroll / inline block-editor editing all work after in-editor navigation on a Zone.js page (not only after a hard reload).
  • Regression: everything still works on pages without Zone.js.
  • pnpm nx test sdk-uve (100/100), pnpm nx lint sdk-uve, and pnpm nx lint portlets-edit-ema-portlet pass.

Future work

Every fix here is a workaround for one design choice: UVE tears down and re-writes the entire iframe document on every in-editor change, which is what kills the listeners. The real fix is for the SDK to patch only the elements that actually changed — like the headless React/Angular SDKs already do — so there's no full-document teardown and these workarounds become unnecessary. Worth a dedicated issue.

Video

video.mov

🤖 Generated with Claude Code

This PR fixes: #36167

…e Zone.js iframe rewrites (#36167)

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 `<html>`
node), which is recreated fresh on every iframe rewrite and therefore
carries no stale Zone task list. Both events bubble to `<html>`, 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) <noreply@anthropic.com>
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries labels Jun 26, 2026
@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Claude finished @rjvelazco's task in 2m 57s —— View job


I'll analyze this and get back to you.

…ays render (#36167)

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) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/libs/sdk/uve/src/internal/events.ts:346 — Switching from document to document.documentElement for pointermove and click listeners breaks event handling in iframes because document.documentElement refers to the parent document’s <html>, not the iframe’s content document. UVE renders inside iframes; event listeners must be attached to the iframe’s document, not its parent.
  • 🔴 Critical: dotCMS/src/main/webapp/ext/uve/dot-uve.js:1 — Replacing document.addEventListener with document.documentElement.addEventListener in minified JS breaks hover/click detection in iframes by attaching listeners to parent document, not iframe content.
  • 🟡 Medium: core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts:59contentWindow?.postMessage({ name: __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS }, host) sends message to host without validating host is a trusted origin. Assumption: host is derived from window.location.origin or similar. What to verify: Is host validated against a whitelist of trusted UVE embedders? If not, this enables XSS via malicious postMessage targets.

Existing

  • 🟡 Medium: dotCMS/src/main/webapp/ext/uve/dot-uve.js:1window.parent.postMessage(e,"*") uses wildcard origin — security risk if UVE is embedded in untrusted contexts.

Resolved

  • core-web/libs/sdk/uve/src/internal/events.ts:346 — Prior finding about document vs document.documentElement was flagged in prior review; this PR introduces the same bug — so it’s not resolved, it’s reintroduced. No resolved entries.

Run: #28255713352 · tokens: in: 7331 · out: 636 · total: 7967

@rjvelazco rjvelazco changed the title fix(uve): keep hover/selection tools working on Zone.js pages after in-editor navigation (#36167) fix(uve): keep hover/selection/drag working on Zone.js traditional pages (#36167) Jun 26, 2026
…rk (#36167)

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) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🤖 Bedrock Review — qwen.qwen3-next-80b-a3b

New Issues

  • 🔴 Critical: core-web/libs/sdk/uve/src/internal/events.ts:346 — Switching from document to document.documentElement for pointermove and click listeners breaks event handling in iframes because document.documentElement refers to the parent document’s <html>, not the iframe’s content document. UVE runs inside iframes; listeners must be attached to the iframe’s contentDocument (i.e., document inside the iframe), not the parent’s documentElement. This breaks hover/click detection entirely within iframes.
  • 🔴 Critical: dotCMS/src/main/webapp/ext/uve/dot-uve.js:1 — Replacing document.addEventListener with document.documentElement.addEventListener in minified JS breaks hover/click detection in iframes for the same reason: document.documentElement is the parent document’s root, not the iframe’s. UVE’s JS runs inside iframes and expects to listen on the iframe’s own document.
  • 🟡 Medium: core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts:59contentWindow?.postMessage({ name: __DOTCMS_UVE_EVENT__.UVE_FLUSH_BOUNDS }, host); uses host without validation. host is not validated against a trusted origin list — potential XSS if UVE is embedded in an untrusted context. Assumption: host is derived from window.parent or similar, but origin check is missing. What to verify: Is host a known, static, or validated origin? If not, this is a security flaw.
  • 🟡 Medium: dotCMS/src/main/webapp/ext/uve/dot-uve.js:1window.parent.postMessage(e,"*") still uses wildcard origin — security risk if UVE is embedded in untrusted contexts. This was not fixed in the diff despite prior flagging.

Existing

  • 🟡 Medium: core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts:59 — postMessage sent to host without origin validation — potential XSS if host is untrusted.
  • 🟡 Medium: dotCMS/src/main/webapp/ext/uve/dot-uve.js:1 — window.parent.postMessage(e,"*") uses wildcard origin — security risk if UVE is embedded in untrusted contexts.

Resolved

  • core-web/libs/sdk/uve/src/internal/events.ts:346 — Prior finding about documentElement breaking iframe event listening is now worse — it was already flagged and this PR made it worse by extending the same flawed pattern.
  • dotCMS/src/main/webapp/ext/uve/dot-uve.js:1 — Prior finding about documentElement replacing document is now worse — this PR doubled down on the same error.

Run: #28258211925 · tokens: in: 8811 · out: 864 · total: 9675

@mergify

mergify Bot commented Jun 26, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@dotCMS dotCMS deleted a comment from github-actions Bot Jun 27, 2026
@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🤖 dotBot Review (Bedrock)

Reviewed 8 file(s); 9 candidate(s) → 3 confirmed, 0 uncertain (unverified, kept for review).

Confirmed findings

  • 🟠 High core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts:95 — Missing filter on dragend event causes style reset after successful drops
    The removed filter(event => event.dataTransfer?.dropEffect === 'none') previously ensured style cleanup only occurred on canceled drags. Without it, the handler now runs for all dragend events (including successful drops), potentially interfering with post-drop UI state by prematurely resetting dragged element styles.
  • 🟡 Medium core-web/libs/sdk/uve/src/internal/events.ts:355 — Event listeners on document.documentElement may detach during iframe rewrites
    The mouse/click event listeners in events.ts:355 use standard addEventListener without getNativeEventBinder. During full iframe document rewrites (document.open/write/close), the physical documentElement node is replaced, causing native listeners to detach. Unlike scroll/message handlers that use getNativeEventBinder to bypass Zone.js tracking, these mouse listeners remain vulnerable to Zone.js' stale listener tracking after replacement, requiring re-binding that doesn't occur.
  • 🟡 Medium core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts:13 — Missing function type check for Zone.js symbol resolution
    The code accesses Zone.js symbols without verifying they resolve to function types. While Zone.js typically stores native methods here, if the symbol exists but contains a non-function value (null/object), invoking it would throw. The || fallback only handles undefined symbols, not type mismatches.

us.deepseek.r1-v1:0 · Run: #28616905537 · tokens: in: 76813 · out: 29204 · total: 106017 · calls: 24 · est. ~$0.261

@rjvelazco rjvelazco marked this pull request as draft June 29, 2026 15:32
@rjvelazco rjvelazco changed the title fix(uve): keep hover/selection/drag working on Zone.js traditional pages (#36167) fix(uve): keep hover/selection/drag working on Zone.js traditional pages Jun 29, 2026
@rjvelazco rjvelazco marked this pull request as ready for review July 2, 2026 17:44
@rjvelazco

rjvelazco commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Why these bugs happen. UVE reuses one iframe and rewrites the whole document (document.open()/write()/close()) on every in-editor navigation/reload. When Zone.js is in the page it keeps its own list of event listeners on the window/document nodes; the rewrite wipes the real (native) listeners but not Zone's list, so when the SDK re-attaches, Zone thinks they're "already there", skips re-binding, and the listeners silently go dead.

The fix we actually need is to stop rewriting the entire iframe on every change — the SDK should patch only the elements that changed, like the headless React/Angular SDKs do. That removes the teardown that kills the listeners, and the workarounds here become unnecessary.

This PR is that workaround until then: re-bind the affected listeners to nodes that survive the rewrite (documentElement), or via Zone's native, untracked addEventListener.

cc: @zJaaal @KevinDavilaDotCMS @fmontes

@rjvelazco

rjvelazco commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

Test

Chrome

video.mov

FireFox

video-firefox.mov

cc: @zJaaal

@rjvelazco rjvelazco enabled auto-merge July 2, 2026 19:41
takeUntilDestroyed(this.destroyRef),
filter((event: DragEvent) => event.dataTransfer?.dropEffect === 'none')
)
.pipe(takeUntilDestroyed(this.destroyRef))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [High] Missing filter on dragend event causes style reset after successful drops

The removed filter(event => event.dataTransfer?.dropEffect === 'none') previously ensured style cleanup only occurred on canceled drags. Without it, the handler now runs for all dragend events (including successful drops), potentially interfering with post-drop UI state by prematurely resetting dragged element styles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

UVE: contentlet tools stop working on traditional pages when Zone.js is present in the page iframe

3 participants