Skip to content

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 change events on style controls must use genuineChangeEvent(); synthetic range input events must not write data-cb-values (only isTrusted drags 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.js and load before all components via library dependencies — never duplicate or bypass them.