Skip to content

Latest commit

 

History

History
152 lines (129 loc) · 10.2 KB

File metadata and controls

152 lines (129 loc) · 10.2 KB

Block inspector — editing complex blocks without Gutenberg

Status: all five steps shipped (v0.4.0 cycle) — code language attribute (plus a hover/caret ⚙ chip popout on editable code blocks), the inspector for attribute/dynamic islands with server-rendered previews (minn-admin/v1/render-blocks replaced the per-block block-renderer idea — it renders whole islands including static parents with dynamic children), one-level child editing, add/remove/reorder children (gated on the "structural" InnerBlocks shape: wrapper HTML preserved verbatim, whitespace-only between children), and the minn_admin_block_forms filter with wrapperText — which delivered the useful subset of "parent editing": declared text inside a wrapper (e.g. the conversation header) is editable via a three-capture-group pattern, replaced only when changed. Adapters ship inside the integrated plugin (Anchor Blocks app/MinnAdmin.php is the reference), not bundled in Minn. Full parent-attribute regeneration for static InnerBlocks wrappers remains deliberately unbuilt — it requires the block's JS save() and stays on the wrong side of the parity treadmill.

Two later additions (v0.5.x cycle): the inspector on a core/embed / core/gallery island offers "Change URL…" / "Replace images…" — these regenerate the island wholesale through the same templates that create embeds and galleries (embedTemplate() / galleryTemplate()), because their content lives in saved HTML where attribute edits alone can't retarget it. Gutenberg-set captions/layout attributes reset by design; the popover note says so. And island previews now render with the site's real front-end styles — minn-admin/v1/editor-styles collects block style handles + theme editor styles + the global stylesheet, and the client scopes every rule to .minn-island-preview before injecting (see editor-direction.md, "The writing surface").

Post-v0.8.0 addition (auto-registered inserts): dynamic third-party blocks no longer need an adapter to be insertable. Minn_Admin::insertable_blocks() walks the block-type registry at boot (dynamic + top-level + inserter-visible + non-core; adapter insert descriptors supersede, insert => false suppresses, minn_admin_insert_blocks filters the result) and the slash menu carries them as search-only entries: the default list stays curated, and typing matches title or namespace. A self-closing comment is valid saved markup only when the block's JS save() is null — is_dynamic is NOT that guarantee (stackable/posts pairs a render_callback with a JS save that emits wrapper HTML; a bare comment renders empty AND fails Gutenberg validation). The server can't see JS save(), so the shipped discriminator is a render probe: candidates whose bare comment renders nothing are excluded (cached in a transient keyed on the candidate set). Static-save blocks remain excluded per "The honest limit" below — but see adapters/stackable.php for the scrape-the-data counter-move: when a plugin publishes its designs as serialized markup (Stackable's CDN design library), those templates insert as valid islands with no runtime.

Generic text runs (same cycle): the honest limit got narrower. A static block's STRUCTURE and STYLING still can't be regenerated outside Gutenberg, but its TEXT is just text nodes in the saved HTML — so the inspector scans an island's raw markup by offset (textRunsOf: block comments, tags and style/script/svg subtrees skipped) and renders one field per text node, however deep the nesting and even for blocks with no registered attributes. Changed runs are spliced back last-to-first with text-node escaping only; untouched runs are never rewritten, preserving byte-identity. Gutenberg stays happy because these blocks SOURCE their text attributes from that same HTML. Children already served by the single-element text editor (childTextOf) keep it — the two paths are mutually exclusive per child. Etch is the exception that lives in the comment: wp:etch/text stores copy as a content JSON attribute (render reads that, not inner HTML), so a sibling scan (etchTextRunsOf) arms those strings as in-place preview runs and splices the quoted JSON value with Gutenberg comment escaping.

Image swaps (same cycle): the sibling move for pictures. Static blocks mirror an image's URL between comment JSON (imageUrl, blockBackgroundMediaUrl) and saved HTML (img src, background-image style), so swapIslandImage() replaces the URL string everywhere (plus its serializeAttributes-escaped form), retargets paired XxxUrl/XxxId media ids scoped to the comment carrying the URL, and updates wp-image-N classes on swapped img tags. The inspector's Images section lists every image in the island with a Replace button into the media picker; pending field edits are folded into the raw first (buildInspectorRaw, shared with Apply) so nothing typed is lost. Embed/gallery keep their dedicated rebuild flows — the generic swap is suppressed there, since core image comments carry a bare id the URL-key heuristic can't safely retarget.

Block islands made complex content safe (see editor-direction.md) — this is the plan for making them workable. The goal: click an island, get a small inspector popover next to it, edit the block's configuration in place, watch the preview update. No React, no build step, no change to the safety model.

Why this is feasible at all

The key realization: for server-registered blocks, everything the inspector needs already exists as core REST endpoints — Minn drives the same server-side machinery Gutenberg does, minus the React shell.

Need Endpoint Verified
What attributes does this block have? GET wp/v2/block-types/<namespace>/<name> → full attribute schema with types and defaults ✓ returns role / label / content for anchor/conversation-message
What does it look like with these attributes? GET wp/v2/block-renderer/<name>?context=edit&attributes={…} → server-rendered HTML ✓ renders anchor/stat-card with arbitrary attrs

Self-closing dynamic blocks (<!-- wp:anchor/stat-card {"value":"85","label":"…"} /-->) are pure attribute containers — no saved HTML exists to invalidate, so editing them is literally rewriting JSON inside a comment. The island's raw markup is already stored verbatim and spliced back on save; the inspector just modifies that stored string. Nothing about the byte-identity model changes.

The design

  1. Trigger — a ⚙ affordance on the island chip (and clicking the chip itself). Opens a popover anchored to the island, same visual family as the slash-command menu.
  2. Schema-driven form — fetch the block type once (cache per session), generate fields from the attribute schema: string → input, boolean → switch, number → number input, enum → select. Skip lock / metadata / style (Gutenberg plumbing).
  3. Apply — rewrite the attributes JSON in the island's stored raw markup: for self-closing blocks, rebuild the whole comment; for wrapped dynamic blocks, rebuild only the opening comment and keep inner HTML untouched.
  4. Preview refresh — POST the new attrs to block-renderer and swap the island preview. Blocks with no render_callback keep their static preview.
  5. Serialize — unchanged. Islands still pass through verbatim from stored raw markup.

Nested islands (anchor/conversation)

The conversation block is a static InnerBlocks wrapper whose children are self-closing dynamic conversation-message blocks. tokenizeBlocks() already splits nested content, so the inspector can list children and edit each one with the same schema-driven form — including add / remove / reorder, since children are attribute-only comments. The parent's own wrapper markup (<div class="wp-block-anchor-conversation"><div class="ab-conv-header">…</div>) is reproducible but is saved by the block's JS save(), so regenerating it risks Gutenberg's block validation — ship child editing first, parent-attribute editing last, behind round-trip tests.

The code block (ships first, independent of the inspector)

<!-- wp:code {"language":"sql"} --> islands today only because language isn't in EDITABLE_ATTRS. Add code: [ 'language' ], map the attribute into the existing toolbar language picker on load, re-emit attribute + language-* class on serialize. The block becomes fully editable and the picker (built in v0.2.0) just works. Small, high value — the most common island on real content.

The honest limit

Static third-party blocks — ones whose save() lives in their editor JS bundle — stay verbatim islands. Changing their attributes requires regenerating HTML only Gutenberg's runtime can produce; that's the parity treadmill editor-direction.md refuses to get on. They already display and survive edits safely, and WordPress has pushed block development server-side for years, so the reachable set keeps growing on its own.

Authors who want first-class Minn editing should follow the content-block contract in content-blocks.md (dynamic leaves, PHP schema, optional minn_admin_block_forms). That is how Anchor Blocks stay editable in Minn without Minn becoming a layout runtime.

Extension point

Schema types alone can't express intent: role is really a user/assistant enum, content deserves a textarea, color is a fixed palette. A minn_admin_block_forms filter (same declarative-descriptor pattern as surfaces and editor panels — see for-plugin-authors.md) lets a plugin refine the generated form for its own blocks: labels, control types, option lists, field order. Anchor Blocks becomes the reference adapter, the same role ACF plays for editor panels.

Sequencing

  1. code.language via EDITABLE_ATTRS — small.
  2. Inspector for attribute-only / dynamic islands (stat-card, callout, timeline-item…) — the core build: schema fetch, form generation, comment rewrite, renderer preview.
  3. Child-block editing inside InnerBlocks islands (conversation messages).
  4. minn_admin_block_forms filter + Anchor Blocks descriptors.
  5. (Maybe, last) parent-attribute editing for simple InnerBlocks wrappers, gated on proven round-trips.