Canvas Builder — Client API & Event Contract¶
The editor JS is split into an always-loaded core (assets/core/core.js) plus per-component libraries. Components communicate through two surfaces only: the Drupal.canvasBuilder namespace and the CustomEvents below. Anything not listed here is a private implementation detail — don't reach into it from another component.
Canvas exposes no stable client event API. The only Canvas-originated event consumed is canvas:updateMediaWidget. Canvas globals touched: drupalSettings.canvas.store — actions publishReview/addOrUpdateAutoSavesHash, layoutModel/setInitialLayoutModel, ui/setHoveredComponent (core.js); ui/setViewportWidth (device-picker.js); layoutModel/{setLayoutModel,shiftNode,deleteNode,insertNodes} and ui/setSelection (ui/src/section-toolbar/, plus getState() reads) — and drupalSettings.canvas.componentSelectionUtils.handleComponentSelection / drupalSettings.canvas.layoutUtils.findParent (core.js's canvasBuilderStepUpOnDoubleClick); layoutModel/setInitialLayoutModel again plus ui/setLatestUndoRedoActionId (style-clipboard.js's paste — see CLAUDE.md §1a). Also touched: Canvas's Radix Themes menu markup (.rt-ContextMenuContent / .rt-BaseMenuViewport / [role="menuitem"] / [aria-haspopup], via style-clipboard.js's MutationObserver — there is no extension point for Canvas's context menu, so this reads its rendered DOM directly). Re-verify all of them on every Canvas update.
Drupal.canvasBuilder.*¶
| Member | Owner | Purpose |
|---|---|---|
FORM_SELECTOR |
core | Selector matching the Canvas component-instance form(s). |
getPreviewDocuments() |
core | Current document + same-origin iframe documents. |
getPreviewDocument() |
core | The front-end-themed preview iframe document, never the admin doc, or null. Canvas double-buffers two [data-canvas-preview] iframes, so this prefers [data-canvas-swap-active="true"], then plain first-match, then the first same-origin iframe with a parsed body. For style probes. |
injectPreviewCss(doc, id, cssText) |
core | Idempotent <style> injection into a preview iframe; guards the admin document, an already-present id, and an unparsed <head>. |
formsForContext(context) |
core | Component forms relevant to a behaviors attach context. |
formComponentUuid(form) |
core | The component instance UUID a given editor form is editing. Consumed cross-stack by the section toolbar (features/toolbar, features/popovers, features/insertion) and by layout-widgets' guides.js/widgets.js. |
getEnabledViewports() |
core | Enabled viewports, narrowest → widest. |
parseJsonAttr(el, attr, fallback) |
core | Safe JSON attribute read. |
parseJsonAttrCached(el, attr, fallback) |
core | Memoised parseJsonAttr for hot paths (data-cb-options). Consumed by select.js, device-picker.js. |
previewColumnsGuides(uuid, fr, cells) / hideColumnsGuides(delayMs) |
layout-widgets (guides.js) | Show/hide the column-guide overlay. Consumed cross-stack by layout-widgets/widgets.js and the section toolbar (layout/layout-guide-preview.js, core/popover.js). |
refreshPropGroupTab(form, groupId) |
prop-groups | Re-evaluates one prop-group tab's applicability without a form re-fetch. The contrib entry point for dynamic tabs — see docs/EXTENDING.md §11. |
svgIcon(path, size) |
core | Inline-SVG icon markup — the single icon innerHTML sink. path MUST be module-defined (e.g. drupalSettings.canvasBuilder.icons), never user or content-derived. |
getActiveViewport(form) / setActiveViewport(form, id) |
core | JS-authoritative active viewport (CLAUDE.md §3a). Never read the hidden input directly. |
getLastPickerViewport() / setLastPickerViewport(id) |
core | Picker's last explicit choice; survives AJAX rebuilds. |
genuineChangeEvent() |
core | Required factory for every synthetic change on a style control — anything else is dropped by the ghost filter (CLAUDE.md §3a). |
resolveViewportValue(el, activeId) |
core | Mobile-first cascade resolution of a data-cb-values map. |
rovingRadioKeydown |
core | Shared radiogroup keyboard handler. |
_editViewportPin |
background → core | Pins the viewport of a deferred bg-tab persist for its dispatch duration. |
syncViewportUI(form) |
device-picker | Re-points all per-viewport style controls, then dispatches cb:viewport-changed. |
updateResponsivePicker(form) |
device-picker | Re-evaluates device-picker visibility for the active tab. |
flushBgTabPersists() |
background | Flushes debounced bg-tab persists (called before viewport switches). |
syncResponsiveWidgets(form) |
layout-widgets | Re-points responsive layout widget families to the active viewport. |
ensureResponsiveControl(baseEl) |
layout-widgets | Builds/returns the single visible control for a responsive widget family. |
effectiveFamilyValue(form, family) |
layout-widgets | Resolves a responsive-family prop to its effective value at the active viewport (mobile-first cascade over form selects). Consumed by visibility.js's controlling-prop check. |
setSelectValue(select, value) |
layout-widgets | No-op-safe <select> value setter (skips the write when already equal). Consumed by defaults.js's fresh-component auto-init. |
refreshBgMediaVisibility(doc) / setBgLayerVisible / evaluateVisibility |
frontend | matchMedia-driven data-cb-show-from/hide-from evaluation. |
sectionToolbarExtras |
section-toolbar (init defensively by any registrant) | {chips: [], menuItems: []} registry. chips: ({form, uuid}) => HTMLElement\|null builders appended after the Style chip on every toolbar render. menuItems: ({addItem, form, uuid, close}) => void builders appended (behind a separator) to the ··· menu; addItem(iconHtml, label, onClick) returns the row element. Builders must never throw — errors are caught and logged. Registrant example: canvas_builder_ai's Generate chip + "Regenerate with AI" row. |
openPropGroupTab(groupId) |
prop-groups | Opens the right-hand panel's prop-group tab (e.g. 'style', 'create') for the current selection, force-opening the contextual panel / settings form if it isn't rendered, then pings the tab. The toolbar's Style and Generate bridges route through this. |
window.* (pattern-sync)¶
plugin.pattern_sync deliberately declares no library dependencies (must
install before Canvas's React app, order-independent — see the file's own
docblock), so Drupal may not exist yet when it runs. These four are bare
window.* globals rather than Drupal.canvasBuilder.* members for that
reason; consumers (e.g. style-clipboard.js) must call them defensively
(typeof window.x === 'function') since load order isn't guaranteed either.
| Member | Purpose |
|---|---|
canvasBuilderInvalidateEntityStyles() |
Invalidates the entity-styles cache; called from core.js's Drupal.Ajax.prototype.success wrapper after every style persist. |
canvasBuilderReadEntityStyles() |
Promise<{uuid: bundle}> — the draft-aware entity styles for the currently tracked layout entity (cached; see above). {} before any layout URL has been seen. |
canvasBuilderCurrentEntityKey() |
'canvas_page:35' or '' before any layout URL has been seen. |
canvasBuilderStageStyleBundles(map) |
Required before writing a bundle into model[uuid].canvasBuilder from anywhere but the AJAX style-persist path — see CLAUDE.md §1a. Returns a Promise<boolean> settling once the carrying layout-save request resolves (or a ~10s timeout). |
CustomEvents¶
| Event | Target | Dispatched by | Consumed by | Detail |
|---|---|---|---|---|
cb:viewport-changed |
component form | syncViewportUI |
background.js, layout-widgets/widgets.js, layout-widgets/visibility.js, box-model.js, section toolbar (mount.js, review-panel.js) | {viewportId, baseId} |
cb:activate-bg-tab |
[data-canvas-bg-tabs] |
background.js | background.js (visual state), bg-media.js (recompute layer windows) | {tabId} |
cb:sync |
style select | syncSelectDisplay on viewport switch (not a user edit — never write data-cb-values from it) |
select.js, overlay.js, color-swatch.js, text-align.js | — |
cb:bg-media-updated |
document |
bg-media.js (end of every updateMediaPreviews()) |
overlay.js — repaints overlays after layers exist (ordering guarantee; see CLAUDE.md §2c) | — |
canvas:updateMediaWidget |
document |
Canvas (React-routed media updates bypass Drupal.attachBehaviors) |
bg-media.js, overlay.js | — |
cb:ai-* |
document |
canvas_builder_ai panel (optional submodule) | open contract — see modules/canvas_builder_ai/docs/AI.md |
varies |
Rules¶
- New cross-component coupling goes through an event or a documented namespace member — never a direct import of another component's private function.
- Synthetic
changeevents on style controls must usegenuineChangeEvent(); synthetic rangeinputevents must not writedata-cb-values(onlyisTrusteddrags do). - Any code that creates or replaces preview media layers must preserve embedded overlay divs and finish by dispatching
cb:bg-media-updated. - The §3a defences (ghost filter, viewport stamp, JS-authoritative viewport) live in
core.jsand load before all components via library dependencies — never duplicate or bypass them.