# Frontend Runtime Extension Contract (V1) This document defines the stable frontend runtime integration surface for plugins that extend FrontEdit in the browser. ## Purpose This contract answers four questions for external integrations: 1. How another plugin may open and control FrontEdit editing. 2. What schema-resolved runtime data FrontEdit guarantees to expose. 3. Which lifecycle hooks and events are stable for observing editing and the standard FrontEdit save flow. 4. Which globals and implementation details are explicitly private. ## Scope This contract covers the browser runtime only. It does not define: 1. PHP handler registration. 2. Schema authoring rules. 3. Internal editor-state layout. 4. REST endpoint internals or private save helpers. 5. Internal DOM classes or data attributes unless explicitly documented here. ## Stability Model FrontEdit exposes one stable base namespace for browser integrations: ```js window.MWP.SFE.PublicApi ``` When FrontEdit Pro is active, FrontEdit also exposes one optional pro-only namespace: ```js window.MWP.SFE.ProApi ``` Everything else on `window.MWP.SFE` is private unless this document explicitly says otherwise. ### Two-tier contract This document uses two stability tiers: 1. `V1 committed surface` External plugins may rely on these methods, events, and return shapes. 2. `Candidate APIs under evaluation` These are roadmap items only. They are not part of the stable contract and may change or never ship. ### Versioning The runtime extension contract is versioned independently from the schema contract. `window.MWP.SFE.PublicApi` must expose: ```js SFE.PublicApi.getApiInfo(); ``` Expected shape: ```js { apiVersion: 1, namespace: 'window.MWP.SFE.PublicApi', features: { editorControl: true, runtimeInspection: true, editableBlockDiscovery: true, editingRuntimeResolution: true, textComponentOperations: true, mediaComponentOperations: true, mediaInspection: true, mediaSessionControl: true, explicitStaging: true, events: true, blockRefresh: true } } ``` Version rules: 1. Additive methods, additive event payload fields, and additive snapshot fields are minor-safe. 2. Removing or renaming methods, changing event semantics, or changing documented return-shape meaning requires an `apiVersion` bump. 3. Private internals may change at any time without notice. ## Public Namespace Rules External plugins may: 1. Call documented `SFE.PublicApi.*` methods. 2. Call documented `SFE.ProApi.*` methods only when the pro plugin is active and the method is documented here as pro-only. 3. Subscribe only to documented `SFE.PublicApi` events. 4. Store and compare documented snapshot data returned by the API. External plugins must not: 1. Monkey-patch FrontEdit methods. 2. Directly mutate `window.MWP.SFE` objects unless a documented API explicitly allows it. 3. Depend on underscore-prefixed properties. 4. Rebuild schema runtime resolution, media descriptor resolution, or block-state hydration from private internals when a public API exists. ## V1 Committed Surface ### Discovery #### Server-side AI discovery When the WordPress Abilities API is available, an authorized FrontEdit editor may call the following post-scoped, read-only abilities: 1. `mwpsfe/list-editable-blocks` with `post_id` to retrieve selectable block UUIDs, block types, edit handler IDs, and source-text summaries. 2. `mwpsfe/get-editable-block` with `post_id` and `uuid` to retrieve focused content for one already-authorized editable block. 3. `mwpsfe/get-frontend-runtime-contract` with `post_id` to retrieve this canonical browser contract. These abilities authorize the current user against the exact requested post; they do not discover WordPress posts/pages, execute browser methods, or create an external save path. WordPress core remains responsible for page discovery. Once the browser runtime is present, integrations must still verify availability through `SFE.PublicApi.getApiInfo()` and use `SFE.PublicApi.getEditableBlocks()` to enumerate the live page. #### `getEditableBlocks() -> EditableBlock[]` Return the FrontEdit-editable blocks currently known to the live page runtime. ```js const blocks = SFE.PublicApi.getEditableBlocks(); const match = blocks.find(block => block.contentText.includes('Pricing')); ``` Each entry is a `BlockSnapshot` plus `contentText`, which is normalized text from the current rendered block element. Use its `uuid` with `resolveEditingRuntime(...)` before choosing a documented edit operation. This method is the supported browser discovery path; integrations must not scrape private FrontEdit DOM attributes to enumerate UUIDs. #### Human save handoff An integration may inspect a block, open FrontEdit, and apply documented runtime operations or staging. It must then hand control to the authorized human to review and complete FrontEdit's standard save UI. V1 has no external direct-save API. #### `getApiInfo()` ```js const info = SFE.PublicApi.getApiInfo(); ``` Returns the contract version and feature flags for this runtime. ### Editor Control #### `openEditor(options) -> Promise` Open FrontEdit editing for a target block through the supported runtime path. ```js await SFE.PublicApi.openEditor({ uuid, element, handlerId, componentId, mode: 'edit', source: 'external' }); ``` Rules: 1. `uuid` is required. 2. `element` is optional when the block can be resolved from `uuid`. 3. `handlerId` is optional when FrontEdit can resolve the applicable handler for the block. 4. `componentId` is optional. When supplied, FrontEdit targets the documented editable component for the session. 5. `mode` defaults to `'edit'`. 6. `source` is a caller label for diagnostics and event payloads. Returns an `EditorSnapshot` when FrontEdit opened an editor session, otherwise `null`. #### `closeEditor(options = {}) -> boolean` Close the active editor session through the supported runtime path. ```js SFE.PublicApi.closeEditor({ uuid, restoreOriginal: true, reason: 'api', source: 'external' }); ``` Rules: 1. `uuid` is optional. When omitted, FrontEdit closes the active editor if one exists. 2. `restoreOriginal` defaults to `true`. 3. `reason` is an informational reason token. 4. `source` is a caller label for diagnostics and event payloads. Returns `true` when a close was attempted through the active supported editor session, otherwise `false`. #### `isEditorOpen() -> boolean` Returns whether FrontEdit currently has an active editor session. #### `getActiveEditor() -> EditorSnapshot|null` Returns a stable snapshot of the current active editor session. The return value is a snapshot, not a mutable live internal object. ### Runtime Inspection #### `resolveRuntime(options) -> ResolvedRuntime|null` Resolve the schema-aware runtime view FrontEdit would use for editing. ```js const runtime = SFE.PublicApi.resolveRuntime({ uuid, element, handlerId }); ``` Returns a stable runtime snapshot for the target block or `null` when no supported runtime could be resolved. #### `resolveEditingRuntime(options) -> ResolvedEditingRuntime|null` Resolve the richer schema-driven editing runtime FrontEdit would use for active editing or proposal materialization. ```js const runtime = SFE.PublicApi.resolveEditingRuntime({ uuid, element, handlerId, blockState, attributeChanges }); ``` Returns a detailed editing runtime snapshot with resolved component metadata and live component element references. Rules: 1. `uuid`, `element`, and `handlerId` follow the same resolution rules as `resolveRuntime()`. 2. `blockState` is optional. When supplied, FrontEdit resolves the runtime against that staged block state instead of the current session baseline. 3. `attributeChanges` is optional. When supplied, FrontEdit resolves the runtime against those pending block attribute changes. 4. The returned runtime data is read-only snapshot data except for documented DOM element references inside component entries. #### `getEditableComponents(options) -> EditableComponent[]` Returns the runtime-editable components for the resolved block. #### `getDefaultComponent(options) -> EditableComponent|null` Returns the default editable component for the resolved block, if one exists. ### Block Attribute Runtime Schema-backed block attribute changes flow through the shared executor. Public callers use `applyBlockAttributeOperations(...)` for both single-operation and multi-operation batches, and each entry must reference one schema-declared operation ID exposed through `resolveEditingRuntime(...).runtime.editableComponents[*].editor.operations`. #### `applyBlockAttributeOperations(options) -> BlockAttributeOperationResult|null` Apply one or more schema-declared block attribute mutations as one runtime batch. ```js const result = SFE.PublicApi.applyBlockAttributeOperations({ uuid, operations: [ { id: 'set_heading_level', value: 2 }, { id: 'set_text_align', value: 'center' }, { id: 'set_align', value: 'wide' }, { id: 'set_column_align', value: 'right', columns: [0] }, { id: 'set_column_align', value: 'right', columns: [0, 1, 2] }, { id: 'set_column_align', value: 'left', columns: 'all' } ] }); ``` Rules: 1. `uuid` is required. 2. The target editor must already be open. 3. `operations` is required. Callers should pass a one-item array when only one mutation is needed. 4. `operations` are applied in order against the current live editor state. 5. Each entry must supply a schema operation `id` plus a concrete `value`. 6. Column-scoped table alignment operations such as `set_column_align` must supply `columns`. 7. `columns` must be either an array of zero-based column indexes such as `[0]` or `[0, 1, 2]`, or the string `'all'`. 8. Scalar convenience values such as `columns: 0` are intentionally invalid; callers must always send an array for explicit column indexes. 9. Public callers must not send raw `block_attribute_change` payloads or direct attribute paths such as `style.typography.textAlign`. ### Component Runtime The Component Runtime API applies schema-backed component content updates to an open editor. It supports replacing the content of one or more runtime components in a single batch while automatically normalizing the supplied content against the component's schema. Use `applyTextComponentOperations(...)` for direct component content replacement. For compatibility with existing integrations, `applyStructuredEdit(...)` continues to provide the higher-level batch API and internally delegates component updates to the same execution pipeline. #### `applyTextComponentOperations(options) -> ComponentOperationResult|null` Apply one or more schema-backed component content replacements as one runtime batch. ```js const result = SFE.PublicApi.applyTextComponentOperations({ uuid, operations: [ { kind: 'replace_component_content', componentId: 'content', bindingSource: 'html', runs: [ { text: 'Updated CTA copy', formats: ['link'], formatAttributes: { link: { href: 'https://example.com', settings: { new_tab: true, no_follow: true } } } } ] } ] }); ``` ```js const result = SFE.PublicApi.applyTextComponentOperations({ uuid, operations: [ { id: 'set_button_link', kind: 'link_change', componentId: 'label', format: 'buttonLink', href: 'https://example.com/pricing', new_tab: true, no_follow: true } ] }); ``` Rules: 1. `uuid` is required. 2. The target editor must already be open. 3. `operations` is required. Callers should pass a one-item array when only one replacement is needed. 4. Each operation must target one runtime `componentId`. 5. `replace_component_content` is the canonical public text/content mutation kind. 6. `link_change` is the canonical public host-link mutation kind for element-scoped anchor components such as `core/button`. 7. `link_change` accepts `href` or `url`, optional `target` or `linkTarget`, optional `rel`, and link settings via either top-level `new_tab` / `no_follow` fields or `settings.{new_tab,no_follow}`. 8. `link_change` is intended for components whose editable host element is itself the canonical anchor. It is not the replacement path for inline text links inside larger rich-text content. 9. Public callers should send normalized `runs` payloads for `replace_component_content`. Literal newline characters inside `runs[*].text` are interpreted through the component's schema/runtime editor options. 10. FrontEdit also accepts `lines` as an undocumented compatibility input while callers migrate to direct `runs`, but `runs` is the stable public contract. 11. Link-like format attributes inside `replace_component_content` runs are normalized by FrontEdit against the component's schema-declared inline format capabilities, including `settings.new_tab` and `settings.no_follow`. #### `ComponentOperationResult` Successful component runtime mutations return: 1. `uuid` (string): target block UUID 2. `updatedComponentIds` (array of strings): component IDs whose live DOM was updated 3. `operationsApplied` (array of strings): applied operation IDs or canonical kinds in execution order ### Media Runtime The Media Runtime API applies schema-backed media replacements to an open media editing session. It uses the same component targeting model as the component content runtime, but delegates the actual preview mutation through FrontEdit's existing media-session host so resolved media attributes and save-time behavior stay aligned with the native editor flow. #### `applyMediaComponentOperations(options) -> MediaOperationResult|null` Apply one or more schema-backed media replacements to the active media session. ```js const result = SFE.PublicApi.applyMediaComponentOperations({ uuid, operations: [ { kind: 'replace_component_media', componentId: 'image', url: 'https://example.com/uploads/updated-image.jpg', attachmentId: 123, source: 'library' } ] }); ``` Rules: 1. `uuid` is required. 2. The target editor must already be open. 3. The target component must already own the active media-editing session. 4. `operations` is required. Callers should pass a one-item array when only one replacement is needed. 5. Each operation must target one runtime `componentId`. 6. `replace_component_media` is the canonical public media mutation kind. 7. `url` is required. 8. `attachmentId` is optional. 9. `source` may be `'library'` or `'input'` and defaults to the input-style transition when omitted. #### `MediaOperationResult` Successful media runtime mutations return: 1. `uuid` (string): target block UUID 2. `updatedComponentIds` (array of strings): component IDs whose live DOM was updated 3. `operationsApplied` (array of strings): applied operation IDs or canonical kinds in execution order ### Structured Edit Runtime Structured non-list edits use the same high-level pattern as public list mutations: 1. open the editor for the target block 2. apply normalized operations through the public API 3. let FrontEdit own DOM mutation, toolbar sync, and history persistence #### `applyStructuredEdit(options) -> StructuredEditResult|null` Apply one normalized structured edit batch to the active schema editor. ```js const result = SFE.PublicApi.applyStructuredEdit({ uuid, componentUpdates: [ { componentId: 'content', bindingSource: 'html', runs: [ { text: 'Updated heading copy' } ] } ], attributeOperations: [ { id: 'set_heading_level', value: 1 }, { id: 'set_align', value: 'full' }, { id: 'set_text_align', value: 'right' } ] }); ``` Rules: 1. `uuid` is required. 2. The target editor must already be open. 3. `componentUpdates` are applied first through the shared component executor. 4. `attributeOperations` are then applied through the shared block-attribute executor. 5. The batch creates at most one FrontEdit history entry after all component and attribute mutations finish. 6. `attributeOperations` must use schema operation IDs, not raw `block_attribute_change` payloads or direct attribute paths. 7. Callers should pass normalized capability-driven payloads rather than manually mutating the editor DOM outside this seam. #### `StructuredEditResult` Successful structured edits return: 1. `uuid` (string): target block UUID 2. `updatedComponentIds` (array of strings): component IDs whose live DOM was updated 3. `operationsApplied` (array of strings): schema operation IDs that mutated the live editor state when available 4. `attributeChanges` (object): current tracked block attribute change map after the batch #### `BlockAttributeOperationResult` Successful block-attribute runtime mutations return: 1. `uuid` (string): target block UUID 2. `operationsApplied` (array of strings): schema operation IDs that actually mutated the live editor state 3. `attributeChanges` (object): current tracked block attribute change map after the batch ### List Runtime `V1` includes a public list-tree runtime for `core/list`-style blocks that are edited as one root block while exposing nested item/list structure to external callers. #### `getListStructure(options) -> ListNode|null` Return the current live structural snapshot for one open or discoverable list block. ```js const structure = SFE.PublicApi.getListStructure({ uuid, element }); ``` Rules: 1. `uuid` is required unless `element` can be resolved to a block UUID. 2. The target block must resolve to a live `UL` or `OL` root. 3. The return value is a read-only structural snapshot of the live DOM tree. #### `applyListOperations(options) -> ListOperationResult|null` Apply one or more structural list mutations as one runtime batch. ```js const result = SFE.PublicApi.applyListOperations({ uuid, operations: [ { kind: 'update_list_item_text', itemUuid: '7db1a4ff-8e25-4f7d-a806-9328d473bb96', contentHtml: 'Alpha' }, { kind: 'toggle_list_type', itemUuid: '7db1a4ff-8e25-4f7d-a806-9328d473bb96', } ] }); ``` Rules: 1. `uuid` is required. 2. The target list editor must already be open. 3. `operations` are applied in order against the live mutated tree. 4. Every operation must supply the correct documented UUID target token family for its kind. 5. FrontEdit resolves each operation's runtime UUIDs against the current post-mutation tree immediately before that operation runs. 6. Public callers must use only the documented high-level list-operation kinds. 7. Some public operations may expand into multiple internal primitive mutations. For example, `insert_child` inserts the new item after the parent item, then indents it so the tracker creates the nested child list through the normal editor path. 8. Successful batches return one updated list structure snapshot. #### Supported list operation kinds List runtime operations currently include: 1. `update_list_item_text` 2. `insert_before` 3. `insert_after` 4. `insert_child` 5. `remove_list_item` 6. `move_before` 7. `move_after` 8. `indent_list_item` 9. `outdent_list_item` 10. `toggle_list_type` These are the public API kinds only. Internally FrontEdit still executes lower-level primitive list operations such as `insert_list_item`, `move_list_item`, and `toggle_list_type`, but only the documented public surface is part of the runtime contract. #### List operation payloads | Kind | Required fields | Optional fields | Description | | --- | --- | --- | --- | | `update_list_item_text` | `kind`, `itemUuid`, `contentHtml` | -- | Replaces the direct text HTML for one existing list item. | | `insert_before` | `kind`, `newItemUuid`, `targetItemUuid`, `contentHtml` | -- | Inserts a new sibling item before the target item. | | `insert_after` | `kind`, `newItemUuid`, `targetItemUuid`, `contentHtml` | -- | Inserts a new sibling item after the target item. | | `insert_child` | `kind`, `newItemUuid`, `targetItemUuid`, `contentHtml` | -- | Creates a new child item under the target item. | | `remove_list_item` | `kind`, `itemUuid` | -- | Removes one existing list item and its nested children. | | `move_before` | `kind`, `itemUuid`, `targetItemUuid` | -- | Moves an existing item before the target item. | | `move_after` | `kind`, `itemUuid`, `targetItemUuid` | -- | Moves an existing item after the target item. | | `indent_list_item` | `kind`, `itemUuid` | -- | Indents one existing item through the normal editor list behavior. | | `outdent_list_item` | `kind`, `itemUuid` | -- | Outdents one existing item through the normal editor list behavior. | | `toggle_list_type` | `kind`, `itemUuid` | -- | Toggles the containing list for the referenced item between ordered and unordered. | `contentHtml` is required for operations that create or replace item content. It represents the direct item text HTML only. It must not include wrapping `
  • `, `