/** * Schema runtime service. * * Reads PHP-provided schema contracts and exposes explicit runtime helpers * consumed by core editor/save modules. * * Exposes: * SFE.SchemaRuntime * SFE.SchemaV2 */ ( function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const RAW_SCHEMA_DATA = window.MWPSFE_Schema_Data || { version: 1, schemas: {} }; const VALID_COMPONENT_TYPES = new Set( [ 'text', 'file' ] ); const VALID_BLOCK_TYPES = new Set( [ 'text', 'media' ] ); const VALID_MEDIA_TYPES = new Set( [ 'image', 'audio', 'video', 'file', 'image_or_video', 'icon' ] ); const VALID_BINDING_SOURCES = new Set( [ 'html', 'plaintext', 'textalignment', 'columnalignment', 'url', 'id', 'media_type', 'list_block' ] ); const VALID_TARGET_SCOPES = new Set( [ 'component', 'block', 'column' ] ); const VALID_ENTER_MODES = new Set( [ 'auto', 'always', 'never', 'linebreak' ] ); const VALID_LINK_UI_MODES = new Set( [ 'auto', 'manual' ] ); const VALID_TAB_ACTIONS = new Set( [ 'none', 'indent', 'outdent', 'nextComponent', 'previousComponent' ] ); const VALID_MISSING_UI_MODES = new Set( [ 'ghost' ] ); const VALID_MISSING_UI_PLACEMENTS = new Set( [ 'append', 'prepend' ] ); const GHOST_COMPONENT_ATTR = 'data-mwp-sfe-ghost-component'; const GHOST_FLAG_ATTR = 'data-mwp-sfe-ghost'; const GHOST_CLASS = 'mwp-sfe-ghost-component'; function isPlainObject( value ) { return !!value && typeof value === 'object' && !Array.isArray( value ); } function isElementNode( value ) { return !!value && value.nodeType === Node.ELEMENT_NODE; } function normalizeBindingDefinition( rawBinding ) { if ( !isPlainObject( rawBinding ) ) return null; const path = typeof rawBinding.path === 'string' ? rawBinding.path.trim() : ''; const source = typeof rawBinding.source === 'string' ? rawBinding.source.trim().toLowerCase() : ''; if ( !path || !VALID_BINDING_SOURCES.has( source ) ) return null; const binding = { path, source }; if ( rawBinding.resolved === true ) { binding.resolved = true; } if ( typeof rawBinding.value === 'string' && rawBinding.value.trim() ) { binding.value = rawBinding.value.trim(); } return binding; } function normalizeEditorExtractionOptions( rawOptions ) { if ( !isPlainObject( rawOptions ) ) return null; const options = {}; if ( rawOptions.preserveNewlines === true ) { options.preserveNewlines = true; } if ( rawOptions.newlinesToBR === true ) { options.newlinesToBR = true; } return Object.keys( options ).length ? options : null; } function normalizeEditorTabMode( rawTabMode ) { if ( !isPlainObject( rawTabMode ) ) return null; const tab = typeof rawTabMode.tab === 'string' ? rawTabMode.tab.trim() : ''; const shiftTab = typeof rawTabMode.shiftTab === 'string' ? rawTabMode.shiftTab.trim() : ''; const normalized = {}; if ( tab && VALID_TAB_ACTIONS.has( tab ) ) { normalized.tab = tab; } if ( shiftTab && VALID_TAB_ACTIONS.has( shiftTab ) ) { normalized.shiftTab = shiftTab; } return Object.keys( normalized ).length ? normalized : null; } function normalizeEditorTarget( rawTarget ) { if ( typeof rawTarget === 'string' ) { const value = rawTarget.trim(); return value || null; } if ( !isPlainObject( rawTarget ) ) return null; const scope = typeof rawTarget.scope === 'string' ? rawTarget.scope.trim().toLowerCase() : ''; if ( scope && !VALID_TARGET_SCOPES.has( scope ) ) return null; const selector = typeof rawTarget.selector === 'string' ? rawTarget.selector.trim() : ''; const contextKey = typeof rawTarget.contextKey === 'string' ? rawTarget.contextKey.trim() : 'column'; return { scope: scope || 'component', selector, contextKey, }; } function normalizeEditorFormats( rawFormats, depth = 0 ) { if ( depth > 3 || !Array.isArray( rawFormats ) ) return null; const normalized = []; rawFormats.forEach( item => { if ( typeof item === 'string' ) { const token = item.trim(); if ( token ) { normalized.push( token ); } return; } if ( Array.isArray( item ) ) { const group = normalizeEditorFormats( item, depth + 1 ); if ( Array.isArray( group ) && group.length ) { normalized.push( group ); } } } ); return normalized.length ? normalized : null; } function normalizeInlineFormatCapability( rawCapability ) { if ( !isPlainObject( rawCapability ) ) return null; const tag = typeof rawCapability.tag === 'string' ? rawCapability.tag.trim().toLowerCase() : ''; if ( !tag ) return null; const normalizeStringArray = rawValue => { if ( !Array.isArray( rawValue ) ) return null; const values = rawValue .map( value => ( typeof value === 'string' ? value.trim() : '' ) ) .filter( Boolean ); return values.length ? values : null; }; const normalized = { tag }; const attributes = normalizeStringArray( rawCapability.attributes ); const requiredAttributes = normalizeStringArray( rawCapability.requiredAttributes ); const allowedTargets = normalizeStringArray( rawCapability.allowedTargets ); const allowedRelTokens = normalizeStringArray( rawCapability.allowedRelTokens ); const allowedProtocols = normalizeStringArray( rawCapability.allowedProtocols ); if ( attributes ) normalized.attributes = attributes; if ( requiredAttributes ) normalized.requiredAttributes = requiredAttributes; if ( allowedTargets ) normalized.allowedTargets = allowedTargets; if ( allowedRelTokens ) normalized.allowedRelTokens = allowedRelTokens; if ( allowedProtocols ) normalized.allowedProtocols = allowedProtocols; if ( typeof rawCapability.allowsRelativeUrls === 'boolean' ) { normalized.allowsRelativeUrls = rawCapability.allowsRelativeUrls; } if ( typeof rawCapability.allowsAnchorLinks === 'boolean' ) { normalized.allowsAnchorLinks = rawCapability.allowsAnchorLinks; } if ( typeof rawCapability.autoProtocol === 'string' && rawCapability.autoProtocol.trim() ) { normalized.autoProtocol = rawCapability.autoProtocol.trim().toLowerCase(); } if ( typeof rawCapability.preservesUnknownRelTokens === 'boolean' ) { normalized.preservesUnknownRelTokens = rawCapability.preservesUnknownRelTokens; } return normalized; } function normalizeInlineFormatCapabilities( rawCapabilities ) { if ( !isPlainObject( rawCapabilities ) ) return null; const normalized = {}; Object.keys( rawCapabilities ).forEach( key => { const cleanKey = typeof key === 'string' ? key.trim() : ''; if ( !cleanKey ) return; const capability = normalizeInlineFormatCapability( rawCapabilities[ key ] ); if ( capability ) { normalized[ cleanKey ] = capability; } } ); return Object.keys( normalized ).length ? normalized : null; } function normalizeEditorAttributeCapabilityValues( rawValues ) { if ( !Array.isArray( rawValues ) ) return null; const values = rawValues.filter( value => ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null ) ); return values.length ? values : null; } function normalizeEditorAttributeCapability( rawCapability ) { if ( !isPlainObject( rawCapability ) ) return null; const normalized = {}; const attribute = typeof rawCapability.attribute === 'string' ? rawCapability.attribute.trim() : ''; const attributes = Array.isArray( rawCapability.attributes ) ? rawCapability.attributes .map( value => ( typeof value === 'string' ? value.trim() : '' ) ) .filter( Boolean ) : null; if ( attribute ) { normalized.attribute = attribute; } else if ( attributes && attributes.length ) { normalized.attributes = attributes; } else { return null; } const values = normalizeEditorAttributeCapabilityValues( rawCapability.values ); if ( values ) { normalized.values = values; } if ( rawCapability.tagChange === true ) { normalized.tagChange = true; } if ( rawCapability.preview === 'inline_style' ) { normalized.preview = 'inline_style'; } if ( Object.prototype.hasOwnProperty.call( rawCapability, 'unsetValue' ) && ( typeof rawCapability.unsetValue === 'string' || typeof rawCapability.unsetValue === 'number' || typeof rawCapability.unsetValue === 'boolean' || rawCapability.unsetValue === null ) ) { normalized.unsetValue = rawCapability.unsetValue; } return normalized; } function normalizeEditorAttributeCapabilities( rawCapabilities ) { if ( !isPlainObject( rawCapabilities ) ) return null; const normalized = {}; Object.keys( rawCapabilities ).forEach( key => { const cleanKey = typeof key === 'string' ? key.trim() : ''; if ( !cleanKey ) return; const capability = normalizeEditorAttributeCapability( rawCapabilities[ key ] ); if ( capability ) { normalized[ cleanKey ] = capability; } } ); return Object.keys( normalized ).length ? normalized : null; } function cloneEditorFormats( formats ) { if ( !Array.isArray( formats ) ) return null; return formats.map( item => ( Array.isArray( item ) ? cloneEditorFormats( item ) : item ) ); } function cloneInlineFormatCapabilities( capabilities ) { if ( !isPlainObject( capabilities ) ) return null; const cloned = {}; Object.keys( capabilities ).forEach( key => { const capability = normalizeInlineFormatCapability( capabilities[ key ] ); if ( !capability ) return; cloned[ key ] = capability; } ); return Object.keys( cloned ).length ? cloned : null; } function cloneEditorAttributeCapabilities( capabilities ) { if ( !isPlainObject( capabilities ) ) return null; const cloned = {}; Object.keys( capabilities ).forEach( key => { const capability = capabilities[ key ]; if ( !isPlainObject( capability ) ) return; cloned[ key ] = { ...capability }; if ( Array.isArray( capability.attributes ) ) { cloned[ key ].attributes = [ ...capability.attributes ]; } if ( Array.isArray( capability.values ) ) { cloned[ key ].values = [ ...capability.values ]; } } ); return Object.keys( cloned ).length ? cloned : null; } function normalizeEditorOperationValues( rawValues ) { if ( !Array.isArray( rawValues ) ) return null; const values = rawValues.filter( value => ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null ) ); return values.length ? values : null; } function normalizeEditorOperationStringArray( rawValues ) { if ( !Array.isArray( rawValues ) ) return null; const values = rawValues .map( value => ( typeof value === 'string' ? value.trim() : '' ) ) .filter( Boolean ); return values.length ? values : null; } function normalizeEditorOperation( rawOperation ) { if ( !isPlainObject( rawOperation ) ) return null; const id = typeof rawOperation.id === 'string' ? rawOperation.id.trim() : ''; const kind = typeof rawOperation.kind === 'string' ? rawOperation.kind.trim() : ''; const component = typeof rawOperation.component === 'string' ? rawOperation.component.trim() : ''; if ( !id || !kind || !component ) return null; const normalized = { id, kind, component }; const attribute = typeof rawOperation.attribute === 'string' ? rawOperation.attribute.trim() : ''; const format = typeof rawOperation.format === 'string' ? rawOperation.format.trim() : ''; const attributes = normalizeEditorOperationStringArray( rawOperation.attributes ); const formats = normalizeEditorOperationStringArray( rawOperation.formats ); const targetModes = normalizeEditorOperationStringArray( rawOperation.targetModes ); const values = normalizeEditorOperationValues( rawOperation.values ); if ( attribute ) { normalized.attribute = attribute; } if ( attributes ) { normalized.attributes = attributes; } if ( format ) { normalized.format = format; } if ( formats ) { normalized.formats = formats; } if ( targetModes ) { normalized.targetModes = targetModes; } if ( values ) { normalized.values = values; } if ( Object.prototype.hasOwnProperty.call( rawOperation, 'unsetValue' ) ) { normalized.unsetValue = rawOperation.unsetValue; } if ( typeof rawOperation.preserveInlineFormatting === 'boolean' ) { normalized.preserveInlineFormatting = rawOperation.preserveInlineFormatting; } if ( typeof rawOperation.preserveUnchangedText === 'boolean' ) { normalized.preserveUnchangedText = rawOperation.preserveUnchangedText; } if ( typeof rawOperation.preserveUnspecifiedAttributes === 'boolean' ) { normalized.preserveUnspecifiedAttributes = rawOperation.preserveUnspecifiedAttributes; } if ( typeof rawOperation.mergeRelTokens === 'boolean' ) { normalized.mergeRelTokens = rawOperation.mergeRelTokens; } if ( rawOperation.tagChange === true ) { normalized.tagChange = true; } return normalized; } function cloneEditorOperations( operations ) { if ( !Array.isArray( operations ) ) return null; const cloned = operations .map( normalizeEditorOperation ) .filter( Boolean ); return cloned.length ? cloned : null; } function normalizeEditorDefinition( rawEditor ) { if ( !isPlainObject( rawEditor ) ) return null; const normalized = {}; const enterMode = typeof rawEditor.enterMode === 'string' ? rawEditor.enterMode.trim().toLowerCase() : ''; if ( enterMode && VALID_ENTER_MODES.has( enterMode ) ) { normalized.enterMode = enterMode; } const linkUIMode = typeof rawEditor.linkUIMode === 'string' ? rawEditor.linkUIMode.trim().toLowerCase() : ''; if ( linkUIMode && VALID_LINK_UI_MODES.has( linkUIMode ) ) { normalized.linkUIMode = linkUIMode; } const tabMode = normalizeEditorTabMode( rawEditor.tabMode ); if ( tabMode ) { normalized.tabMode = tabMode; } if ( isPlainObject( rawEditor.formatTargets ) ) { const formatTargets = {}; Object.keys( rawEditor.formatTargets ).forEach( key => { const cleanKey = typeof key === 'string' ? key.trim() : ''; if ( !cleanKey ) return; const normalizedTarget = normalizeEditorTarget( rawEditor.formatTargets[ key ] ); if ( normalizedTarget ) { formatTargets[ cleanKey ] = normalizedTarget; } } ); if ( Object.keys( formatTargets ).length ) { normalized.formatTargets = formatTargets; } } const formats = normalizeEditorFormats( rawEditor.formats ); if ( formats ) { normalized.formats = formats; } const inlineFormatCapabilities = normalizeInlineFormatCapabilities( rawEditor.inlineFormatCapabilities ); if ( inlineFormatCapabilities ) { normalized.inlineFormatCapabilities = inlineFormatCapabilities; } const attributeCapabilities = normalizeEditorAttributeCapabilities( rawEditor.attributeCapabilities ); if ( attributeCapabilities ) { normalized.attributeCapabilities = attributeCapabilities; } const operations = cloneEditorOperations( rawEditor.operations ); if ( operations ) { normalized.operations = operations; } const options = normalizeEditorExtractionOptions( rawEditor.options ); if ( options ) { normalized.options = options; } return Object.keys( normalized ).length ? normalized : null; } function normalizeRepeatDefinition( rawRepeat ) { if ( !isPlainObject( rawRepeat ) ) return null; const mode = typeof rawRepeat.mode === 'string' ? rawRepeat.mode.trim().toLowerCase() : ''; if ( mode === 'tree_path' ) { const itemSelector = typeof rawRepeat.itemSelector === 'string' ? rawRepeat.itemSelector.trim() : ''; const pathKey = typeof rawRepeat.pathKey === 'string' ? rawRepeat.pathKey.trim() : 'path'; if ( !itemSelector || !pathKey ) return null; return { mode: 'tree_path', itemSelector, pathKey, }; } const rowSelector = typeof rawRepeat.rowSelector === 'string' ? rawRepeat.rowSelector.trim() : ''; const cellSelector = typeof rawRepeat.cellSelector === 'string' ? rawRepeat.cellSelector.trim() : ''; if ( !rowSelector || !cellSelector ) return null; return { rowSelector, cellSelector }; } function normalizeFileTargetDefinition( rawTarget ) { if ( !isPlainObject( rawTarget ) ) return null; const selector = typeof rawTarget.selector === 'string' ? rawTarget.selector.trim() : ''; const attribute = typeof rawTarget.attribute === 'string' ? rawTarget.attribute.trim().toLowerCase() : ''; const mediaType = typeof rawTarget.mediaType === 'string' ? rawTarget.mediaType.trim().toLowerCase() : ''; if ( !selector || !attribute || !VALID_MEDIA_TYPES.has( mediaType ) ) { return null; } return { selector, attribute, mediaType, }; } function normalizeMissingUIDefinition( rawMissingUI ) { if ( !isPlainObject( rawMissingUI ) ) return null; const mode = typeof rawMissingUI.mode === 'string' ? rawMissingUI.mode.trim().toLowerCase() : ''; const mountSelector = typeof rawMissingUI.mountSelector === 'string' ? rawMissingUI.mountSelector.trim() : ''; const placement = typeof rawMissingUI.placement === 'string' ? rawMissingUI.placement.trim().toLowerCase() : 'append'; const tag = typeof rawMissingUI.tag === 'string' ? rawMissingUI.tag.trim().toLowerCase() : ''; if ( !VALID_MISSING_UI_MODES.has( mode ) || !mountSelector || !VALID_MISSING_UI_PLACEMENTS.has( placement ) || !tag ) { return null; } const attributes = {}; if ( isPlainObject( rawMissingUI.attributes ) ) { Object.keys( rawMissingUI.attributes ).forEach( key => { const attrName = typeof key === 'string' ? key.trim() : ''; const attrValue = typeof rawMissingUI.attributes[ key ] === 'string' ? rawMissingUI.attributes[ key ].trim() : ''; if ( attrName && attrValue ) { attributes[ attrName ] = attrValue; } } ); } let when = null; if ( isPlainObject( rawMissingUI.when ) ) { const path = typeof rawMissingUI.when.path === 'string' ? rawMissingUI.when.path.trim() : ''; const hasEquals = Object.prototype.hasOwnProperty.call( rawMissingUI.when, 'equals' ); const equals = hasEquals ? rawMissingUI.when.equals : undefined; const isComparableScalar = ( typeof equals === 'string' || typeof equals === 'number' || typeof equals === 'boolean' || equals === null ); if ( path && hasEquals && isComparableScalar ) { when = { path, equals }; } } let placementWhen = null; if ( isPlainObject( rawMissingUI.placementWhen ) ) { const path = typeof rawMissingUI.placementWhen.path === 'string' ? rawMissingUI.placementWhen.path.trim() : ''; const hasEquals = Object.prototype.hasOwnProperty.call( rawMissingUI.placementWhen, 'equals' ); const equals = hasEquals ? rawMissingUI.placementWhen.equals : undefined; const conditionalPlacement = typeof rawMissingUI.placementWhen.placement === 'string' ? rawMissingUI.placementWhen.placement.trim().toLowerCase() : ''; const isComparableScalar = ( typeof equals === 'string' || typeof equals === 'number' || typeof equals === 'boolean' || equals === null ); if ( path && hasEquals && isComparableScalar && VALID_MISSING_UI_PLACEMENTS.has( conditionalPlacement ) ) { placementWhen = { path, equals, placement: conditionalPlacement }; } } return { mode, mountSelector, placement, tag, attributes: Object.keys( attributes ).length ? attributes : {}, when, placementWhen, }; } function normalizeComponentDefinition( rawComponent ) { if ( !isPlainObject( rawComponent ) ) return null; const id = typeof rawComponent.id === 'string' ? rawComponent.id.trim() : ''; const label = typeof rawComponent.label === 'string' ? rawComponent.label.trim() : ''; const type = typeof rawComponent.type === 'string' ? rawComponent.type.trim().toLowerCase() : ''; const selector = typeof rawComponent.selector === 'string' ? rawComponent.selector.trim() : ''; if ( !id || !label || !selector || !VALID_COMPONENT_TYPES.has( type ) ) { return null; } const bindings = Array.isArray( rawComponent.bindings ) ? rawComponent.bindings .map( normalizeBindingDefinition ) .filter( Boolean ) : []; if ( !bindings.length ) { return null; } const normalized = { id, label, type, selector, bindings, default: !!rawComponent.default, uiEditable: rawComponent.uiEditable !== false, }; const placeholder = typeof rawComponent.placeholder === 'string' ? rawComponent.placeholder.trim() : ''; if ( placeholder ) { normalized.placeholder = placeholder; } const repeat = normalizeRepeatDefinition( rawComponent.repeat ); if ( repeat ) { normalized.repeat = repeat; } const editor = normalizeEditorDefinition( rawComponent.editor ); if ( editor ) { normalized.editor = editor; } if ( type === 'text' ) { const missingUI = normalizeMissingUIDefinition( rawComponent.missingUI ); if ( missingUI ) { normalized.missingUI = missingUI; } normalized.required = rawComponent.required === true; } if ( type === 'file' ) { const target = normalizeFileTargetDefinition( rawComponent.target ); if ( !target ) return null; normalized.target = target; } return normalized; } function normalizeSchemaDefinition( rawSchema, handlerId ) { if ( !isPlainObject( rawSchema ) ) return null; const version = Number.parseInt( rawSchema.version, 10 ); if ( !Number.isInteger( version ) || version < 1 ) return null; if ( !isPlainObject( rawSchema.block ) ) return null; const blockName = typeof rawSchema.block.name === 'string' ? rawSchema.block.name.trim() : ''; if ( !blockName ) return null; const blockType = typeof rawSchema.block.type === 'string' ? rawSchema.block.type.trim().toLowerCase() : ''; if ( !VALID_BLOCK_TYPES.has( blockType ) ) return null; const components = Array.isArray( rawSchema.components ) ? rawSchema.components .map( normalizeComponentDefinition ) .filter( Boolean ) : []; if ( !components.length ) return null; return { handlerId, version, block: { name: blockName, type: blockType, }, components, }; } function buildSchemaRegistry( rawData ) { if ( !isPlainObject( rawData ) || !isPlainObject( rawData.schemas ) ) { return {}; } const registry = {}; Object.keys( rawData.schemas ).forEach( handlerId => { const normalized = normalizeSchemaDefinition( rawData.schemas[ handlerId ], handlerId ); if ( normalized ) { registry[ handlerId ] = normalized; } } ); return registry; } function getSchemaForHandler( handler, registry ) { if ( !handler || typeof handler.id !== 'string' ) return null; return registry[ handler.id ] || null; } function findScopedElements( rootElement, selector ) { if ( !isElementNode( rootElement ) || !selector ) return []; const scoped = []; if ( rootElement.matches( selector ) ) { const owner = rootElement.closest( '[data-mwp-sfe-uuid]' ); if ( !owner || owner === rootElement ) { scoped.push( rootElement ); } } const matches = rootElement.querySelectorAll( selector ); matches.forEach( candidate => { const owner = candidate.closest( '[data-mwp-sfe-uuid]' ); if ( !owner || owner === rootElement ) { scoped.push( candidate ); } } ); return scoped; } function findScopedElement( rootElement, selector ) { const matches = findScopedElements( rootElement, selector ); return matches.length ? matches[ 0 ] : null; } function isGhostElement( element ) { return isElementNode( element ) && element.getAttribute( GHOST_FLAG_ATTR ) === '1'; } function findRealScopedElements( rootElement, selector ) { return findScopedElements( rootElement, selector ).filter( element => !isGhostElement( element ) ); } function getManagedMissingComponentElements( rootElement, componentId ) { if ( !isElementNode( rootElement ) || !componentId ) return []; return Array.from( rootElement.querySelectorAll( `[${ GHOST_COMPONENT_ATTR }]` ) ).filter( element => ( element.getAttribute( GHOST_COMPONENT_ATTR ) === componentId ) ); } function getManagedMissingComponentElement( rootElement, componentId ) { const matches = getManagedMissingComponentElements( rootElement, componentId ); return matches.length ? matches[ 0 ] : null; } function getGhostComponentElements( rootElement, componentId ) { return getManagedMissingComponentElements( rootElement, componentId ).filter( isGhostElement ); } function getGhostComponentElement( rootElement, componentId ) { const matches = getGhostComponentElements( rootElement, componentId ); return matches.length ? matches[ 0 ] : null; } function removeGhostComponentElements( rootElement, componentId ) { getGhostComponentElements( rootElement, componentId ).forEach( element => element.remove() ); } function resolveGhostMountElement( rootElement, mountSelector ) { const selector = typeof mountSelector === 'string' ? mountSelector.trim() : ''; if ( !selector ) return null; if ( selector === ':scope' ) return rootElement; return findScopedElement( rootElement, selector ); } function applyGhostShellAttributes( ghostElement, componentDefinition ) { if ( !isElementNode( ghostElement ) || !componentDefinition ) return; const ghostAttributes = isPlainObject( componentDefinition?.missingUI?.attributes ) ? componentDefinition.missingUI.attributes : {}; Object.keys( ghostAttributes ).forEach( attrName => { const attrValue = ghostAttributes[ attrName ]; if ( typeof attrValue === 'string' && attrValue ) { if ( attrName === 'class' ) { attrValue.split( /\s+/ ).filter( Boolean ).forEach( cls => ghostElement.classList.add( cls ) ); } else { ghostElement.setAttribute( attrName, attrValue ); } } } ); ghostElement.setAttribute( GHOST_COMPONENT_ATTR, componentDefinition.id ); } function insertGhostElement( mountElement, ghostElement, placement ) { if ( !isElementNode( mountElement ) || !isElementNode( ghostElement ) ) return; if ( placement === 'prepend' ) { mountElement.insertBefore( ghostElement, mountElement.firstChild ); return; } mountElement.appendChild( ghostElement ); } function resolveComponentAttributeState( options = {} ) { const directAttributeState = isPlainObject( options.attributeState ) ? options.attributeState : null; if ( directAttributeState ) { return directAttributeState; } const blockAttributes = isPlainObject( options.blockAttributes ) ? options.blockAttributes : null; const attributeChanges = isPlainObject( options.attributeChanges ) ? options.attributeChanges : null; if ( blockAttributes && attributeChanges ) { return applyAttributeChangesToAttributes( blockAttributes, attributeChanges ); } if ( blockAttributes ) { return deepCloneAttributes( blockAttributes ); } if ( attributeChanges ) { return applyAttributeChangesToAttributes( {}, attributeChanges ); } return null; } function isRequiredTextComponent( componentDefinition ) { return !!( componentDefinition && componentDefinition.type === 'text' && componentDefinition.required === true ); } function supportsGhostManagedTextComponent( componentDefinition ) { return !!( componentDefinition && componentDefinition.type === 'text' && componentDefinition?.missingUI?.mode === 'ghost' ); } function shouldMaterializeGhostComponent( componentDefinition, options = {} ) { if ( !componentDefinition || componentDefinition.type !== 'text' || componentDefinition?.missingUI?.mode !== 'ghost' ) { return false; } const when = isPlainObject( componentDefinition.missingUI.when ) ? componentDefinition.missingUI.when : null; if ( when?.path ) { const attributeState = resolveComponentAttributeState( options ); if ( !attributeState ) { return false; } if ( getValueByPath( attributeState, when.path ) !== when.equals ) { return false; } } return true; } function hasMeaningfulManagedComponentContent( element ) { if ( !isElementNode( element ) ) return false; const placeholderApi = SFE.RichTextPlaceholder || null; if ( placeholderApi && typeof placeholderApi.hasMeaningfulContent === 'function' ) { return placeholderApi.hasMeaningfulContent( element ); } const text = String( element.textContent || '' ) .replace( /\uFEFF/g, '' ) .replace( /\u00A0/g, ' ' ) .trim(); if ( text.length ) { return true; } return !!element.querySelector?.( 'img, audio, video, iframe, embed, object, svg, canvas, hr, input, textarea, select, button, table' ); } function hasMeaningfulComponentBindingValue( value, source ) { if ( value === undefined || value === null ) return false; if ( source === 'plaintext' ) { return String( value ) .replace( /\uFEFF/g, '' ) .replace( /\u00A0/g, ' ' ) .trim() .length > 0; } if ( source === 'html' ) { const html = String( value || '' ); if ( !html.trim() ) { return false; } const probe = document.createElement( 'div' ); probe.innerHTML = html; return hasMeaningfulManagedComponentContent( probe ); } return !!value; } function componentHasMeaningfulAttributeContent( componentDefinition, attributeState ) { if ( !componentDefinition || !Array.isArray( componentDefinition.bindings ) || !attributeState ) { return false; } return componentDefinition.bindings.some( binding => { if ( !binding || ( binding.source !== 'html' && binding.source !== 'plaintext' ) ) { return false; } return hasMeaningfulComponentBindingValue( getValueByPath( attributeState, binding.path ), binding.source ); } ); } function resolveAttributeStateFromRawContent( rawContent ) { const content = typeof rawContent === 'string' ? rawContent.trim() : ''; if ( !content || !window.wp?.blocks?.parse ) { return null; } try { const parsedBlocks = wp.blocks.parse( content ); const parsedBlock = Array.isArray( parsedBlocks ) ? parsedBlocks[ 0 ] : null; return isPlainObject( parsedBlock?.attributes ) ? parsedBlock.attributes : {}; } catch ( error ) { console.error( 'FrontEdit Schema: failed to parse raw block content for rendered snapshot reconciliation', error ); return null; } } /** * Reconcile a cloned live block snapshot against canonical schema attrs. * * Keeps full-page/theme-aware DOM classes from the rendered block while removing * empty optional missingUI shells that no longer exist in the serialized block * payload. This prevents batch-edit state from restoring stale optional markup. * * @param {HTMLElement} rootElement Snapshot root to mutate. * @param {Object} handler Active edit handler. * @param {Object} options Reconciliation options. * @returns {HTMLElement|null} The mutated root element, or null when no schema applies. */ function reconcileRenderedSnapshot( rootElement, handler, options = {} ) { if ( !isElementNode( rootElement ) || !handler ) return null; const schema = getSchemaForHandler( handler, schemaRegistry ); if ( !schema || !Array.isArray( schema.components ) ) { return null; } const attributeState = resolveComponentAttributeState( options ) || resolveAttributeStateFromRawContent( options.rawContent || '' ); if ( !attributeState ) { return rootElement; } schema.components.forEach( componentDefinition => { if ( !supportsGhostManagedTextComponent( componentDefinition ) ) { return; } const shouldMaterialize = shouldMaterializeGhostComponent( componentDefinition, { attributeState, } ); const hasCanonicalContent = componentHasMeaningfulAttributeContent( componentDefinition, attributeState ); if ( shouldMaterialize && hasCanonicalContent ) { return; } findScopedElements( rootElement, componentDefinition.selector ).forEach( element => { if ( hasMeaningfulManagedComponentContent( element ) ) { return; } element.remove(); } ); } ); return rootElement; } /** * Synchronize schema-managed optional component shell state. * * @param {HTMLElement} element Candidate schema component element. * @param {?Object} componentDefinition Optional schema component definition. * @param {Object} options Sync options. * @param {boolean} options.removeInactiveEmpty Remove empty inactive shells. * @param {boolean} options.forceRemoveInactive Remove inactive empties even when block root is still marked editing-active. * @returns {void} */ function syncManagedMissingComponentState( element, componentDefinition = null, options = {} ) { if ( !isElementNode( element ) ) return; const managesGhostState = !!element.getAttribute( GHOST_COMPONENT_ATTR ) || supportsGhostManagedTextComponent( componentDefinition ); if ( !managesGhostState ) return; const persistGhostFlag = !!element.getAttribute( GHOST_COMPONENT_ATTR ); if ( hasMeaningfulManagedComponentContent( element ) ) { element.classList.remove( GHOST_CLASS ); element.removeAttribute( GHOST_FLAG_ATTR ); return; } const removeInactiveEmpty = options?.removeInactiveEmpty !== false; const forceRemoveInactive = options?.forceRemoveInactive === true; const ownerElement = element.closest( '[data-mwp-sfe-uuid]' ); const ownerIsInlineEditing = !!( isElementNode( ownerElement ) && ( ownerElement.classList.contains( 'mwp-sfe-element-active' ) || ownerElement.classList.contains( 'mwp-sfe-editing-active' ) || ownerElement.classList.contains( 'mwp-sfe-inline-editor' ) ) ); const isInactiveManagedComponent = ( element.getAttribute( 'contenteditable' ) !== 'true' && !element.getAttribute( 'data-mwp-sfe-active-component' ) && !element.classList.contains( 'mwp-sfe-component-active' ) && !element.classList.contains( 'mwp-sfe-inline-editor' ) && !element.classList.contains( 'mwp-sfe-editable-component' ) ); if ( removeInactiveEmpty && isInactiveManagedComponent && ( forceRemoveInactive || !ownerIsInlineEditing ) ) { element.remove(); return; } element.classList.add( GHOST_CLASS ); if ( persistGhostFlag ) { element.setAttribute( GHOST_FLAG_ATTR, '1' ); return; } element.removeAttribute( GHOST_FLAG_ATTR ); } /** * Resolve the schema-declared placement for one optional ghost component. * * @param {Object} componentDefinition Normalized component schema. * @param {Object} options Runtime block attribute state. * @returns {'append'|'prepend'} Effective ghost placement. */ function resolveGhostPlacement( componentDefinition, options = {} ) { const missingUI = componentDefinition?.missingUI || null; const placementWhen = isPlainObject( missingUI?.placementWhen ) ? missingUI.placementWhen : null; const attributeState = resolveComponentAttributeState( options ); if ( placementWhen?.path && attributeState && getValueByPath( attributeState, placementWhen.path ) === placementWhen.equals ) { return placementWhen.placement; } return missingUI?.placement || 'append'; } /** * Materialize one schema-managed optional text component when its canonical * node is absent from rendered markup. * * @param {HTMLElement} rootElement Block root element. * @param {Object} componentDefinition Normalized component schema. * @param {Object} options Runtime block attribute state. * @returns {HTMLElement|null} Materialized ghost element, if active. */ function materializeGhostComponentElement( rootElement, componentDefinition, options = {} ) { if ( !isElementNode( rootElement ) || !componentDefinition || componentDefinition.type !== 'text' || componentDefinition?.missingUI?.mode !== 'ghost' ) { return null; } const mountElement = resolveGhostMountElement( rootElement, componentDefinition.missingUI.mountSelector ); if ( !mountElement ) { removeGhostComponentElements( rootElement, componentDefinition.id ); return null; } const existingManaged = getManagedMissingComponentElements( rootElement, componentDefinition.id ); let ghostElement = existingManaged.find( element => element.parentNode ) || null; existingManaged.slice( ghostElement ? 1 : 0 ).forEach( element => element.remove() ); const desiredTag = componentDefinition.missingUI.tag.toUpperCase(); if ( ghostElement && ghostElement.tagName !== desiredTag ) { ghostElement.remove(); ghostElement = null; } if ( !ghostElement ) { ghostElement = document.createElement( componentDefinition.missingUI.tag ); } applyGhostShellAttributes( ghostElement, componentDefinition ); if ( ghostElement.parentNode !== mountElement ) { insertGhostElement( mountElement, ghostElement, resolveGhostPlacement( componentDefinition, options ) ); } // Sync ghost lifecycle only after the shell is attached so owner-state // checks can see whether the block is actually active. When this runs // outside an edit session, empty optional shells are removed immediately // instead of lingering as bare DOM nodes. syncManagedMissingComponentState( ghostElement, componentDefinition ); return ghostElement.parentNode ? ghostElement : null; } function materializeRepeatedComponentElements( rootElement, componentDefinition ) { if ( !isElementNode( rootElement ) || !componentDefinition?.repeat || componentDefinition.repeat.mode !== 'tree_path' ) { return []; } const listTracker = SFE.ListBlockTracker || null; if ( listTracker && typeof listTracker.syncEditableTextSurfaces === 'function' ) { listTracker.syncEditableTextSurfaces( rootElement ); } return findRealScopedElements( rootElement, componentDefinition.selector ); } function resolveSchemaComponentElements( rootElement, componentDefinition, options = {} ) { if ( !isElementNode( rootElement ) || !componentDefinition ) return []; const { materializeGhost = false, } = options; let realMatches = findRealScopedElements( rootElement, componentDefinition.selector ); if ( !realMatches.length ) { realMatches = materializeRepeatedComponentElements( rootElement, componentDefinition ); } if ( realMatches.length ) { removeGhostComponentElements( rootElement, componentDefinition.id ); return realMatches; } if ( componentDefinition.type !== 'text' || componentDefinition?.missingUI?.mode !== 'ghost' ) { removeGhostComponentElements( rootElement, componentDefinition.id ); return []; } if ( !shouldMaterializeGhostComponent( componentDefinition, options ) ) { removeGhostComponentElements( rootElement, componentDefinition.id ); return []; } const ghostElement = materializeGhost ? materializeGhostComponentElement( rootElement, componentDefinition, options ) : getManagedMissingComponentElement( rootElement, componentDefinition.id ); return ghostElement ? [ ghostElement ] : []; } function resolveLiveComponentElement( rootElement, component ) { if ( !isElementNode( rootElement ) || !component || typeof component !== 'object' ) { return null; } const existingElement = component.element; if ( isElementNode( existingElement ) && rootElement.contains( existingElement ) ) { return existingElement; } const selector = typeof component.selector === 'string' ? component.selector.trim() : ''; if ( selector ) { const resolvedBySelector = findScopedElement( rootElement, selector ); if ( resolvedBySelector ) { return resolvedBySelector; } } if ( typeof component.id === 'string' && component.id ) { const ghostElement = getManagedMissingComponentElement( rootElement, component.id ); if ( ghostElement ) { return ghostElement; } } return isElementNode( existingElement ) ? existingElement : null; } function getNthOfTypeIndex( node ) { let index = 1; let cursor = node; while ( cursor && cursor.previousElementSibling ) { cursor = cursor.previousElementSibling; if ( cursor.tagName === node.tagName ) { index++; } } return index; } function buildSelectorWithinRoot( rootElement, targetElement ) { if ( !isElementNode( rootElement ) || !isElementNode( targetElement ) ) return ''; if ( targetElement === rootElement ) return ':scope'; const segments = []; let cursor = targetElement; while ( cursor && cursor !== rootElement ) { const tag = cursor.tagName.toLowerCase(); const nth = getNthOfTypeIndex( cursor ); segments.unshift( `${ tag }:nth-of-type(${ nth })` ); cursor = cursor.parentElement; } if ( cursor !== rootElement ) return ''; return segments.join( ' > ' ); } function resolveRepeatedContext( rootElement, element, repeatDefinition ) { if ( repeatDefinition?.mode === 'tree_path' ) { const itemSelector = typeof repeatDefinition.itemSelector === 'string' ? repeatDefinition.itemSelector.trim() : ''; const pathKey = typeof repeatDefinition.pathKey === 'string' ? repeatDefinition.pathKey.trim() : 'path'; const itemNode = itemSelector ? element.closest( itemSelector ) : null; if ( !itemNode || !rootElement.contains( itemNode ) ) { return null; } const indexes = []; let currentItem = itemNode; while ( currentItem && rootElement.contains( currentItem ) ) { const parentList = currentItem.parentElement; if ( !isElementNode( parentList ) ) { return null; } const siblingItems = Array.from( parentList.children || [] ).filter( child => child.tagName === 'LI' ); const itemIndex = siblingItems.indexOf( currentItem ); if ( itemIndex < 0 ) { return null; } indexes.unshift( itemIndex ); const nextItem = parentList.closest( itemSelector ); if ( !nextItem || !rootElement.contains( nextItem ) ) { break; } currentItem = nextItem; } if ( !indexes.length ) { return null; } const humanPath = indexes.map( index => index + 1 ).join( '.' ); return { [ pathKey ]: indexes.join( '_' ), pathLabel: humanPath, depth: indexes.length - 1, }; } const rowNodes = findScopedElements( rootElement, repeatDefinition.rowSelector ); const rowNode = element.closest( repeatDefinition.rowSelector ); if ( !rowNode ) return null; const rowIndex = rowNodes.indexOf( rowNode ); if ( rowIndex < 0 ) return null; const rowChildren = Array.from( rowNode.children ).filter( isElementNode ); let columnIndex = Number.isInteger( element?.cellIndex ) ? element.cellIndex : -1; // Prefer the DOM cellIndex (true sibling position) so column targets stay aligned // across mixed th/td rows; fall back to selector-filtered indexing for non-table cases. if ( columnIndex < 0 || columnIndex >= rowChildren.length || rowChildren[ columnIndex ] !== element ) { const cellNodes = rowChildren.filter( child => child.matches( repeatDefinition.cellSelector ) ); columnIndex = cellNodes.indexOf( element ); } if ( columnIndex < 0 ) return null; return { row: rowIndex, column: columnIndex, }; } function applyContextTokens( template, context ) { return template.replace( /\{([a-zA-Z0-9_]+)\}/g, ( _, token ) => { if ( Object.prototype.hasOwnProperty.call( context, token ) ) { return String( context[ token ] ); } return ''; } ); } function buildRuntimeComponentId( componentDefinition, context, fallbackIndex ) { if ( !componentDefinition.repeat ) { return componentDefinition.id; } if ( typeof context?.path === 'string' && context.path.trim() ) { return `${ componentDefinition.id }__p${ context.path.trim() }`; } const parts = []; if ( Number.isInteger( context.row ) ) { parts.push( `r${ context.row }` ); } if ( Number.isInteger( context.column ) ) { parts.push( `c${ context.column }` ); } if ( !parts.length ) { parts.push( String( fallbackIndex ) ); } return `${ componentDefinition.id }__${ parts.join( '_' ) }`; } function resolveRuntimeFormatTarget( targetDefinition, context ) { if ( typeof targetDefinition === 'string' ) { return targetDefinition; } if ( !isPlainObject( targetDefinition ) ) return ''; if ( targetDefinition.scope === 'block' ) { return 'block'; } if ( targetDefinition.scope === 'component' ) { return 'selection'; } if ( targetDefinition.scope === 'column' ) { const selector = typeof targetDefinition.selector === 'string' ? targetDefinition.selector.trim() : ''; const contextKey = targetDefinition.contextKey || 'column'; const contextValue = Number.parseInt( context[ contextKey ], 10 ); if ( !selector || !Number.isInteger( contextValue ) || contextValue < 0 ) { return ''; } return `all:${ selector }:nth-child(${ contextValue + 1 })`; } if ( targetDefinition.selector ) { return targetDefinition.selector; } return ''; } function buildRuntimeEditorOptions( componentDefinition, context ) { const editorDefinition = componentDefinition.editor; if ( !editorDefinition ) { return null; } const editorOptions = {}; if ( editorDefinition.enterMode ) { editorOptions.enterMode = editorDefinition.enterMode; } if ( editorDefinition.linkUIMode ) { editorOptions.linkUIMode = editorDefinition.linkUIMode; } if ( isPlainObject( editorDefinition.tabMode ) ) { editorOptions.tabMode = { ...editorDefinition.tabMode }; } if ( isPlainObject( editorDefinition.formatTargets ) ) { const formatTargets = {}; Object.keys( editorDefinition.formatTargets ).forEach( key => { const resolved = resolveRuntimeFormatTarget( editorDefinition.formatTargets[ key ], context ); if ( resolved ) { formatTargets[ key ] = resolved; } } ); if ( Object.keys( formatTargets ).length ) { editorOptions.formatTargets = formatTargets; } } if ( Array.isArray( editorDefinition.formats ) ) { editorOptions.formats = cloneEditorFormats( editorDefinition.formats ); } if ( isPlainObject( editorDefinition.inlineFormatCapabilities ) ) { const inlineFormatCapabilities = cloneInlineFormatCapabilities( editorDefinition.inlineFormatCapabilities ); if ( inlineFormatCapabilities ) { editorOptions.inlineFormatCapabilities = inlineFormatCapabilities; } } if ( isPlainObject( editorDefinition.attributeCapabilities ) ) { const attributeCapabilities = cloneEditorAttributeCapabilities( editorDefinition.attributeCapabilities ); if ( attributeCapabilities ) { editorOptions.attributeCapabilities = attributeCapabilities; } } if ( Array.isArray( editorDefinition.operations ) ) { const operations = cloneEditorOperations( editorDefinition.operations ); if ( operations ) { editorOptions.operations = operations; } } if ( isPlainObject( editorDefinition.options ) ) { editorOptions.options = { ...editorDefinition.options }; } return Object.keys( editorOptions ).length ? editorOptions : null; } function buildRuntimeFromSchema( rootElement, schema, options = {} ) { if ( !schema || !isElementNode( rootElement ) ) return null; const runtime = { schemaHandlerId: schema.handlerId, blockName: schema.block.name, mode: 'text', editableComponents: [], componentsById: {}, mediaComponent: null, primaryTextComponent: null, }; let defaultAssigned = false; let hasTextComponent = false; let hasFileComponent = false; schema.components.forEach( componentDefinition => { const matches = resolveSchemaComponentElements( rootElement, componentDefinition, { materializeGhost: true, blockAttributes: options.blockAttributes, attributeChanges: options.attributeChanges, } ); if ( !matches.length ) return; matches.forEach( ( matchedElement, matchIndex ) => { const context = componentDefinition.repeat ? resolveRepeatedContext( rootElement, matchedElement, componentDefinition.repeat ) : {}; if ( componentDefinition.repeat && !context ) { return; } const resolvedBindings = componentDefinition.bindings.map( binding => ( { path: applyContextTokens( binding.path, context || {} ), source: binding.source, resolved: !!binding.resolved, value: typeof binding.value === 'string' ? binding.value : '', } ) ); const selector = buildSelectorWithinRoot( rootElement, matchedElement ); if ( !selector ) return; const runtimeId = buildRuntimeComponentId( componentDefinition, context || {}, matchIndex ); const editorOptions = buildRuntimeEditorOptions( componentDefinition, context || {} ); const label = buildRuntimeComponentLabel( componentDefinition, context || {} ); if ( componentDefinition.type === 'file' ) { hasFileComponent = true; const target = componentDefinition.target || null; if ( !target ) return; const urlBinding = resolvedBindings.find( binding => binding.source === 'url' ) || null; const idBinding = resolvedBindings.find( binding => binding.source === 'id' ) || null; const mediaDescriptor = { componentId: runtimeId, scopeSelector: selector, targetSelector: target.selector, attribute: target.attribute, mediaType: target.mediaType, }; const editableComponent = { id: runtimeId, label, selector, element: matchedElement, type: 'file', default: !!componentDefinition.default && !defaultAssigned, target: { selector: target.selector, attribute: target.attribute, mediaType: target.mediaType, }, mediaDescriptor, urlBindingPath: urlBinding?.path || '', idBindingPath: idBinding?.path || '', }; if ( editorOptions ) { editableComponent.editor = editorOptions; } if ( editableComponent.default ) { defaultAssigned = true; } if ( componentDefinition.uiEditable !== false ) { runtime.editableComponents.push( editableComponent ); } runtime.componentsById[ runtimeId ] = { id: runtimeId, label, element: matchedElement, selector, type: 'file', uiEditable: componentDefinition.uiEditable !== false, bindings: resolvedBindings, schemaSelector: componentDefinition.selector, mediaDescriptor, target: { selector: target.selector, attribute: target.attribute, mediaType: target.mediaType, }, editorOptions: editorOptions || null, }; if ( !runtime.mediaComponent ) { runtime.mediaComponent = { id: runtimeId, selector, mediaDescriptor, target: { selector: target.selector, attribute: target.attribute, mediaType: target.mediaType, }, bindings: resolvedBindings, }; } return; } const contentBinding = resolvedBindings.find( binding => ( binding.source === 'html' || binding.source === 'plaintext' || binding.source === 'list_block' ) ); if ( !contentBinding ) return; hasTextComponent = true; const editableComponent = { id: runtimeId, label, selector, element: matchedElement, attribute: contentBinding.path, type: 'text', default: !!componentDefinition.default && !defaultAssigned, isGhost: isGhostElement( matchedElement ), }; if ( componentDefinition.placeholder ) { editableComponent.placeholder = componentDefinition.placeholder; } if ( componentDefinition.missingUI ) { editableComponent.missingUI = componentDefinition.missingUI; } editableComponent.required = componentDefinition.required === true; if ( editorOptions ) { editableComponent.editor = editorOptions; } if ( editableComponent.default ) { defaultAssigned = true; } if ( componentDefinition.uiEditable !== false ) { runtime.editableComponents.push( editableComponent ); } runtime.componentsById[ runtimeId ] = { id: runtimeId, label, element: matchedElement, selector, type: 'text', uiEditable: componentDefinition.uiEditable !== false, bindings: resolvedBindings, schemaSelector: componentDefinition.selector, missingUI: componentDefinition.missingUI || null, required: componentDefinition.required === true, editorOptions: editorOptions || null, }; } ); } ); if ( !runtime.editableComponents.length ) { return null; } if ( !runtime.editableComponents.some( component => component.default ) ) { runtime.editableComponents[ 0 ].default = true; } const primaryTextComponent = runtime.editableComponents.find( component => component.type === 'text' && component.default ) || runtime.editableComponents.find( component => component.type === 'text' ) || null; if ( primaryTextComponent ) { runtime.primaryTextComponent = { selector: primaryTextComponent.selector, attribute: primaryTextComponent.attribute, }; } if ( hasTextComponent && hasFileComponent ) { runtime.mode = 'mixed'; } else if ( hasTextComponent ) { runtime.mode = 'text'; } else if ( hasFileComponent ) { runtime.mode = 'media'; } else { return null; } return runtime; } function createSchemaAwareHandler( handler, runtime ) { const baseConfig = isPlainObject( handler.client_config ) ? { ...handler.client_config } : {}; const runtimeHandler = { ...handler, client_config: baseConfig, }; if ( runtime.mode === 'text' || runtime.mode === 'mixed' ) { runtimeHandler.contentType = 'text'; runtimeHandler.client_config.editableComponents = runtime.editableComponents; return runtimeHandler; } if ( runtime.mode === 'media' && runtime.mediaComponent ) { runtimeHandler.contentType = 'media'; runtimeHandler.client_config.editableComponents = runtime.editableComponents; return runtimeHandler; } return runtimeHandler; } function readTextAlignmentFromElement( element ) { if ( !isElementNode( element ) || !element.classList ) return undefined; const dataAlign = typeof element.getAttribute === 'function' ? String( element.getAttribute( 'data-align' ) || '' ).trim().toLowerCase() : ''; if ( dataAlign === 'left' || dataAlign === 'center' || dataAlign === 'right' || dataAlign === 'justify' ) { return dataAlign; } if ( element.classList.contains( 'has-text-align-justify' ) ) return 'justify'; if ( element.classList.contains( 'has-text-align-right' ) ) return 'right'; if ( element.classList.contains( 'has-text-align-center' ) ) return 'center'; if ( element.classList.contains( 'has-text-align-left' ) ) return 'left'; return undefined; } function isVideoUrl( url ) { return /\.(mp4|webm|ogv|mov|avi|wmv|m4v)(\?.*)?$/i.test( String( url || '' ) ); } function splitBindingPath( path ) { return String( path || '' ) .split( '.' ) .map( segment => segment.trim() ) .filter( Boolean ) .map( segment => ( /^\d+$/.test( segment ) ? Number.parseInt( segment, 10 ) : segment ) ); } /** * Determine whether one existing nested attribute container matches the * structure required by the next path segment. * * WordPress can round-trip empty nested style objects such as * `style.typography` back into array-shaped containers. When that happens we * must coerce the container back to an object before assigning a nested key, * otherwise JSON serialization silently drops string-key writes on arrays. * * @param {*} value Existing nested container candidate. * @param {number|string} nextKey Upcoming path segment. * @returns {boolean} True when the container already has the required shape. */ function isCompatiblePathContainer( value, nextKey ) { return ( typeof nextKey === 'number' ) ? Array.isArray( value ) : isPlainObject( value ); } /** * Determine whether one nested attribute value is structurally empty. * * Empty objects/arrays left behind after unsetting one deep attribute should * be pruned so default/unset block attrs disappear cleanly instead of * persisting as `style: { typography: [] }`. * * @param {*} value Candidate nested value. * @returns {boolean} True when the value is an empty container. */ function isEmptyPathContainer( value ) { if ( Array.isArray( value ) ) { return value.length === 0 && Object.keys( value ).length === 0; } if ( !isPlainObject( value ) ) { return false; } return Object.keys( value ).length === 0; } function setValueByPath( target, path, value ) { const parts = splitBindingPath( path ); if ( !parts.length ) return; let cursor = target; const parentTrail = []; for ( let i = 0; i < parts.length - 1; i++ ) { const key = parts[ i ]; const nextKey = parts[ i + 1 ]; if ( typeof key === 'number' ) { if ( !Array.isArray( cursor ) ) return; if ( cursor[ key ] === undefined || cursor[ key ] === null || !isCompatiblePathContainer( cursor[ key ], nextKey ) ) { cursor[ key ] = ( typeof nextKey === 'number' ) ? [] : {}; } parentTrail.push( { container: cursor, key } ); cursor = cursor[ key ]; continue; } if ( !isPlainObject( cursor ) && !Array.isArray( cursor ) ) return; if ( cursor[ key ] === undefined || cursor[ key ] === null || !isCompatiblePathContainer( cursor[ key ], nextKey ) ) { cursor[ key ] = ( typeof nextKey === 'number' ) ? [] : {}; } parentTrail.push( { container: cursor, key } ); cursor = cursor[ key ]; } const finalKey = parts[ parts.length - 1 ]; if ( typeof finalKey === 'number' ) { if ( !Array.isArray( cursor ) ) return; cursor[ finalKey ] = value; return; } if ( value === undefined || value === null ) { if ( isPlainObject( cursor ) || Array.isArray( cursor ) ) { delete cursor[ finalKey ]; for ( let i = parentTrail.length - 1; i >= 0; i-- ) { const parentEntry = parentTrail[ i ]; const childValue = parentEntry?.container?.[ parentEntry.key ]; if ( !isEmptyPathContainer( childValue ) ) { break; } delete parentEntry.container[ parentEntry.key ]; } } return; } cursor[ finalKey ] = value; } function getValueByPath( source, path ) { const parts = splitBindingPath( path ); if ( !parts.length ) return undefined; let cursor = source; for ( let i = 0; i < parts.length; i++ ) { const key = parts[ i ]; if ( cursor === undefined || cursor === null ) { return undefined; } if ( typeof key === 'number' ) { if ( !Array.isArray( cursor ) ) { return undefined; } cursor = cursor[ key ]; continue; } if ( !isPlainObject( cursor ) && !Array.isArray( cursor ) ) { return undefined; } cursor = cursor[ key ]; } return cursor; } function stripEditorPlaceholderChars( value ) { return ( typeof value === 'string' ) ? value.replace( /\uFEFF/g, '' ) : value; } function extractBindingValue( binding, component, editorState, componentMeta = null ) { const ElementPrep = SFE.ElementPrep; const element = component?.element || null; const editorOptions = isPlainObject( componentMeta?.editorOptions ) ? componentMeta.editorOptions : null; const extractionOptions = isPlainObject( editorOptions?.options ) ? editorOptions.options : null; if ( binding.source === 'url' ) { const mediaChanges = element?._mwpMediaChanges || editorState?.element?._mwpMediaChanges || null; if ( !mediaChanges ) { return undefined; } if ( binding.resolved ) { const resolvedValueKey = binding.value === 'width' ? 'resolvedWidth' : ( binding.value === 'height' ? 'resolvedHeight' : 'resolvedUrl' ); const resolvedValue = mediaChanges?.[ resolvedValueKey ]; return ( resolvedValue === undefined || resolvedValue === null ) ? undefined : resolvedValue; } return mediaChanges.url || undefined; } if ( binding.source === 'id' ) { const idValue = element?._mwpMediaChanges?.id ?? editorState?.element?._mwpMediaChanges?.id; return ( idValue === undefined || idValue === null ) ? undefined : idValue; } if ( binding.source === 'media_type' ) { const urlValue = element?._mwpMediaChanges?.url || editorState?.element?._mwpMediaChanges?.url || ''; if ( typeof urlValue !== 'string' || !urlValue.trim() ) { return undefined; } return isVideoUrl( urlValue ) ? 'video' : 'image'; } if ( !element || !ElementPrep || typeof ElementPrep.clean !== 'function' ) { return undefined; } const cleaned = ElementPrep.clean( element, { removeIdentity: true, removeControls: true, clone: true, } ); if ( binding.source === 'html' ) { let html = cleaned.innerHTML; if ( extractionOptions?.preserveNewlines ) { const clone = cleaned.cloneNode( true ); clone.querySelectorAll( 'br' ).forEach( br => { br.replaceWith( '\n' ); } ); html = clone.innerHTML; } if ( extractionOptions?.newlinesToBR && typeof html === 'string' ) { html = html.replace( /\n/g, '
' ); } if ( typeof html === 'string' ) { html = html.replace( /(?:\u00A0| | )/g, ' ' ); } return stripEditorPlaceholderChars( html ); } if ( binding.source === 'plaintext' ) { return stripEditorPlaceholderChars( cleaned.textContent.replace( /\u00A0/g, ' ' ).trim() ); } if ( binding.source === 'textalignment' || binding.source === 'columnalignment' ) { return readTextAlignmentFromElement( element ); } return undefined; } function deepCloneAttributes( attrs ) { try { return JSON.parse( JSON.stringify( attrs || {} ) ); } catch ( error ) { return { ...( attrs || {} ) }; } } /** * Clone one attribute change payload value so schema merges never retain * live references back into editor state. * * @param {*} value Attribute change value. * @returns {*} Cloned value when structured, otherwise the original scalar. */ function cloneAttributeChangeValue( value ) { if ( Array.isArray( value ) || isPlainObject( value ) ) { try { return JSON.parse( JSON.stringify( value ) ); } catch ( error ) { if ( Array.isArray( value ) ) { return [ ...value ]; } return { ...value }; } } return value; } /** * Apply unsaved editor attr changes onto canonical block attrs using the * same dotted-path semantics exposed through the schema contract. * * @param {?Object} baseAttributes Canonical parsed block attrs. * @param {?Object} attributeChanges Unsaved editor attr mutations. * @returns {Object} Merged attribute snapshot. */ function applyAttributeChangesToAttributes( baseAttributes, attributeChanges ) { const mergedAttributes = deepCloneAttributes( baseAttributes || {} ); if ( !isPlainObject( attributeChanges ) ) { return mergedAttributes; } Object.keys( attributeChanges ).forEach( rawPath => { const path = typeof rawPath === 'string' ? rawPath.trim() : ''; if ( !path ) { return; } const value = attributeChanges[ rawPath ]; if ( path.indexOf( '.' ) === -1 && isPlainObject( value ) && isPlainObject( mergedAttributes[ path ] ) ) { mergedAttributes[ path ] = applyAttributeChangesToAttributes( mergedAttributes[ path ], value ); return; } setValueByPath( mergedAttributes, path, cloneAttributeChangeValue( value ) ); } ); return mergedAttributes; } function isListElement( element ) { return !!element && ( element.tagName === 'UL' || element.tagName === 'OL' ); } function findSchemaListBindingState( rootElement, editorState, runtime ) { const editableComponents = Array.isArray( editorState?.editableComponents ) ? editorState.editableComponents : []; for ( const component of editableComponents ) { const componentMeta = runtime?.componentsById?.[ component.id ]; if ( !componentMeta || !Array.isArray( componentMeta.bindings ) ) { continue; } const hasListBinding = componentMeta.bindings.some( binding => binding.source === 'list_block' ); if ( !hasListBinding ) { continue; } const liveComponentElement = resolveLiveComponentElement( rootElement, component ); if ( liveComponentElement && component.element !== liveComponentElement ) { component.element = liveComponentElement; } const componentElement = liveComponentElement || component.element || null; let listElement = null; if ( isListElement( componentElement ) ) { listElement = componentElement; } else if ( componentElement && typeof componentElement.querySelector === 'function' ) { listElement = componentElement.querySelector( ':scope > ul, :scope > ol' ) || componentElement.querySelector( 'ul, ol' ); } if ( !isListElement( listElement ) ) { return { component, componentMeta, listElement: null, }; } return { component, componentMeta, listElement, }; } return null; } function buildRuntimeComponentLabel( componentDefinition, context ) { if ( typeof context?.pathLabel === 'string' && context.pathLabel.trim() ) { return `${ componentDefinition.label } (${ context.pathLabel.trim() })`; } if ( Number.isInteger( context?.row ) && Number.isInteger( context?.column ) ) { return `${ componentDefinition.label } (${ context.row + 1 },${ context.column + 1 })`; } return componentDefinition.label; } /** * Determine whether a list item contains meaningful inline or nested-list * content after placeholder artifacts have been stripped. * * Empty parent items that only own empty descendants should still fail * required validation, while parent items with meaningful nested list content * remain valid even if their own inline text span is blank. * * @param {HTMLElement|null} listItem List item to inspect. * @returns {boolean} True when the list item is meaningfully populated. */ function listItemHasMeaningfulSchemaContent( listItem ) { if ( !isElementNode( listItem ) || listItem.tagName !== 'LI' ) { return false; } const directText = Array.from( listItem.childNodes || [] ).some( node => { if ( !node ) return false; if ( node.nodeType === Node.TEXT_NODE ) { return String( node.textContent || '' ) .replace( /\uFEFF/g, '' ) .replace( /\u00A0/g, ' ' ) .trim() .length > 0; } if ( node.nodeType !== Node.ELEMENT_NODE ) { return false; } if ( node.tagName === 'OL' || node.tagName === 'UL' || node.tagName === 'BR' ) { return false; } return hasMeaningfulManagedComponentContent( node ); } ); if ( directText ) { return true; } const nestedLists = Array.from( listItem.children || [] ).filter( child => ( child.tagName === 'OL' || child.tagName === 'UL' ) ); return nestedLists.some( nestedList => ( Array.from( nestedList.children || [] ).some( child => listItemHasMeaningfulSchemaContent( child ) ) ) ); } /** * Validate that a required list component does not contain empty list items. * * @param {Object|null} listBindingState Resolved list binding state. * @returns {string|null} Validation error message when the list is invalid. */ function validateRequiredSchemaListBindingState( listBindingState ) { const componentMeta = listBindingState?.componentMeta || null; const listElement = listBindingState?.listElement || null; if ( !isRequiredTextComponent( componentMeta ) ) { return null; } if ( !isListElement( listElement ) ) { return `${ listBindingState?.component?.label || 'Required text field' } is missing and cannot be saved empty. Add some text or cancel to restore original.`; } const allListItems = Array.from( listElement.querySelectorAll( 'li' ) ); const hasEmptyItem = allListItems.some( listItem => !listItemHasMeaningfulSchemaContent( listItem ) ); if ( hasEmptyItem ) { return `${ listBindingState?.component?.label || 'Required text field' } cannot contain empty items. Add text to each item or cancel to restore original.`; } return null; } function buildSchemaListPayload( listBindingState, editorState, attrChanges ) { const ListBlockTracker = SFE.ListBlockTracker; const listElement = listBindingState?.listElement || null; const tracker = editorState?.listTracker || listElement?._mwpListTracker || ListBlockTracker?.active || null; if ( !listElement || !ListBlockTracker || typeof ListBlockTracker.parseListToBlock !== 'function' || !tracker ) { return null; } const parsedListBlock = ListBlockTracker.parseListToBlock( listElement, tracker ); if ( !parsedListBlock || typeof parsedListBlock !== 'object' ) { return null; } const safeAttrChanges = isPlainObject( attrChanges ) ? { ...attrChanges } : {}; if ( Object.prototype.hasOwnProperty.call( safeAttrChanges, 'ordered' ) ) { delete safeAttrChanges.ordered; } const nextBlock = { ...parsedListBlock, attributes: applyAttributeChangesToAttributes( parsedListBlock.attributes || {}, safeAttrChanges ), }; let serialized = ''; try { serialized = wp.blocks.serialize( [ nextBlock ] ); } catch ( error ) { console.error( 'FrontEdit Schema: wp.blocks.serialize failed for list binding', error ); return null; } return { _type: 'raw_block_content', rawContent: serialized, }; } function applySchemaBindingToAttributes( attrs, binding, component, editorState, componentMeta = null ) { if ( binding.source === 'list_block' ) { return; } const value = extractBindingValue( binding, component, editorState, componentMeta ); // URL/id bindings are sparse patch values; when unchanged, preserve attrs from parsed raw block content. if ( ( binding.source === 'url' || binding.source === 'id' || binding.source === 'media_type' ) && ( value === undefined || value === null ) ) { return; } setValueByPath( attrs, binding.path, value ); } function validateRequiredSchemaTextComponents( rootElement, editorState, runtime ) { if ( !runtime || !Array.isArray( editorState?.editableComponents ) ) { return null; } for ( const component of editorState.editableComponents ) { const componentMeta = runtime.componentsById?.[ component?.id ] || null; if ( !isRequiredTextComponent( componentMeta ) ) { continue; } const liveComponentElement = resolveLiveComponentElement( rootElement, component ); if ( liveComponentElement && component.element !== liveComponentElement ) { component.element = liveComponentElement; } const componentElement = liveComponentElement || component?.element || null; if ( !isElementNode( componentElement ) ) { return `${ component?.label || 'Required text field' } is missing and cannot be saved empty. Add some text or cancel to restore original.`; } if ( !hasMeaningfulManagedComponentContent( componentElement ) ) { return `${ component?.label || 'Required text field' } cannot be empty. Add some text or cancel to restore original.`; } } return null; } function buildSchemaPayload( element, editorState ) { const runtime = editorState?._mwpSchemaRuntime; const blockState = editorState?.blockState; if ( !runtime || !blockState?.rawContent ) return null; if ( !window.wp?.blocks?.parse || !window.wp?.blocks?.serialize ) return null; let parsedBlock = null; try { [ parsedBlock ] = wp.blocks.parse( blockState.rawContent ); } catch ( error ) { console.error( 'FrontEdit Schema: failed to parse raw block content', error ); return null; } if ( !parsedBlock ) return null; const attrChanges = { ...( isPlainObject( editorState?.attributeChanges ) ? editorState.attributeChanges : {} ), ...( isPlainObject( element?._mwpEditor?.attributeChanges ) ? element._mwpEditor.attributeChanges : {} ), ...( isPlainObject( element?._mwpMediaChanges ) ? element._mwpMediaChanges : {} ), }; const listBindingState = findSchemaListBindingState( element, editorState, runtime ); if ( listBindingState ) { const requiredListValidationError = validateRequiredSchemaListBindingState( listBindingState ); if ( requiredListValidationError ) { throw new Error( requiredListValidationError ); } const listPayload = buildSchemaListPayload( listBindingState, editorState, attrChanges ); if ( listPayload ) { return listPayload; } return null; } const requiredValidationError = validateRequiredSchemaTextComponents( element, editorState, runtime ); if ( requiredValidationError ) { throw new Error( requiredValidationError ); } const newAttrs = applyAttributeChangesToAttributes( parsedBlock.attributes || {}, attrChanges ); if ( runtime.mode === 'media' && runtime.mediaComponent ) { const mediaComponentMeta = runtime.componentsById?.[ runtime.mediaComponent.id ] || null; runtime.mediaComponent.bindings.forEach( binding => { applySchemaBindingToAttributes( newAttrs, binding, { element }, editorState, mediaComponentMeta ); } ); } const editableComponents = Array.isArray( editorState.editableComponents ) ? editorState.editableComponents : []; editableComponents.forEach( component => { const componentMeta = runtime.componentsById[ component.id ]; if ( !componentMeta ) return; const liveComponentElement = resolveLiveComponentElement( element, component ); if ( liveComponentElement && component.element !== liveComponentElement ) { component.element = liveComponentElement; } const componentState = liveComponentElement ? { ...component, element: liveComponentElement } : component; componentMeta.bindings.forEach( binding => { applySchemaBindingToAttributes( newAttrs, binding, componentState, editorState, componentMeta ); } ); } ); const nextBlock = { ...parsedBlock, attributes: newAttrs, }; let serialized = ''; try { serialized = wp.blocks.serialize( [ nextBlock ] ); } catch ( error ) { console.error( 'FrontEdit Schema: wp.blocks.serialize failed', error ); return null; } return { _type: 'raw_block_content', rawContent: serialized, }; } /** * Sync schema placeholders and optional missingUI shell lifecycle for a block. * * @param {HTMLElement} rootElement Block root element. * @param {Object} handler Active edit handler. * @param {Object} options Runtime options. * @param {Object|null} options.blockAttributes Canonical block attributes. * @param {Object|null} options.attributeChanges Unsaved attribute changes. * @param {boolean} options.removeInactiveEmpty Remove empty inactive shells. * @returns {void} */ function syncSchemaPlaceholders( rootElement, handler, options = {} ) { const placeholderApi = SFE.RichTextPlaceholder || null; if ( !isElementNode( rootElement ) || !handler || !placeholderApi || typeof placeholderApi.syncElement !== 'function' ) { return; } const schema = getSchemaForHandler( handler, schemaRegistry ); if ( !schema || !Array.isArray( schema.components ) ) { return; } schema.components.forEach( componentDefinition => { if ( !componentDefinition || componentDefinition.type !== 'text' ) { return; } const matches = resolveSchemaComponentElements( rootElement, componentDefinition, { materializeGhost: true, blockAttributes: options.blockAttributes, attributeChanges: options.attributeChanges, } ); if ( !matches.length ) return; matches.forEach( matchedElement => { if ( typeof componentDefinition.placeholder === 'string' && componentDefinition.placeholder.trim() ) { placeholderApi.syncElement( matchedElement, componentDefinition.placeholder ); } syncManagedMissingComponentState( matchedElement, componentDefinition, { removeInactiveEmpty: options.removeInactiveEmpty !== false, } ); } ); } ); } function syncExistingBoundPlaceholders() { if ( typeof document === 'undefined' ) return; document.querySelectorAll( '[data-mwp-sfe-uuid]' ).forEach( element => { const handlers = Array.isArray( element._mwpSfeHandlers ) ? element._mwpSfeHandlers : []; const editHandler = handlers.find( handler => handler?.capability === 'edit' ) || null; if ( editHandler ) { syncSchemaPlaceholders( element, editHandler ); } } ); } /** * Sync missingUI shell visibility/removal for a concrete component element. * * @param {HTMLElement} element Component element. * @param {?Object} componentDefinition Optional schema component definition. * @param {Object} options Sync options. * @param {boolean} options.removeInactiveEmpty Remove empty inactive shells. * @returns {void} */ function syncManagedMissingComponentStateForElement( element, componentDefinition = null, options = {} ) { if ( !isElementNode( element ) ) return; syncManagedMissingComponentState( element, componentDefinition, options ); } const schemaRegistry = buildSchemaRegistry( RAW_SCHEMA_DATA ); /** * Resolve schema runtime metadata for an edit session. * * @param {HTMLElement} rootElement Block root element. * @param {Object} handler Candidate edit handler. * @param {Object} options Resolution options. * @param {?Object} options.blockState Block-state payload containing attrs. * @param {?Object} options.blockAttributes Canonical block attrs. * @param {?Object} options.attributeChanges Unsaved attribute overrides. * @param {boolean} options.removeInactiveEmpty Remove empty inactive shells. * @returns {?Object} Schema resolution bundle, or null when not schema-backed. */ function resolveForEditing( rootElement, handler, options = {} ) { if ( !isElementNode( rootElement ) || !handler ) return null; const schema = getSchemaForHandler( handler, schemaRegistry ); if ( !schema ) return null; const blockAttributes = isPlainObject( options.blockState?.attrs ) ? options.blockState.attrs : ( isPlainObject( options.blockAttributes ) ? options.blockAttributes : null ); const attributeChanges = isPlainObject( options.attributeChanges ) ? options.attributeChanges : null; syncSchemaPlaceholders( rootElement, handler, { blockAttributes, attributeChanges, removeInactiveEmpty: options.removeInactiveEmpty !== false, } ); const runtime = buildRuntimeFromSchema( rootElement, schema, { blockAttributes, attributeChanges, } ); if ( !runtime ) return null; return { schema, runtime, runtimeHandler: createSchemaAwareHandler( handler, runtime ), }; } function refreshEditorState( editorState, options = {} ) { if ( !editorState?.element || !editorState?.handler ) return null; const resolved = resolveForEditing( editorState.element, editorState.handler, { blockState: options.blockState || editorState.blockState || null, blockAttributes: options.blockAttributes || null, attributeChanges: options.attributeChanges || editorState.attributeChanges || null, } ); if ( !resolved?.runtimeHandler ) { return null; } editorState.handler = resolved.runtimeHandler; editorState._mwpSchemaRuntime = resolved.runtime; editorState._mwpSchemaHandlerId = resolved.schema?.handlerId || editorState.handler.id || ''; return resolved; } function runtimeHasListBinding( runtime ) { if ( !runtime || !isPlainObject( runtime.componentsById ) ) return false; const componentIds = Object.keys( runtime.componentsById ); for ( const id of componentIds ) { const componentMeta = runtime.componentsById[ id ]; if ( !componentMeta || !Array.isArray( componentMeta.bindings ) ) { continue; } if ( componentMeta.bindings.some( binding => binding.source === 'list_block' ) ) { return true; } } return false; } function hasListBinding( editorState, runtimeOverride = null ) { const runtime = runtimeOverride || editorState?._mwpSchemaRuntime || null; return runtimeHasListBinding( runtime ); } function collectRuntimeMediaUrlBindingPaths( runtime, editorState = null ) { if ( !runtime ) return []; const paths = []; const seen = new Set(); const addPath = path => { const cleanPath = typeof path === 'string' ? path.trim() : ''; if ( !cleanPath || seen.has( cleanPath ) ) return; seen.add( cleanPath ); paths.push( cleanPath ); }; const addFromBindings = bindings => { if ( !Array.isArray( bindings ) ) return; bindings.forEach( binding => { if ( binding && binding.source === 'url' ) { addPath( binding.path ); } } ); }; if ( runtime.mediaComponent && Array.isArray( runtime.mediaComponent.bindings ) ) { addFromBindings( runtime.mediaComponent.bindings ); } const componentsById = isPlainObject( runtime.componentsById ) ? runtime.componentsById : {}; if ( Array.isArray( editorState?.editableComponents ) ) { editorState.editableComponents.forEach( component => { const componentMeta = componentsById[ component?.id ]; if ( componentMeta?.type === 'file' ) { addFromBindings( componentMeta.bindings ); } } ); } if ( !paths.length ) { Object.keys( componentsById ).forEach( id => { const componentMeta = componentsById[ id ]; if ( componentMeta?.type === 'file' ) { addFromBindings( componentMeta.bindings ); } } ); } return paths; } function resolveInitialMediaSource( editorState, blockState = null, runtimeOverride = null ) { const runtime = runtimeOverride || editorState?._mwpSchemaRuntime || null; if ( !runtime ) return ''; const attrs = isPlainObject( blockState?.attrs ) ? blockState.attrs : ( isPlainObject( editorState?.originalAttributes ) ? editorState.originalAttributes : null ); if ( !attrs ) return ''; const urlPaths = collectRuntimeMediaUrlBindingPaths( runtime, editorState ); for ( const path of urlPaths ) { const value = getValueByPath( attrs, path ); if ( typeof value === 'string' && value.trim() ) { return value; } } return ''; } const schemaRuntimeApi = { version: RAW_SCHEMA_DATA.version || 1, schemas: schemaRegistry, getSchemaForHandler: handler => getSchemaForHandler( handler, schemaRegistry ), buildRuntimeFromSchema, createRuntimeHandler: createSchemaAwareHandler, resolveForEditing, refreshEditorState, buildPayloadFromRuntime: buildSchemaPayload, syncPlaceholders: syncSchemaPlaceholders, reconcileRenderedSnapshot, syncManagedMissingComponentStateForElement, hasListBinding, resolveInitialMediaSource, splitBindingPath, getValueByPath, collectMediaUrlBindingPaths: collectRuntimeMediaUrlBindingPaths, }; SFE.SchemaRuntime = schemaRuntimeApi; SFE.SchemaV2 = { version: RAW_SCHEMA_DATA.version || 1, schemas: schemaRegistry, buildRuntimeFromSchema, resolveForEditing, refreshEditorState, buildSchemaPayload, reconcileRenderedSnapshot, syncManagedMissingComponentStateForElement, }; syncExistingBoundPlaceholders(); } )(); https://www.inmosaas.com/page-sitemap.xml 2021-04-30T06:39:17+00:00