Skip to content

Canvas iframe

The Canvas 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 Canvas.

The page

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

ThemeHooks::jsAlter() keeps the builder's own scripts, htmx and Shoelace out of the canvas page. It tells the page apart by the display_builder/canvas_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, canvas_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/canvas_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 Canvas 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 canvas_frame_inner, Shoelace's theme does not.

Refresh

A change swaps the content of the Canvas 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.dbCanvas, so two builders on a page keep theirs apart.

JavaScript working on the Canvas

Canvas nodes are in another document:

  • builder.querySelectorAll() does not reach them. Drupal.displayBuilder.queryAll(builder, selector) searches the builder page, then its Canvas iframes.
  • A request changing the Canvas resolves before the iframe reloads. Drupal.displayBuilder.afterCanvasRefresh(builder) resolves once the iframe shows the current state.
  • Do not call the builder page's once() on a Canvas 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 Canvas header with the Highlight and Preview toggles, sets the width and the zoom of the Canvas 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 Canvas 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.canvasFrame() is the Canvas frame shown, and dragIntoCanvas() drags onto one of its nodes, from the builder page or from the Canvas itself. htmxReady() waits for a Canvas 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 canvas page is covered by CanvasControllerTest, which resolves the scripts as the aggregate controller does.