Skip to content
/**
* 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