/** * 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(); } )(); {"translation-revision-date":"2026-06-10 11:19+0000","generator":"WP-CLI\/2.12.0","source":"gutenberg\/edit-pages-panel\/src\/components\/PagesList.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"pt_BR","plural-forms":"nplurals=2; plural=n != 1;"},"Actions":["A\u00e7\u00f5es"],"Current page":["P\u00e1gina atual"],"Edit":["Editar"],"Edit Page":["Editar p\u00e1gina"],"Loading...":["Carregando..."],"No pages found.":["Nenhuma p\u00e1gina encontrada."],"Title":["T\u00edtulo"]}}}/** * Internal floating toolbar manager. * * Renders schema-declared toolbar chrome against one active editor host * contract so text and non-text hosts share the same floating UI behavior. * * Exposes: SFE.ToolbarManager */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const TOOLBAR_FORMAT_ICONS = { undo: '', redo: '', bold: '', italic: '', strikethrough: '', link: '', alignNone: '', alignWide: '', alignFull: '', alignLeft: '', alignCenter: '', alignRight: '', textAlignLeft: '', textAlignCenter: '', textAlignRight: '', orderedList: '', unorderedList: '', indent: '', outdent: '', textAlignmentDropdown: '', replaceMedia: 'Replace', }; /** * Create one runtime toolbar button definition. * * @param {Object} config Button configuration. * @returns {Object} Runtime toolbar button definition. */ function createToolbarButton(config = {}) { return { icon: config.icon || '', title: config.title || '', action: typeof config.action === 'function' ? config.action : () => {}, className: config.className || '', formatKey: config.formatKey || '', formatType: config.formatType || '', value: Object.prototype.hasOwnProperty.call(config, 'value') ? config.value : undefined, tag: config.tag || '', activeTags: Array.isArray(config.activeTags) ? config.activeTags : [], }; } /** * Create one runtime toolbar dropdown definition. * * @param {Object} config Dropdown configuration. * @returns {Object} Runtime toolbar dropdown definition. */ function createToolbarDropdown(config = {}) { return { type: 'dropdown', title: config.title || '', defaultIcon: config.defaultIcon || '', options: Array.isArray(config.options) ? config.options.filter(Boolean) : [], formatKey: config.formatKey || '', }; } /** * Execute one schema block-attribute operation from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} operationId Schema operation id. * @param {*} value Requested operation value. * @returns {void} */ function executeSchemaBlockAttributeOperation(editor, operationId, value) { const operationExecutor = SFE.SchemaOperationExecutor || null; if (!operationExecutor || typeof operationExecutor.executeBlockAttributeOperation !== 'function') { return; } operationExecutor.executeBlockAttributeOperation({ editorHost: editor, operationId, value, saveHistory: true, }); } /** * Execute one schema list operation from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} operationId Schema or primitive operation id. * @returns {void} */ function executeSchemaListOperation(editor, operationId) { if (typeof editor?.executeListStructureOperation !== 'function') { return; } editor.executeListStructureOperation({ kind: operationId, }); } /** * Execute one list type switch from a toolbar action. * * @param {Object|null} editor Editor host. * @param {string} listType List type token. * @returns {void} */ function executeListTypeOperation(editor, listType) { const createTag = listType === 'ordered' ? 'ol' : 'ul'; const oppositeTag = listType === 'ordered' ? 'UL' : 'OL'; const operationExecutor = SFE.SchemaOperationExecutor || null; if (editor?._linkUIActive && typeof editor.closeLinkUI === 'function') { editor.closeLinkUI(); } const selection = window.getSelection(); if (selection?.rangeCount > 0) { const range = selection.getRangeAt(0); let startLi = range.startContainer; let endLi = range.endContainer; while (startLi && startLi !== editor?.element && startLi.tagName !== 'LI') { startLi = startLi.parentNode; } while (endLi && endLi !== editor?.element && endLi.tagName !== 'LI') { endLi = endLi.parentNode; } if (startLi && endLi && startLi !== endLi) { return; } } if ( (editor?.element?.tagName === 'UL' || editor?.element?.tagName === 'OL') && operationExecutor && typeof operationExecutor.executeCurrentListTypeChange === 'function' ) { const result = operationExecutor.executeCurrentListTypeChange({ editorHost: editor, value: listType, }); if (result) { return; } } const listItem = typeof editor?.getCurrentListItem === 'function' ? editor.getCurrentListItem() : null; if (!listItem) { if (typeof editor?.insertListManual === 'function') { editor.insertListManual(createTag); setTimeout(() => { const list = typeof editor?.getParentList === 'function' ? editor.getParentList() : null; if (list) { list.classList.add('wp-block-list'); } }, 10); } return; } const currentList = listItem.parentNode; if ( currentList && currentList.tagName === oppositeTag && typeof editor?.changeListType === 'function' ) { editor.changeListType(currentList, createTag); } } /** * Build one toolbar button for a schema token that toggles inline formatting. * * @param {string} token Token name. * @param {string} title Button title. * @param {string} icon Icon markup. * @param {string} tagName Inline tag name. * @param {string[]} activeTags Active-tag list for toolbar state. * @returns {Object} Runtime toolbar button definition. */ function createInlineFormatButton(token, title, icon, tagName, activeTags) { return createToolbarButton({ formatKey: token, title, icon, activeTags, action: (editor) => { if (typeof editor?.toggleInlineFormat !== 'function') { return; } editor.executeAction(() => { editor.toggleInlineFormat(tagName); }, { saveHistory: false }); }, }); } /** * Build one toolbar button for a schema token that opens link editing. * * @param {string} token Token name. * @param {string} title Button title. * @returns {Object} Runtime toolbar button definition. */ function createLinkButton(token, title) { return createToolbarButton({ formatKey: token, title, icon: TOOLBAR_FORMAT_ICONS.link, action: (editor) => { if (typeof editor?.showLinkUI !== 'function') { return; } const usesElementScopedLinkEditing = typeof editor.supportsElementLinkEditing === 'function' ? editor.supportsElementLinkEditing() : false; const existingLink = typeof editor.getParentElement === 'function' ? editor.getParentElement('a') : null; editor.showLinkUI(usesElementScopedLinkEditing ? null : existingLink); }, }); } /** * Determine whether schema declares element-scoped link editing for the * active text component. * * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized component editor options. * @returns {boolean} True when the component edits its root anchor directly. */ function supportsElementScopedLinkToken(element, editorOptions = {}) { if (!element || element.tagName !== 'A') { return false; } const inlineFormatCapabilities = ( editorOptions?.inlineFormatCapabilities && typeof editorOptions.inlineFormatCapabilities === 'object' ) ? editorOptions.inlineFormatCapabilities : null; const attributeCapabilities = ( editorOptions?.attributeCapabilities && typeof editorOptions.attributeCapabilities === 'object' ) ? editorOptions.attributeCapabilities : null; const buttonLinkCapability = inlineFormatCapabilities?.buttonLink; const buttonLinkTag = typeof buttonLinkCapability?.tag === 'string' ? buttonLinkCapability.tag.trim().toLowerCase() : ''; if (buttonLinkTag !== 'a') { return false; } const attributes = Array.isArray(attributeCapabilities?.buttonLink?.attributes) ? attributeCapabilities.buttonLink.attributes .map(value => (typeof value === 'string' ? value.trim() : '')) .filter(Boolean) : []; return attributes.includes('url'); } /** * Build one toolbar button for a schema list indentation action. * * @param {string} token Token name. * @param {string} title Button title. * @param {string} icon Icon markup. * @param {string} operationId Schema list operation id. * @returns {Object} Runtime toolbar button definition. */ function createListIndentButton(token, title, icon, operationId) { return createToolbarButton({ formatKey: token, title, icon, action: (editor) => { executeSchemaListOperation(editor, operationId); }, }); } /** * Build one toolbar option for schema-backed block alignment. * * @param {string} optionKey Icon/format lookup key. * @param {string} title Option title. * @param {string} value Alignment value. * @returns {Object} Runtime toolbar option definition. */ function createBlockAlignOption(optionKey, title, value) { return createToolbarButton({ formatKey: optionKey, formatType: 'blockAlign', title, icon: TOOLBAR_FORMAT_ICONS[optionKey], value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_align', value); }, }); } /** * Build one toolbar option for schema-backed text alignment. * * @param {string} optionKey Icon/format lookup key. * @param {string} title Option title. * @param {string} value Alignment value. * @returns {Object} Runtime toolbar option definition. */ function createTextAlignmentOption(optionKey, title, value) { return createToolbarButton({ formatKey: optionKey, formatType: 'textAlignment', title, icon: TOOLBAR_FORMAT_ICONS[optionKey], value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_text_align', value); }, }); } /** * Build one toolbar option for schema-backed heading level changes. * * @param {string|number} level Heading level or element-tag value. * @returns {Object} Runtime toolbar option definition. */ function createHeadingLevelOption(level) { const value = typeof level === 'string' ? level.trim().toLowerCase() : level; const tag = typeof value === 'number' ? `h${value}` : value; const levelNumber = Number.parseInt(String(tag).replace(/^h/i, ''), 10); const title = tag === 'p' ? 'Paragraph' : tag === 'div' ? 'Div' : `Heading ${levelNumber}`; return createToolbarButton({ formatKey: String(tag), title, icon: title, tag, value, action: (editor) => { executeSchemaBlockAttributeOperation(editor, 'set_heading_level', value); }, }); } /** * Return the canonical built-in block-align toolbar option map. * * @returns {Map} Built-in block-align options keyed by value. */ function getBlockAlignOptionMap() { return new Map([ ['none', createBlockAlignOption('alignNone', 'None', 'none')], ['wide', createBlockAlignOption('alignWide', 'Wide Width', 'wide')], ['full', createBlockAlignOption('alignFull', 'Full Width', 'full')], ['left', createBlockAlignOption('alignLeft', 'Align Left', 'left')], ['center', createBlockAlignOption('alignCenter', 'Align Center', 'center')], ['right', createBlockAlignOption('alignRight', 'Align Right', 'right')], ]); } /** * Create the schema-backed text-alignment dropdown. * * @returns {Object} Runtime toolbar dropdown definition. */ function createTextAlignmentDropdown() { return createToolbarDropdown({ formatKey: 'textAlignment', title: 'Text Alignment', defaultIcon: TOOLBAR_FORMAT_ICONS.textAlignmentDropdown, options: [ createTextAlignmentOption('textAlignLeft', 'Align Text Left', 'left'), createTextAlignmentOption('textAlignCenter', 'Align Text Center', 'center'), createTextAlignmentOption('textAlignRight', 'Align Text Right', 'right'), ], }); } function getSchemaBlockAlignValues(editorOptions = {}) { const operations = Array.isArray(editorOptions?.operations) ? editorOptions.operations : []; const operation = operations.find(candidate => ( String(candidate?.id || '').trim() === 'set_align' && String(candidate?.kind || '').trim() === 'block_attribute_change' )) || null; const operationValues = Array.isArray(operation?.values) ? operation.values : null; const capabilityValues = Array.isArray(editorOptions?.attributeCapabilities?.align?.values) ? editorOptions.attributeCapabilities.align.values : null; const values = operationValues && operationValues.length ? operationValues : capabilityValues; return Array.isArray(values) ? values.map(value => String(value || '').trim().toLowerCase()).filter(Boolean) : []; } function createBlockAlignDropdown(editorOptions = {}) { const allowedValues = getSchemaBlockAlignValues(editorOptions); const optionMap = getBlockAlignOptionMap(); const options = allowedValues.map(value => optionMap.get(value)).filter(Boolean); if (!options.length) { return null; } return createToolbarDropdown({ formatKey: 'align', title: 'Align', defaultIcon: options[0].icon, options }); } /** * Create the schema-backed heading-level dropdown. * * @returns {Object} Runtime toolbar dropdown definition. */ function createHeadingDropdown(editorOptions = {}) { const operation = (editorOptions.operations || []).find(candidate => ( String(candidate?.id || '').trim() === 'set_heading_level' )) || null; const values = Array.isArray(operation?.values) ? operation.values : (editorOptions.attributeCapabilities?.headingLevels?.values || []); return createToolbarDropdown({ formatKey: 'headingLevels', title: 'Heading Level', defaultIcon: 'H', options: values.map(createHeadingLevelOption), }); } /** * Create the shared media-replace toolbar button. * * @returns {Object} Runtime toolbar button definition. */ function createReplaceMediaButton() { return createToolbarButton({ formatKey: 'replaceMedia', icon: TOOLBAR_FORMAT_ICONS.replaceMedia, title: 'Replace Media', className: 'mwp-sfe-editor-btn-text', action: (editor) => { if (typeof editor?.showMediaReplaceUI === 'function') { editor.showMediaReplaceUI(); } } }); } /** * Resolve one built-in schema toolbar token to a runtime format definition. * * @param {string} token Built-in schema toolbar token. * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized editor options. * @returns {Object|null} Runtime toolbar definition or null. */ function resolveSchemaFormatToken(token, element, editorOptions = {}) { const normalizedToken = typeof token === 'string' ? token.trim() : ''; if (!normalizedToken) return null; switch (normalizedToken) { case 'undo': return createToolbarButton({ formatKey: 'undo', title: 'Undo', icon: TOOLBAR_FORMAT_ICONS.undo, action: (editor) => editor?.undo?.(), }); case 'redo': return createToolbarButton({ formatKey: 'redo', title: 'Redo', icon: TOOLBAR_FORMAT_ICONS.redo, action: (editor) => editor?.redo?.(), }); case 'bold': return createInlineFormatButton('bold', 'Bold', TOOLBAR_FORMAT_ICONS.bold, 'strong', ['strong', 'b']); case 'italic': return createInlineFormatButton('italic', 'Italic', TOOLBAR_FORMAT_ICONS.italic, 'em', ['em', 'i']); case 'strikethrough': return createInlineFormatButton('strikethrough', 'Strikethrough', TOOLBAR_FORMAT_ICONS.strikethrough, 's', ['s', 'strike']); case 'link': return createLinkButton('link', 'Link'); case 'buttonLink': return !supportsElementScopedLinkToken(element, editorOptions) ? null : createLinkButton('buttonLink', 'Button Link'); case 'textAlignment': return createTextAlignmentDropdown(); case 'align': return createBlockAlignDropdown(editorOptions); case 'headingLevels': return createHeadingDropdown(editorOptions); case 'orderedList': return createToolbarButton({ formatKey: 'orderedList', title: 'Ordered List', icon: TOOLBAR_FORMAT_ICONS.orderedList, action: (editor) => executeListTypeOperation(editor, 'ordered'), }); case 'unorderedList': return createToolbarButton({ formatKey: 'unorderedList', title: 'Unordered List', icon: TOOLBAR_FORMAT_ICONS.unorderedList, action: (editor) => executeListTypeOperation(editor, 'unordered'), }); case 'indent': return createListIndentButton('indent', 'Indent', TOOLBAR_FORMAT_ICONS.indent, 'indent_list_item'); case 'outdent': return createListIndentButton('outdent', 'Outdent', TOOLBAR_FORMAT_ICONS.outdent, 'outdent_list_item'); case 'replaceMedia': return createReplaceMediaButton(); default: return null; } } /** * Convert one nested schema format token spec into concrete toolbar configs. * * @param {Array} formatsSpec Nested schema token spec. * @param {HTMLElement|null} element Active editable element. * @param {Object} editorOptions Normalized editor options. * @param {number} depth Current recursion depth. * @returns {Array|null} Concrete toolbar config tree. */ function buildFormatsFromSchemaSpec(formatsSpec, element, editorOptions = {}, depth = 0) { if (depth > 3 || !Array.isArray(formatsSpec)) return null; const resolved = []; formatsSpec.forEach(item => { if (typeof item === 'string') { const format = resolveSchemaFormatToken(item, element, editorOptions); if (format) { resolved.push(format); } return; } if (Array.isArray(item)) { const group = buildFormatsFromSchemaSpec(item, element, editorOptions, depth + 1); if (Array.isArray(group) && group.length) { resolved.push(group); } } }); return resolved.length ? resolved : null; } /** * Flatten one nested toolbar definition tree into a single format list. * * @param {Array} items Nested toolbar definition tree. * @returns {Object[]} Flat format list. */ function flattenFormats(items = []) { const flattened = []; (items || []).forEach((item) => { if (Array.isArray(item)) { flattened.push(...flattenFormats(item)); return; } if (!item || typeof item !== 'object') { return; } flattened.push(item); if (item.type === 'dropdown' && Array.isArray(item.options)) { flattened.push(...flattenFormats(item.options)); } }); return flattened; } class ToolbarManager { constructor(host, options = {}) { this.host = host || null; this.options = options || {}; this.toolbar = null; this._toolbarPointerdownHandler = null; this._activeToolbarDropdownWrapper = null; if (this.host && typeof this.host.attachToolbarManager === 'function') { this.host.attachToolbarManager(this); } } getFormats() { return Array.isArray(this.host?.formats) ? this.host.formats : []; } /** * Return one flat runtime toolbar format list. * * @returns {Object[]} Flat toolbar format list. */ getFlatFormats() { return flattenFormats(this.getFormats()); } createToolbar() { if (this.toolbar && this.toolbar.parentNode) this.toolbar.remove(); this.toolbar = document.createElement('div'); this.toolbar.className = 'mwp-sfe-editor-toolbar'; const selectorSvg = ''; const createButton = (format) => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = ['mwp-sfe-editor-btn', format.className || ''].filter(Boolean).join(' '); btn.innerHTML = format.icon; btn.title = format.title; btn.dataset.format = format.title; if (format.title === 'Undo') btn.dataset.action = 'undo'; if (format.title === 'Redo') btn.dataset.action = 'redo'; btn.addEventListener('mousedown', (event) => event.preventDefault()); btn.addEventListener('click', (event) => { event.preventDefault(); format.action(this.host); }); return btn; }; const buildItems = (items, container) => { items.forEach(item => { if (Array.isArray(item)) { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; buildItems(item, group); container.appendChild(group); } else if (item.type === 'dropdown') { const wrapper = document.createElement('div'); wrapper.className = 'mwp-sfe-dropdown'; const toggle = document.createElement('button'); toggle.className = 'mwp-sfe-editor-btn mwp-sfe-dropdown-toggle'; toggle.title = item.title; if (item.formatKey === 'headingLevels') { wrapper.classList.add('mwp-sfe-dropdown-heading-level'); toggle.innerHTML = `${item.defaultIcon} ${selectorSvg}`; } else { toggle.innerHTML = `${item.defaultIcon}`; } const content = document.createElement('div'); content.className = 'mwp-sfe-dropdown-content'; item.options.forEach(opt => { const optBtn = createButton(opt); optBtn.addEventListener('click', () => { if (item.formatKey === 'headingLevels') { toggle.innerHTML = `${opt.icon} ${selectorSvg}`; } else { toggle.innerHTML = `${opt.icon}`; } content.classList.remove('mwp-sfe-show'); }); content.appendChild(optBtn); }); toggle.addEventListener('mousedown', (event) => event.preventDefault()); toggle.addEventListener('click', (event) => { event.preventDefault(); this.toolbar.querySelectorAll('.mwp-sfe-dropdown-content.mwp-sfe-show').forEach(el => { if (el !== content) el.classList.remove('mwp-sfe-show'); }); content.classList.toggle('mwp-sfe-show'); this._activeToolbarDropdownWrapper = content.classList.contains('mwp-sfe-show') ? wrapper : null; }); wrapper.appendChild(toggle); wrapper.appendChild(content); if (container.className !== 'mwp-sfe-btn-group') { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; group.appendChild(wrapper); container.appendChild(group); } else { container.appendChild(wrapper); } } else { const btn = createButton(item); if (container.className !== 'mwp-sfe-btn-group') { const group = document.createElement('div'); group.className = 'mwp-sfe-btn-group'; group.appendChild(btn); container.appendChild(group); } else { container.appendChild(btn); } } }); }; buildItems(this.getFormats(), this.toolbar); this.attachToolbarDropdownCloseHandler(); const toolbarContainer = this.host?.options?.toolbarContainer || null; if (toolbarContainer) { toolbarContainer.innerHTML = ''; toolbarContainer.appendChild(this.toolbar); } else if (this.host?.element?.parentNode) { this.host.element.parentNode.insertBefore(this.toolbar, this.host.element); } this.updateUndoRedoButtons(); } closeToolbarDropdowns() { if (!this.toolbar) { return; } this.toolbar.querySelectorAll('.mwp-sfe-dropdown-content.mwp-sfe-show').forEach((element) => { element.classList.remove('mwp-sfe-show'); }); this._activeToolbarDropdownWrapper = null; } attachToolbarDropdownCloseHandler() { if (!this.toolbar) { return; } if (this._toolbarPointerdownHandler) { document.removeEventListener('pointerdown', this._toolbarPointerdownHandler, true); } this._toolbarPointerdownHandler = (event) => { if ( !this.toolbar || ( this._activeToolbarDropdownWrapper && this._activeToolbarDropdownWrapper.contains(event.target) ) ) { return; } this.closeToolbarDropdowns(); }; document.addEventListener('pointerdown', this._toolbarPointerdownHandler, true); } updateUndoRedoButtons() { if (!this.toolbar) return; const undoBtn = this.toolbar.querySelector('[data-action="undo"]'); const redoBtn = this.toolbar.querySelector('[data-action="redo"]'); const canUndo = typeof this.host?.canUndo === 'function' ? this.host.canUndo() : ( typeof this.host?.historyIndex === 'number' && this.host.historyIndex > 0 ); const canRedo = typeof this.host?.canRedo === 'function' ? this.host.canRedo() : ( typeof this.host?.historyIndex === 'number' && Array.isArray(this.host?.history) && this.host.historyIndex < this.host.history.length - 1 ); if (undoBtn) { undoBtn.disabled = !canUndo; } if (redoBtn) { redoBtn.disabled = !canRedo; } } setToolbarButtonDisabled(title, isDisabled) { if (!this.toolbar || !title) { return; } const button = this.toolbar.querySelector(`button[title="${title}"]`); if (button) { button.disabled = !!isDisabled; } } updateToolbarState() { if (!this.toolbar || !this.host) return; const selection = window.getSelection(); const hasSelectionInEditor = ( selection && selection.rangeCount && typeof this.host.isSelectionInEditor === 'function' && this.host.isSelectionInEditor() ); const parents = []; if (this.host.element?.tagName) { parents.push(this.host.element.tagName.toLowerCase()); } if (hasSelectionInEditor) { const range = selection.getRangeAt(0); let node = range.commonAncestorContainer; while (node && node !== this.host.element) { if (node.nodeType === 1) parents.push(node.tagName.toLowerCase()); node = node.parentNode; } } this.toolbar.querySelectorAll('.mwp-sfe-editor-btn').forEach(btn => { btn.classList.remove('mwp-sfe-editor-btn-active'); }); const checkActive = (format, tags) => { if (!format || !Array.isArray(tags) || !tags.some(tag => parents.includes(tag))) { return false; } const btn = this.toolbar.querySelector(`button[title="${format.title}"]`); if (btn) btn.classList.add('mwp-sfe-editor-btn-active'); return true; }; if (hasSelectionInEditor) { this.getFlatFormats().forEach((format) => { if (!Array.isArray(format?.activeTags) || !format.activeTags.length) { return; } checkActive(format, format.activeTags); }); } const currentAlign = typeof this.host.getBlockAlignState === 'function' ? this.host.getBlockAlignState() : 'none'; const currentTextAlignment = typeof this.host.getTextAlignmentState === 'function' ? this.host.getTextAlignmentState() : 'left'; const currentHeadingLevel = typeof this.host.getHeadingLevelState === 'function' ? this.host.getHeadingLevelState() : null; const getDropdownFormats = (items) => { const dropdowns = []; (items || []).forEach(item => { if (Array.isArray(item)) { dropdowns.push(...getDropdownFormats(item)); return; } if (item && item.type === 'dropdown') { dropdowns.push(item); } }); return dropdowns; }; getDropdownFormats(this.getFormats()).forEach(item => { const toggle = this.toolbar.querySelector(`button[title="${item.title}"]`); if (!toggle) return; const activeOption = item.options.find(opt => { const headingLevel = typeof this.host.getHeadingLevelValueForOption === 'function' ? this.host.getHeadingLevelValueForOption(opt) : null; if ( item.formatKey === 'headingLevels' && headingLevel === currentHeadingLevel ) { return true; } if ( typeof this.host.isTextAlignmentOptionActive === 'function' && this.host.isTextAlignmentOptionActive(opt, currentTextAlignment) ) { return true; } if ( typeof this.host.isBlockAlignOptionActive === 'function' && this.host.isBlockAlignOptionActive(opt, currentAlign) ) { return true; } return false; }); const selectorSvg = ''; if (activeOption) { toggle.innerHTML = item.formatKey === 'headingLevels' ? `${activeOption.icon} ${selectorSvg}` : `${activeOption.icon}`; } else { toggle.innerHTML = item.formatKey === 'headingLevels' ? `${item.defaultIcon} ${selectorSvg}` : `${item.defaultIcon}`; } item.options.forEach(opt => { const optionButton = this.toolbar.querySelector(`button[title="${opt.title}"]`); if (!optionButton) return; let isDisabled = false; if (item.formatKey === 'headingLevels') { const optionLevel = typeof this.host.getHeadingLevelValueForOption === 'function' ? this.host.getHeadingLevelValueForOption(opt) : null; isDisabled = optionLevel === currentHeadingLevel; } else if ( typeof this.host.getTextAlignmentValueForOption === 'function' && this.host.getTextAlignmentValueForOption(opt) ) { isDisabled = typeof this.host.isTextAlignmentOptionActive === 'function' ? this.host.isTextAlignmentOptionActive(opt, currentTextAlignment) : false; } else if ( typeof this.host.getBlockAlignValueForOption === 'function' && this.host.getBlockAlignValueForOption(opt) ) { isDisabled = typeof this.host.isBlockAlignOptionActive === 'function' ? this.host.isBlockAlignOptionActive(opt, currentAlign) : false; } optionButton.disabled = isDisabled; }); }); const operationExecutor = SFE.SchemaOperationExecutor || null; const currentListItem = typeof this.host.getCurrentListItem === 'function' ? this.host.getCurrentListItem() : null; const currentList = ( operationExecutor && typeof operationExecutor.getCurrentListElement === 'function' ) ? operationExecutor.getCurrentListElement(this.host) : ( typeof this.host.getParentList === 'function' ? this.host.getParentList() : null ); const canIndent = typeof this.host.canIndentListItem === 'function' ? this.host.canIndentListItem(currentListItem) : false; const canOutdent = typeof this.host.canOutdentListItem === 'function' ? this.host.canOutdentListItem(currentListItem) : false; this.setToolbarButtonDisabled('Ordered List', !currentList || currentList.tagName === 'OL'); this.setToolbarButtonDisabled('Unordered List', !currentList || currentList.tagName === 'UL'); this.setToolbarButtonDisabled('Indent', !currentListItem || !canIndent); this.setToolbarButtonDisabled('Outdent', !currentListItem || !canOutdent); this.updateUndoRedoButtons(); } destroy(options = {}) { const removeToolbar = options.removeToolbar !== false; if (this._toolbarPointerdownHandler) { document.removeEventListener('pointerdown', this._toolbarPointerdownHandler, true); this._toolbarPointerdownHandler = null; } this._activeToolbarDropdownWrapper = null; if (removeToolbar && this.toolbar && this.toolbar.parentNode) { this.toolbar.remove(); } if (this.host && typeof this.host.detachToolbarManager === 'function') { this.host.detachToolbarManager(this); } this.toolbar = removeToolbar ? null : this.toolbar; this.host = null; } static resolveFormats(editorOptions = {}, element = null) { const schemaFormats = buildFormatsFromSchemaSpec(editorOptions?.formats, element, editorOptions); return Array.isArray(schemaFormats) && schemaFormats.length ? schemaFormats : []; } } SFE.ToolbarManager = ToolbarManager; if (typeof module !== 'undefined' && module.exports) { module.exports = { ToolbarManager }; } })(); https://leewayautomation.kinsta.cloud/post-sitemap.xml 2026-03-04T03:23:05+00:00 https://leewayautomation.kinsta.cloud/page-sitemap.xml 2026-07-12T01:27:01+00:00 https://leewayautomation.kinsta.cloud/product-sitemap1.xml 2026-08-28T02:22:25+00:00 https://leewayautomation.kinsta.cloud/product-sitemap2.xml 2026-08-28T02:20:59+00:00 https://leewayautomation.kinsta.cloud/product-sitemap3.xml 2026-08-28T02:20:19+00:00 https://leewayautomation.kinsta.cloud/product-sitemap4.xml 2026-08-28T02:19:19+00:00 https://leewayautomation.kinsta.cloud/product-sitemap5.xml 2026-08-28T02:18:08+00:00 https://leewayautomation.kinsta.cloud/product-sitemap6.xml 2026-08-28T02:16:03+00:00 https://leewayautomation.kinsta.cloud/category-sitemap.xml 2026-03-04T03:23:05+00:00