Skip to content

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

Merged
rjvelazco merged 11 commits into
mainfrom
issue-36167-uve-zonejs-iframe-listeners
Jul 3, 2026
Merged

fix(uve): keep hover/selection/drag working on Zone.js traditional pages#36333
rjvelazco merged 11 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

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 13s —— View job


Rollback-Safety Analysis

  • Read docs/core/ROLLBACK_UNSAFE_CATEGORIES.md
  • Get full PR diff (127f274e...cd3bfd7e)
  • Analyze diff against every unsafe category
  • Post verdict (label)

Verdict: ✅ Safe to Rollback

This PR only touches front-end/browser-side event-listener wiring for the UVE editor:

  • core-web/libs/sdk/uve/src/internal/events.ts, core-web/libs/sdk/uve/src/lib/dom/dom.utils.ts, core-web/libs/sdk/uve/src/script/utils.ts — rebind scroll/message/click/pointermove/DOMContentLoaded listeners via a Zone.js-safe native binder (getNativeEventBinder) or to document.documentElement instead of document/window.
  • dotCMS/src/main/webapp/ext/uve/dot-uve.js — regenerated static JS bundle reflecting the same change (served fresh to browsers on every load, not a persisted consumer contract).
  • dotCMS/src/main/webapp/ext/uve/package.json — new npm package metadata file for the SDK; no runtime/server behavior.
  • core-web/libs/portlets/edit-ema/portlet/.../dot-uve-drag-drop.service.ts and edit-ema-editor.component.ts — Angular service refactor (drops contentWindow/host params in favor of an injected UveIframeMessengerService), still uses postMessage for iframe communication.

None of these match the unsafe categories:

  • No runonce DB migration tasks, no ALTER/DROP/RENAME TABLE/COLUMN, no PK changes (rules out C-1, C-4, H-1, H-2, H-3, M-1).
  • No Elasticsearch mapping / ESMappingAPIImpl / ESMappingConstants changes (rules out C-2).
  • No contentlet_as_json / CURRENT_MODEL_VERSION changes (rules out C-3).
  • No new ContentType field type, storage provider, or stored procedure changes (rules out H-4, H-5, H-6).
  • No ViewTool/VTL viewtool contract changes — nothing here is registered in toolbox.xml or reachable from VTL (rules out H-8).
  • No push-publishing bundle XML changes (rules out M-2).
  • No REST/GraphQL contract changes — the browser-side SDK bundle is served fresh on every page load (not versioned/cached like a REST contract), so N-1 and N never have a version mismatch between client and server here (rules out M-3).
  • No OSGi interface changes (rules out M-4).

This is a client-side JS/TS bug fix (event listener re-binding workaround for Zone.js) with no database, index, or API contract impact. Rolling back to N-1 simply serves the previous SDK bundle/Angular code — no data or index inconsistency results.

Label applied: AI: Safe To Rollback

…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); 5 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 — Duplicate state reset in drag-drop handling
    The dragend event subscription (line 95) triggers resetDragState() unconditionally after every drag operation, while successful drops also call resetDragState() (line 100 in original snippet). This results in duplicate state resets when drags end with successful drops, risking UI state corruption.
  • 🟠 High core-web/libs/portlets/edit-ema/portlet/src/lib/services/dot-uve-drag-drop/dot-uve-drag-drop.service.ts:28 — Insecure postMessage usage without targetOrigin
    The postMessage call in dot-uve-drag-drop.service.ts line 28 was modified to remove the targetOrigin parameter. This allows the message to be sent to any origin, creating a security risk for cross-site scripting attacks. MDN documentation emphasizes that specifying targetOrigin is critical for security, and '*' should only be used if communication with any origin is explicitly required.
  • 🟡 Medium dotCMS/src/main/webapp/ext/uve/package.json:6 — Unstable dependency version specifier
    The dev dependency @dotcms/types uses 'latest' version specifier which may lead to unexpected breaking changes during installations. This violates dependency management best practices that require version pinning for deterministic builds.

us.deepseek.r1-v1:0 · Run: #28667583125 · tokens: in: 50178 · out: 12782 · total: 62960 · calls: 17 · est. ~$0.137

@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
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) <noreply@anthropic.com>
@rjvelazco rjvelazco added this pull request to the merge queue Jul 3, 2026
Merged via the queue into main with commit d4d2a2e Jul 3, 2026
62 of 63 checks passed
@rjvelazco rjvelazco deleted the issue-36167-uve-zonejs-iframe-listeners branch July 3, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback 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