Skip to content

Builder iframe

The Builder renders in a same-origin iframe, a page of its own on the front theme, like the Preview. This page is for contributors, and for modules whose JavaScript works on the Builder.

The page

The route display_builder.builder_island, /display-builder/builder/{display_builder_instance}, renders the Builder island alone, with the view access of the instance. BuilderController builds the island with its internal in_iframe configuration: on the builder page, BuilderPanel renders the iframe and a refresh token; on the builder page, the root dropzone and the nodes.

ThemeHooks::jsAlter() keeps the builder's own scripts, htmx and Shoelace out of the builder page. It tells the page apart by the display_builder/builder_frame_inner library, not by the route: an aggregate is built by a request of its own, on the route of the aggregates. Core's JS asset cache only tells apart the libraries that have a script, so that library has one, builder_frame_inner.js, which registers the db-dropzone and db-block custom elements in the iframe: a dropzone, and the box of a node whose markup cannot carry its attributes. The front theme's scripts run as on any page.

Driven by the builder page

components/display_builder/js/builder_frame.js listens on the iframe document from the builder page:

  • A click on a node selects it, through the builder page's htmx. A link or a form submit is blocked.
  • Keys reach the builder's shortcuts, unless a field has the focus, a field in a web component's shadow root included.
  • A right click opens the contextual menu at the pointer.
  • Reordering inside the Builder uses the iframe's own Sortable. A drag from the builder page crosses an overlay over the iframe, which draws the insert line inside it and scrolls it near its edges.
  • The builder's state classes, highlight and dragging, are mirrored on the iframe's <html>, with the tokens the chrome reads, resolved in the builder page: they follow its light or dark theme. The chrome rules load in the iframe with builder_frame_inner, Shoelace's theme does not.

Refresh

A change swaps the content of the Builder refresh token out of band, as for the Preview. The iframe reloads into a hidden buffer, swapped in once loaded, with the scroll and the selection kept. One refresh runs at a time, and a drag holds both the start of a refresh and the swap of one already loaded, until its end. The state lives on the iframe's wrapper, wrap.dbBuilder, so two builders on a page keep theirs apart.

JavaScript working on the Builder

Builder nodes are in another document:

  • builder.querySelectorAll() does not reach them. Drupal.displayBuilder.queryAll(builder, selector) searches the builder page, then its Builder iframes.
  • A request changing the Builder resolves before the iframe reloads. Drupal.displayBuilder.afterBuilderRefresh(builder) resolves once the iframe shows the current state.
  • Do not call the builder page's once() on a Builder node: it returns an empty array for an element of another document, and throws for the iframe document. Flag the node with a property instead.
  • instanceof checks fail across documents: test nodeType or tagName.

Viewport and zoom

The viewport switcher, in the Builder header with the Highlight and Preview toggles, sets the width and the zoom of the Builder iframe and of the preview shown over it, which reflow for real. Its default zoom, Fit, scales a device width wider than the pane down to it. The Builder iframe is scaled only away from 100%, marked by data-db-scaled on the builder: a scaled iframe misses clicks for a moment after each reload.

Tests

  • Playwright: displayBuilder.builderFrame() is the Builder frame shown, and dragIntoBuilder() drags onto one of its nodes, from the builder page or from the Builder itself. htmxReady() waits for a Builder reload too.
  • Mink does not load iframes: a Functional test follows the iframe's src.
  • The local Playwright server serves no JS aggregate. The aggregated builder page is covered by BuilderControllerTest, which resolves the scripts as the aggregate controller does.