/** * 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; } /** * Normalize the input contract declared by one schema operation. * * Operation inputs are part of the schema-owned runtime contract. Preserve * only the generic input types that FrontEdit's operation executor supports * so downstream editor hosts do not receive an ambiguous or mutable shape. * * @param {Object} rawInputs Raw schema input definitions. * @returns {Object|null} Normalized input definitions or null. */ function normalizeEditorOperationInputs( rawInputs ) { if ( !isPlainObject( rawInputs ) ) return null; const normalized = {}; Object.keys( rawInputs ).forEach( rawInputName => { const inputName = typeof rawInputName === 'string' ? rawInputName.trim() : ''; const rawDefinition = rawInputs[ rawInputName ]; const type = typeof rawDefinition?.type === 'string' ? rawDefinition.type.trim() : ''; if ( !/^[a-z][a-z0-9_]*$/.test( inputName ) || !isPlainObject( rawDefinition ) || ![ 'scalar', 'zero_based_indexes_or_all', 'rich_text_runs', 'url' ].includes( type ) ) { return; } normalized[ inputName ] = { required: rawDefinition.required === true, type, }; } ); return Object.keys( normalized ).length ? normalized : 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 ); const inputs = normalizeEditorOperationInputs( rawOperation.inputs ); if ( kind === 'block_attribute_change' && ( !inputs || !inputs.value || inputs.value.required !== true || inputs.value.type !== 'scalar' ) ) { return null; } 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 ( inputs ) { normalized.inputs = inputs; } if ( rawOperation.publicOperation === true ) { normalized.publicOperation = true; } 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, bindingSource: contentBinding.source, 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, bindingSource: contentBinding.source, 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(); } )(); @keyframes pounding-logo { 0% { transform: scale( .60 ); } 10% { transform: scale( 1 ); } 30% { transform: scale( .90 ); } 40% { transform: scale( 1 ); } 50% { transform: scale( .90 ); } 60% { transform: scale( 1 ); } 70% { transform: scale( .90 ); } 90% { transform: scale( 1 ); } 100% { transform: scale( .60 ); } } .pounding-logo { animation: pounding-logo 8s infinite; } html { margin: 0; padding: 0; min-height: 100%; position: relative; background-color: #F0F0F1; } body { background-color: #F0F0F1; height: 100%; } a.link:link, a.link:active, a.link:visited { color: var(--jb-dark-orange); } #logo { display: block; height: 55px; background: url("../images/logo-loader.png") 0% 0% no-repeat; background-size: 250px; margin-top: 5px; } #logo span { display: none; } #menu { background-color: #ffffff; position: absolute; top: 0; left: 0; bottom: 0; overflow: hidden; width: 230px; min-height: 100vh; height: 100%; box-shadow: 10px 0 20px 0 rgba(0,0,0,0.1); z-index: 998; transition: width 200ms; } #container { margin-left: 230px; position: relative; z-index: 999; transition: margin-left 200ms; } ul#navigation { padding: 0; margin: 10px 0 0; list-style: none; white-space: nowrap; } ul#navigation li a { display: block; background-color: transparent; color: #1F2C38; padding: 10px 20px; border-radius: 10px; margin: 0 15px 10px; text-decoration: none; transition: all 200ms; font-size: 13px; white-space: nowrap; } ul#navigation li a i { font-size: 16px; width: 22.5px; transition: font-size 400ms; text-align: center; } ul#navigation li.active a, ul#navigation li a:hover { background-color: #1F2C38; color: #ffffff; } ul#navigation li a span { margin-left: 10px; display: inline-block; } .menu-closed #menu { width: 73px; } .menu-closed #container { margin-left: 73px; } .menu-closed ul#navigation li a { border-radius: 50px; padding: 10px; } .menu-closed ul#navigation li a i { font-size: 20px; } .menu-closed ul#navigation li a span { display: none; } .menu-closed #logo { background-image: url("../images/logo-small.png"); } header { background-color: #ffffff; height: 80px; display: block; } section { padding: 20px; } .dropdown ul { border: 0 none; box-shadow: 0 0 20px 0 rgba(0,0,0,0.1); } .dropdown-item { transition: all 100ms; } .text-right { text-align: right; } .rounded-box { background-color: #ffffff; padding: 20px; border-radius: 15px; } .rounded-widget { background-color: #ffffff; padding: 15px; border-radius: 10px; } .rounded-widget.orange { color: var(--jb-dark-blue); background-color: var(--jb-xtra-light-orange); } .rounded-widget.orange .icon { color: var(--jb-dark-orange); } .rounded-widget.gray { color: var(--jb-dark-blue); background-color: var(--jb-xtra-light-gray); } .rounded-widget.gray .icon { color: var(--jb-dark-blue); } .rounded-widget.blue { color: var(--jb-dark-blue); background-color: var(--jb-xtra-light-navi); } .rounded-widget.blue .icon { color: var(--jb-dark-navi); } .rounded-widget.green { color: var(--jb-dark-blue); background-color: var(--jb-xtra-light-green); } .rounded-widget.green .icon { color: var(--jb-dark-green); } .rounded-widget.red { color: var(--jb-dark-blue); background-color: var(--jb-xtra-light-red); } .rounded-widget.red .icon { color: var(--jb-dark-red); } .rounded-widget .icon { display: table-cell; vertical-align: middle; font-size: 30px; padding: 0 20px 0 10px; } .rounded-widget .text { display: table-cell; vertical-align: middle; } .rounded-widget .text .stat { font-weight: bold; font-size: 26px; line-height: 24px; } .rounded-widget .text .stat span { font-weight: normal; font-size: 12px; line-height: 12px; } .rounded-widget .text .title { font-weight: normal; font-size: 12px; } .box-title { font-size: 18px; font-weight: bold; margin: 0 15px 15px; display: block; } .box-title::before { content: ""; width: 6px; height: 30px; background-color: #1a1d20; display: table-cell; border-radius: 5px; } .box-title div { display: table-cell; vertical-align: middle; padding: 10px 0 10px 10px; } .box-title div p { padding: 0; margin: 0; font-weight: normal; font-size: 12px; } .box-controllers { padding: 0 15px; } .btn span { margin-left: 5px; } p.help { font-size: 12px; margin: 0; } .col-form-label { font-weight: bold; font-size: 14px; } .fields-container { padding: 15px; } label { cursor: pointer; } .dropdown button, input[type=text] { padding: 0 10px; cursor: text; width: 100%; height: 39.5px; font-size: 13px; box-shadow: none !important; } .dropdown button:focus { background-color: #F5F5F5; } .dropdown button { text-align: left; clear: both; } .dropdown button:after { float: right; font-family: "Font Awesome 6 Sharp"; font-weight: 300; content: "\f107"; border: 0 none; padding-top: 3px; transition: all 200ms; } .dropdown button.show:after { transform: rotate(-90deg) } .dropdown-menu { width: 100%; } /* .dropdown-item.active, .dropdown-item:active { background-color: ; } */ input[type="radio"], input[type="checkbox"] { appearance: none; -webkit-appearance: none; width: 16px; height: 16px; /* border: 1px solid var(--jb-dark-orange); */ border-radius: 50%; background-color: transparent; position: relative; top: 4px; cursor: pointer; } /* input[type="radio"]:checked::before, input[type="checkbox"]:checked::before { content: ""; display: block; width: 8px; height: 8px; background-color: var(--jb-dark-orange); border-radius: 50%; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: appear 0.5s; cursor: pointer; } */ input[type="checkbox"]:not(:checked)::before { content: ""; display: block; width: 8px; height: 8px; background-color: #ffffff; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: disappear 0.5s; cursor: pointer; } input[type="checkbox"] { border-radius: 3px; } input[type="checkbox"]:checked::before, input[type="checkbox"]:not(:checked)::before { border-radius: 2px; } .radio label, .checkbox label { margin-right: 10px; font-size: 14px; } .radio label:last-child, .checkbox label:last-child { margin-right: 0; } @keyframes appear { 0% { transform: translate(-50%, -50%) scale(0); background-color: #fff; } 45% { transform: translate(-50%, -50%) scale(1.6); background-color: var(--jb-dark-orange); } 50% { transform: translate(-50%, -50%) scale(1.7); background-color: var(--jb-dark-orange); } 55% { transform: translate(-50%, -50%) scale(1.6); } 100% { transform: translate(-50%, -50%) scale(1); background-color: var(--jb-dark-orange); } } @keyframes disappear { 100% { transform: translate(-50%, -50%) scale(0); background-color: #fff; } 55% { transform: translate(-50%, -50%) scale(1.6); background-color: var(--jb-dark-orange); } 50% { transform: translate(-50%, -50%) scale(1.7); background-color: var(--jb-dark-orange); } 45% { transform: translate(-50%, -50%) scale(1.6); } 0% { transform: translate(-50%, -50%) scale(1); background-color: var(--jb-dark-orange); } } label.search-box { background-color: #F5F5F5; border: 0 none; border-radius: 10px; padding: 0 10px; cursor: text; width: 100%; } label.search-box i { margin: 0 10px 0 5px; } label.search-box input { background-color: transparent; border: 0 none; font-size: 13px; height: 39.5px; width: calc( 100% - 40px ); } label.search-box input:focus, label.search-box input:active { border: 0 none; } .rounded-box.table-box { padding: 15px 0; } .rounded-box.table-box table thead tr { border-bottom: 0 none; } .rounded-box.table-box table thead th { padding: 15px 10px; font-size: 13px; font-weight: normal; } .rounded-box.table-box table tbody td { padding: 15px 10px; font-size: 12px; border-bottom: 1px solid var(--jb-xtra-light-gray); } .rounded-box.table-box table tbody td.no-result { text-align: center; font-size: 14px; font-weight: bold; padding: 30px; } .label { border-radius: 20px; padding: 3px 8px; color: #ffffff; font-size: 12px; } ul.pagination { padding: 0; font-size: 13px; display: inline-block; margin-right: 15px; } ul.pagination li { margin-right: 5px; float: left; } ul.pagination li:last-child { margin-right: 0; } ul.pagination li a { display: block; padding: 10px 15px; border: 1px solid var(--jb-xtra-light-gray); border-radius: 8px; text-decoration: none; color: var(--jb-dark-blue); } ul.pagination li.more a { border: 1px solid #ffffff; } ul.pagination li.active a,ul.pagination li:hover a { border: 1px solid var(--jb-dark-blue); background-color: var(--jb-dark-blue); color: #ffffff; } th > button { border: 0 none; background: transparent; display: inline-block; } .pagination-text { display: table-cell; padding: 10px 15px; } .row + .row, .grid + .grid { margin-top: 20px; } .row > div { margin-bottom: 5px; } .system-alerts { padding: 20px; } .system-alerts > div { margin-bottom: 10px; } .dropdown-btn button, .btn { border-radius: 10px; border: 0 none; font-size: 13px; padding: 10px 15px; } .dropdown-btn button, .btn.btn-rounded { border-radius: 30px; } .input-group-text { border: 0 none; background-color: #e3e3e3; } #menu-bars { display: none; font-size: 36px; position: fixed; right: 20px; top: 15px; z-index: 1001; } .modal.left .modal-dialog, .modal.right .modal-dialog { position: fixed; margin: auto; width: 33%; max-width: 33%; height: 100%; -webkit-transform: translate3d(0%, 0, 0); -ms-transform: translate3d(0%, 0, 0); -o-transform: translate3d(0%, 0, 0); transform: translate3d(0%, 0, 0); } .modal.left .modal-content, .modal.right .modal-content { height: 100%; overflow-y: auto; } .modal.left .modal-body, .modal.right .modal-body { padding: 15px 15px 80px; } /*Left*/ .modal.left.fade .modal-dialog{ left: -33%; -webkit-transition: opacity 0.3s linear, left 0.3s ease-out; -moz-transition: opacity 0.3s linear, left 0.3s ease-out; -o-transition: opacity 0.3s linear, left 0.3s ease-out; transition: opacity 0.3s linear, left 0.3s ease-out; } .modal.left.fade.show .modal-dialog{ left: 0; } /*Right*/ .modal.right.fade .modal-dialog { right: -33%; -webkit-transition: opacity 0.3s linear, right 0.3s ease-out; -moz-transition: opacity 0.3s linear, right 0.3s ease-out; -o-transition: opacity 0.3s linear, right 0.3s ease-out; transition: opacity 0.3s linear, right 0.3s ease-out; } .modal.right.fade.show .modal-dialog { right: 0; } .modal.left .modal-content, .modal.right .modal-content { border-radius: 0; border: 0 none; } .modal .box-title { margin: 0; } button.dropdown-toggle { white-space: nowrap; } .pretty.p-children-selected .state.p-success label::after, .pretty.p-toggle .state.p-success label::after { background-color: #5fbd5f !important; } .pretty.p-smooth.p-children-selected + .state label::after { transition: all .3s ease; } .pretty.p-smooth.p-default.p-children-selected + .state label::after { -webkit-animation: zoom .2s ease; animation: zoom .2s ease; } PULSA808 Portal : Tren Hiburan Game dan Keuntungan Fantastis
Jackpot Games
Meta Space
Meta Space
Odin Territory
Odin Territory
Starlight Princess
Starlight Princess
Monkey Warrior
Monkey Warrior
Sweet Bonanza Super Scatter
Sweet Bonanza Super Scatter
Monkeys Gold
Monkeys Gold
New Games
Wild Skullz
Wild Skullz
Sweet Bonanza 1000 Dice
Sweet Bonanza 1000 Dice
Witch Heart
Witch Heart
Stalight Princess Super Scatter
Stalight Princess Super Scatter
Waves Of Poseidon
Waves Of Poseidon
Wild Wildebeest Wins
Wild Wildebeest Wins
Game Populer
Wukong Black Scatter
Wukong Black Scatter
World Of Emoji
World Of Emoji
Zeus
Zeus
Wisdom Athena
Wisdom Athena
Wild Spells
Wild Spells
Xways Hoarder 2
Xways Hoarder 2

PULSA808 Portal : Tren Hiburan Game dan Keuntungan Fantastis