/**
* Stable frontend runtime PublicApi implementation.
*
* Exposes: SFE.PublicApi
* {
* getApiInfo, on, off, isEditorOpen, getActiveEditor,
* applyActiveMediaSelection, getPageContext, getRestContext, setRestNonce,
* getElementByUuid, getUuidForElement, getBlockSnapshot, getEditableBlocks,
* resolveRuntime,
* resolveEditingRuntime, getEditableComponents, getListStructure,
* getDefaultComponent, getMediaDescriptor, getMediaContext,
* isMediaEditable, openEditor, closeEditor, applyTextComponentOperations,
* applyMediaComponentOperations,
* applyBlockAttributeOperations,
* applyListOperations, applyStructuredEdit, stageBlockState,
* clearStagedBlockState, refreshBlock, getDirtyBlocks, hasDirtyBlocks,
* isBatchSessionActive, resetDirtyBlocks
* }
*
* Keeps mutable runtime internals private while exposing the documented browser
* integration surface for external consumers such as ABE.
*/
(function() {
'use strict';
window.MWP = window.MWP || {};
window.MWP.SFE = window.MWP.SFE || {};
const SFE = window.MWP.SFE;
SFE.ManagerData = SFE.ManagerData || {};
const PublicApi = SFE.PublicApi || {};
const bridge = SFE.PublicApiBridge || null;
if (!bridge) {
return;
}
/**
* Resolve the shared runtime context object.
*
* @returns {Object} Shared FrontEdit context.
*/
function getContext() {
return SFE.Context || {};
}
/**
* Resolve the localized bootstrap payload.
*
* @returns {Object} Localized manager data.
*/
function getManagerData() {
return SFE.ManagerData || {};
}
/**
* Normalize one candidate REST nonce.
*
* @param {*} nonce Candidate nonce value.
* @returns {string} Trimmed nonce or an empty string.
*/
function normalizeRestNonce(nonce) {
return String(nonce || '').trim();
}
/**
* Return the live block element for one UUID.
*
* @param {string} uuid Block UUID.
* @returns {Element|null} Matching element.
*/
function getElementByUuid(uuid) {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || typeof document.querySelector !== 'function') {
return null;
}
try {
return document.querySelector(`[data-mwp-sfe-uuid="${CSS.escape(normalizedUuid)}"]`);
} catch (error) {
return document.querySelector(`[data-mwp-sfe-uuid="${normalizedUuid.replace(/"/g, '\\"')}"]`);
}
}
/**
* Resolve one UUID from a supplied element or nested child.
*
* @param {Element|null} element Candidate DOM node.
* @returns {string} Block UUID or an empty string.
*/
function getUuidForElement(element) {
if (!(element instanceof Element)) {
return '';
}
const candidate = element.closest('[data-mwp-sfe-uuid]');
return candidate instanceof Element
? String(candidate.getAttribute('data-mwp-sfe-uuid') || '').trim()
: '';
}
/**
* Resolve one uuid-map entry from the shared context.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} UUID map entry.
*/
function getUuidEntry(uuid) {
const context = getContext();
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || !context.uuidMap || !context.uuidMap[normalizedUuid]) {
return null;
}
return context.uuidMap[normalizedUuid];
}
/**
* Resolve one edit-capable handler for the supplied target.
*
* @param {Object} options Resolution options.
* @returns {Object|null} Edit handler when supported.
*/
function resolveEditHandler(options = {}) {
const element = options.element instanceof Element ? options.element : null;
const preferredHandlerId = String(options.handlerId || '').trim();
const handlers = Array.isArray(element?._mwpSfeHandlers)
? element._mwpSfeHandlers
: [];
if (preferredHandlerId) {
const exact = handlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
));
if (exact) {
return exact;
}
}
const fromElement = handlers.find(handler => handler && handler.capability === 'edit') || null;
if (fromElement) {
return fromElement;
}
const uuidEntry = getUuidEntry(options.uuid);
const managerData = getManagerData();
const registeredHandlers = Array.isArray(managerData.handlers) ? managerData.handlers : [];
const handlerIds = Array.isArray(uuidEntry?.handlers) ? uuidEntry.handlers : [];
if (preferredHandlerId) {
return registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
)) || null;
}
for (let index = 0; index < handlerIds.length; index++) {
const handlerId = String(handlerIds[index] || '').trim();
const match = registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === handlerId
));
if (match) {
return match;
}
}
return null;
}
/**
* Resolve the current block-state payload used for schema hydration.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} Block-state payload when available.
*/
function resolveBlockState(uuid) {
const batchManager = SFE.BatchEditManager || null;
if (
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive() &&
typeof batchManager.getBlockStateForUuid === 'function'
) {
return batchManager.getBlockStateForUuid(uuid);
}
return null;
}
/**
* Resolve the schema runtime bundle for one target block.
*
* @param {Object} options Runtime resolution options.
* @returns {Object|null} Schema runtime bundle.
*/
function resolveRuntimeBundle(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const schemaRuntime = SFE.SchemaRuntime || null;
if (!uuid || !(element instanceof Element) || !handler || !schemaRuntime || typeof schemaRuntime.resolveForEditing !== 'function') {
return null;
}
try {
const blockState = options.blockState && typeof options.blockState === 'object'
? options.blockState
: resolveBlockState(uuid);
const attributeChanges = options.attributeChanges && typeof options.attributeChanges === 'object'
? options.attributeChanges
: undefined;
const resolved = schemaRuntime.resolveForEditing(element, handler, {
blockState,
attributeChanges,
});
if (!resolved || !resolved.runtimeHandler || !resolved.runtime) {
return null;
}
return {
uuid,
element,
handler: resolved.runtimeHandler,
runtime: resolved.runtime,
schema: resolved.schema || null,
};
} catch (error) {
console.warn('FrontEdit: PublicApi resolveRuntime failed', error);
return null;
}
}
/**
* Build one detailed editable-component snapshot from a schema runtime entry.
*
* @param {Object|null} editableComponent Runtime editable component.
* @param {Object|null} componentMeta Additional component metadata.
* @returns {Object|null} Detailed component snapshot.
*/
function buildEditingRuntimeComponentDetail(editableComponent, componentMeta = null) {
const snapshot = buildEditableComponentSnapshot(editableComponent);
if (!snapshot) {
return null;
}
const meta = componentMeta && typeof componentMeta === 'object' ? componentMeta : {};
snapshot.element = editableComponent?.element instanceof Element
? editableComponent.element
: (meta.element instanceof Element ? meta.element : null);
const bindings = Array.isArray(meta.bindings)
? bridge.clonePlainData(meta.bindings)
: [];
if (bindings.length) {
snapshot.bindings = bindings;
}
const target = meta.target && typeof meta.target === 'object'
? bridge.clonePlainData(meta.target)
: (snapshot.target || null);
if (target) {
snapshot.target = target;
}
const editorOptions = editableComponent?.editorOptions && typeof editableComponent.editorOptions === 'object'
? bridge.clonePlainData(editableComponent.editorOptions)
: (meta.editorOptions && typeof meta.editorOptions === 'object'
? bridge.clonePlainData(meta.editorOptions)
: null);
if (editorOptions) {
snapshot.editorOptions = editorOptions;
snapshot.editor = bridge.clonePlainData(editorOptions);
}
return snapshot;
}
/**
* Build one detailed schema-resolved editing runtime snapshot.
*
* This exposes the same resolved component metadata FrontEdit uses for editing
* while keeping handler resolution and schema runtime execution owned by FrontEdit.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Detailed editing runtime snapshot.
*/
function buildResolvedEditingRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const runtimeComponentsById = bundle.runtime.componentsById && typeof bundle.runtime.componentsById === 'object'
? bundle.runtime.componentsById
: {};
const componentDetails = [];
const componentsById = {};
(Array.isArray(bundle.runtime.editableComponents) ? bundle.runtime.editableComponents : []).forEach(component => {
const componentId = String(component?.id || '').trim();
const detail = buildEditingRuntimeComponentDetail(component, componentId ? runtimeComponentsById[componentId] : null);
if (!detail || !componentId) {
return;
}
componentDetails.push(detail);
componentsById[componentId] = detail;
});
Object.keys(runtimeComponentsById).forEach(componentId => {
if (componentsById[componentId]) {
return;
}
const detail = buildEditingRuntimeComponentDetail(runtimeComponentsById[componentId], runtimeComponentsById[componentId]);
if (!detail) {
return;
}
detail.id = componentId;
componentDetails.push(detail);
componentsById[componentId] = detail;
});
const mediaComponentId = String(bundle.runtime?.mediaComponent?.id || '').trim();
const mediaComponent = mediaComponentId && componentsById[mediaComponentId]
? componentsById[mediaComponentId]
: null;
return {
uuid: bundle.uuid,
element: bundle.element instanceof Element ? bundle.element : null,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
runtimeHandler: {
id: String(bundle.handler.id || '').trim(),
},
runtime: {
blockName: String(bundle.runtime.blockName || '').trim(),
editableComponents: componentDetails,
componentsById,
mediaComponent,
},
schema: {
handlerId: String(bundle.schema?.handlerId || bundle.handler.id || '').trim(),
},
};
}
/**
* Return a plain snapshot of one editable component.
*
* @param {Object|null} component Runtime component.
* @returns {Object|null} Public component snapshot.
*/
function buildEditableComponentSnapshot(component) {
if (!component || typeof component !== 'object') {
return null;
}
const snapshot = {
id: String(component.id || '').trim(),
label: String(component.label || component.id || '').trim(),
type: String(component.type || '').trim().toLowerCase(),
selector: String(component.selector || '').trim(),
default: !!component.default,
};
if (!snapshot.id || !snapshot.type) {
return null;
}
if (component.required === true) {
snapshot.required = true;
}
if (typeof component.placeholder === 'string' && component.placeholder.trim()) {
snapshot.placeholder = component.placeholder;
}
if (component.target && typeof component.target === 'object') {
snapshot.target = bridge.clonePlainData(component.target);
}
if (component.mediaDescriptor && typeof component.mediaDescriptor === 'object') {
snapshot.mediaDescriptor = bridge.clonePlainData(component.mediaDescriptor);
}
if (component.editorOptions && typeof component.editorOptions === 'object') {
snapshot.editor = bridge.clonePlainData(component.editorOptions);
}
return snapshot;
}
/**
* Resolve the public runtime mode label.
*
* @param {Object|null} runtime Schema runtime.
* @returns {string} Runtime mode label.
*/
function resolvePublicRuntimeMode(runtime) {
const components = Array.isArray(runtime?.editableComponents) ? runtime.editableComponents : [];
const hasText = components.some(component => String(component?.type || '').trim().toLowerCase() !== 'file');
const hasFile = components.some(component => String(component?.type || '').trim().toLowerCase() === 'file');
if (hasText && hasFile) {
return 'mixed';
}
if (hasFile) {
return 'file';
}
return 'text';
}
/**
* Build one public runtime snapshot from a schema bundle.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Public runtime snapshot.
*/
function buildResolvedRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const components = Array.isArray(bundle.runtime.editableComponents)
? bundle.runtime.editableComponents.map(buildEditableComponentSnapshot).filter(Boolean)
: [];
const defaultComponent = components.find(component => component.default) || components[0] || null;
return {
uuid: bundle.uuid,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
mode: resolvePublicRuntimeMode(bundle.runtime),
defaultComponentId: defaultComponent ? defaultComponent.id : '',
components,
};
}
/**
* Resolve a public media descriptor for one target block.
*
* @param {Object} options Media resolution options.
* @returns {Object|null} Media descriptor snapshot.
*/
function resolveMediaDescriptor(options = {}) {
const runtime = resolveRuntimeBundle(options);
if (!runtime) {
return null;
}
const componentId = String(options.componentId || '').trim();
const components = Array.isArray(runtime.runtime.editableComponents) ? runtime.runtime.editableComponents : [];
const selected = componentId
? components.find(component => component && component.id === componentId)
: (
(runtime.runtime.mediaComponent && typeof runtime.runtime.mediaComponent === 'object'
? runtime.runtime.mediaComponent
: components.find(component => String(component?.type || '').trim().toLowerCase() === 'file')
) || null
);
if (!selected || !selected.mediaDescriptor || typeof selected.mediaDescriptor !== 'object') {
return null;
}
return bridge.clonePlainData(selected.mediaDescriptor);
}
/**
* Build one stable block snapshot.
*
* @param {string} uuid Target UUID.
* @returns {Object|null} Block snapshot.
*/
function buildBlockSnapshot(uuid) {
const normalizedUuid = String(uuid || '').trim();
const entry = getUuidEntry(normalizedUuid);
if (!normalizedUuid || !entry) {
return null;
}
return {
uuid: normalizedUuid,
blockName: String(entry.blockName || '').trim(),
handlerIds: Array.isArray(entry.handlers) ? entry.handlers.slice() : [],
isPending: !!entry.is_pending,
pendingInfo: entry.pending_info ? bridge.clonePlainData(entry.pending_info) : null,
elementPresent: !!getElementByUuid(normalizedUuid),
};
}
/**
* Return stable discovery snapshots for every editable block on this page.
*
* @returns {Array} Editable block snapshots in FrontEdit UUID-map order.
*/
function getEditableBlocks() {
const context = getContext();
const uuidMap = context.uuidMap && typeof context.uuidMap === 'object'
? context.uuidMap
: {};
return Object.keys(uuidMap)
.map(uuid => {
const snapshot = buildBlockSnapshot(uuid);
if (!snapshot) {
return null;
}
const element = getElementByUuid(uuid);
return {
...snapshot,
contentText: element instanceof Element
? String(element.textContent || '').replace(/\s+/g, ' ').trim()
: '',
};
})
.filter(Boolean);
}
/**
* Determine whether a block currently has tracked unsaved changes.
*
* @param {string} uuid Block UUID.
* @returns {boolean} Whether the block is dirty.
*/
function isDirtyUuid(uuid) {
const batchManager = SFE.BatchEditManager || null;
return !!(
uuid &&
batchManager &&
batchManager.dirtyBlocks instanceof Map &&
batchManager.dirtyBlocks.has(uuid)
);
}
/**
* Build one public editor snapshot from a live editor state object.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object|null} Stable editor snapshot.
*/
function buildEditorSnapshot(editorState) {
if (!editorState || !editorState.uuid) {
return null;
}
const draftEditState = getContext().draftEditState;
const isDraftSession = !!(
draftEditState &&
draftEditState.draftElement &&
draftEditState.draftElement === editorState.element
);
const activeComponentId = String(
editorState.activeComponentId
|| editorState.activeEditableComponent?.id
|| ''
).trim();
const activeComponentType = String(
editorState._mwpActiveComponentType
|| editorState.activeEditableComponent?.type
|| (editorState.isMediaEditor ? 'file' : 'text')
).trim().toLowerCase();
const activeEditorHost = resolveActiveEditorHost(editorState);
return {
uuid: String(editorState.uuid || '').trim(),
handlerId: String(editorState._mwpSchemaHandlerId || editorState.handler?.id || '').trim(),
mode: String(editorState.requestedMode || (isDraftSession ? 'draft' : 'edit')).trim() || 'edit',
blockName: String(editorState.blockName || editorState.blockState?.blockName || '').trim(),
isOpen: true,
componentType: activeComponentType || 'text',
componentId: activeComponentId,
saveStrategy: String(editorState.saveStrategy || 'single').trim() || 'single',
hasMediaSession: typeof activeEditorHost?.applyMediaSelection === 'function',
isDirty: isDirtyUuid(editorState.uuid),
isDraftSession,
isBatchSession: !!(
editorState.saveStrategy === 'batch' &&
SFE.BatchEditManager &&
typeof SFE.BatchEditManager.isSessionActive === 'function' &&
SFE.BatchEditManager.isSessionActive()
),
};
}
/**
* Switch the active editable component inside one open editor session.
*
* @param {Object} editorState Active editor state.
* @param {string} componentId Target component identifier.
* @returns {boolean} Whether the switch succeeded.
*/
function activateEditorComponent(editorState, componentId) {
if (!editorState || !componentId) {
return false;
}
if (SFE.TextEditor && typeof SFE.TextEditor.activateEditableComponentById === 'function') {
return !!SFE.TextEditor.activateEditableComponentById(editorState, componentId);
}
return false;
}
/**
* Open one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Open-editor options.
* @returns {Promise} Editor snapshot after opening.
*/
async function openEditor(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const componentId = String(options.componentId || '').trim();
if (!uuid || !(element instanceof Element) || !handler || typeof SFE.startEditing !== 'function') {
return null;
}
const activeEditor = getContext().activeEditor || null;
if (activeEditor && String(activeEditor.uuid || '').trim() === uuid) {
if (componentId) {
activateEditorComponent(activeEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || activeEditor);
}
if (activeEditor && typeof SFE.closeInPlaceEditor === 'function') {
SFE.closeInPlaceEditor(activeEditor, true, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
});
}
await Promise.resolve(
SFE.startEditing(
element,
handler,
uuid,
null,
false,
String(options.mode || 'edit').trim() || 'edit',
{
initialComponentId: componentId,
source: String(options.source || 'external').trim() || 'external',
originalSnapshots: options.originalSnapshots && typeof options.originalSnapshots === 'object'
? {
restoreOuterHTML: String(options.originalSnapshots.restoreOuterHTML || '').trim(),
cleanOuterHTML: String(options.originalSnapshots.cleanOuterHTML || '').trim(),
}
: null,
}
)
);
const openedEditor = getContext().activeEditor || null;
if (openedEditor && componentId) {
activateEditorComponent(openedEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || openedEditor);
}
/**
* Close one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Close-editor options.
* @returns {boolean} Whether a close was attempted.
*/
function closeEditor(options = {}) {
const activeEditor = getContext().activeEditor || null;
if (!activeEditor || typeof SFE.closeInPlaceEditor !== 'function') {
return false;
}
const targetUuid = String(options.uuid || '').trim();
if (targetUuid && String(activeEditor.uuid || '').trim() !== targetUuid) {
return false;
}
return SFE.closeInPlaceEditor(activeEditor, options.restoreOriginal !== false, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
}) !== false;
}
/**
* Resolve the active editor state for one target block UUID.
*
* Schema-backed editor operations intentionally require a live open editor so
* the DOM mutation stays inside the same save/history session FrontEdit owns.
*
* @param {Object} options Editor-operation options.
* @returns {Object|null} Active editor state.
*/
function resolveActiveEditorState(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
return resolveActiveEditorHost(activeEditor) ? activeEditor : null;
}
function resolveActiveEditorHost(editorState) {
return SFE.SchemaEditorHost?.resolveActiveEditorHost?.(editorState) || null;
}
/**
* Resolve the session-owned history API for one active editor host.
*
* External runtime mutations should commit through the same shared block
* session contract used internally instead of reaching into ad hoc
* editor-state history fields.
*
* @param {Object|null} editorHost Active schema editor host.
* @returns {Object|null} Session history API, or null when unavailable.
*/
function getSessionHistoryApiForHost(editorHost) {
if (editorHost?.historyApi && typeof editorHost.historyApi === 'object') {
return editorHost.historyApi;
}
if (typeof editorHost?.getSessionHistoryApi === 'function') {
return editorHost.getSessionHistoryApi();
}
const blockEditSession = editorHost?.blockEditSession || null;
if (!blockEditSession || typeof blockEditSession.getHistoryApi !== 'function') {
return null;
}
return blockEditSession.getHistoryApi(
String(editorHost?.blockEditSessionScopeId || '').trim() || 'text'
);
}
/**
* Refresh one active editor state after a schema-driven DOM mutation.
*
* @param {Object} editorState Active editor state.
* @returns {void}
*/
function syncActiveEditorState(editorState) {
if (!editorState?.handler) {
return;
}
const editorHost = resolveActiveEditorHost(editorState);
const blockRootElement = (
editorHost &&
typeof editorHost.getBlockRootElement === 'function'
)
? editorHost.getBlockRootElement()
: null;
if (blockRootElement && blockRootElement !== editorState.element) {
// The active schema host element can be one editable component (for
// example, one table cell), while block-session history and restore must
// keep targeting the outer block root. Rebasing `editorState.element` to
// the component host corrupts undo/redo by replaying full block markup
// into that component instead of the block wrapper.
editorState.element = blockRootElement;
}
if (!editorState.element) {
return;
}
const schemaRuntime = SFE.SchemaRuntime || null;
if (
schemaRuntime &&
typeof schemaRuntime.syncPlaceholders === 'function'
) {
schemaRuntime.syncPlaceholders(editorState.element, editorState.handler, {
blockAttributes: editorState.blockState?.attrs || null,
attributeChanges: editorState.attributeChanges || null,
});
}
if (
SFE.TextEditor &&
typeof SFE.TextEditor.refreshEditableComponents === 'function'
) {
SFE.TextEditor.refreshEditableComponents(editorState);
}
}
/**
* Build one component map for the active schema editor state.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object} Editable components keyed by ID.
*/
function getActiveEditorComponentMap(editorState) {
if (!editorState || !Array.isArray(editorState.editableComponents)) {
return {};
}
return editorState.editableComponents.reduce((map, component) => {
const componentId = String(component?.id || '').trim();
if (!componentId || !component?.element) {
return map;
}
map[componentId] = component;
return map;
}, {});
}
/**
* Return the lowercase host tag name for one candidate component element.
*
* @param {Element|null} hostElement Candidate host element.
* @returns {string} Lowercase host tag name.
*/
function getHostElementTagName(hostElement) {
return hostElement && hostElement.tagName
? String(hostElement.tagName || '').trim().toLowerCase()
: '';
}
/**
* Build one inline format wrapper for a structured run token.
*
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {string} formatToken Format token.
* @param {Object|null} formatAttributes Optional format attributes.
* @returns {HTMLElement|null} Wrapper element.
*/
function buildStructuredFormatWrapper(inlineCapabilities, formatToken, formatAttributes = null) {
const tagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!tagName) {
return null;
}
const element = document.createElement(tagName);
if (tagName === 'a' && formatAttributes && typeof formatAttributes === 'object') {
const href = String(formatAttributes.href || '').trim();
if (href) {
element.setAttribute('href', href);
}
const target = String(formatAttributes.target || '').trim();
if (target) {
element.setAttribute('target', target);
}
const rel = String(formatAttributes.rel || '').trim();
if (rel) {
element.setAttribute('rel', rel);
}
}
return element;
}
/**
* Hoist same-tag inline format attributes onto the host component element.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Element|null} hostElement Live host element.
* @returns {{runs: Array, hostAttributes: Object}} Hoisted run data.
*/
function hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement = null) {
const hostTagName = getHostElementTagName(hostElement);
if (!hostTagName) {
return {
runs: Array.isArray(runs) ? runs : [],
hostAttributes: {},
};
}
const hostAttributes = {};
const normalizedRuns = (Array.isArray(runs) ? runs : []).map(run => {
const formats = Array.isArray(run?.formats) ? run.formats : [];
const formatAttributes = run?.formatAttributes && typeof run.formatAttributes === 'object'
? { ...run.formatAttributes }
: {};
const nextFormats = [];
formats.forEach(formatToken => {
const formatTagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!formatTagName || formatTagName !== hostTagName) {
nextFormats.push(formatToken);
return;
}
const attributes = formatAttributes?.[formatToken];
if (attributes && typeof attributes === 'object' && !Array.isArray(attributes)) {
Object.assign(hostAttributes, attributes);
}
delete formatAttributes[formatToken];
});
return {
...run,
formats: nextFormats,
formatAttributes,
};
});
return {
runs: normalizedRuns,
hostAttributes,
};
}
/**
* Apply hoisted inline-format attributes onto one host element.
*
* @param {Element|null} hostElement Live host element.
* @param {Object|null} hostAttributes Hoisted host attributes.
* @returns {void}
*/
function applyStructuredHostLevelFormatAttributes(hostElement, hostAttributes = null) {
if (!hostElement || typeof hostElement.setAttribute !== 'function' || typeof hostElement.removeAttribute !== 'function') {
return;
}
const attributes = hostAttributes && typeof hostAttributes === 'object' ? hostAttributes : {};
['href', 'target', 'rel'].forEach(attributeName => {
const value = typeof attributes[attributeName] === 'string'
? attributes[attributeName].trim()
: '';
if (value) {
hostElement.setAttribute(attributeName, value);
return;
}
hostElement.removeAttribute(attributeName);
});
}
/**
* Build deterministic HTML from one normalized structured run sequence.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Object} editorOptions Runtime editor options.
* @param {Element|null} hostElement Live host element.
* @returns {{html: string, hostAttributes: Object}} Rendered HTML fragment.
*/
function buildStructuredHtmlFromRuns(runs, inlineCapabilities, editorOptions = {}, hostElement = null) {
const container = document.createElement('div');
const extractionOptions = editorOptions?.options && typeof editorOptions.options === 'object'
? editorOptions.options
: {};
const hoisted = hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement);
(Array.isArray(hoisted.runs) ? hoisted.runs : []).forEach(run => {
const text = String(run?.text || '');
if (!text) {
return;
}
const segments = extractionOptions.newlinesToBR === true || String(editorOptions?.enterMode || '').trim() === 'linebreak'
? text.split('\n')
: [text];
segments.forEach((segment, index) => {
if (index > 0) {
container.appendChild(document.createElement('br'));
}
if (!segment) {
return;
}
let node = document.createTextNode(segment);
(Array.isArray(run.formats) ? run.formats : []).forEach(formatToken => {
const wrapper = buildStructuredFormatWrapper(
inlineCapabilities,
formatToken,
run.formatAttributes && typeof run.formatAttributes === 'object'
? run.formatAttributes[formatToken] || null
: null
);
if (!wrapper) {
return;
}
wrapper.appendChild(node);
node = wrapper;
});
container.appendChild(node);
});
});
return {
html: container.innerHTML,
hostAttributes: hoisted.hostAttributes,
};
}
/**
* Apply structured component updates onto the active editor DOM.
*
* @param {Object} editorState Active editor state.
* @param {Array} componentUpdates Normalized component updates.
* @returns {string[]} Updated component IDs.
*/
function applyStructuredComponentUpdates(editorState, componentUpdates) {
const componentMap = getActiveEditorComponentMap(editorState);
const updatedComponentIds = [];
(Array.isArray(componentUpdates) ? componentUpdates : []).forEach(componentUpdate => {
const componentId = String(componentUpdate?.componentId || '').trim();
const component = componentMap[componentId] || null;
if (!component || !component.element) {
return;
}
if (componentUpdate.bindingSource === 'plaintext') {
component.element.textContent = (Array.isArray(componentUpdate.runs) ? componentUpdate.runs : [])
.map(run => String(run?.text || ''))
.join('');
updatedComponentIds.push(componentId);
return;
}
if (componentUpdate.bindingSource === 'html') {
const rendered = buildStructuredHtmlFromRuns(
componentUpdate.runs,
componentUpdate.inlineCapabilities || {},
componentUpdate.editorOptions || component.editorOptions || {},
component.element
);
component.element.innerHTML = rendered.html;
applyStructuredHostLevelFormatAttributes(component.element, rendered.hostAttributes);
updatedComponentIds.push(componentId);
}
});
return Array.from(new Set(updatedComponentIds));
}
/**
* Resolve the active list-editor state for one target block UUID.
*
* Structural list operations intentionally require a live open editor so the
* mutation happens inside the same DOM/save session FrontEdit already owns.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Active list editor state.
*/
function resolveActiveListEditorState(options = {}) {
const activeEditor = resolveActiveEditorState(options);
if (!activeEditor) {
return null;
}
const listTracker = SFE.ListBlockTracker || null;
if (
!listTracker ||
!activeEditor.element ||
(activeEditor.element.tagName !== 'UL' && activeEditor.element.tagName !== 'OL')
) {
return null;
}
if (!activeEditor.listTracker || activeEditor.listTracker.listElement !== activeEditor.element) {
activeEditor.listTracker = listTracker.init(
activeEditor.element,
activeEditor.blockState || {}
);
}
return activeEditor.listTracker ? activeEditor : null;
}
/**
* Refresh one active list editor after a structural DOM mutation.
*
* @param {Object} editorState Active list editor state.
* @returns {void}
*/
function syncActiveListEditorState(editorState) {
if (!editorState?.element || !editorState?.handler) {
return;
}
if (editorState.listTracker?.listElement && editorState.listTracker.listElement !== editorState.element) {
editorState.element = editorState.listTracker.listElement;
const editorHost = resolveActiveEditorHost(editorState);
if (editorHost && typeof editorHost === 'object') {
editorHost.element = editorState.listTracker.listElement;
}
}
syncActiveEditorState(editorState);
}
/**
* Apply one or more schema-declared block-attribute operations to the active editor.
*
* @param {Object} options Block-attribute operation options.
* @returns {Object|null} Result summary and current tracked attribute changes.
*/
function applyBlockAttributeOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeBlockAttributeOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
attributeChanges: bridge.clonePlainData(executionResult.attributeChanges || {}),
};
}
/**
* Apply one or more component-content replacement operations to the active editor.
*
* The shared executor owns rich-text run rendering and link normalization so
* external callers can target component surfaces without reimplementing FrontEdit's
* browser mutation semantics.
*
* @param {Object} options Component-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyTextComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeComponentOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more schema-backed media replacements to the active editor.
*
* This targets the currently active media-session host so external callers
* can replace schema media components without bypassing FrontEdit's own preview and
* resolved-media update pipeline.
*
* @param {Object} options Media-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyMediaComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeMediaOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeMediaOperations({
editorState,
editorHost,
operations,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more high-level public list operations to the currently open
* list editor.
*
* The runtime API accepts UUID-oriented public commands and lets the shared
* executor translate them into the internal primitive tracker operations.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Result summary and next list structure.
*/
function applyListOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveListEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations
: (options.operation ? [ options.operation ] : []);
const operationExecutor = SFE.SchemaOperationExecutor || null;
const listTracker = SFE.ListBlockTracker || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeListOperations !== 'function' ||
!listTracker
) {
return null;
}
const executionResult = operationExecutor.executeListOperations({
tracker: editorState.listTracker,
editorHost,
operations,
targetResolutionMode: 'api_uuid',
saveHistory: true,
afterSync: function() {
syncActiveListEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
structure: listTracker.getStructure(editorState.listTracker),
};
}
/**
* Apply one normalized structured edit payload to the active schema editor.
*
* This keeps non-list staged edits inside the same public runtime contract as
* list operations: open the editor first, then let FrontEdit own the mutation and
* history bookkeeping.
*
* @param {Object} options Structured-edit options.
* @returns {Object|null} Result summary for the applied edit.
*/
function applyStructuredEditToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const componentUpdates = Array.isArray(options.componentUpdates)
? options.componentUpdates.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const attributeOperations = Array.isArray(options.attributeOperations)
? options.attributeOperations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (!editorState || !editorHost || (!componentUpdates.length && !attributeOperations.length)) {
return null;
}
let attributeResult = null;
let componentResult = null;
if (
componentUpdates.length
&& operationExecutor
&& typeof operationExecutor.executeComponentOperations === 'function'
) {
componentResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations: componentUpdates,
saveHistory: false,
});
}
if (
attributeOperations.length &&
operationExecutor &&
typeof operationExecutor.executeBlockAttributeOperations === 'function'
) {
attributeResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations: attributeOperations,
saveHistory: false,
});
}
const didApplyAttributes = !!(
attributeResult &&
Array.isArray(attributeResult.operationsApplied) &&
attributeResult.operationsApplied.length
);
const didApplyComponents = !!(
componentResult &&
Array.isArray(componentResult.updatedComponentIds) &&
componentResult.updatedComponentIds.length
);
if (!didApplyComponents && !didApplyAttributes) {
return null;
}
syncActiveEditorState(editorState);
const historyApi = getSessionHistoryApiForHost(editorHost);
if (typeof historyApi?.saveToHistory === 'function') {
historyApi.saveToHistory();
}
if (typeof editorHost.updateToolbarState === 'function') {
editorHost.updateToolbarState();
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(componentResult?.updatedComponentIds)
? componentResult.updatedComponentIds
: [],
operationsApplied: [
...(Array.isArray(componentResult?.operationsApplied) ? componentResult.operationsApplied : []),
...(Array.isArray(attributeResult?.operationsApplied) ? attributeResult.operationsApplied : []),
],
attributeChanges: bridge.clonePlainData(editorHost.attributeChanges || {}),
};
}
/**
* Return a structural snapshot for one list block.
*
* This resolves against the live DOM so external tooling can inspect the
* current nested list tree without parsing serialized block markup itself.
*
* @param {Object} options Lookup options.
* @returns {Object|null} List structure snapshot.
*/
function getListStructure(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const listTracker = SFE.ListBlockTracker || null;
if (
!uuid ||
!(element instanceof Element) ||
!listTracker ||
(element.tagName !== 'UL' && element.tagName !== 'OL')
) {
return null;
}
const tracker = element._mwpListTracker
|| listTracker.init(element, resolveBlockState(uuid) || {});
return tracker && typeof listTracker.getStructure === 'function'
? listTracker.getStructure(tracker)
: null;
}
/**
* Apply one selected media item to the active schema-media editor session.
*
* This uses the same internal selection transition FrontEdit normally runs after
* the user picks an item from the media library or upload flow.
*
* @param {Object} options Media-selection options.
* @returns {Object|null} Stable editor snapshot after the selection is applied.
*/
function applyActiveMediaSelection(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim();
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
const editorHost = resolveActiveEditorHost(activeEditor);
const url = String(options.url || '').trim();
if (!editorHost || typeof editorHost.applyMediaSelection !== 'function' || !url) {
return null;
}
const attachmentId = Object.prototype.hasOwnProperty.call(options, 'attachmentId')
? options.attachmentId
: null;
const fromState = String(options.source || '').trim().toLowerCase() === 'library'
? 'library'
: 'input';
const didApply = editorHost.applyMediaSelection(url, attachmentId, { fromState });
return didApply === false ? null : buildEditorSnapshot(activeEditor);
}
/**
* Refresh one block through the supported server-rendered replacement path.
*
* @param {string} uuid Block UUID.
* @param {Object} [options={}] Refresh options.
* @returns {Promise} Refreshed block snapshot.
*/
async function refreshBlock(uuid, options = {}) {
const normalizedUuid = String(uuid || '').trim();
const element = getElementByUuid(normalizedUuid);
if (
!normalizedUuid ||
!(element instanceof Element) ||
!SFE.SaveHelpers ||
typeof SFE.SaveHelpers.fetchRenderedBlockHTML !== 'function' ||
!SFE.ElementUpdater ||
typeof SFE.ElementUpdater.applyNewHTML !== 'function'
) {
return null;
}
const html = await SFE.SaveHelpers.fetchRenderedBlockHTML(normalizedUuid, options || {});
if (!html) {
return null;
}
const newElement = SFE.ElementUpdater.applyNewHTML(element, normalizedUuid, html);
if (!newElement) {
return null;
}
if (typeof SFE.ElementUpdater.rebindElement === 'function') {
SFE.ElementUpdater.rebindElement(newElement);
}
const block = buildBlockSnapshot(normalizedUuid);
if (block) {
bridge.emitEvent('block:refreshed', {
source: String(options.source || 'sfe').trim() || 'sfe',
block,
});
}
return block;
}
/**
* Return one public dirty-block snapshot.
*
* @param {Object} entry Dirty entry from the batch manager.
* @returns {Object|null} Dirty block snapshot.
*/
function buildDirtyBlockSnapshot(entry) {
if (!entry || typeof entry !== 'object') {
return null;
}
return {
uuid: String(entry.element_uuid || '').trim(),
handlerId: String(entry.handler_id || '').trim(),
blockName: String(entry.blockName || '').trim(),
beforeRaw: String(entry.before || '').trim(),
afterRaw: String(entry.after || '').trim(),
};
}
/**
* Reset dirty batch state for one or more target UUIDs.
*
* @param {string[]} uuids Target UUIDs.
* @returns {boolean} Whether any batch state was reset.
*/
function resetDirtyBlocks(uuids) {
const batchManager = SFE.BatchEditManager || null;
const normalizedUuids = Array.isArray(uuids)
? uuids.map(uuid => String(uuid || '').trim()).filter(Boolean)
: [];
if (!normalizedUuids.length || !batchManager) {
return false;
}
if (typeof batchManager.resetDirtyBlocks === 'function') {
return !!batchManager.resetDirtyBlocks(normalizedUuids);
}
let changed = false;
normalizedUuids.forEach(uuid => {
if (batchManager.dirtyBlocks instanceof Map && batchManager.dirtyBlocks.has(uuid)) {
batchManager.dirtyBlocks.delete(uuid);
changed = true;
}
if (
batchManager.currentBlockStates instanceof Map &&
batchManager.baseBlockStates instanceof Map &&
batchManager.baseBlockStates.has(uuid)
) {
batchManager.currentBlockStates.set(
uuid,
bridge.clonePlainData(batchManager.baseBlockStates.get(uuid))
);
changed = true;
}
});
return changed;
}
/**
* Return the stable PublicApi discovery snapshot.
*
* @returns {Object} API metadata.
*/
function getApiInfo() {
return {
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,
},
};
}
PublicApi._buildEditorSnapshot = buildEditorSnapshot;
PublicApi.getApiInfo = getApiInfo;
PublicApi.isEditorOpen = function isEditorOpen() {
return !!getContext().activeEditor;
};
PublicApi.getActiveEditor = function getActiveEditor() {
return buildEditorSnapshot(getContext().activeEditor || null);
};
PublicApi.applyActiveMediaSelection = function applyActiveMediaSelectionPublic(options = {}) {
return applyActiveMediaSelection(options);
};
PublicApi.getPageContext = function getPageContext() {
const context = getContext();
const managerData = getManagerData();
return {
postId: Number(managerData.postId || 0) || 0,
permissions: bridge.clonePlainData(managerData.permissions || {}),
hasDraftPreview: !!(
document.body.classList.contains('mwp-sfe-active-preview') ||
context.activeMode === 'draft'
),
isEditorOpen: !!context.activeEditor,
activeMode: String(context.activeMode || '').trim(),
};
};
PublicApi.getRestContext = function getRestContext() {
const managerData = getManagerData();
return {
baseUrl: String(managerData.restBase || '').trim(),
namespaceUrl: String(managerData.restUrl || '').trim(),
nonce: String(managerData.nonce || '').trim(),
};
};
PublicApi.setRestNonce = function setRestNonce(nonce) {
const normalizedNonce = normalizeRestNonce(nonce);
if (!normalizedNonce) {
return PublicApi.getRestContext();
}
const managerData = getManagerData();
managerData.nonce = normalizedNonce;
SFE.ManagerData = managerData;
return PublicApi.getRestContext();
};
PublicApi.getElementByUuid = getElementByUuid;
PublicApi.getUuidForElement = getUuidForElement;
PublicApi.getBlockSnapshot = function getBlockSnapshot(uuid) {
return buildBlockSnapshot(uuid);
};
PublicApi.getEditableBlocks = getEditableBlocks;
PublicApi.resolveRuntime = function resolveRuntime(options = {}) {
return buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.resolveEditingRuntime = function resolveEditingRuntime(options = {}) {
return buildResolvedEditingRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.getEditableComponents = function getEditableComponents(options = {}) {
const runtime = buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
return runtime ? runtime.components : [];
};
PublicApi.getListStructure = getListStructure;
PublicApi.getDefaultComponent = function getDefaultComponent(options = {}) {
const components = PublicApi.getEditableComponents(options);
return components.find(component => component.default) || components[0] || null;
};
PublicApi.getMediaDescriptor = function getMediaDescriptor(options = {}) {
return resolveMediaDescriptor(options);
};
PublicApi.getMediaContext = function getMediaContext(options = {}) {
const descriptor = resolveMediaDescriptor(options);
const mediaHelper = SFE.MediaHelper || null;
if (!descriptor || !mediaHelper || typeof mediaHelper.getMediaType !== 'function') {
return null;
}
const mediaType = String(mediaHelper.getMediaType(descriptor) || '').trim();
if (!mediaType) {
return null;
}
return {
supported: true,
componentId: String(descriptor.componentId || '').trim(),
mediaType,
accept: typeof mediaHelper.getAcceptTypes === 'function'
? String(mediaHelper.getAcceptTypes(descriptor) || '*/*').trim() || '*/*'
: '*/*',
label: mediaType === 'audio' ? 'audio block' : (mediaType === 'video' ? 'video block' : (mediaType === 'file' ? 'file block' : 'media block')),
descriptor,
};
};
PublicApi.isMediaEditable = function isMediaEditable(options = {}) {
return !!PublicApi.getMediaContext(options);
};
PublicApi.openEditor = openEditor;
PublicApi.closeEditor = closeEditor;
PublicApi.applyTextComponentOperations = applyTextComponentOperationsToActiveEditor;
PublicApi.applyMediaComponentOperations = applyMediaComponentOperationsToActiveEditor;
PublicApi.applyBlockAttributeOperations = applyBlockAttributeOperationsToActiveEditor;
PublicApi.applyListOperations = applyListOperationsToActiveEditor;
PublicApi.applyStructuredEdit = applyStructuredEditToActiveEditor;
PublicApi.stageBlockState = function stageBlockState(stage) {
const uuid = String(stage && stage.uuid ? stage.uuid : '').trim();
if (!uuid || !stage || typeof stage !== 'object' || !stage.blockState || typeof stage.blockState !== 'object') {
return;
}
bridge.stagedBlockStates.set(uuid, {
handlerId: String(stage.handlerId || '').trim(),
source: String(stage.source || 'external').trim() || 'external',
blockState: bridge.clonePlainData(stage.blockState),
});
bridge.emitEvent('block:staged', {
source: String(stage.source || 'external').trim() || 'external',
uuid,
handlerId: String(stage.handlerId || '').trim(),
});
};
PublicApi.clearStagedBlockState = function clearStagedBlockState(uuid, source = 'external') {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid) {
return;
}
bridge.stagedBlockStates.delete(normalizedUuid);
bridge.emitEvent('block:stageCleared', {
source: String(source || 'external').trim() || 'external',
uuid: normalizedUuid,
});
};
PublicApi.refreshBlock = refreshBlock;
PublicApi.getDirtyBlocks = function getDirtyBlocks() {
const batchManager = SFE.BatchEditManager || null;
if (!batchManager || !(batchManager.dirtyBlocks instanceof Map)) {
return [];
}
return Array.from(batchManager.dirtyBlocks.values())
.map(buildDirtyBlockSnapshot)
.filter(entry => entry && entry.uuid);
};
PublicApi.hasDirtyBlocks = function hasDirtyBlocks() {
return PublicApi.getDirtyBlocks().length > 0;
};
PublicApi.isBatchSessionActive = function isBatchSessionActive() {
const batchManager = SFE.BatchEditManager || null;
return !!(
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive()
);
};
PublicApi.resetDirtyBlocks = function resetDirtyBlocksPublic(uuids) {
return resetDirtyBlocks(uuids);
};
SFE.PublicApi = PublicApi;
})();
/**
* Stable frontend runtime PublicApi implementation.
*
* Exposes: SFE.PublicApi
* {
* getApiInfo, on, off, isEditorOpen, getActiveEditor,
* applyActiveMediaSelection, getPageContext, getRestContext, setRestNonce,
* getElementByUuid, getUuidForElement, getBlockSnapshot, getEditableBlocks,
* resolveRuntime,
* resolveEditingRuntime, getEditableComponents, getListStructure,
* getDefaultComponent, getMediaDescriptor, getMediaContext,
* isMediaEditable, openEditor, closeEditor, applyTextComponentOperations,
* applyMediaComponentOperations,
* applyBlockAttributeOperations,
* applyListOperations, applyStructuredEdit, stageBlockState,
* clearStagedBlockState, refreshBlock, getDirtyBlocks, hasDirtyBlocks,
* isBatchSessionActive, resetDirtyBlocks
* }
*
* Keeps mutable runtime internals private while exposing the documented browser
* integration surface for external consumers such as ABE.
*/
(function() {
'use strict';
window.MWP = window.MWP || {};
window.MWP.SFE = window.MWP.SFE || {};
const SFE = window.MWP.SFE;
SFE.ManagerData = SFE.ManagerData || {};
const PublicApi = SFE.PublicApi || {};
const bridge = SFE.PublicApiBridge || null;
if (!bridge) {
return;
}
/**
* Resolve the shared runtime context object.
*
* @returns {Object} Shared FrontEdit context.
*/
function getContext() {
return SFE.Context || {};
}
/**
* Resolve the localized bootstrap payload.
*
* @returns {Object} Localized manager data.
*/
function getManagerData() {
return SFE.ManagerData || {};
}
/**
* Normalize one candidate REST nonce.
*
* @param {*} nonce Candidate nonce value.
* @returns {string} Trimmed nonce or an empty string.
*/
function normalizeRestNonce(nonce) {
return String(nonce || '').trim();
}
/**
* Return the live block element for one UUID.
*
* @param {string} uuid Block UUID.
* @returns {Element|null} Matching element.
*/
function getElementByUuid(uuid) {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || typeof document.querySelector !== 'function') {
return null;
}
try {
return document.querySelector(`[data-mwp-sfe-uuid="${CSS.escape(normalizedUuid)}"]`);
} catch (error) {
return document.querySelector(`[data-mwp-sfe-uuid="${normalizedUuid.replace(/"/g, '\\"')}"]`);
}
}
/**
* Resolve one UUID from a supplied element or nested child.
*
* @param {Element|null} element Candidate DOM node.
* @returns {string} Block UUID or an empty string.
*/
function getUuidForElement(element) {
if (!(element instanceof Element)) {
return '';
}
const candidate = element.closest('[data-mwp-sfe-uuid]');
return candidate instanceof Element
? String(candidate.getAttribute('data-mwp-sfe-uuid') || '').trim()
: '';
}
/**
* Resolve one uuid-map entry from the shared context.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} UUID map entry.
*/
function getUuidEntry(uuid) {
const context = getContext();
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || !context.uuidMap || !context.uuidMap[normalizedUuid]) {
return null;
}
return context.uuidMap[normalizedUuid];
}
/**
* Resolve one edit-capable handler for the supplied target.
*
* @param {Object} options Resolution options.
* @returns {Object|null} Edit handler when supported.
*/
function resolveEditHandler(options = {}) {
const element = options.element instanceof Element ? options.element : null;
const preferredHandlerId = String(options.handlerId || '').trim();
const handlers = Array.isArray(element?._mwpSfeHandlers)
? element._mwpSfeHandlers
: [];
if (preferredHandlerId) {
const exact = handlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
));
if (exact) {
return exact;
}
}
const fromElement = handlers.find(handler => handler && handler.capability === 'edit') || null;
if (fromElement) {
return fromElement;
}
const uuidEntry = getUuidEntry(options.uuid);
const managerData = getManagerData();
const registeredHandlers = Array.isArray(managerData.handlers) ? managerData.handlers : [];
const handlerIds = Array.isArray(uuidEntry?.handlers) ? uuidEntry.handlers : [];
if (preferredHandlerId) {
return registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
)) || null;
}
for (let index = 0; index < handlerIds.length; index++) {
const handlerId = String(handlerIds[index] || '').trim();
const match = registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === handlerId
));
if (match) {
return match;
}
}
return null;
}
/**
* Resolve the current block-state payload used for schema hydration.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} Block-state payload when available.
*/
function resolveBlockState(uuid) {
const batchManager = SFE.BatchEditManager || null;
if (
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive() &&
typeof batchManager.getBlockStateForUuid === 'function'
) {
return batchManager.getBlockStateForUuid(uuid);
}
return null;
}
/**
* Resolve the schema runtime bundle for one target block.
*
* @param {Object} options Runtime resolution options.
* @returns {Object|null} Schema runtime bundle.
*/
function resolveRuntimeBundle(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const schemaRuntime = SFE.SchemaRuntime || null;
if (!uuid || !(element instanceof Element) || !handler || !schemaRuntime || typeof schemaRuntime.resolveForEditing !== 'function') {
return null;
}
try {
const blockState = options.blockState && typeof options.blockState === 'object'
? options.blockState
: resolveBlockState(uuid);
const attributeChanges = options.attributeChanges && typeof options.attributeChanges === 'object'
? options.attributeChanges
: undefined;
const resolved = schemaRuntime.resolveForEditing(element, handler, {
blockState,
attributeChanges,
});
if (!resolved || !resolved.runtimeHandler || !resolved.runtime) {
return null;
}
return {
uuid,
element,
handler: resolved.runtimeHandler,
runtime: resolved.runtime,
schema: resolved.schema || null,
};
} catch (error) {
console.warn('FrontEdit: PublicApi resolveRuntime failed', error);
return null;
}
}
/**
* Build one detailed editable-component snapshot from a schema runtime entry.
*
* @param {Object|null} editableComponent Runtime editable component.
* @param {Object|null} componentMeta Additional component metadata.
* @returns {Object|null} Detailed component snapshot.
*/
function buildEditingRuntimeComponentDetail(editableComponent, componentMeta = null) {
const snapshot = buildEditableComponentSnapshot(editableComponent);
if (!snapshot) {
return null;
}
const meta = componentMeta && typeof componentMeta === 'object' ? componentMeta : {};
snapshot.element = editableComponent?.element instanceof Element
? editableComponent.element
: (meta.element instanceof Element ? meta.element : null);
const bindings = Array.isArray(meta.bindings)
? bridge.clonePlainData(meta.bindings)
: [];
if (bindings.length) {
snapshot.bindings = bindings;
}
const target = meta.target && typeof meta.target === 'object'
? bridge.clonePlainData(meta.target)
: (snapshot.target || null);
if (target) {
snapshot.target = target;
}
const editorOptions = editableComponent?.editorOptions && typeof editableComponent.editorOptions === 'object'
? bridge.clonePlainData(editableComponent.editorOptions)
: (meta.editorOptions && typeof meta.editorOptions === 'object'
? bridge.clonePlainData(meta.editorOptions)
: null);
if (editorOptions) {
snapshot.editorOptions = editorOptions;
snapshot.editor = bridge.clonePlainData(editorOptions);
}
return snapshot;
}
/**
* Build one detailed schema-resolved editing runtime snapshot.
*
* This exposes the same resolved component metadata FrontEdit uses for editing
* while keeping handler resolution and schema runtime execution owned by FrontEdit.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Detailed editing runtime snapshot.
*/
function buildResolvedEditingRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const runtimeComponentsById = bundle.runtime.componentsById && typeof bundle.runtime.componentsById === 'object'
? bundle.runtime.componentsById
: {};
const componentDetails = [];
const componentsById = {};
(Array.isArray(bundle.runtime.editableComponents) ? bundle.runtime.editableComponents : []).forEach(component => {
const componentId = String(component?.id || '').trim();
const detail = buildEditingRuntimeComponentDetail(component, componentId ? runtimeComponentsById[componentId] : null);
if (!detail || !componentId) {
return;
}
componentDetails.push(detail);
componentsById[componentId] = detail;
});
Object.keys(runtimeComponentsById).forEach(componentId => {
if (componentsById[componentId]) {
return;
}
const detail = buildEditingRuntimeComponentDetail(runtimeComponentsById[componentId], runtimeComponentsById[componentId]);
if (!detail) {
return;
}
detail.id = componentId;
componentDetails.push(detail);
componentsById[componentId] = detail;
});
const mediaComponentId = String(bundle.runtime?.mediaComponent?.id || '').trim();
const mediaComponent = mediaComponentId && componentsById[mediaComponentId]
? componentsById[mediaComponentId]
: null;
return {
uuid: bundle.uuid,
element: bundle.element instanceof Element ? bundle.element : null,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
runtimeHandler: {
id: String(bundle.handler.id || '').trim(),
},
runtime: {
blockName: String(bundle.runtime.blockName || '').trim(),
editableComponents: componentDetails,
componentsById,
mediaComponent,
},
schema: {
handlerId: String(bundle.schema?.handlerId || bundle.handler.id || '').trim(),
},
};
}
/**
* Return a plain snapshot of one editable component.
*
* @param {Object|null} component Runtime component.
* @returns {Object|null} Public component snapshot.
*/
function buildEditableComponentSnapshot(component) {
if (!component || typeof component !== 'object') {
return null;
}
const snapshot = {
id: String(component.id || '').trim(),
label: String(component.label || component.id || '').trim(),
type: String(component.type || '').trim().toLowerCase(),
selector: String(component.selector || '').trim(),
default: !!component.default,
};
if (!snapshot.id || !snapshot.type) {
return null;
}
if (component.required === true) {
snapshot.required = true;
}
if (typeof component.placeholder === 'string' && component.placeholder.trim()) {
snapshot.placeholder = component.placeholder;
}
if (component.target && typeof component.target === 'object') {
snapshot.target = bridge.clonePlainData(component.target);
}
if (component.mediaDescriptor && typeof component.mediaDescriptor === 'object') {
snapshot.mediaDescriptor = bridge.clonePlainData(component.mediaDescriptor);
}
if (component.editorOptions && typeof component.editorOptions === 'object') {
snapshot.editor = bridge.clonePlainData(component.editorOptions);
}
return snapshot;
}
/**
* Resolve the public runtime mode label.
*
* @param {Object|null} runtime Schema runtime.
* @returns {string} Runtime mode label.
*/
function resolvePublicRuntimeMode(runtime) {
const components = Array.isArray(runtime?.editableComponents) ? runtime.editableComponents : [];
const hasText = components.some(component => String(component?.type || '').trim().toLowerCase() !== 'file');
const hasFile = components.some(component => String(component?.type || '').trim().toLowerCase() === 'file');
if (hasText && hasFile) {
return 'mixed';
}
if (hasFile) {
return 'file';
}
return 'text';
}
/**
* Build one public runtime snapshot from a schema bundle.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Public runtime snapshot.
*/
function buildResolvedRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const components = Array.isArray(bundle.runtime.editableComponents)
? bundle.runtime.editableComponents.map(buildEditableComponentSnapshot).filter(Boolean)
: [];
const defaultComponent = components.find(component => component.default) || components[0] || null;
return {
uuid: bundle.uuid,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
mode: resolvePublicRuntimeMode(bundle.runtime),
defaultComponentId: defaultComponent ? defaultComponent.id : '',
components,
};
}
/**
* Resolve a public media descriptor for one target block.
*
* @param {Object} options Media resolution options.
* @returns {Object|null} Media descriptor snapshot.
*/
function resolveMediaDescriptor(options = {}) {
const runtime = resolveRuntimeBundle(options);
if (!runtime) {
return null;
}
const componentId = String(options.componentId || '').trim();
const components = Array.isArray(runtime.runtime.editableComponents) ? runtime.runtime.editableComponents : [];
const selected = componentId
? components.find(component => component && component.id === componentId)
: (
(runtime.runtime.mediaComponent && typeof runtime.runtime.mediaComponent === 'object'
? runtime.runtime.mediaComponent
: components.find(component => String(component?.type || '').trim().toLowerCase() === 'file')
) || null
);
if (!selected || !selected.mediaDescriptor || typeof selected.mediaDescriptor !== 'object') {
return null;
}
return bridge.clonePlainData(selected.mediaDescriptor);
}
/**
* Build one stable block snapshot.
*
* @param {string} uuid Target UUID.
* @returns {Object|null} Block snapshot.
*/
function buildBlockSnapshot(uuid) {
const normalizedUuid = String(uuid || '').trim();
const entry = getUuidEntry(normalizedUuid);
if (!normalizedUuid || !entry) {
return null;
}
return {
uuid: normalizedUuid,
blockName: String(entry.blockName || '').trim(),
handlerIds: Array.isArray(entry.handlers) ? entry.handlers.slice() : [],
isPending: !!entry.is_pending,
pendingInfo: entry.pending_info ? bridge.clonePlainData(entry.pending_info) : null,
elementPresent: !!getElementByUuid(normalizedUuid),
};
}
/**
* Return stable discovery snapshots for every editable block on this page.
*
* @returns {Array} Editable block snapshots in FrontEdit UUID-map order.
*/
function getEditableBlocks() {
const context = getContext();
const uuidMap = context.uuidMap && typeof context.uuidMap === 'object'
? context.uuidMap
: {};
return Object.keys(uuidMap)
.map(uuid => {
const snapshot = buildBlockSnapshot(uuid);
if (!snapshot) {
return null;
}
const element = getElementByUuid(uuid);
return {
...snapshot,
contentText: element instanceof Element
? String(element.textContent || '').replace(/\s+/g, ' ').trim()
: '',
};
})
.filter(Boolean);
}
/**
* Determine whether a block currently has tracked unsaved changes.
*
* @param {string} uuid Block UUID.
* @returns {boolean} Whether the block is dirty.
*/
function isDirtyUuid(uuid) {
const batchManager = SFE.BatchEditManager || null;
return !!(
uuid &&
batchManager &&
batchManager.dirtyBlocks instanceof Map &&
batchManager.dirtyBlocks.has(uuid)
);
}
/**
* Build one public editor snapshot from a live editor state object.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object|null} Stable editor snapshot.
*/
function buildEditorSnapshot(editorState) {
if (!editorState || !editorState.uuid) {
return null;
}
const draftEditState = getContext().draftEditState;
const isDraftSession = !!(
draftEditState &&
draftEditState.draftElement &&
draftEditState.draftElement === editorState.element
);
const activeComponentId = String(
editorState.activeComponentId
|| editorState.activeEditableComponent?.id
|| ''
).trim();
const activeComponentType = String(
editorState._mwpActiveComponentType
|| editorState.activeEditableComponent?.type
|| (editorState.isMediaEditor ? 'file' : 'text')
).trim().toLowerCase();
const activeEditorHost = resolveActiveEditorHost(editorState);
return {
uuid: String(editorState.uuid || '').trim(),
handlerId: String(editorState._mwpSchemaHandlerId || editorState.handler?.id || '').trim(),
mode: String(editorState.requestedMode || (isDraftSession ? 'draft' : 'edit')).trim() || 'edit',
blockName: String(editorState.blockName || editorState.blockState?.blockName || '').trim(),
isOpen: true,
componentType: activeComponentType || 'text',
componentId: activeComponentId,
saveStrategy: String(editorState.saveStrategy || 'single').trim() || 'single',
hasMediaSession: typeof activeEditorHost?.applyMediaSelection === 'function',
isDirty: isDirtyUuid(editorState.uuid),
isDraftSession,
isBatchSession: !!(
editorState.saveStrategy === 'batch' &&
SFE.BatchEditManager &&
typeof SFE.BatchEditManager.isSessionActive === 'function' &&
SFE.BatchEditManager.isSessionActive()
),
};
}
/**
* Switch the active editable component inside one open editor session.
*
* @param {Object} editorState Active editor state.
* @param {string} componentId Target component identifier.
* @returns {boolean} Whether the switch succeeded.
*/
function activateEditorComponent(editorState, componentId) {
if (!editorState || !componentId) {
return false;
}
if (SFE.TextEditor && typeof SFE.TextEditor.activateEditableComponentById === 'function') {
return !!SFE.TextEditor.activateEditableComponentById(editorState, componentId);
}
return false;
}
/**
* Open one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Open-editor options.
* @returns {Promise} Editor snapshot after opening.
*/
async function openEditor(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const componentId = String(options.componentId || '').trim();
if (!uuid || !(element instanceof Element) || !handler || typeof SFE.startEditing !== 'function') {
return null;
}
const activeEditor = getContext().activeEditor || null;
if (activeEditor && String(activeEditor.uuid || '').trim() === uuid) {
if (componentId) {
activateEditorComponent(activeEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || activeEditor);
}
if (activeEditor && typeof SFE.closeInPlaceEditor === 'function') {
SFE.closeInPlaceEditor(activeEditor, true, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
});
}
await Promise.resolve(
SFE.startEditing(
element,
handler,
uuid,
null,
false,
String(options.mode || 'edit').trim() || 'edit',
{
initialComponentId: componentId,
source: String(options.source || 'external').trim() || 'external',
originalSnapshots: options.originalSnapshots && typeof options.originalSnapshots === 'object'
? {
restoreOuterHTML: String(options.originalSnapshots.restoreOuterHTML || '').trim(),
cleanOuterHTML: String(options.originalSnapshots.cleanOuterHTML || '').trim(),
}
: null,
}
)
);
const openedEditor = getContext().activeEditor || null;
if (openedEditor && componentId) {
activateEditorComponent(openedEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || openedEditor);
}
/**
* Close one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Close-editor options.
* @returns {boolean} Whether a close was attempted.
*/
function closeEditor(options = {}) {
const activeEditor = getContext().activeEditor || null;
if (!activeEditor || typeof SFE.closeInPlaceEditor !== 'function') {
return false;
}
const targetUuid = String(options.uuid || '').trim();
if (targetUuid && String(activeEditor.uuid || '').trim() !== targetUuid) {
return false;
}
return SFE.closeInPlaceEditor(activeEditor, options.restoreOriginal !== false, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
}) !== false;
}
/**
* Resolve the active editor state for one target block UUID.
*
* Schema-backed editor operations intentionally require a live open editor so
* the DOM mutation stays inside the same save/history session FrontEdit owns.
*
* @param {Object} options Editor-operation options.
* @returns {Object|null} Active editor state.
*/
function resolveActiveEditorState(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
return resolveActiveEditorHost(activeEditor) ? activeEditor : null;
}
function resolveActiveEditorHost(editorState) {
return SFE.SchemaEditorHost?.resolveActiveEditorHost?.(editorState) || null;
}
/**
* Resolve the session-owned history API for one active editor host.
*
* External runtime mutations should commit through the same shared block
* session contract used internally instead of reaching into ad hoc
* editor-state history fields.
*
* @param {Object|null} editorHost Active schema editor host.
* @returns {Object|null} Session history API, or null when unavailable.
*/
function getSessionHistoryApiForHost(editorHost) {
if (editorHost?.historyApi && typeof editorHost.historyApi === 'object') {
return editorHost.historyApi;
}
if (typeof editorHost?.getSessionHistoryApi === 'function') {
return editorHost.getSessionHistoryApi();
}
const blockEditSession = editorHost?.blockEditSession || null;
if (!blockEditSession || typeof blockEditSession.getHistoryApi !== 'function') {
return null;
}
return blockEditSession.getHistoryApi(
String(editorHost?.blockEditSessionScopeId || '').trim() || 'text'
);
}
/**
* Refresh one active editor state after a schema-driven DOM mutation.
*
* @param {Object} editorState Active editor state.
* @returns {void}
*/
function syncActiveEditorState(editorState) {
if (!editorState?.handler) {
return;
}
const editorHost = resolveActiveEditorHost(editorState);
const blockRootElement = (
editorHost &&
typeof editorHost.getBlockRootElement === 'function'
)
? editorHost.getBlockRootElement()
: null;
if (blockRootElement && blockRootElement !== editorState.element) {
// The active schema host element can be one editable component (for
// example, one table cell), while block-session history and restore must
// keep targeting the outer block root. Rebasing `editorState.element` to
// the component host corrupts undo/redo by replaying full block markup
// into that component instead of the block wrapper.
editorState.element = blockRootElement;
}
if (!editorState.element) {
return;
}
const schemaRuntime = SFE.SchemaRuntime || null;
if (
schemaRuntime &&
typeof schemaRuntime.syncPlaceholders === 'function'
) {
schemaRuntime.syncPlaceholders(editorState.element, editorState.handler, {
blockAttributes: editorState.blockState?.attrs || null,
attributeChanges: editorState.attributeChanges || null,
});
}
if (
SFE.TextEditor &&
typeof SFE.TextEditor.refreshEditableComponents === 'function'
) {
SFE.TextEditor.refreshEditableComponents(editorState);
}
}
/**
* Build one component map for the active schema editor state.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object} Editable components keyed by ID.
*/
function getActiveEditorComponentMap(editorState) {
if (!editorState || !Array.isArray(editorState.editableComponents)) {
return {};
}
return editorState.editableComponents.reduce((map, component) => {
const componentId = String(component?.id || '').trim();
if (!componentId || !component?.element) {
return map;
}
map[componentId] = component;
return map;
}, {});
}
/**
* Return the lowercase host tag name for one candidate component element.
*
* @param {Element|null} hostElement Candidate host element.
* @returns {string} Lowercase host tag name.
*/
function getHostElementTagName(hostElement) {
return hostElement && hostElement.tagName
? String(hostElement.tagName || '').trim().toLowerCase()
: '';
}
/**
* Build one inline format wrapper for a structured run token.
*
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {string} formatToken Format token.
* @param {Object|null} formatAttributes Optional format attributes.
* @returns {HTMLElement|null} Wrapper element.
*/
function buildStructuredFormatWrapper(inlineCapabilities, formatToken, formatAttributes = null) {
const tagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!tagName) {
return null;
}
const element = document.createElement(tagName);
if (tagName === 'a' && formatAttributes && typeof formatAttributes === 'object') {
const href = String(formatAttributes.href || '').trim();
if (href) {
element.setAttribute('href', href);
}
const target = String(formatAttributes.target || '').trim();
if (target) {
element.setAttribute('target', target);
}
const rel = String(formatAttributes.rel || '').trim();
if (rel) {
element.setAttribute('rel', rel);
}
}
return element;
}
/**
* Hoist same-tag inline format attributes onto the host component element.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Element|null} hostElement Live host element.
* @returns {{runs: Array, hostAttributes: Object}} Hoisted run data.
*/
function hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement = null) {
const hostTagName = getHostElementTagName(hostElement);
if (!hostTagName) {
return {
runs: Array.isArray(runs) ? runs : [],
hostAttributes: {},
};
}
const hostAttributes = {};
const normalizedRuns = (Array.isArray(runs) ? runs : []).map(run => {
const formats = Array.isArray(run?.formats) ? run.formats : [];
const formatAttributes = run?.formatAttributes && typeof run.formatAttributes === 'object'
? { ...run.formatAttributes }
: {};
const nextFormats = [];
formats.forEach(formatToken => {
const formatTagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!formatTagName || formatTagName !== hostTagName) {
nextFormats.push(formatToken);
return;
}
const attributes = formatAttributes?.[formatToken];
if (attributes && typeof attributes === 'object' && !Array.isArray(attributes)) {
Object.assign(hostAttributes, attributes);
}
delete formatAttributes[formatToken];
});
return {
...run,
formats: nextFormats,
formatAttributes,
};
});
return {
runs: normalizedRuns,
hostAttributes,
};
}
/**
* Apply hoisted inline-format attributes onto one host element.
*
* @param {Element|null} hostElement Live host element.
* @param {Object|null} hostAttributes Hoisted host attributes.
* @returns {void}
*/
function applyStructuredHostLevelFormatAttributes(hostElement, hostAttributes = null) {
if (!hostElement || typeof hostElement.setAttribute !== 'function' || typeof hostElement.removeAttribute !== 'function') {
return;
}
const attributes = hostAttributes && typeof hostAttributes === 'object' ? hostAttributes : {};
['href', 'target', 'rel'].forEach(attributeName => {
const value = typeof attributes[attributeName] === 'string'
? attributes[attributeName].trim()
: '';
if (value) {
hostElement.setAttribute(attributeName, value);
return;
}
hostElement.removeAttribute(attributeName);
});
}
/**
* Build deterministic HTML from one normalized structured run sequence.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Object} editorOptions Runtime editor options.
* @param {Element|null} hostElement Live host element.
* @returns {{html: string, hostAttributes: Object}} Rendered HTML fragment.
*/
function buildStructuredHtmlFromRuns(runs, inlineCapabilities, editorOptions = {}, hostElement = null) {
const container = document.createElement('div');
const extractionOptions = editorOptions?.options && typeof editorOptions.options === 'object'
? editorOptions.options
: {};
const hoisted = hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement);
(Array.isArray(hoisted.runs) ? hoisted.runs : []).forEach(run => {
const text = String(run?.text || '');
if (!text) {
return;
}
const segments = extractionOptions.newlinesToBR === true || String(editorOptions?.enterMode || '').trim() === 'linebreak'
? text.split('\n')
: [text];
segments.forEach((segment, index) => {
if (index > 0) {
container.appendChild(document.createElement('br'));
}
if (!segment) {
return;
}
let node = document.createTextNode(segment);
(Array.isArray(run.formats) ? run.formats : []).forEach(formatToken => {
const wrapper = buildStructuredFormatWrapper(
inlineCapabilities,
formatToken,
run.formatAttributes && typeof run.formatAttributes === 'object'
? run.formatAttributes[formatToken] || null
: null
);
if (!wrapper) {
return;
}
wrapper.appendChild(node);
node = wrapper;
});
container.appendChild(node);
});
});
return {
html: container.innerHTML,
hostAttributes: hoisted.hostAttributes,
};
}
/**
* Apply structured component updates onto the active editor DOM.
*
* @param {Object} editorState Active editor state.
* @param {Array} componentUpdates Normalized component updates.
* @returns {string[]} Updated component IDs.
*/
function applyStructuredComponentUpdates(editorState, componentUpdates) {
const componentMap = getActiveEditorComponentMap(editorState);
const updatedComponentIds = [];
(Array.isArray(componentUpdates) ? componentUpdates : []).forEach(componentUpdate => {
const componentId = String(componentUpdate?.componentId || '').trim();
const component = componentMap[componentId] || null;
if (!component || !component.element) {
return;
}
if (componentUpdate.bindingSource === 'plaintext') {
component.element.textContent = (Array.isArray(componentUpdate.runs) ? componentUpdate.runs : [])
.map(run => String(run?.text || ''))
.join('');
updatedComponentIds.push(componentId);
return;
}
if (componentUpdate.bindingSource === 'html') {
const rendered = buildStructuredHtmlFromRuns(
componentUpdate.runs,
componentUpdate.inlineCapabilities || {},
componentUpdate.editorOptions || component.editorOptions || {},
component.element
);
component.element.innerHTML = rendered.html;
applyStructuredHostLevelFormatAttributes(component.element, rendered.hostAttributes);
updatedComponentIds.push(componentId);
}
});
return Array.from(new Set(updatedComponentIds));
}
/**
* Resolve the active list-editor state for one target block UUID.
*
* Structural list operations intentionally require a live open editor so the
* mutation happens inside the same DOM/save session FrontEdit already owns.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Active list editor state.
*/
function resolveActiveListEditorState(options = {}) {
const activeEditor = resolveActiveEditorState(options);
if (!activeEditor) {
return null;
}
const listTracker = SFE.ListBlockTracker || null;
if (
!listTracker ||
!activeEditor.element ||
(activeEditor.element.tagName !== 'UL' && activeEditor.element.tagName !== 'OL')
) {
return null;
}
if (!activeEditor.listTracker || activeEditor.listTracker.listElement !== activeEditor.element) {
activeEditor.listTracker = listTracker.init(
activeEditor.element,
activeEditor.blockState || {}
);
}
return activeEditor.listTracker ? activeEditor : null;
}
/**
* Refresh one active list editor after a structural DOM mutation.
*
* @param {Object} editorState Active list editor state.
* @returns {void}
*/
function syncActiveListEditorState(editorState) {
if (!editorState?.element || !editorState?.handler) {
return;
}
if (editorState.listTracker?.listElement && editorState.listTracker.listElement !== editorState.element) {
editorState.element = editorState.listTracker.listElement;
const editorHost = resolveActiveEditorHost(editorState);
if (editorHost && typeof editorHost === 'object') {
editorHost.element = editorState.listTracker.listElement;
}
}
syncActiveEditorState(editorState);
}
/**
* Apply one or more schema-declared block-attribute operations to the active editor.
*
* @param {Object} options Block-attribute operation options.
* @returns {Object|null} Result summary and current tracked attribute changes.
*/
function applyBlockAttributeOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeBlockAttributeOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
attributeChanges: bridge.clonePlainData(executionResult.attributeChanges || {}),
};
}
/**
* Apply one or more component-content replacement operations to the active editor.
*
* The shared executor owns rich-text run rendering and link normalization so
* external callers can target component surfaces without reimplementing FrontEdit's
* browser mutation semantics.
*
* @param {Object} options Component-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyTextComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeComponentOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more schema-backed media replacements to the active editor.
*
* This targets the currently active media-session host so external callers
* can replace schema media components without bypassing FrontEdit's own preview and
* resolved-media update pipeline.
*
* @param {Object} options Media-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyMediaComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeMediaOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeMediaOperations({
editorState,
editorHost,
operations,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more high-level public list operations to the currently open
* list editor.
*
* The runtime API accepts UUID-oriented public commands and lets the shared
* executor translate them into the internal primitive tracker operations.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Result summary and next list structure.
*/
function applyListOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveListEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations
: (options.operation ? [ options.operation ] : []);
const operationExecutor = SFE.SchemaOperationExecutor || null;
const listTracker = SFE.ListBlockTracker || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeListOperations !== 'function' ||
!listTracker
) {
return null;
}
const executionResult = operationExecutor.executeListOperations({
tracker: editorState.listTracker,
editorHost,
operations,
targetResolutionMode: 'api_uuid',
saveHistory: true,
afterSync: function() {
syncActiveListEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
structure: listTracker.getStructure(editorState.listTracker),
};
}
/**
* Apply one normalized structured edit payload to the active schema editor.
*
* This keeps non-list staged edits inside the same public runtime contract as
* list operations: open the editor first, then let FrontEdit own the mutation and
* history bookkeeping.
*
* @param {Object} options Structured-edit options.
* @returns {Object|null} Result summary for the applied edit.
*/
function applyStructuredEditToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const componentUpdates = Array.isArray(options.componentUpdates)
? options.componentUpdates.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const attributeOperations = Array.isArray(options.attributeOperations)
? options.attributeOperations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (!editorState || !editorHost || (!componentUpdates.length && !attributeOperations.length)) {
return null;
}
let attributeResult = null;
let componentResult = null;
if (
componentUpdates.length
&& operationExecutor
&& typeof operationExecutor.executeComponentOperations === 'function'
) {
componentResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations: componentUpdates,
saveHistory: false,
});
}
if (
attributeOperations.length &&
operationExecutor &&
typeof operationExecutor.executeBlockAttributeOperations === 'function'
) {
attributeResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations: attributeOperations,
saveHistory: false,
});
}
const didApplyAttributes = !!(
attributeResult &&
Array.isArray(attributeResult.operationsApplied) &&
attributeResult.operationsApplied.length
);
const didApplyComponents = !!(
componentResult &&
Array.isArray(componentResult.updatedComponentIds) &&
componentResult.updatedComponentIds.length
);
if (!didApplyComponents && !didApplyAttributes) {
return null;
}
syncActiveEditorState(editorState);
const historyApi = getSessionHistoryApiForHost(editorHost);
if (typeof historyApi?.saveToHistory === 'function') {
historyApi.saveToHistory();
}
if (typeof editorHost.updateToolbarState === 'function') {
editorHost.updateToolbarState();
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(componentResult?.updatedComponentIds)
? componentResult.updatedComponentIds
: [],
operationsApplied: [
...(Array.isArray(componentResult?.operationsApplied) ? componentResult.operationsApplied : []),
...(Array.isArray(attributeResult?.operationsApplied) ? attributeResult.operationsApplied : []),
],
attributeChanges: bridge.clonePlainData(editorHost.attributeChanges || {}),
};
}
/**
* Return a structural snapshot for one list block.
*
* This resolves against the live DOM so external tooling can inspect the
* current nested list tree without parsing serialized block markup itself.
*
* @param {Object} options Lookup options.
* @returns {Object|null} List structure snapshot.
*/
function getListStructure(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const listTracker = SFE.ListBlockTracker || null;
if (
!uuid ||
!(element instanceof Element) ||
!listTracker ||
(element.tagName !== 'UL' && element.tagName !== 'OL')
) {
return null;
}
const tracker = element._mwpListTracker
|| listTracker.init(element, resolveBlockState(uuid) || {});
return tracker && typeof listTracker.getStructure === 'function'
? listTracker.getStructure(tracker)
: null;
}
/**
* Apply one selected media item to the active schema-media editor session.
*
* This uses the same internal selection transition FrontEdit normally runs after
* the user picks an item from the media library or upload flow.
*
* @param {Object} options Media-selection options.
* @returns {Object|null} Stable editor snapshot after the selection is applied.
*/
function applyActiveMediaSelection(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim();
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
const editorHost = resolveActiveEditorHost(activeEditor);
const url = String(options.url || '').trim();
if (!editorHost || typeof editorHost.applyMediaSelection !== 'function' || !url) {
return null;
}
const attachmentId = Object.prototype.hasOwnProperty.call(options, 'attachmentId')
? options.attachmentId
: null;
const fromState = String(options.source || '').trim().toLowerCase() === 'library'
? 'library'
: 'input';
const didApply = editorHost.applyMediaSelection(url, attachmentId, { fromState });
return didApply === false ? null : buildEditorSnapshot(activeEditor);
}
/**
* Refresh one block through the supported server-rendered replacement path.
*
* @param {string} uuid Block UUID.
* @param {Object} [options={}] Refresh options.
* @returns {Promise} Refreshed block snapshot.
*/
async function refreshBlock(uuid, options = {}) {
const normalizedUuid = String(uuid || '').trim();
const element = getElementByUuid(normalizedUuid);
if (
!normalizedUuid ||
!(element instanceof Element) ||
!SFE.SaveHelpers ||
typeof SFE.SaveHelpers.fetchRenderedBlockHTML !== 'function' ||
!SFE.ElementUpdater ||
typeof SFE.ElementUpdater.applyNewHTML !== 'function'
) {
return null;
}
const html = await SFE.SaveHelpers.fetchRenderedBlockHTML(normalizedUuid, options || {});
if (!html) {
return null;
}
const newElement = SFE.ElementUpdater.applyNewHTML(element, normalizedUuid, html);
if (!newElement) {
return null;
}
if (typeof SFE.ElementUpdater.rebindElement === 'function') {
SFE.ElementUpdater.rebindElement(newElement);
}
const block = buildBlockSnapshot(normalizedUuid);
if (block) {
bridge.emitEvent('block:refreshed', {
source: String(options.source || 'sfe').trim() || 'sfe',
block,
});
}
return block;
}
/**
* Return one public dirty-block snapshot.
*
* @param {Object} entry Dirty entry from the batch manager.
* @returns {Object|null} Dirty block snapshot.
*/
function buildDirtyBlockSnapshot(entry) {
if (!entry || typeof entry !== 'object') {
return null;
}
return {
uuid: String(entry.element_uuid || '').trim(),
handlerId: String(entry.handler_id || '').trim(),
blockName: String(entry.blockName || '').trim(),
beforeRaw: String(entry.before || '').trim(),
afterRaw: String(entry.after || '').trim(),
};
}
/**
* Reset dirty batch state for one or more target UUIDs.
*
* @param {string[]} uuids Target UUIDs.
* @returns {boolean} Whether any batch state was reset.
*/
function resetDirtyBlocks(uuids) {
const batchManager = SFE.BatchEditManager || null;
const normalizedUuids = Array.isArray(uuids)
? uuids.map(uuid => String(uuid || '').trim()).filter(Boolean)
: [];
if (!normalizedUuids.length || !batchManager) {
return false;
}
if (typeof batchManager.resetDirtyBlocks === 'function') {
return !!batchManager.resetDirtyBlocks(normalizedUuids);
}
let changed = false;
normalizedUuids.forEach(uuid => {
if (batchManager.dirtyBlocks instanceof Map && batchManager.dirtyBlocks.has(uuid)) {
batchManager.dirtyBlocks.delete(uuid);
changed = true;
}
if (
batchManager.currentBlockStates instanceof Map &&
batchManager.baseBlockStates instanceof Map &&
batchManager.baseBlockStates.has(uuid)
) {
batchManager.currentBlockStates.set(
uuid,
bridge.clonePlainData(batchManager.baseBlockStates.get(uuid))
);
changed = true;
}
});
return changed;
}
/**
* Return the stable PublicApi discovery snapshot.
*
* @returns {Object} API metadata.
*/
function getApiInfo() {
return {
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,
},
};
}
PublicApi._buildEditorSnapshot = buildEditorSnapshot;
PublicApi.getApiInfo = getApiInfo;
PublicApi.isEditorOpen = function isEditorOpen() {
return !!getContext().activeEditor;
};
PublicApi.getActiveEditor = function getActiveEditor() {
return buildEditorSnapshot(getContext().activeEditor || null);
};
PublicApi.applyActiveMediaSelection = function applyActiveMediaSelectionPublic(options = {}) {
return applyActiveMediaSelection(options);
};
PublicApi.getPageContext = function getPageContext() {
const context = getContext();
const managerData = getManagerData();
return {
postId: Number(managerData.postId || 0) || 0,
permissions: bridge.clonePlainData(managerData.permissions || {}),
hasDraftPreview: !!(
document.body.classList.contains('mwp-sfe-active-preview') ||
context.activeMode === 'draft'
),
isEditorOpen: !!context.activeEditor,
activeMode: String(context.activeMode || '').trim(),
};
};
PublicApi.getRestContext = function getRestContext() {
const managerData = getManagerData();
return {
baseUrl: String(managerData.restBase || '').trim(),
namespaceUrl: String(managerData.restUrl || '').trim(),
nonce: String(managerData.nonce || '').trim(),
};
};
PublicApi.setRestNonce = function setRestNonce(nonce) {
const normalizedNonce = normalizeRestNonce(nonce);
if (!normalizedNonce) {
return PublicApi.getRestContext();
}
const managerData = getManagerData();
managerData.nonce = normalizedNonce;
SFE.ManagerData = managerData;
return PublicApi.getRestContext();
};
PublicApi.getElementByUuid = getElementByUuid;
PublicApi.getUuidForElement = getUuidForElement;
PublicApi.getBlockSnapshot = function getBlockSnapshot(uuid) {
return buildBlockSnapshot(uuid);
};
PublicApi.getEditableBlocks = getEditableBlocks;
PublicApi.resolveRuntime = function resolveRuntime(options = {}) {
return buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.resolveEditingRuntime = function resolveEditingRuntime(options = {}) {
return buildResolvedEditingRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.getEditableComponents = function getEditableComponents(options = {}) {
const runtime = buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
return runtime ? runtime.components : [];
};
PublicApi.getListStructure = getListStructure;
PublicApi.getDefaultComponent = function getDefaultComponent(options = {}) {
const components = PublicApi.getEditableComponents(options);
return components.find(component => component.default) || components[0] || null;
};
PublicApi.getMediaDescriptor = function getMediaDescriptor(options = {}) {
return resolveMediaDescriptor(options);
};
PublicApi.getMediaContext = function getMediaContext(options = {}) {
const descriptor = resolveMediaDescriptor(options);
const mediaHelper = SFE.MediaHelper || null;
if (!descriptor || !mediaHelper || typeof mediaHelper.getMediaType !== 'function') {
return null;
}
const mediaType = String(mediaHelper.getMediaType(descriptor) || '').trim();
if (!mediaType) {
return null;
}
return {
supported: true,
componentId: String(descriptor.componentId || '').trim(),
mediaType,
accept: typeof mediaHelper.getAcceptTypes === 'function'
? String(mediaHelper.getAcceptTypes(descriptor) || '*/*').trim() || '*/*'
: '*/*',
label: mediaType === 'audio' ? 'audio block' : (mediaType === 'video' ? 'video block' : (mediaType === 'file' ? 'file block' : 'media block')),
descriptor,
};
};
PublicApi.isMediaEditable = function isMediaEditable(options = {}) {
return !!PublicApi.getMediaContext(options);
};
PublicApi.openEditor = openEditor;
PublicApi.closeEditor = closeEditor;
PublicApi.applyTextComponentOperations = applyTextComponentOperationsToActiveEditor;
PublicApi.applyMediaComponentOperations = applyMediaComponentOperationsToActiveEditor;
PublicApi.applyBlockAttributeOperations = applyBlockAttributeOperationsToActiveEditor;
PublicApi.applyListOperations = applyListOperationsToActiveEditor;
PublicApi.applyStructuredEdit = applyStructuredEditToActiveEditor;
PublicApi.stageBlockState = function stageBlockState(stage) {
const uuid = String(stage && stage.uuid ? stage.uuid : '').trim();
if (!uuid || !stage || typeof stage !== 'object' || !stage.blockState || typeof stage.blockState !== 'object') {
return;
}
bridge.stagedBlockStates.set(uuid, {
handlerId: String(stage.handlerId || '').trim(),
source: String(stage.source || 'external').trim() || 'external',
blockState: bridge.clonePlainData(stage.blockState),
});
bridge.emitEvent('block:staged', {
source: String(stage.source || 'external').trim() || 'external',
uuid,
handlerId: String(stage.handlerId || '').trim(),
});
};
PublicApi.clearStagedBlockState = function clearStagedBlockState(uuid, source = 'external') {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid) {
return;
}
bridge.stagedBlockStates.delete(normalizedUuid);
bridge.emitEvent('block:stageCleared', {
source: String(source || 'external').trim() || 'external',
uuid: normalizedUuid,
});
};
PublicApi.refreshBlock = refreshBlock;
PublicApi.getDirtyBlocks = function getDirtyBlocks() {
const batchManager = SFE.BatchEditManager || null;
if (!batchManager || !(batchManager.dirtyBlocks instanceof Map)) {
return [];
}
return Array.from(batchManager.dirtyBlocks.values())
.map(buildDirtyBlockSnapshot)
.filter(entry => entry && entry.uuid);
};
PublicApi.hasDirtyBlocks = function hasDirtyBlocks() {
return PublicApi.getDirtyBlocks().length > 0;
};
PublicApi.isBatchSessionActive = function isBatchSessionActive() {
const batchManager = SFE.BatchEditManager || null;
return !!(
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive()
);
};
PublicApi.resetDirtyBlocks = function resetDirtyBlocksPublic(uuids) {
return resetDirtyBlocks(uuids);
};
SFE.PublicApi = PublicApi;
})();
/**
* Stable frontend runtime PublicApi implementation.
*
* Exposes: SFE.PublicApi
* {
* getApiInfo, on, off, isEditorOpen, getActiveEditor,
* applyActiveMediaSelection, getPageContext, getRestContext, setRestNonce,
* getElementByUuid, getUuidForElement, getBlockSnapshot, getEditableBlocks,
* resolveRuntime,
* resolveEditingRuntime, getEditableComponents, getListStructure,
* getDefaultComponent, getMediaDescriptor, getMediaContext,
* isMediaEditable, openEditor, closeEditor, applyTextComponentOperations,
* applyMediaComponentOperations,
* applyBlockAttributeOperations,
* applyListOperations, applyStructuredEdit, stageBlockState,
* clearStagedBlockState, refreshBlock, getDirtyBlocks, hasDirtyBlocks,
* isBatchSessionActive, resetDirtyBlocks
* }
*
* Keeps mutable runtime internals private while exposing the documented browser
* integration surface for external consumers such as ABE.
*/
(function() {
'use strict';
window.MWP = window.MWP || {};
window.MWP.SFE = window.MWP.SFE || {};
const SFE = window.MWP.SFE;
SFE.ManagerData = SFE.ManagerData || {};
const PublicApi = SFE.PublicApi || {};
const bridge = SFE.PublicApiBridge || null;
if (!bridge) {
return;
}
/**
* Resolve the shared runtime context object.
*
* @returns {Object} Shared FrontEdit context.
*/
function getContext() {
return SFE.Context || {};
}
/**
* Resolve the localized bootstrap payload.
*
* @returns {Object} Localized manager data.
*/
function getManagerData() {
return SFE.ManagerData || {};
}
/**
* Normalize one candidate REST nonce.
*
* @param {*} nonce Candidate nonce value.
* @returns {string} Trimmed nonce or an empty string.
*/
function normalizeRestNonce(nonce) {
return String(nonce || '').trim();
}
/**
* Return the live block element for one UUID.
*
* @param {string} uuid Block UUID.
* @returns {Element|null} Matching element.
*/
function getElementByUuid(uuid) {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || typeof document.querySelector !== 'function') {
return null;
}
try {
return document.querySelector(`[data-mwp-sfe-uuid="${CSS.escape(normalizedUuid)}"]`);
} catch (error) {
return document.querySelector(`[data-mwp-sfe-uuid="${normalizedUuid.replace(/"/g, '\\"')}"]`);
}
}
/**
* Resolve one UUID from a supplied element or nested child.
*
* @param {Element|null} element Candidate DOM node.
* @returns {string} Block UUID or an empty string.
*/
function getUuidForElement(element) {
if (!(element instanceof Element)) {
return '';
}
const candidate = element.closest('[data-mwp-sfe-uuid]');
return candidate instanceof Element
? String(candidate.getAttribute('data-mwp-sfe-uuid') || '').trim()
: '';
}
/**
* Resolve one uuid-map entry from the shared context.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} UUID map entry.
*/
function getUuidEntry(uuid) {
const context = getContext();
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid || !context.uuidMap || !context.uuidMap[normalizedUuid]) {
return null;
}
return context.uuidMap[normalizedUuid];
}
/**
* Resolve one edit-capable handler for the supplied target.
*
* @param {Object} options Resolution options.
* @returns {Object|null} Edit handler when supported.
*/
function resolveEditHandler(options = {}) {
const element = options.element instanceof Element ? options.element : null;
const preferredHandlerId = String(options.handlerId || '').trim();
const handlers = Array.isArray(element?._mwpSfeHandlers)
? element._mwpSfeHandlers
: [];
if (preferredHandlerId) {
const exact = handlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
));
if (exact) {
return exact;
}
}
const fromElement = handlers.find(handler => handler && handler.capability === 'edit') || null;
if (fromElement) {
return fromElement;
}
const uuidEntry = getUuidEntry(options.uuid);
const managerData = getManagerData();
const registeredHandlers = Array.isArray(managerData.handlers) ? managerData.handlers : [];
const handlerIds = Array.isArray(uuidEntry?.handlers) ? uuidEntry.handlers : [];
if (preferredHandlerId) {
return registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === preferredHandlerId
)) || null;
}
for (let index = 0; index < handlerIds.length; index++) {
const handlerId = String(handlerIds[index] || '').trim();
const match = registeredHandlers.find(handler => (
handler &&
handler.capability === 'edit' &&
handler.id === handlerId
));
if (match) {
return match;
}
}
return null;
}
/**
* Resolve the current block-state payload used for schema hydration.
*
* @param {string} uuid Block UUID.
* @returns {Object|null} Block-state payload when available.
*/
function resolveBlockState(uuid) {
const batchManager = SFE.BatchEditManager || null;
if (
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive() &&
typeof batchManager.getBlockStateForUuid === 'function'
) {
return batchManager.getBlockStateForUuid(uuid);
}
return null;
}
/**
* Resolve the schema runtime bundle for one target block.
*
* @param {Object} options Runtime resolution options.
* @returns {Object|null} Schema runtime bundle.
*/
function resolveRuntimeBundle(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const schemaRuntime = SFE.SchemaRuntime || null;
if (!uuid || !(element instanceof Element) || !handler || !schemaRuntime || typeof schemaRuntime.resolveForEditing !== 'function') {
return null;
}
try {
const blockState = options.blockState && typeof options.blockState === 'object'
? options.blockState
: resolveBlockState(uuid);
const attributeChanges = options.attributeChanges && typeof options.attributeChanges === 'object'
? options.attributeChanges
: undefined;
const resolved = schemaRuntime.resolveForEditing(element, handler, {
blockState,
attributeChanges,
});
if (!resolved || !resolved.runtimeHandler || !resolved.runtime) {
return null;
}
return {
uuid,
element,
handler: resolved.runtimeHandler,
runtime: resolved.runtime,
schema: resolved.schema || null,
};
} catch (error) {
console.warn('FrontEdit: PublicApi resolveRuntime failed', error);
return null;
}
}
/**
* Build one detailed editable-component snapshot from a schema runtime entry.
*
* @param {Object|null} editableComponent Runtime editable component.
* @param {Object|null} componentMeta Additional component metadata.
* @returns {Object|null} Detailed component snapshot.
*/
function buildEditingRuntimeComponentDetail(editableComponent, componentMeta = null) {
const snapshot = buildEditableComponentSnapshot(editableComponent);
if (!snapshot) {
return null;
}
const meta = componentMeta && typeof componentMeta === 'object' ? componentMeta : {};
snapshot.element = editableComponent?.element instanceof Element
? editableComponent.element
: (meta.element instanceof Element ? meta.element : null);
const bindings = Array.isArray(meta.bindings)
? bridge.clonePlainData(meta.bindings)
: [];
if (bindings.length) {
snapshot.bindings = bindings;
}
const target = meta.target && typeof meta.target === 'object'
? bridge.clonePlainData(meta.target)
: (snapshot.target || null);
if (target) {
snapshot.target = target;
}
const editorOptions = editableComponent?.editorOptions && typeof editableComponent.editorOptions === 'object'
? bridge.clonePlainData(editableComponent.editorOptions)
: (meta.editorOptions && typeof meta.editorOptions === 'object'
? bridge.clonePlainData(meta.editorOptions)
: null);
if (editorOptions) {
snapshot.editorOptions = editorOptions;
snapshot.editor = bridge.clonePlainData(editorOptions);
}
return snapshot;
}
/**
* Build one detailed schema-resolved editing runtime snapshot.
*
* This exposes the same resolved component metadata FrontEdit uses for editing
* while keeping handler resolution and schema runtime execution owned by FrontEdit.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Detailed editing runtime snapshot.
*/
function buildResolvedEditingRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const runtimeComponentsById = bundle.runtime.componentsById && typeof bundle.runtime.componentsById === 'object'
? bundle.runtime.componentsById
: {};
const componentDetails = [];
const componentsById = {};
(Array.isArray(bundle.runtime.editableComponents) ? bundle.runtime.editableComponents : []).forEach(component => {
const componentId = String(component?.id || '').trim();
const detail = buildEditingRuntimeComponentDetail(component, componentId ? runtimeComponentsById[componentId] : null);
if (!detail || !componentId) {
return;
}
componentDetails.push(detail);
componentsById[componentId] = detail;
});
Object.keys(runtimeComponentsById).forEach(componentId => {
if (componentsById[componentId]) {
return;
}
const detail = buildEditingRuntimeComponentDetail(runtimeComponentsById[componentId], runtimeComponentsById[componentId]);
if (!detail) {
return;
}
detail.id = componentId;
componentDetails.push(detail);
componentsById[componentId] = detail;
});
const mediaComponentId = String(bundle.runtime?.mediaComponent?.id || '').trim();
const mediaComponent = mediaComponentId && componentsById[mediaComponentId]
? componentsById[mediaComponentId]
: null;
return {
uuid: bundle.uuid,
element: bundle.element instanceof Element ? bundle.element : null,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
runtimeHandler: {
id: String(bundle.handler.id || '').trim(),
},
runtime: {
blockName: String(bundle.runtime.blockName || '').trim(),
editableComponents: componentDetails,
componentsById,
mediaComponent,
},
schema: {
handlerId: String(bundle.schema?.handlerId || bundle.handler.id || '').trim(),
},
};
}
/**
* Return a plain snapshot of one editable component.
*
* @param {Object|null} component Runtime component.
* @returns {Object|null} Public component snapshot.
*/
function buildEditableComponentSnapshot(component) {
if (!component || typeof component !== 'object') {
return null;
}
const snapshot = {
id: String(component.id || '').trim(),
label: String(component.label || component.id || '').trim(),
type: String(component.type || '').trim().toLowerCase(),
selector: String(component.selector || '').trim(),
default: !!component.default,
};
if (!snapshot.id || !snapshot.type) {
return null;
}
if (component.required === true) {
snapshot.required = true;
}
if (typeof component.placeholder === 'string' && component.placeholder.trim()) {
snapshot.placeholder = component.placeholder;
}
if (component.target && typeof component.target === 'object') {
snapshot.target = bridge.clonePlainData(component.target);
}
if (component.mediaDescriptor && typeof component.mediaDescriptor === 'object') {
snapshot.mediaDescriptor = bridge.clonePlainData(component.mediaDescriptor);
}
if (component.editorOptions && typeof component.editorOptions === 'object') {
snapshot.editor = bridge.clonePlainData(component.editorOptions);
}
return snapshot;
}
/**
* Resolve the public runtime mode label.
*
* @param {Object|null} runtime Schema runtime.
* @returns {string} Runtime mode label.
*/
function resolvePublicRuntimeMode(runtime) {
const components = Array.isArray(runtime?.editableComponents) ? runtime.editableComponents : [];
const hasText = components.some(component => String(component?.type || '').trim().toLowerCase() !== 'file');
const hasFile = components.some(component => String(component?.type || '').trim().toLowerCase() === 'file');
if (hasText && hasFile) {
return 'mixed';
}
if (hasFile) {
return 'file';
}
return 'text';
}
/**
* Build one public runtime snapshot from a schema bundle.
*
* @param {Object|null} bundle Runtime bundle.
* @returns {Object|null} Public runtime snapshot.
*/
function buildResolvedRuntimeSnapshot(bundle) {
if (!bundle || !bundle.runtime || !bundle.handler) {
return null;
}
const components = Array.isArray(bundle.runtime.editableComponents)
? bundle.runtime.editableComponents.map(buildEditableComponentSnapshot).filter(Boolean)
: [];
const defaultComponent = components.find(component => component.default) || components[0] || null;
return {
uuid: bundle.uuid,
handlerId: String(bundle.handler.id || '').trim(),
blockName: String(bundle.runtime.blockName || getUuidEntry(bundle.uuid)?.blockName || '').trim(),
schemaVersion: Number(bundle.schema?.version || 1) || 1,
mode: resolvePublicRuntimeMode(bundle.runtime),
defaultComponentId: defaultComponent ? defaultComponent.id : '',
components,
};
}
/**
* Resolve a public media descriptor for one target block.
*
* @param {Object} options Media resolution options.
* @returns {Object|null} Media descriptor snapshot.
*/
function resolveMediaDescriptor(options = {}) {
const runtime = resolveRuntimeBundle(options);
if (!runtime) {
return null;
}
const componentId = String(options.componentId || '').trim();
const components = Array.isArray(runtime.runtime.editableComponents) ? runtime.runtime.editableComponents : [];
const selected = componentId
? components.find(component => component && component.id === componentId)
: (
(runtime.runtime.mediaComponent && typeof runtime.runtime.mediaComponent === 'object'
? runtime.runtime.mediaComponent
: components.find(component => String(component?.type || '').trim().toLowerCase() === 'file')
) || null
);
if (!selected || !selected.mediaDescriptor || typeof selected.mediaDescriptor !== 'object') {
return null;
}
return bridge.clonePlainData(selected.mediaDescriptor);
}
/**
* Build one stable block snapshot.
*
* @param {string} uuid Target UUID.
* @returns {Object|null} Block snapshot.
*/
function buildBlockSnapshot(uuid) {
const normalizedUuid = String(uuid || '').trim();
const entry = getUuidEntry(normalizedUuid);
if (!normalizedUuid || !entry) {
return null;
}
return {
uuid: normalizedUuid,
blockName: String(entry.blockName || '').trim(),
handlerIds: Array.isArray(entry.handlers) ? entry.handlers.slice() : [],
isPending: !!entry.is_pending,
pendingInfo: entry.pending_info ? bridge.clonePlainData(entry.pending_info) : null,
elementPresent: !!getElementByUuid(normalizedUuid),
};
}
/**
* Return stable discovery snapshots for every editable block on this page.
*
* @returns {Array} Editable block snapshots in FrontEdit UUID-map order.
*/
function getEditableBlocks() {
const context = getContext();
const uuidMap = context.uuidMap && typeof context.uuidMap === 'object'
? context.uuidMap
: {};
return Object.keys(uuidMap)
.map(uuid => {
const snapshot = buildBlockSnapshot(uuid);
if (!snapshot) {
return null;
}
const element = getElementByUuid(uuid);
return {
...snapshot,
contentText: element instanceof Element
? String(element.textContent || '').replace(/\s+/g, ' ').trim()
: '',
};
})
.filter(Boolean);
}
/**
* Determine whether a block currently has tracked unsaved changes.
*
* @param {string} uuid Block UUID.
* @returns {boolean} Whether the block is dirty.
*/
function isDirtyUuid(uuid) {
const batchManager = SFE.BatchEditManager || null;
return !!(
uuid &&
batchManager &&
batchManager.dirtyBlocks instanceof Map &&
batchManager.dirtyBlocks.has(uuid)
);
}
/**
* Build one public editor snapshot from a live editor state object.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object|null} Stable editor snapshot.
*/
function buildEditorSnapshot(editorState) {
if (!editorState || !editorState.uuid) {
return null;
}
const draftEditState = getContext().draftEditState;
const isDraftSession = !!(
draftEditState &&
draftEditState.draftElement &&
draftEditState.draftElement === editorState.element
);
const activeComponentId = String(
editorState.activeComponentId
|| editorState.activeEditableComponent?.id
|| ''
).trim();
const activeComponentType = String(
editorState._mwpActiveComponentType
|| editorState.activeEditableComponent?.type
|| (editorState.isMediaEditor ? 'file' : 'text')
).trim().toLowerCase();
const activeEditorHost = resolveActiveEditorHost(editorState);
return {
uuid: String(editorState.uuid || '').trim(),
handlerId: String(editorState._mwpSchemaHandlerId || editorState.handler?.id || '').trim(),
mode: String(editorState.requestedMode || (isDraftSession ? 'draft' : 'edit')).trim() || 'edit',
blockName: String(editorState.blockName || editorState.blockState?.blockName || '').trim(),
isOpen: true,
componentType: activeComponentType || 'text',
componentId: activeComponentId,
saveStrategy: String(editorState.saveStrategy || 'single').trim() || 'single',
hasMediaSession: typeof activeEditorHost?.applyMediaSelection === 'function',
isDirty: isDirtyUuid(editorState.uuid),
isDraftSession,
isBatchSession: !!(
editorState.saveStrategy === 'batch' &&
SFE.BatchEditManager &&
typeof SFE.BatchEditManager.isSessionActive === 'function' &&
SFE.BatchEditManager.isSessionActive()
),
};
}
/**
* Switch the active editable component inside one open editor session.
*
* @param {Object} editorState Active editor state.
* @param {string} componentId Target component identifier.
* @returns {boolean} Whether the switch succeeded.
*/
function activateEditorComponent(editorState, componentId) {
if (!editorState || !componentId) {
return false;
}
if (SFE.TextEditor && typeof SFE.TextEditor.activateEditableComponentById === 'function') {
return !!SFE.TextEditor.activateEditableComponentById(editorState, componentId);
}
return false;
}
/**
* Open one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Open-editor options.
* @returns {Promise} Editor snapshot after opening.
*/
async function openEditor(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const handler = resolveEditHandler({
uuid,
element,
handlerId: options.handlerId,
});
const componentId = String(options.componentId || '').trim();
if (!uuid || !(element instanceof Element) || !handler || typeof SFE.startEditing !== 'function') {
return null;
}
const activeEditor = getContext().activeEditor || null;
if (activeEditor && String(activeEditor.uuid || '').trim() === uuid) {
if (componentId) {
activateEditorComponent(activeEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || activeEditor);
}
if (activeEditor && typeof SFE.closeInPlaceEditor === 'function') {
SFE.closeInPlaceEditor(activeEditor, true, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
});
}
await Promise.resolve(
SFE.startEditing(
element,
handler,
uuid,
null,
false,
String(options.mode || 'edit').trim() || 'edit',
{
initialComponentId: componentId,
source: String(options.source || 'external').trim() || 'external',
originalSnapshots: options.originalSnapshots && typeof options.originalSnapshots === 'object'
? {
restoreOuterHTML: String(options.originalSnapshots.restoreOuterHTML || '').trim(),
cleanOuterHTML: String(options.originalSnapshots.cleanOuterHTML || '').trim(),
}
: null,
}
)
);
const openedEditor = getContext().activeEditor || null;
if (openedEditor && componentId) {
activateEditorComponent(openedEditor, componentId);
}
return buildEditorSnapshot(getContext().activeEditor || openedEditor);
}
/**
* Close one editor session through the supported FrontEdit runtime path.
*
* @param {Object} options Close-editor options.
* @returns {boolean} Whether a close was attempted.
*/
function closeEditor(options = {}) {
const activeEditor = getContext().activeEditor || null;
if (!activeEditor || typeof SFE.closeInPlaceEditor !== 'function') {
return false;
}
const targetUuid = String(options.uuid || '').trim();
if (targetUuid && String(activeEditor.uuid || '').trim() !== targetUuid) {
return false;
}
return SFE.closeInPlaceEditor(activeEditor, options.restoreOriginal !== false, {
closeReason: String(options.reason || 'api').trim() || 'api',
source: String(options.source || 'external').trim() || 'external',
}) !== false;
}
/**
* Resolve the active editor state for one target block UUID.
*
* Schema-backed editor operations intentionally require a live open editor so
* the DOM mutation stays inside the same save/history session FrontEdit owns.
*
* @param {Object} options Editor-operation options.
* @returns {Object|null} Active editor state.
*/
function resolveActiveEditorState(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
return resolveActiveEditorHost(activeEditor) ? activeEditor : null;
}
function resolveActiveEditorHost(editorState) {
return SFE.SchemaEditorHost?.resolveActiveEditorHost?.(editorState) || null;
}
/**
* Resolve the session-owned history API for one active editor host.
*
* External runtime mutations should commit through the same shared block
* session contract used internally instead of reaching into ad hoc
* editor-state history fields.
*
* @param {Object|null} editorHost Active schema editor host.
* @returns {Object|null} Session history API, or null when unavailable.
*/
function getSessionHistoryApiForHost(editorHost) {
if (editorHost?.historyApi && typeof editorHost.historyApi === 'object') {
return editorHost.historyApi;
}
if (typeof editorHost?.getSessionHistoryApi === 'function') {
return editorHost.getSessionHistoryApi();
}
const blockEditSession = editorHost?.blockEditSession || null;
if (!blockEditSession || typeof blockEditSession.getHistoryApi !== 'function') {
return null;
}
return blockEditSession.getHistoryApi(
String(editorHost?.blockEditSessionScopeId || '').trim() || 'text'
);
}
/**
* Refresh one active editor state after a schema-driven DOM mutation.
*
* @param {Object} editorState Active editor state.
* @returns {void}
*/
function syncActiveEditorState(editorState) {
if (!editorState?.handler) {
return;
}
const editorHost = resolveActiveEditorHost(editorState);
const blockRootElement = (
editorHost &&
typeof editorHost.getBlockRootElement === 'function'
)
? editorHost.getBlockRootElement()
: null;
if (blockRootElement && blockRootElement !== editorState.element) {
// The active schema host element can be one editable component (for
// example, one table cell), while block-session history and restore must
// keep targeting the outer block root. Rebasing `editorState.element` to
// the component host corrupts undo/redo by replaying full block markup
// into that component instead of the block wrapper.
editorState.element = blockRootElement;
}
if (!editorState.element) {
return;
}
const schemaRuntime = SFE.SchemaRuntime || null;
if (
schemaRuntime &&
typeof schemaRuntime.syncPlaceholders === 'function'
) {
schemaRuntime.syncPlaceholders(editorState.element, editorState.handler, {
blockAttributes: editorState.blockState?.attrs || null,
attributeChanges: editorState.attributeChanges || null,
});
}
if (
SFE.TextEditor &&
typeof SFE.TextEditor.refreshEditableComponents === 'function'
) {
SFE.TextEditor.refreshEditableComponents(editorState);
}
}
/**
* Build one component map for the active schema editor state.
*
* @param {Object|null} editorState Active editor state.
* @returns {Object} Editable components keyed by ID.
*/
function getActiveEditorComponentMap(editorState) {
if (!editorState || !Array.isArray(editorState.editableComponents)) {
return {};
}
return editorState.editableComponents.reduce((map, component) => {
const componentId = String(component?.id || '').trim();
if (!componentId || !component?.element) {
return map;
}
map[componentId] = component;
return map;
}, {});
}
/**
* Return the lowercase host tag name for one candidate component element.
*
* @param {Element|null} hostElement Candidate host element.
* @returns {string} Lowercase host tag name.
*/
function getHostElementTagName(hostElement) {
return hostElement && hostElement.tagName
? String(hostElement.tagName || '').trim().toLowerCase()
: '';
}
/**
* Build one inline format wrapper for a structured run token.
*
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {string} formatToken Format token.
* @param {Object|null} formatAttributes Optional format attributes.
* @returns {HTMLElement|null} Wrapper element.
*/
function buildStructuredFormatWrapper(inlineCapabilities, formatToken, formatAttributes = null) {
const tagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!tagName) {
return null;
}
const element = document.createElement(tagName);
if (tagName === 'a' && formatAttributes && typeof formatAttributes === 'object') {
const href = String(formatAttributes.href || '').trim();
if (href) {
element.setAttribute('href', href);
}
const target = String(formatAttributes.target || '').trim();
if (target) {
element.setAttribute('target', target);
}
const rel = String(formatAttributes.rel || '').trim();
if (rel) {
element.setAttribute('rel', rel);
}
}
return element;
}
/**
* Hoist same-tag inline format attributes onto the host component element.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Element|null} hostElement Live host element.
* @returns {{runs: Array, hostAttributes: Object}} Hoisted run data.
*/
function hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement = null) {
const hostTagName = getHostElementTagName(hostElement);
if (!hostTagName) {
return {
runs: Array.isArray(runs) ? runs : [],
hostAttributes: {},
};
}
const hostAttributes = {};
const normalizedRuns = (Array.isArray(runs) ? runs : []).map(run => {
const formats = Array.isArray(run?.formats) ? run.formats : [];
const formatAttributes = run?.formatAttributes && typeof run.formatAttributes === 'object'
? { ...run.formatAttributes }
: {};
const nextFormats = [];
formats.forEach(formatToken => {
const formatTagName = String(inlineCapabilities?.[formatToken]?.tag || '').trim().toLowerCase();
if (!formatTagName || formatTagName !== hostTagName) {
nextFormats.push(formatToken);
return;
}
const attributes = formatAttributes?.[formatToken];
if (attributes && typeof attributes === 'object' && !Array.isArray(attributes)) {
Object.assign(hostAttributes, attributes);
}
delete formatAttributes[formatToken];
});
return {
...run,
formats: nextFormats,
formatAttributes,
};
});
return {
runs: normalizedRuns,
hostAttributes,
};
}
/**
* Apply hoisted inline-format attributes onto one host element.
*
* @param {Element|null} hostElement Live host element.
* @param {Object|null} hostAttributes Hoisted host attributes.
* @returns {void}
*/
function applyStructuredHostLevelFormatAttributes(hostElement, hostAttributes = null) {
if (!hostElement || typeof hostElement.setAttribute !== 'function' || typeof hostElement.removeAttribute !== 'function') {
return;
}
const attributes = hostAttributes && typeof hostAttributes === 'object' ? hostAttributes : {};
['href', 'target', 'rel'].forEach(attributeName => {
const value = typeof attributes[attributeName] === 'string'
? attributes[attributeName].trim()
: '';
if (value) {
hostElement.setAttribute(attributeName, value);
return;
}
hostElement.removeAttribute(attributeName);
});
}
/**
* Build deterministic HTML from one normalized structured run sequence.
*
* @param {Array} runs Structured text runs.
* @param {Object} inlineCapabilities Allowed inline format capability map.
* @param {Object} editorOptions Runtime editor options.
* @param {Element|null} hostElement Live host element.
* @returns {{html: string, hostAttributes: Object}} Rendered HTML fragment.
*/
function buildStructuredHtmlFromRuns(runs, inlineCapabilities, editorOptions = {}, hostElement = null) {
const container = document.createElement('div');
const extractionOptions = editorOptions?.options && typeof editorOptions.options === 'object'
? editorOptions.options
: {};
const hoisted = hoistStructuredHostLevelFormats(runs, inlineCapabilities, hostElement);
(Array.isArray(hoisted.runs) ? hoisted.runs : []).forEach(run => {
const text = String(run?.text || '');
if (!text) {
return;
}
const segments = extractionOptions.newlinesToBR === true || String(editorOptions?.enterMode || '').trim() === 'linebreak'
? text.split('\n')
: [text];
segments.forEach((segment, index) => {
if (index > 0) {
container.appendChild(document.createElement('br'));
}
if (!segment) {
return;
}
let node = document.createTextNode(segment);
(Array.isArray(run.formats) ? run.formats : []).forEach(formatToken => {
const wrapper = buildStructuredFormatWrapper(
inlineCapabilities,
formatToken,
run.formatAttributes && typeof run.formatAttributes === 'object'
? run.formatAttributes[formatToken] || null
: null
);
if (!wrapper) {
return;
}
wrapper.appendChild(node);
node = wrapper;
});
container.appendChild(node);
});
});
return {
html: container.innerHTML,
hostAttributes: hoisted.hostAttributes,
};
}
/**
* Apply structured component updates onto the active editor DOM.
*
* @param {Object} editorState Active editor state.
* @param {Array} componentUpdates Normalized component updates.
* @returns {string[]} Updated component IDs.
*/
function applyStructuredComponentUpdates(editorState, componentUpdates) {
const componentMap = getActiveEditorComponentMap(editorState);
const updatedComponentIds = [];
(Array.isArray(componentUpdates) ? componentUpdates : []).forEach(componentUpdate => {
const componentId = String(componentUpdate?.componentId || '').trim();
const component = componentMap[componentId] || null;
if (!component || !component.element) {
return;
}
if (componentUpdate.bindingSource === 'plaintext') {
component.element.textContent = (Array.isArray(componentUpdate.runs) ? componentUpdate.runs : [])
.map(run => String(run?.text || ''))
.join('');
updatedComponentIds.push(componentId);
return;
}
if (componentUpdate.bindingSource === 'html') {
const rendered = buildStructuredHtmlFromRuns(
componentUpdate.runs,
componentUpdate.inlineCapabilities || {},
componentUpdate.editorOptions || component.editorOptions || {},
component.element
);
component.element.innerHTML = rendered.html;
applyStructuredHostLevelFormatAttributes(component.element, rendered.hostAttributes);
updatedComponentIds.push(componentId);
}
});
return Array.from(new Set(updatedComponentIds));
}
/**
* Resolve the active list-editor state for one target block UUID.
*
* Structural list operations intentionally require a live open editor so the
* mutation happens inside the same DOM/save session FrontEdit already owns.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Active list editor state.
*/
function resolveActiveListEditorState(options = {}) {
const activeEditor = resolveActiveEditorState(options);
if (!activeEditor) {
return null;
}
const listTracker = SFE.ListBlockTracker || null;
if (
!listTracker ||
!activeEditor.element ||
(activeEditor.element.tagName !== 'UL' && activeEditor.element.tagName !== 'OL')
) {
return null;
}
if (!activeEditor.listTracker || activeEditor.listTracker.listElement !== activeEditor.element) {
activeEditor.listTracker = listTracker.init(
activeEditor.element,
activeEditor.blockState || {}
);
}
return activeEditor.listTracker ? activeEditor : null;
}
/**
* Refresh one active list editor after a structural DOM mutation.
*
* @param {Object} editorState Active list editor state.
* @returns {void}
*/
function syncActiveListEditorState(editorState) {
if (!editorState?.element || !editorState?.handler) {
return;
}
if (editorState.listTracker?.listElement && editorState.listTracker.listElement !== editorState.element) {
editorState.element = editorState.listTracker.listElement;
const editorHost = resolveActiveEditorHost(editorState);
if (editorHost && typeof editorHost === 'object') {
editorHost.element = editorState.listTracker.listElement;
}
}
syncActiveEditorState(editorState);
}
/**
* Apply one or more schema-declared block-attribute operations to the active editor.
*
* @param {Object} options Block-attribute operation options.
* @returns {Object|null} Result summary and current tracked attribute changes.
*/
function applyBlockAttributeOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeBlockAttributeOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
attributeChanges: bridge.clonePlainData(executionResult.attributeChanges || {}),
};
}
/**
* Apply one or more component-content replacement operations to the active editor.
*
* The shared executor owns rich-text run rendering and link normalization so
* external callers can target component surfaces without reimplementing FrontEdit's
* browser mutation semantics.
*
* @param {Object} options Component-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyTextComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeComponentOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations,
saveHistory: true,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more schema-backed media replacements to the active editor.
*
* This targets the currently active media-session host so external callers
* can replace schema media components without bypassing FrontEdit's own preview and
* resolved-media update pipeline.
*
* @param {Object} options Media-operation options.
* @returns {Object|null} Result summary and updated component IDs.
*/
function applyMediaComponentOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeMediaOperations !== 'function'
) {
return null;
}
const executionResult = operationExecutor.executeMediaOperations({
editorState,
editorHost,
operations,
afterSync: function() {
syncActiveEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(executionResult.updatedComponentIds)
? executionResult.updatedComponentIds.slice()
: [],
operationsApplied: Array.isArray(executionResult.operationsApplied)
? executionResult.operationsApplied.slice()
: [],
};
}
/**
* Apply one or more high-level public list operations to the currently open
* list editor.
*
* The runtime API accepts UUID-oriented public commands and lets the shared
* executor translate them into the internal primitive tracker operations.
*
* @param {Object} options List-operation options.
* @returns {Object|null} Result summary and next list structure.
*/
function applyListOperationsToActiveEditor(options = {}) {
const editorState = resolveActiveListEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const operations = Array.isArray(options.operations)
? options.operations
: (options.operation ? [ options.operation ] : []);
const operationExecutor = SFE.SchemaOperationExecutor || null;
const listTracker = SFE.ListBlockTracker || null;
if (
!editorState ||
!editorHost ||
!operations.length ||
!operationExecutor ||
typeof operationExecutor.executeListOperations !== 'function' ||
!listTracker
) {
return null;
}
const executionResult = operationExecutor.executeListOperations({
tracker: editorState.listTracker,
editorHost,
operations,
targetResolutionMode: 'api_uuid',
saveHistory: true,
afterSync: function() {
syncActiveListEditorState(editorState);
},
});
if (!executionResult) {
return null;
}
return {
uuid: String(editorState.uuid || '').trim(),
operationsApplied: executionResult.operationsApplied,
structure: listTracker.getStructure(editorState.listTracker),
};
}
/**
* Apply one normalized structured edit payload to the active schema editor.
*
* This keeps non-list staged edits inside the same public runtime contract as
* list operations: open the editor first, then let FrontEdit own the mutation and
* history bookkeeping.
*
* @param {Object} options Structured-edit options.
* @returns {Object|null} Result summary for the applied edit.
*/
function applyStructuredEditToActiveEditor(options = {}) {
const editorState = resolveActiveEditorState(options);
const editorHost = resolveActiveEditorHost(editorState);
const componentUpdates = Array.isArray(options.componentUpdates)
? options.componentUpdates.filter(operation => operation && typeof operation === 'object' && !Array.isArray(operation))
: [];
const attributeOperations = Array.isArray(options.attributeOperations)
? options.attributeOperations.filter(operation => (
operation &&
typeof operation === 'object' &&
!Array.isArray(operation) &&
typeof operation.id === 'string' &&
operation.id.trim() &&
Object.prototype.hasOwnProperty.call(operation, 'value')
))
: [];
const operationExecutor = SFE.SchemaOperationExecutor || null;
if (!editorState || !editorHost || (!componentUpdates.length && !attributeOperations.length)) {
return null;
}
let attributeResult = null;
let componentResult = null;
if (
componentUpdates.length
&& operationExecutor
&& typeof operationExecutor.executeComponentOperations === 'function'
) {
componentResult = operationExecutor.executeComponentOperations({
editorState,
editorHost,
operations: componentUpdates,
saveHistory: false,
});
}
if (
attributeOperations.length &&
operationExecutor &&
typeof operationExecutor.executeBlockAttributeOperations === 'function'
) {
attributeResult = operationExecutor.executeBlockAttributeOperations({
editorHost,
operations: attributeOperations,
saveHistory: false,
});
}
const didApplyAttributes = !!(
attributeResult &&
Array.isArray(attributeResult.operationsApplied) &&
attributeResult.operationsApplied.length
);
const didApplyComponents = !!(
componentResult &&
Array.isArray(componentResult.updatedComponentIds) &&
componentResult.updatedComponentIds.length
);
if (!didApplyComponents && !didApplyAttributes) {
return null;
}
syncActiveEditorState(editorState);
const historyApi = getSessionHistoryApiForHost(editorHost);
if (typeof historyApi?.saveToHistory === 'function') {
historyApi.saveToHistory();
}
if (typeof editorHost.updateToolbarState === 'function') {
editorHost.updateToolbarState();
}
return {
uuid: String(editorState.uuid || '').trim(),
updatedComponentIds: Array.isArray(componentResult?.updatedComponentIds)
? componentResult.updatedComponentIds
: [],
operationsApplied: [
...(Array.isArray(componentResult?.operationsApplied) ? componentResult.operationsApplied : []),
...(Array.isArray(attributeResult?.operationsApplied) ? attributeResult.operationsApplied : []),
],
attributeChanges: bridge.clonePlainData(editorHost.attributeChanges || {}),
};
}
/**
* Return a structural snapshot for one list block.
*
* This resolves against the live DOM so external tooling can inspect the
* current nested list tree without parsing serialized block markup itself.
*
* @param {Object} options Lookup options.
* @returns {Object|null} List structure snapshot.
*/
function getListStructure(options = {}) {
const uuid = String(options.uuid || '').trim() || getUuidForElement(options.element);
const element = options.element instanceof Element ? options.element : getElementByUuid(uuid);
const listTracker = SFE.ListBlockTracker || null;
if (
!uuid ||
!(element instanceof Element) ||
!listTracker ||
(element.tagName !== 'UL' && element.tagName !== 'OL')
) {
return null;
}
const tracker = element._mwpListTracker
|| listTracker.init(element, resolveBlockState(uuid) || {});
return tracker && typeof listTracker.getStructure === 'function'
? listTracker.getStructure(tracker)
: null;
}
/**
* Apply one selected media item to the active schema-media editor session.
*
* This uses the same internal selection transition FrontEdit normally runs after
* the user picks an item from the media library or upload flow.
*
* @param {Object} options Media-selection options.
* @returns {Object|null} Stable editor snapshot after the selection is applied.
*/
function applyActiveMediaSelection(options = {}) {
const activeEditor = getContext().activeEditor || null;
const targetUuid = String(options.uuid || '').trim();
if (!activeEditor || !targetUuid || String(activeEditor.uuid || '').trim() !== targetUuid) {
return null;
}
const editorHost = resolveActiveEditorHost(activeEditor);
const url = String(options.url || '').trim();
if (!editorHost || typeof editorHost.applyMediaSelection !== 'function' || !url) {
return null;
}
const attachmentId = Object.prototype.hasOwnProperty.call(options, 'attachmentId')
? options.attachmentId
: null;
const fromState = String(options.source || '').trim().toLowerCase() === 'library'
? 'library'
: 'input';
const didApply = editorHost.applyMediaSelection(url, attachmentId, { fromState });
return didApply === false ? null : buildEditorSnapshot(activeEditor);
}
/**
* Refresh one block through the supported server-rendered replacement path.
*
* @param {string} uuid Block UUID.
* @param {Object} [options={}] Refresh options.
* @returns {Promise} Refreshed block snapshot.
*/
async function refreshBlock(uuid, options = {}) {
const normalizedUuid = String(uuid || '').trim();
const element = getElementByUuid(normalizedUuid);
if (
!normalizedUuid ||
!(element instanceof Element) ||
!SFE.SaveHelpers ||
typeof SFE.SaveHelpers.fetchRenderedBlockHTML !== 'function' ||
!SFE.ElementUpdater ||
typeof SFE.ElementUpdater.applyNewHTML !== 'function'
) {
return null;
}
const html = await SFE.SaveHelpers.fetchRenderedBlockHTML(normalizedUuid, options || {});
if (!html) {
return null;
}
const newElement = SFE.ElementUpdater.applyNewHTML(element, normalizedUuid, html);
if (!newElement) {
return null;
}
if (typeof SFE.ElementUpdater.rebindElement === 'function') {
SFE.ElementUpdater.rebindElement(newElement);
}
const block = buildBlockSnapshot(normalizedUuid);
if (block) {
bridge.emitEvent('block:refreshed', {
source: String(options.source || 'sfe').trim() || 'sfe',
block,
});
}
return block;
}
/**
* Return one public dirty-block snapshot.
*
* @param {Object} entry Dirty entry from the batch manager.
* @returns {Object|null} Dirty block snapshot.
*/
function buildDirtyBlockSnapshot(entry) {
if (!entry || typeof entry !== 'object') {
return null;
}
return {
uuid: String(entry.element_uuid || '').trim(),
handlerId: String(entry.handler_id || '').trim(),
blockName: String(entry.blockName || '').trim(),
beforeRaw: String(entry.before || '').trim(),
afterRaw: String(entry.after || '').trim(),
};
}
/**
* Reset dirty batch state for one or more target UUIDs.
*
* @param {string[]} uuids Target UUIDs.
* @returns {boolean} Whether any batch state was reset.
*/
function resetDirtyBlocks(uuids) {
const batchManager = SFE.BatchEditManager || null;
const normalizedUuids = Array.isArray(uuids)
? uuids.map(uuid => String(uuid || '').trim()).filter(Boolean)
: [];
if (!normalizedUuids.length || !batchManager) {
return false;
}
if (typeof batchManager.resetDirtyBlocks === 'function') {
return !!batchManager.resetDirtyBlocks(normalizedUuids);
}
let changed = false;
normalizedUuids.forEach(uuid => {
if (batchManager.dirtyBlocks instanceof Map && batchManager.dirtyBlocks.has(uuid)) {
batchManager.dirtyBlocks.delete(uuid);
changed = true;
}
if (
batchManager.currentBlockStates instanceof Map &&
batchManager.baseBlockStates instanceof Map &&
batchManager.baseBlockStates.has(uuid)
) {
batchManager.currentBlockStates.set(
uuid,
bridge.clonePlainData(batchManager.baseBlockStates.get(uuid))
);
changed = true;
}
});
return changed;
}
/**
* Return the stable PublicApi discovery snapshot.
*
* @returns {Object} API metadata.
*/
function getApiInfo() {
return {
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,
},
};
}
PublicApi._buildEditorSnapshot = buildEditorSnapshot;
PublicApi.getApiInfo = getApiInfo;
PublicApi.isEditorOpen = function isEditorOpen() {
return !!getContext().activeEditor;
};
PublicApi.getActiveEditor = function getActiveEditor() {
return buildEditorSnapshot(getContext().activeEditor || null);
};
PublicApi.applyActiveMediaSelection = function applyActiveMediaSelectionPublic(options = {}) {
return applyActiveMediaSelection(options);
};
PublicApi.getPageContext = function getPageContext() {
const context = getContext();
const managerData = getManagerData();
return {
postId: Number(managerData.postId || 0) || 0,
permissions: bridge.clonePlainData(managerData.permissions || {}),
hasDraftPreview: !!(
document.body.classList.contains('mwp-sfe-active-preview') ||
context.activeMode === 'draft'
),
isEditorOpen: !!context.activeEditor,
activeMode: String(context.activeMode || '').trim(),
};
};
PublicApi.getRestContext = function getRestContext() {
const managerData = getManagerData();
return {
baseUrl: String(managerData.restBase || '').trim(),
namespaceUrl: String(managerData.restUrl || '').trim(),
nonce: String(managerData.nonce || '').trim(),
};
};
PublicApi.setRestNonce = function setRestNonce(nonce) {
const normalizedNonce = normalizeRestNonce(nonce);
if (!normalizedNonce) {
return PublicApi.getRestContext();
}
const managerData = getManagerData();
managerData.nonce = normalizedNonce;
SFE.ManagerData = managerData;
return PublicApi.getRestContext();
};
PublicApi.getElementByUuid = getElementByUuid;
PublicApi.getUuidForElement = getUuidForElement;
PublicApi.getBlockSnapshot = function getBlockSnapshot(uuid) {
return buildBlockSnapshot(uuid);
};
PublicApi.getEditableBlocks = getEditableBlocks;
PublicApi.resolveRuntime = function resolveRuntime(options = {}) {
return buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.resolveEditingRuntime = function resolveEditingRuntime(options = {}) {
return buildResolvedEditingRuntimeSnapshot(resolveRuntimeBundle(options));
};
PublicApi.getEditableComponents = function getEditableComponents(options = {}) {
const runtime = buildResolvedRuntimeSnapshot(resolveRuntimeBundle(options));
return runtime ? runtime.components : [];
};
PublicApi.getListStructure = getListStructure;
PublicApi.getDefaultComponent = function getDefaultComponent(options = {}) {
const components = PublicApi.getEditableComponents(options);
return components.find(component => component.default) || components[0] || null;
};
PublicApi.getMediaDescriptor = function getMediaDescriptor(options = {}) {
return resolveMediaDescriptor(options);
};
PublicApi.getMediaContext = function getMediaContext(options = {}) {
const descriptor = resolveMediaDescriptor(options);
const mediaHelper = SFE.MediaHelper || null;
if (!descriptor || !mediaHelper || typeof mediaHelper.getMediaType !== 'function') {
return null;
}
const mediaType = String(mediaHelper.getMediaType(descriptor) || '').trim();
if (!mediaType) {
return null;
}
return {
supported: true,
componentId: String(descriptor.componentId || '').trim(),
mediaType,
accept: typeof mediaHelper.getAcceptTypes === 'function'
? String(mediaHelper.getAcceptTypes(descriptor) || '*/*').trim() || '*/*'
: '*/*',
label: mediaType === 'audio' ? 'audio block' : (mediaType === 'video' ? 'video block' : (mediaType === 'file' ? 'file block' : 'media block')),
descriptor,
};
};
PublicApi.isMediaEditable = function isMediaEditable(options = {}) {
return !!PublicApi.getMediaContext(options);
};
PublicApi.openEditor = openEditor;
PublicApi.closeEditor = closeEditor;
PublicApi.applyTextComponentOperations = applyTextComponentOperationsToActiveEditor;
PublicApi.applyMediaComponentOperations = applyMediaComponentOperationsToActiveEditor;
PublicApi.applyBlockAttributeOperations = applyBlockAttributeOperationsToActiveEditor;
PublicApi.applyListOperations = applyListOperationsToActiveEditor;
PublicApi.applyStructuredEdit = applyStructuredEditToActiveEditor;
PublicApi.stageBlockState = function stageBlockState(stage) {
const uuid = String(stage && stage.uuid ? stage.uuid : '').trim();
if (!uuid || !stage || typeof stage !== 'object' || !stage.blockState || typeof stage.blockState !== 'object') {
return;
}
bridge.stagedBlockStates.set(uuid, {
handlerId: String(stage.handlerId || '').trim(),
source: String(stage.source || 'external').trim() || 'external',
blockState: bridge.clonePlainData(stage.blockState),
});
bridge.emitEvent('block:staged', {
source: String(stage.source || 'external').trim() || 'external',
uuid,
handlerId: String(stage.handlerId || '').trim(),
});
};
PublicApi.clearStagedBlockState = function clearStagedBlockState(uuid, source = 'external') {
const normalizedUuid = String(uuid || '').trim();
if (!normalizedUuid) {
return;
}
bridge.stagedBlockStates.delete(normalizedUuid);
bridge.emitEvent('block:stageCleared', {
source: String(source || 'external').trim() || 'external',
uuid: normalizedUuid,
});
};
PublicApi.refreshBlock = refreshBlock;
PublicApi.getDirtyBlocks = function getDirtyBlocks() {
const batchManager = SFE.BatchEditManager || null;
if (!batchManager || !(batchManager.dirtyBlocks instanceof Map)) {
return [];
}
return Array.from(batchManager.dirtyBlocks.values())
.map(buildDirtyBlockSnapshot)
.filter(entry => entry && entry.uuid);
};
PublicApi.hasDirtyBlocks = function hasDirtyBlocks() {
return PublicApi.getDirtyBlocks().length > 0;
};
PublicApi.isBatchSessionActive = function isBatchSessionActive() {
const batchManager = SFE.BatchEditManager || null;
return !!(
batchManager &&
typeof batchManager.isSessionActive === 'function' &&
batchManager.isSessionActive()
);
};
PublicApi.resetDirtyBlocks = function resetDirtyBlocksPublic(uuids) {
return resetDirtyBlocks(uuids);
};
SFE.PublicApi = PublicApi;
})();