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 withcanvas_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. instanceofchecks fail across documents: testnodeTypeortagName.
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, anddragIntoCanvas()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.