Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 134 additions & 2 deletions docs/Hooks-and-Events.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Hooks and Events

The visual editor exposes extension points through three mechanisms:
The visual editor exposes extension points through four mechanisms:

- **PHP filters** — `applyFilters('ap.visual-editor.*', ...)` for value transformation (resource map, site-editor entities, etc.)
- **PHP filters** — `applyFilters('ap.visual-editor.*', ...)` for value transformation (resource map, site-editor entities, server-rendered block HTML, etc.)
- **PHP actions** — `doAction('ap.visual-editor.*', ...)` for side effects
- **JavaScript filters** — `addFilter('ap.visual-editor.*', ...)` via `@wordpress/hooks` for editor-side extension (document panels, background controls, canvas styles)
- **Browser events** — `CustomEvent` dispatched on `window` for client-side integration (autosave, change, save)

All PHP hooks use the global helpers from [`artisanpack-ui/hooks`](https://github.com/ArtisanPack-UI/hooks). All browser events are plain `CustomEvent` instances dispatched on `window`.
Expand Down Expand Up @@ -91,6 +92,137 @@ See the [`artisanpack-ui/icons`](https://github.com/ArtisanPack-UI/icons) docs f

---

### `ap.visual-editor.rendered-block`

Last-mile PHP filter applied to the rendered HTML of every block (static or dynamic) inside `packages/visual-editor-renderer-blade`'s `BlockRenderer::renderBlock()`. Runs on the server, at render-time, on every request that emits post content — not at save-time and not in JavaScript. Callbacks decorate output without each host having to fork the renderer.

**Signature:** `string $html, string $blockName, array $attributes -> string`

```php
addFilter('ap.visual-editor.rendered-block', function (string $html, string $name, array $attributes): string {
if ('artisanpack/group' !== $name) {
return $html;
}
// …wrap, sanitize, inject data-* attributes, etc.
return $html;
}, 10, 3);
```

**Recursion:** the renderer walks inner blocks through the same code path, so the filter fires once per block **at every level** of the tree. A callback that wraps a container block will also wrap every descendant unless it gates on `$name` / `$attributes`.

**Attributes shape:** `$attributes` is post-normalization and may already carry package-internal `_resolved*` side-channel keys (site-meta, loginout). Treat those as read-only; they're not stable public attributes.

---

## JavaScript filters

Editor-side filters run through `@wordpress/hooks` inside the browser. Register callbacks with `addFilter(name, namespace, callback)` at editor bootstrap (the moment the app imports your package's entry module is enough — the editor evaluates each filter on every relevant render).

### `ap.visual-editor.background-controls`

Contribute panels to the shared background / appearance area of any block that opts into a background support (`supports.background` for image / gradient backgrounds, or `supports.color.background` for the color background). Applied by the built-in `editor.BlockEdit` HOC so external packages don't have to enumerate target blocks or wrap `BlockEdit` themselves — the target-block decision lives in the editor.

**Signature:** `BackgroundControl[] -> BackgroundControl[]`

```ts
type BackgroundControl = {
id: string // stable, namespaced (e.g. 'liquid-glass')
label: string // panel heading (translate before returning)
priority?: number // sort key, default 10, lower first
render: () => ReactNode
}

type BackgroundControlContext = {
// Frozen — call setAttributes to modify.
attributes: Readonly<Record<string, unknown>>
setAttributes: (attrs: Record<string, unknown>) => void
clientId: string
blockName: string
// Deep-cloned from the block-type registry — treat as read-only.
blockSupports: Readonly<Record<string, unknown>>
}
```

**Example:**

```ts
import { addFilter } from '@wordpress/hooks'

addFilter(
'ap.visual-editor.background-controls',
'artisanpack-ui/liquid-glass',
(controls, { attributes, setAttributes, blockSupports }) => {
// The HOC gates on either `supports.background` (image / gradient
// background) or `supports.color.background` (color background),
// so a block with only text-color support won't fire this filter.
// Narrow further to the shape your control cares about:
const hasBackground =
Boolean(blockSupports.background) ||
(typeof blockSupports.color === 'object' &&
blockSupports.color !== null &&
(blockSupports.color as Record<string, unknown>).background !==
false)

if (! hasBackground) {
return controls
}

return [
...controls,
{
id: 'liquid-glass',
label: 'Liquid Glass',
priority: 20,
render: () => (
<LiquidGlassPanel
value={attributes.liquidGlass}
onChange={(liquidGlass) => setAttributes({ liquidGlass })}
/>
),
},
]
},
)
```

Controls are deduped by `id` **before** sorting — a later `addFilter` at the same id overrides an earlier one regardless of priority, mirroring how `@wordpress/hooks` composes filters. Surviving controls are then sorted by `priority` (default `10`, lower first) with ties falling back to registration order.

If a filter callback throws, the exception is caught, logged to `console.error`, and the block renders without any contributed panels for that render — one buggy third-party filter can't trip Gutenberg's per-block crash boundary. Callbacks with a non-numeric `priority` are silently rejected.

Attributes are declared the standard Gutenberg way (`blocks.registerBlockType` filter). For static blocks, save-side rendering continues to go through `blocks.getSaveContent.extraProps`; for dynamic blocks, use the block's PHP render or the `ap.visual-editor.rendered-block` PHP filter (see the PHP filters section above).

---

### `ap.visual-editor.document-panels`

Contribute panels to the Document tab of the inspector sidebar. Runs against an empty descriptor list at inspector render time; the return value replaces it.

**Signature:** `DocumentPanelSpec[] -> DocumentPanelSpec[]`

```ts
type DocumentPanelSpec = {
id: string
title: string
initialOpen?: boolean
order?: number // sort key, default 100, lower first
render: () => ReactNode
}
```

Descriptors are sorted by `order` (default `100`, lower first) with ties falling back to registration order and deduped by `id` (last-wins). The slot-fill companion `<PluginDocumentSettingPanel>` mirrors the WordPress component of the same name for panels rendered as part of the editor tree instead of registered at bootstrap.

---

### `ap.visual-editor.canvas-styles`

Contribute additional stylesheets injected into the block canvas iframe. Applied once at module-load time inside the editor bundle and frozen for the session — register your filter before importing the editor entry module.

**Signature:** `CanvasStyle[] -> CanvasStyle[]` where `CanvasStyle = { css: string }`.

Entries whose `css` isn't a string are dropped. Non-array return values fall back to the built-in list.

---

## Browser events

The editor dispatches three `CustomEvent`s on `window` whenever content state changes.
Expand Down
Loading
Loading