/** * Text editor - all contenteditable text-editing setup * * Reads (via globals): * SFE.Context - .draftEditState * SFE.ElementPrep - .getCleanHTML, .getContent * SFE.OverlayManager * SFE.PositionManager - .positionFloatingElements * SFE.FocusManager - .createFocusManager * SFE.handleInlineSave - set by SaveManager * SFE.closeInPlaceEditor - set by EditorLifecycle * SFE.startMediaEditing - set by frontend-inline-edit.js (via SFE.MediaEditor) * SFE.MWPEditor - rich text editor class * SFE.ManagerData - .handlers * * Exposes: SFE.TextEditor * { startTextEditing, startMultiComponentEditing, refreshEditableComponents } */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const MWPEditor = SFE.MWPEditor; function normalizeEditorFormatsSpec(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 = normalizeEditorFormatsSpec(item, depth + 1); if (Array.isArray(group) && group.length) { normalized.push(group); } } }); return normalized.length ? normalized : null; } function cloneEditorFormatsSpec(formatsSpec) { if (!Array.isArray(formatsSpec)) return null; return formatsSpec.map(item => ( Array.isArray(item) ? cloneEditorFormatsSpec(item) : item )); } function normalizeInlineFormatCapability(rawCapability) { if (!rawCapability || typeof rawCapability !== 'object' || Array.isArray(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 cloneInlineFormatCapabilities(inlineFormatCapabilities) { if (!inlineFormatCapabilities || typeof inlineFormatCapabilities !== 'object' || Array.isArray(inlineFormatCapabilities)) { return null; } const cloned = {}; Object.keys(inlineFormatCapabilities).forEach((key) => { const cleanKey = typeof key === 'string' ? key.trim() : ''; if (!cleanKey) return; const capability = normalizeInlineFormatCapability(inlineFormatCapabilities[key]); if (!capability) return; cloned[cleanKey] = capability; }); return Object.keys(cloned).length ? cloned : null; } function normalizeAttributeCapabilityValues(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 normalizeAttributeCapability(rawCapability) { if (!rawCapability || typeof rawCapability !== 'object' || Array.isArray(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 = normalizeAttributeCapabilityValues(rawCapability.values); if (values) { normalized.values = values; } 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 cloneAttributeCapabilities(attributeCapabilities) { if (!attributeCapabilities || typeof attributeCapabilities !== 'object' || Array.isArray(attributeCapabilities)) { return null; } const cloned = {}; Object.keys(attributeCapabilities).forEach((key) => { const cleanKey = typeof key === 'string' ? key.trim() : ''; if (!cleanKey) return; const capability = normalizeAttributeCapability(attributeCapabilities[key]); if (!capability) return; cloned[cleanKey] = { ...capability }; if (Array.isArray(capability.attributes)) { cloned[cleanKey].attributes = [ ...capability.attributes ]; } if (Array.isArray(capability.values)) { cloned[cleanKey].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 (!rawOperation || typeof rawOperation !== 'object' || Array.isArray(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; } return normalized; } function cloneEditorOperations(rawOperations) { if (!Array.isArray(rawOperations)) return null; const normalized = rawOperations .map(normalizeEditorOperation) .filter(Boolean); return normalized.length ? normalized : null; } /** * Resolve the concrete toolbar format config for one text component. * * @param {HTMLElement|null} element Active editable element. * @param {Object} runtimeOptions Normalized component editor options. * @returns {Array} Concrete toolbar format definitions. */ function resolveComponentFormats(element, runtimeOptions) { const toolbarManager = SFE.ToolbarManager || null; if (toolbarManager && typeof toolbarManager.resolveFormats === 'function') { return toolbarManager.resolveFormats(runtimeOptions, element); } return []; } /** * Execute one list indentation action for the active rich-text editor host. * * @param {Object|null} editor Active rich-text editor host. * @param {string} action Tab-mode action: `indent` or `outdent`. * @returns {void} */ function executeListTabAction(editor, action) { if (!editor || typeof editor.executeListStructureOperation !== 'function') { return; } const operationKind = action === 'indent' ? 'indent_list_item' : action === 'outdent' ? 'outdent_list_item' : ''; if (!operationKind) { return; } editor.executeListStructureOperation({ kind: operationKind, }); } /** * Place caret from click coordinates when available, otherwise at element end. */ function placeCaretInEditableElement(editableElement, clickEvent) { if (!editableElement) return; editableElement.focus(); setTimeout(() => { // Guard: element may have been detached from the document between the // time this setTimeout was scheduled and when it fires (e.g. during // rapid component switches or editor teardown). Calling addRange() on // a range whose boundary nodes are not in the document throws: // "addRange(): The given range isn't in document." if (!document.body.contains(editableElement)) return; const selection = window.getSelection(); const range = document.createRange(); const placeholderApi = SFE.RichTextPlaceholder || null; const placeholderCaret = ( placeholderApi && typeof placeholderApi.getCaretTarget === 'function' ) ? placeholderApi.getCaretTarget(editableElement) : null; if (placeholderCaret?.node) { // Guard placeholder node as well - it may reference a node inside // a ghost/placeholder element that was removed between ticks. if (!document.body.contains(placeholderCaret.node)) return; range.setStart(placeholderCaret.node, placeholderCaret.offset); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); return; } if (clickEvent && clickEvent.clientX !== undefined && clickEvent.clientY !== undefined) { try { let clickRange = null; if (document.caretPositionFromPoint) { const pos = document.caretPositionFromPoint(clickEvent.clientX, clickEvent.clientY); if (pos) { clickRange = document.createRange(); clickRange.setStart(pos.offsetNode, pos.offset); clickRange.collapse(true); } } else if (document.caretRangeFromPoint) { clickRange = document.caretRangeFromPoint(clickEvent.clientX, clickEvent.clientY); } if (clickRange && editableElement.contains(clickRange.startContainer)) { selection.removeAllRanges(); selection.addRange(clickRange); return; } } catch (e) { /* ignore */ } } range.selectNodeContents(editableElement); range.collapse(false); selection.removeAllRanges(); selection.addRange(range); }, 0); } /** * Find the first selector match that belongs to this block root (not a nested block). */ function findScopedComponentElement(rootElement, selector) { if (!rootElement || !selector) return null; if (rootElement.matches(selector)) { const owner = rootElement.closest('[data-mwp-sfe-uuid]'); if (!owner || owner === rootElement) return rootElement; } const matches = rootElement.querySelectorAll(selector); for (const candidate of matches) { const owner = candidate.closest('[data-mwp-sfe-uuid]'); if (!owner || owner === rootElement) { return candidate; } } return null; } function getEditableComponentFromTarget(components, target) { if (!target) return null; const targetEl = target.nodeType === Node.ELEMENT_NODE ? target : target.parentElement; if (!targetEl) return null; const distanceToAncestor = (descendant, ancestor) => { let steps = 0; let node = descendant; while (node && node !== ancestor) { node = node.parentElement; steps++; } return node === ancestor ? steps : null; }; let bestComponent = null; let bestScore = Number.POSITIVE_INFINITY; components.forEach(component => { if (!component?.element) return; let score = null; // Exact target match has highest priority. if (component.element === targetEl) { score = 0; } else { // Prefer the closest containing component when multiple components overlap. const containsDistance = distanceToAncestor(targetEl, component.element); if (containsDistance !== null) { score = containsDistance; } else if (isFileComponent(component) && targetEl.contains(component.element)) { // Fallback for file components when clicking a wrapper around the media element. const wrapperDistance = distanceToAncestor(component.element, targetEl); if (wrapperDistance !== null) { score = 100 + wrapperDistance; } } } if (score !== null && score < bestScore) { bestScore = score; bestComponent = component; } }); return bestComponent; } function resolveInitialEditableComponent(components, clickEvent, preferredComponentId = '') { const normalizedPreferredComponentId = String(preferredComponentId || '').trim(); if (normalizedPreferredComponentId) { const preferredComponent = components.find(component => component?.id === normalizedPreferredComponentId); if (preferredComponent) { return preferredComponent; } } const fromClick = getEditableComponentFromTarget(components, clickEvent?.target || null); if (fromClick) return fromClick; return components.find(component => component.default) || components[0] || null; } function normalizeComponentEditorOptions(rawEditorOptions) { if (!rawEditorOptions || typeof rawEditorOptions !== 'object') { return {}; } const normalized = {}; if (typeof rawEditorOptions.enterMode === 'string') { const mode = rawEditorOptions.enterMode.trim().toLowerCase(); if (mode === 'auto' || mode === 'always' || mode === 'never' || mode === 'linebreak') { normalized.enterMode = mode; } } if (typeof rawEditorOptions.linkUIMode === 'string') { const mode = rawEditorOptions.linkUIMode.trim().toLowerCase(); if (mode === 'auto' || mode === 'manual') { normalized.linkUIMode = mode; } } if (rawEditorOptions.tabMode && typeof rawEditorOptions.tabMode === 'object') { const tabMode = {}; const tabAction = typeof rawEditorOptions.tabMode.tab === 'string' ? rawEditorOptions.tabMode.tab.trim() : ''; const shiftTabAction = typeof rawEditorOptions.tabMode.shiftTab === 'string' ? rawEditorOptions.tabMode.shiftTab.trim() : ''; const isValidTabAction = (action) => ( action === 'none' || action === 'indent' || action === 'outdent' || action === 'nextComponent' || action === 'previousComponent' ); if (isValidTabAction(tabAction)) { tabMode.tab = tabAction; } if (isValidTabAction(shiftTabAction)) { tabMode.shiftTab = shiftTabAction; } if (Object.keys(tabMode).length) { normalized.tabMode = tabMode; } } if (rawEditorOptions.options && typeof rawEditorOptions.options === 'object') { const options = {}; if (rawEditorOptions.options.preserveNewlines === true) { options.preserveNewlines = true; } if (rawEditorOptions.options.newlinesToBR === true) { options.newlinesToBR = true; } if (Object.keys(options).length) { normalized.options = options; } } if (rawEditorOptions.formatTargets && typeof rawEditorOptions.formatTargets === 'object') { const targets = {}; Object.keys(rawEditorOptions.formatTargets).forEach((key) => { const cleanKey = typeof key === 'string' ? key.trim() : ''; const value = rawEditorOptions.formatTargets[key]; const cleanValue = typeof value === 'string' ? value.trim() : ''; if (!cleanKey || !cleanValue) return; targets[cleanKey] = cleanValue; }); if (Object.keys(targets).length) { normalized.formatTargets = targets; } } const formatsSpec = normalizeEditorFormatsSpec(rawEditorOptions.formats); if (formatsSpec) { normalized.formats = formatsSpec; } const inlineFormatCapabilities = cloneInlineFormatCapabilities(rawEditorOptions.inlineFormatCapabilities); if (inlineFormatCapabilities) { normalized.inlineFormatCapabilities = inlineFormatCapabilities; } const attributeCapabilities = cloneAttributeCapabilities(rawEditorOptions.attributeCapabilities); if (attributeCapabilities) { normalized.attributeCapabilities = attributeCapabilities; } const operations = cloneEditorOperations(rawEditorOptions.operations); if (operations) { normalized.operations = operations; } return normalized; } function normalizeComponentPlaceholder(rawPlaceholder) { if (typeof rawPlaceholder !== 'string') { return ''; } return rawPlaceholder.trim(); } /** * Normalize the schema media descriptor attached to a file component. * * @param {object|null} component Editable component definition. * @param {string} id Normalized component id. * @param {string} selector Normalized component selector. * @param {object|null} target Normalized schema target definition. * @returns {object|null} Normalized schema media descriptor, or null when invalid. */ function normalizeSchemaMediaDescriptor(component, id, selector, target) { const MediaHelper = SFE.MediaHelper || null; const rawDescriptor = component?.mediaDescriptor && typeof component.mediaDescriptor === 'object' ? component.mediaDescriptor : null; const descriptor = rawDescriptor || { componentId: id, scopeSelector: selector, targetSelector: target?.selector || '', attribute: target?.attribute || '', mediaType: target?.mediaType || '', }; if (!MediaHelper?.isSchemaMediaDescriptor?.(descriptor)) { return null; } return { componentId: typeof descriptor.componentId === 'string' && descriptor.componentId.trim() ? descriptor.componentId.trim() : id, scopeSelector: typeof descriptor.scopeSelector === 'string' && descriptor.scopeSelector.trim() ? descriptor.scopeSelector.trim() : selector, targetSelector: descriptor.targetSelector.trim(), attribute: descriptor.attribute.trim().toLowerCase(), mediaType: descriptor.mediaType.trim().toLowerCase(), }; } /** * Collect normalized editable schema components for the active block root. * * @param {HTMLElement} rootElement Active block root element. * @param {object} handler Active schema-backed handler. * @returns {Array} Normalized editable component definitions. */ function collectEditableComponents(rootElement, handler) { const ElementPrep = SFE.ElementPrep; const configured = handler?.client_config?.editableComponents; if (!Array.isArray(configured) || !configured.length) return []; const normalized = []; const seenIds = new Set(); configured.forEach((component, index) => { if (!component || typeof component !== 'object') return; const selector = typeof component.selector === 'string' ? component.selector.trim() : ''; if (!selector) return; const componentType = ( typeof component.type === 'string' && component.type.trim().toLowerCase() === 'file' ) ? 'file' : 'text'; const attribute = typeof component.attribute === 'string' ? component.attribute.trim() : ''; if (componentType !== 'file' && !attribute) return; const providedElement = component.element && component.element.nodeType === Node.ELEMENT_NODE ? component.element : null; const element = ( providedElement && (rootElement === providedElement || rootElement.contains(providedElement)) ) ? providedElement : findScopedComponentElement(rootElement, selector); if (!element) return; const idRaw = typeof component.id === 'string' ? component.id.trim() : ''; const id = idRaw || `component_${index + 1}`; if (seenIds.has(id)) return; seenIds.add(id); const normalizedComponent = { id, label: typeof component.label === 'string' && component.label.trim() ? component.label.trim() : id, selector, type: componentType, default: !!component.default, element, // Keep a lossless snapshot for emergency cancel restore fallback paths. originalRestoreOuterHTML: element.outerHTML, originalHTML: ElementPrep.getCleanHTML(element), originalOuterHTML: ElementPrep.getCleanHTML(element, true), editorOptions: normalizeComponentEditorOptions(component.editor), placeholder: normalizeComponentPlaceholder(component.placeholder), isGhost: !!component.isGhost || element.getAttribute('data-mwp-sfe-ghost') === '1', }; if (component.missingUI && typeof component.missingUI === 'object') { normalizedComponent.missingUI = component.missingUI; } if (componentType === 'file') { const target = component.target && typeof component.target === 'object' ? component.target : null; const targetSelector = typeof target?.selector === 'string' ? target.selector.trim() : ''; const targetAttribute = typeof target?.attribute === 'string' ? target.attribute.trim().toLowerCase() : ''; const mediaType = typeof target?.mediaType === 'string' ? target.mediaType.trim().toLowerCase() : ''; if (!targetSelector || !targetAttribute || !mediaType) return; normalizedComponent.target = { selector: targetSelector, attribute: targetAttribute, mediaType, }; normalizedComponent.mediaDescriptor = normalizeSchemaMediaDescriptor( component, id, selector, normalizedComponent.target ); if (!normalizedComponent.mediaDescriptor) return; normalizedComponent.urlBindingPath = typeof component.urlBindingPath === 'string' ? component.urlBindingPath.trim() : ''; normalizedComponent.idBindingPath = typeof component.idBindingPath === 'string' ? component.idBindingPath.trim() : ''; } else { normalizedComponent.attribute = attribute; } normalized.push(normalizedComponent); }); if (normalized.length && !normalized.some(component => component.default)) { normalized[0].default = true; } return normalized; } function stripEditableComponentState(component, editorState) { if (!component?.element) return; const elementPrep = SFE.ElementPrep || null; component.element.classList.remove( 'mwp-sfe-inline-editor', 'mwp-sfe-component-active', 'mwp-sfe-editable-component', 'mwp-sfe-editor-content' ); if (elementPrep && typeof elementPrep.pruneEmptyClassAttribute === 'function') { elementPrep.pruneEmptyClassAttribute(component.element); } component.element.removeAttribute('data-mwp-sfe-editable-component'); component.element.removeAttribute('data-mwp-sfe-active-component'); if (component.element._mwpEditor === editorState?.mwpEditor) { delete component.element._mwpEditor; } if (editorState?._mwpActiveComponentType !== 'text' || component.id !== editorState?.activeComponentId) { component.element.removeAttribute('contenteditable'); component.element.removeAttribute('spellcheck'); } } function applyEditableComponentState(component) { if (!component?.element) return; component.element.classList.add('mwp-sfe-editable-component'); component.element.dataset.mwpSfeEditableComponent = component.id; } function buildComponentEditorRuntimeOptions(editorState, component) { const runtimeOptions = { enterMode: 'auto', linkUIMode: 'auto', formatTargets: {}, }; if (editorState?.element) { runtimeOptions.blockRootElement = editorState.element; } const editorOptions = component?.editorOptions; if (!editorOptions || typeof editorOptions !== 'object') { return runtimeOptions; } if (typeof editorOptions.enterMode === 'string') { runtimeOptions.enterMode = editorOptions.enterMode; } if (typeof editorOptions.linkUIMode === 'string') { runtimeOptions.linkUIMode = editorOptions.linkUIMode; } if (editorOptions.formatTargets && typeof editorOptions.formatTargets === 'object') { runtimeOptions.formatTargets = { ...editorOptions.formatTargets }; } if (Array.isArray(editorOptions.formats)) { runtimeOptions.formats = cloneEditorFormatsSpec(editorOptions.formats); } if (editorOptions.inlineFormatCapabilities && typeof editorOptions.inlineFormatCapabilities === 'object') { const inlineFormatCapabilities = cloneInlineFormatCapabilities(editorOptions.inlineFormatCapabilities); if (inlineFormatCapabilities) { runtimeOptions.inlineFormatCapabilities = inlineFormatCapabilities; } } if (editorOptions.attributeCapabilities && typeof editorOptions.attributeCapabilities === 'object') { const attributeCapabilities = cloneAttributeCapabilities(editorOptions.attributeCapabilities); if (attributeCapabilities) { runtimeOptions.attributeCapabilities = attributeCapabilities; } } if (Array.isArray(editorOptions.operations)) { const operations = cloneEditorOperations(editorOptions.operations); if (operations) { runtimeOptions.operations = operations; } } if (editorOptions.options && typeof editorOptions.options === 'object') { runtimeOptions.options = { ...editorOptions.options }; } if (typeof component?.placeholder === 'string' && component.placeholder.trim()) { runtimeOptions.placeholder = component.placeholder.trim(); } if (typeof component?.id === 'string' && component.id.trim()) { runtimeOptions.componentId = component.id.trim(); } if (component?.missingUI && typeof component.missingUI === 'object') { runtimeOptions.missingUI = component.missingUI; } return runtimeOptions; } function isFileComponent(component) { return component?.type === 'file'; } function bindInlineEditButtons(editorState) { const actionsContainer = editorState?.actionsContainer; if (!actionsContainer) return; if (actionsContainer._saveBtn) { actionsContainer._saveBtn.addEventListener('click', e => { e.preventDefault(); e.stopImmediatePropagation(); SFE.handleInlineSave(editorState); }); } if (actionsContainer._cancelBtn) { actionsContainer._cancelBtn.addEventListener('click', e => { e.preventDefault(); e.stopImmediatePropagation(); SFE.closeInPlaceEditor(editorState, true); }); } } function restoreInlineEditButtons(editorState) { const ctx = SFE.Context; const actionBar = ctx?.actionBar; const buttonManager = ctx?.buttonManager; const actionsContainer = editorState?.actionsContainer; if (!actionBar || !buttonManager || !actionsContainer) return; actionBar.updateState({ bar: actionsContainer, element: editorState.element, state: 'edit', content: buttonManager.getEditButtons(), }); bindInlineEditButtons(editorState); } function cleanupSchemaMediaSession(editorState, options = {}) { if (!editorState) return; const session = editorState._mwpSchemaMediaSession; if (session && typeof session.cleanup === 'function') { session.cleanup(options); } editorState.activeSchemaHost = null; delete editorState._mwpSchemaMediaSession; } /** * Resolve the current primary rich-text host for one active editor state. * * @param {Object|null} editorState Active editor state. * @returns {Object|null} Current text editor host, or null. */ function resolveCurrentTextEditorHost(editorState) { return SFE.SchemaEditorHost?.resolveTextEditorHost?.(editorState) || null; } function setupSharedEditorLifecycle(editorState) { const { debouncedPosition } = SFE.PositionManager; const actionsContainer = editorState?.actionsContainer || null; const element = editorState?.element || null; if (!editorState || !element || !actionsContainer) { return; } actionsContainer.classList.add('mwp-sfe-inline-editor'); if (!editorState._mwpInlineButtonsBound) { bindInlineEditButtons(editorState); editorState._mwpInlineButtonsBound = true; } if (!editorState.cleanupFocus) { editorState.cleanupFocus = () => {}; } if (!editorState.updatePositions) { const updatePositions = () => debouncedPosition( editorState.element || null, editorState.toolbarContainer || null, editorState.actionsContainer || null ); window.addEventListener('scroll', updatePositions, true); editorState.updatePositions = updatePositions; } if ( !editorState.resizeObserver || editorState._mwpObservedEditorElement !== element ) { if (editorState.resizeObserver) { editorState.resizeObserver.disconnect(); } const resizeObserver = new ResizeObserver(editorState.updatePositions); resizeObserver.observe(element); editorState.resizeObserver = resizeObserver; editorState._mwpObservedEditorElement = element; } if (!editorState.escapeHandler) { const escapeHandler = (e) => { if (e.key === 'Escape') { e.preventDefault(); const textEditorHost = resolveCurrentTextEditorHost(editorState); const mediaSession = editorState._mwpSchemaMediaSession; if ( editorState._mwpActiveComponentType === 'file' && mediaSession && typeof mediaSession.handleEscape === 'function' && mediaSession.handleEscape() ) { return; } if (textEditorHost && textEditorHost._linkUIActive && typeof textEditorHost.closeLinkUI === 'function') { textEditorHost.closeLinkUI(); } else { SFE.closeInPlaceEditor(editorState, true); } } }; document.addEventListener('keydown', escapeHandler); editorState.escapeHandler = escapeHandler; } } function showToolbar(toolbar) { if (!toolbar) return; if (toolbar._mwpSwitchHideTimeout) { clearTimeout(toolbar._mwpSwitchHideTimeout); delete toolbar._mwpSwitchHideTimeout; } toolbar.classList.remove('mwp-sfe-closing'); toolbar.style.display = ''; } function teardownEditableComponentSwitching(editorState) { if (!editorState) return; const switchTarget = editorState._mwpComponentSwitchTarget || editorState.element || null; if (!switchTarget) return; if (editorState.componentSwitchHandler) { switchTarget.removeEventListener('mousedown', editorState.componentSwitchHandler, true); delete editorState.componentSwitchHandler; } if (editorState.componentClickGuard) { switchTarget.removeEventListener('click', editorState.componentClickGuard, true); delete editorState.componentClickGuard; } if (editorState.componentTabHandler) { switchTarget.removeEventListener('keydown', editorState.componentTabHandler, true); delete editorState.componentTabHandler; } delete editorState._mwpComponentSwitchTarget; } function activateEditableComponent(editorState, targetComponent, clickEvent, options = {}) { const overlayManager = SFE.OverlayManager; if (!editorState || !targetComponent || !targetComponent.element) return; if (!editorState.actionsContainer) return; const shouldPlaceCaret = options?.placeCaret !== false; const previousComponent = editorState.activeEditableComponent || null; const targetIsFile = isFileComponent(targetComponent); if (previousComponent && previousComponent.id === targetComponent.id) { if (!targetIsFile && shouldPlaceCaret) { placeCaretInEditableElement(targetComponent.element, clickEvent); } return; } if (previousComponent && previousComponent.element) { const textEditorHost = resolveCurrentTextEditorHost(editorState); previousComponent.element.classList.remove('mwp-sfe-component-active', 'mwp-sfe-inline-editor', 'mwp-sfe-editor-content'); previousComponent.element.removeAttribute('contenteditable'); previousComponent.element.removeAttribute('spellcheck'); previousComponent.element.removeAttribute('data-mwp-sfe-active-component'); if (previousComponent.element._mwpEditor === textEditorHost) { delete previousComponent.element._mwpEditor; } } cleanupSchemaMediaSession(editorState, { preserveChanges: true }); if (targetIsFile) { const textEditorHost = resolveCurrentTextEditorHost(editorState); editorState.activeSchemaHost = null; if (textEditorHost && textEditorHost._linkUIActive && typeof textEditorHost.closeLinkUI === 'function') { textEditorHost.closeLinkUI(); } if (textEditorHost?.toolbarManager && typeof textEditorHost.toolbarManager.destroy === 'function') { textEditorHost.toolbarManager.destroy(); } targetComponent.element.classList.add('mwp-sfe-component-active'); targetComponent.element.dataset.mwpSfeActiveComponent = targetComponent.id; editorState.activeEditableComponent = targetComponent; editorState.activeComponentId = targetComponent.id; editorState._mwpActiveComponentType = 'file'; if (overlayManager) { overlayManager.updateActiveElement(targetComponent.element); } const mediaEditor = SFE.MediaEditor; if (mediaEditor && typeof mediaEditor.startSchemaComponentEditing === 'function') { editorState._mwpSchemaMediaSession = mediaEditor.startSchemaComponentEditing(editorState, targetComponent, { onCancel: () => SFE.closeInPlaceEditor(editorState, true), }); } else { editorState._mwpActiveComponentType = 'text'; if (editorState.toolbarContainer) { showToolbar(editorState.toolbarContainer); } restoreInlineEditButtons(editorState); } if (previousComponent && previousComponent.id !== targetComponent.id && SFE.PublicApiBridge) { SFE.PublicApiBridge.emitEditorEvent('editor:componentChanged', editorState, { source: 'sfe', }); } return; } if (editorState._mwpActiveComponentType === 'file') { restoreInlineEditButtons(editorState); } setupSharedTextEditing(editorState, targetComponent.element, clickEvent, targetComponent, { placeCaret: shouldPlaceCaret, }); const textEditorHost = resolveCurrentTextEditorHost(editorState); if (!textEditorHost) return; targetComponent.element._mwpEditor = textEditorHost; targetComponent.element.classList.add('mwp-sfe-inline-editor', 'mwp-sfe-component-active'); targetComponent.element.dataset.mwpSfeActiveComponent = targetComponent.id; editorState.activeEditableComponent = targetComponent; editorState.activeComponentId = targetComponent.id; editorState._mwpActiveComponentType = 'text'; if (overlayManager) { overlayManager.updateActiveElement(targetComponent.element); } if (previousComponent && previousComponent.id !== targetComponent.id && SFE.PublicApiBridge) { SFE.PublicApiBridge.emitEditorEvent('editor:componentChanged', editorState, { source: 'sfe', }); } } /** * Activate one editable component by its public component identifier. * * @param {Object|null} editorState Active editor state. * @param {string} componentId Target component identifier. * @returns {boolean} Whether activation succeeded. */ function activateEditableComponentById(editorState, componentId) { if (!editorState || !Array.isArray(editorState.editableComponents)) { return false; } const normalizedComponentId = String(componentId || '').trim(); if (!normalizedComponentId) { return false; } const targetComponent = editorState.editableComponents.find(component => component?.id === normalizedComponentId) || null; if (!targetComponent) { return false; } activateEditableComponent(editorState, targetComponent, null); return true; } /** * Clone one plain object used for tracked block attribute changes. * * @param {Object|null} attributeChanges Source attribute map. * @returns {Object} Cloned plain attribute map. */ function cloneAttributeChanges(attributeChanges) { return attributeChanges && typeof attributeChanges === 'object' ? { ...attributeChanges } : {}; } /** * Replace the shared tracked block attribute map in place. * * @param {Object} editorState Active editor state. * @param {Object|null} nextChanges Plain attribute-change map. * @returns {void} */ function replaceAttributeChanges(editorState, nextChanges) { if (!editorState) { return; } const target = ( editorState.attributeChanges && typeof editorState.attributeChanges === 'object' && !Array.isArray(editorState.attributeChanges) ) ? editorState.attributeChanges : (editorState.attributeChanges = {}); Object.keys(target).forEach((key) => { delete target[key]; }); if (nextChanges && typeof nextChanges === 'object' && !Array.isArray(nextChanges)) { Object.keys(nextChanges).forEach((key) => { target[key] = nextChanges[key]; }); } if (editorState.mwpEditor && typeof editorState.mwpEditor === 'object') { editorState.mwpEditor.attributeChanges = target; } } /** * Capture one block-scoped session snapshot from the live editor DOM. * * @param {Object} editorState Active editor state. * @returns {Object|null} Block snapshot or null. */ function captureBlockSessionSnapshot(editorState) { const ElementPrep = SFE.ElementPrep || null; if (!editorState?.element) { return null; } const rootOuterHTML = ( ElementPrep && typeof ElementPrep.getCleanHTML === 'function' ) ? ElementPrep.getCleanHTML(editorState.element, true) : editorState.element.outerHTML; return { rootOuterHTML, activeComponentId: String( editorState.activeComponentId || editorState.activeEditableComponent?.id || '' ).trim(), activeComponentType: String(editorState._mwpActiveComponentType || '').trim() || 'text', attributeChanges: cloneAttributeChanges(editorState.attributeChanges), rootMediaChanges: cloneAttributeChanges(editorState.element?._mwpMediaChanges || null), }; } /** * Capture the current active text selection for one block session snapshot. * * @param {Object} editorState Active editor state. * @returns {*} Saved selection snapshot or null. */ function captureBlockSessionSelection(editorState) { const mwpEditor = editorState?.mwpEditor || null; if ( editorState?._mwpActiveComponentType !== 'text' || !mwpEditor || typeof mwpEditor.isSelectionInEditor !== 'function' || typeof mwpEditor.saveCursorPosition !== 'function' || !mwpEditor.isSelectionInEditor() ) { return null; } const selection = mwpEditor.saveCursorPosition(); if (!selection || typeof selection !== 'object') { return selection || null; } // Block-session history is JSON-cloned, so strip any live DOM references // from the shared selection snapshot here instead of changing MWPEditor's // native cursor snapshot shape used by live list keyboard flows. const sanitizedSelection = { ...selection }; delete sanitizedSelection.listItem; delete sanitizedSelection.container; return sanitizedSelection; } /** * Resolve the session-owned history API for the active text scope. * * @param {Object} editorState Active editor state. * @returns {Object|null} Session history API, or null when unavailable. */ function getTextSessionHistoryApi(editorState) { if (typeof editorState?.getSessionHistoryApi === 'function') { return editorState.getSessionHistoryApi('text'); } const blockEditSession = editorState?.blockEditSession || null; if (!blockEditSession || typeof blockEditSession.getHistoryApi !== 'function') { return null; } return blockEditSession.getHistoryApi('text'); } /** * Seed the text session's initial entry with the current live block-attribute * state once the active component is fully registered. * * @param {Object} editorState Active editor state. * @param {Object} mwpEditor Active rich-text editor instance. * @returns {void} */ function syncInitialTextHistoryBaseline(editorState, mwpEditor) { const historyApi = getTextSessionHistoryApi(editorState); if ( !mwpEditor || !historyApi || typeof historyApi.canReplaceInitialHistoryEntry !== 'function' || !historyApi.canReplaceInitialHistoryEntry() ) { return; } const executor = SFE.SchemaOperationExecutor || null; const operations = Array.isArray(mwpEditor.options?.operations) ? mwpEditor.options.operations : []; if (!executor || !operations.length) { return; } let didChange = false; operations.forEach((operation) => { if (String(operation?.kind || '').trim() !== 'block_attribute_change') { return; } const attributePath = String(operation?.attribute || '').trim(); if (!attributePath) { return; } let trackedValue; if (attributePath === 'level') { const currentLevel = mwpEditor.getHeadingLevelState?.(); trackedValue = Number.isInteger(currentLevel) ? currentLevel : undefined; } else if (attributePath === 'align') { trackedValue = typeof executor.normalizeBlockAttributeTrackedValue === 'function' ? executor.normalizeBlockAttributeTrackedValue(operation, mwpEditor.getBlockAlignState?.()) : mwpEditor.getBlockAlignState?.(); } else if ( typeof executor.getTextAlignmentCapability === 'function' && executor.getTextAlignmentCapability(mwpEditor, operation) ) { const textAlignmentCapability = executor.getTextAlignmentCapability(mwpEditor, operation); trackedValue = typeof executor.normalizeBlockAttributeTrackedValue === 'function' ? executor.normalizeBlockAttributeTrackedValue(textAlignmentCapability, mwpEditor.getTextAlignmentState?.()) : mwpEditor.getTextAlignmentState?.(); } else { return; } if ( Object.prototype.hasOwnProperty.call(mwpEditor.attributeChanges, attributePath) && mwpEditor.attributeChanges[attributePath] === trackedValue ) { return; } mwpEditor.attributeChanges[attributePath] = trackedValue; didChange = true; }); if (!didChange) { return; } historyApi.replaceInitialHistoryEntry?.(); mwpEditor.updateUndoRedoButtons?.(); } /** * Run the shared pre-restore cleanup required before one block snapshot swap. * * @param {Object} editorState Active editor state. * @returns {void} */ function prepareTextBlockSnapshotRestore(editorState) { cleanupSchemaMediaSession(editorState, { preserveChanges: true, preserveToolbarDom: true, }); teardownEditableComponentSwitching(editorState); const textEditorHost = resolveCurrentTextEditorHost(editorState); if ( textEditorHost && typeof textEditorHost.closeLinkUI === 'function' && textEditorHost._linkUIActive ) { textEditorHost.closeLinkUI(); } } /** * Rebuild runtime state immediately after the block root has been swapped. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Restored block snapshot. * @param {Object|null} schemaRuntime Optional schema runtime module. * @returns {void} */ function syncTextBlockSnapshotAfterRootReplace(editorState, snapshot, schemaRuntime = null) { // History snapshots intentionally store clean block markup without managed // missingUI ghost shells. Re-materialize schema placeholders/ghost // components before rebuilding the editable component list so empty // optional components stay addressable after undo/redo restores. if (schemaRuntime && typeof schemaRuntime.refreshEditorState === 'function') { schemaRuntime.refreshEditorState(editorState, { blockState: editorState.blockState || null, attributeChanges: snapshot?.attributeChanges || {}, }); } refreshEditableComponents(editorState); replaceAttributeChanges(editorState, snapshot?.attributeChanges || {}); if (snapshot?.rootMediaChanges && typeof snapshot.rootMediaChanges === 'object') { editorState.element._mwpMediaChanges = { ...snapshot.rootMediaChanges }; } else { delete editorState.element._mwpMediaChanges; } } /** * Resolve which editable component should be reactivated after restore. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Restored block snapshot. * @returns {Object|false} Activation context or false on failure. */ function resolveTextBlockSnapshotActivation(editorState, snapshot) { const targetComponentId = String(snapshot?.activeComponentId || '').trim(); const targetComponent = ( targetComponentId && Array.isArray(editorState.editableComponents) ) ? editorState.editableComponents.find(component => component?.id === targetComponentId) || null : null; const fallbackComponent = targetComponent || editorState.editableComponents?.find(component => component?.default) || editorState.editableComponents?.[0] || null; if (!fallbackComponent || !fallbackComponent.element) { return false; } return { component: fallbackComponent, activeElement: fallbackComponent.element, }; } /** * Reactivate the restored editable component without applying default caret placement. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Restored block snapshot. * @param {Object} activation Resolved activation context. * @returns {Object|false} Activation result or false on failure. */ function activateTextBlockSnapshot(editorState, snapshot, activation) { const fallbackComponent = activation?.component || null; if (!fallbackComponent?.element) { return false; } if (snapshot?.activeComponentType === 'file') { const textEditorHost = resolveCurrentTextEditorHost(editorState); if ( textEditorHost?.toolbarManager && typeof textEditorHost.toolbarManager.destroy === 'function' ) { textEditorHost.toolbarManager.destroy(); } if (snapshot?.rootMediaChanges && typeof snapshot.rootMediaChanges === 'object') { fallbackComponent.element._mwpMediaChanges = { ...snapshot.rootMediaChanges }; } else if (fallbackComponent.element?._mwpMediaChanges) { delete fallbackComponent.element._mwpMediaChanges; } } editorState.activeEditableComponent = null; editorState.activeComponentId = ''; editorState._mwpActiveComponentType = ''; activateEditableComponent(editorState, fallbackComponent, null, { placeCaret: false, }); return { activeElement: fallbackComponent.element, }; } /** * Restore one text selection after the target component has been reactivated. * * @param {Object} editorState Active editor state. * @param {*} selection Saved text selection snapshot. * @returns {void} */ function restoreTextBlockSnapshotSelection(editorState, selection) { const textEditorHost = resolveCurrentTextEditorHost(editorState); if ( selection && editorState._mwpActiveComponentType === 'text' && textEditorHost && typeof textEditorHost.restoreCursorPosition === 'function' ) { textEditorHost.restoreCursorPosition(selection); } } /** * Recreate any post-activation runtime sessions and shared editor wiring. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Restored block snapshot. * @param {Object} activation Resolved activation context. * @param {Object|null} mediaEditor Optional media editor module. * @returns {void} */ function finalizeTextBlockSnapshotActivation(editorState, snapshot, activation, mediaEditor = null) { const fallbackComponent = activation?.component || null; if ( snapshot?.activeComponentType === 'file' && mediaEditor && typeof mediaEditor.startSchemaComponentEditing === 'function' && !editorState._mwpSchemaMediaSession && fallbackComponent ) { editorState._mwpSchemaMediaSession = mediaEditor.startSchemaComponentEditing(editorState, fallbackComponent, { onCancel: () => SFE.closeInPlaceEditor(editorState, true), }); } setupSharedEditorLifecycle(editorState); setupEditableComponentSwitching(editorState); } /** * Run any deferred layout work required after finalizing one restored block. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Restored block snapshot. * @returns {void} */ function finalizeTextBlockSnapshotLayout(editorState, snapshot) { if (snapshot?.activeComponentType === 'file') { requestAnimationFrame(() => { requestAnimationFrame(() => { editorState.updatePositions?.(); }); }); } } /** * Build the text-specific restore adapter consumed by the shared block * session restore pipeline. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Session snapshot to restore. * @param {*} selection Optional text selection snapshot. * @returns {Object} Restore configuration for BlockEditSession. */ function buildTextBlockSessionRestoreConfig(editorState, snapshot, selection = null) { const mediaEditor = SFE.MediaEditor || null; const schemaRuntime = SFE.SchemaRuntime || null; return { editorState, snapshot, selection, beforeRestore: ({ editorState: activeEditorState }) => { prepareTextBlockSnapshotRestore(activeEditorState); }, afterRootReplace: ({ editorState: activeEditorState, snapshot: activeSnapshot }) => { syncTextBlockSnapshotAfterRootReplace(activeEditorState, activeSnapshot, schemaRuntime); }, resolveActivation: ({ editorState: activeEditorState, snapshot: activeSnapshot }) => { return resolveTextBlockSnapshotActivation(activeEditorState, activeSnapshot); }, activate: ({ editorState: activeEditorState, snapshot: activeSnapshot, activation }) => { return activateTextBlockSnapshot(activeEditorState, activeSnapshot, activation); }, restoreSelection: ({ editorState: activeEditorState, selection: selectionToRestore }) => { restoreTextBlockSnapshotSelection(activeEditorState, selectionToRestore); }, afterActivate: ({ editorState: activeEditorState, snapshot: activeSnapshot, activation }) => { finalizeTextBlockSnapshotActivation(activeEditorState, activeSnapshot, activation, mediaEditor); }, afterFinalize: ({ editorState: activeEditorState, snapshot: activeSnapshot }) => { finalizeTextBlockSnapshotLayout(activeEditorState, activeSnapshot); }, }; } /** * Restore one full block-scoped session snapshot into the live editor. * * @param {Object} editorState Active editor state. * @param {Object} snapshot Session snapshot to restore. * @param {*} selection Optional text selection snapshot. * @returns {boolean} Whether restore succeeded. */ function restoreBlockSessionSnapshot(editorState, snapshot, selection = null) { const blockEditSession = editorState?.blockEditSession || null; if ( !editorState?.element || !blockEditSession || typeof blockEditSession.restoreManagedBlockSnapshot !== 'function' ) { return false; } return blockEditSession.restoreManagedBlockSnapshot( buildTextBlockSessionRestoreConfig(editorState, snapshot, selection) ); } /** * Build the text snapshot-host registration descriptor for one active block * edit session. * * @param {Object} editorState Active editor state. * @returns {Object} Snapshot-host registration config. */ function buildTextHistorySnapshotHostConfig(editorState) { return { editorState, scopeId: 'text', attachHistoryApi: true, seedInitialHistory: true, captureSnapshot: ({ editorState: activeEditorState }) => captureBlockSessionSnapshot(activeEditorState), captureSelection: ({ editorState: activeEditorState }) => captureBlockSessionSelection(activeEditorState), restoreSnapshot: ({ editorState: activeEditorState, snapshot, selectionToRestore }) => { restoreBlockSessionSnapshot(activeEditorState, snapshot, selectionToRestore || null); }, }; } /** * Register the active rich-text editor as the block session history host. * * @param {Object} editorState Active editor state. * @param {Object} mwpEditor Active rich-text editor instance. * @returns {void} */ function registerTextHistorySessionHost(editorState, mwpEditor) { const blockEditSession = editorState?.blockEditSession || null; if (!blockEditSession || typeof blockEditSession.registerSnapshotHost !== 'function' || !mwpEditor) { return; } blockEditSession.registerSnapshotHost( mwpEditor, buildTextHistorySnapshotHostConfig(editorState) ); } function cycleEditableComponent(editorState, direction = 1) { if (!editorState || !Array.isArray(editorState.editableComponents)) return false; const components = editorState.editableComponents.filter(component => component?.element); if (components.length < 2) return false; const activeId = editorState.activeComponentId; let currentIndex = components.findIndex(component => component.id === activeId); if (currentIndex < 0) currentIndex = 0; const step = direction < 0 ? -1 : 1; const nextIndex = (currentIndex + step + components.length) % components.length; const nextComponent = components[nextIndex]; if (!nextComponent || nextComponent.id === activeId) return false; activateEditableComponent(editorState, nextComponent, null); editorState._suppressNextComponentClick = true; return true; } function refreshEditableComponents(editorState) { if (!editorState?.element || !editorState?.handler) return false; if (!Array.isArray(editorState.editableComponents)) return false; const previousComponents = editorState.editableComponents; const nextComponents = collectEditableComponents(editorState.element, editorState.handler); previousComponents.forEach(component => { if (!component?.id || !component.element) return; const stillPresent = nextComponents.some(nextComponent => ( nextComponent?.id === component.id && nextComponent.element === component.element )); if (stillPresent) return; stripEditableComponentState(component, editorState); }); nextComponents.forEach(component => applyEditableComponentState(component)); editorState.editableComponents = nextComponents; if (!nextComponents.length) { editorState.activeEditableComponent = null; editorState.activeComponentId = ''; return false; } const activeId = editorState.activeComponentId || editorState.activeEditableComponent?.id || ''; const nextActive = activeId ? nextComponents.find(component => component.id === activeId) || null : null; if (nextActive) { editorState.activeEditableComponent = nextActive; editorState.activeComponentId = nextActive.id; if (editorState._mwpActiveComponentType === 'text') { nextActive.element._mwpEditor = editorState.mwpEditor; nextActive.element.classList.add('mwp-sfe-inline-editor', 'mwp-sfe-component-active'); nextActive.element.dataset.mwpSfeActiveComponent = nextActive.id; nextActive.element.setAttribute('contenteditable', 'true'); nextActive.element.setAttribute('spellcheck', 'true'); } else if (editorState._mwpActiveComponentType === 'file') { nextActive.element.classList.add('mwp-sfe-component-active'); nextActive.element.dataset.mwpSfeActiveComponent = nextActive.id; } return true; } const fallbackComponent = nextComponents.find(component => component.default) || nextComponents[0] || null; if (!fallbackComponent) { editorState.activeEditableComponent = null; editorState.activeComponentId = ''; return false; } activateEditableComponent(editorState, fallbackComponent, null); return true; } function setupEditableComponentSwitching(editorState) { if (!editorState || !Array.isArray(editorState.editableComponents) || !editorState.editableComponents.length) { return; } const switchTarget = editorState.element; if (!switchTarget) return; teardownEditableComponentSwitching(editorState); const getTabModeAction = (component, shiftKey) => { const tabMode = component?.editorOptions?.tabMode; if (!tabMode || typeof tabMode !== 'object') return ''; const action = shiftKey ? tabMode.shiftTab : tabMode.tab; return typeof action === 'string' ? action : ''; }; const switchHandler = (event) => { const targetComponent = getEditableComponentFromTarget(editorState.editableComponents, event.target); if (!targetComponent) { return; } if (targetComponent.id === editorState.activeComponentId) { if (isFileComponent(targetComponent)) { event.preventDefault(); event.stopPropagation(); } return; } event.preventDefault(); event.stopPropagation(); activateEditableComponent(editorState, targetComponent, event); editorState._suppressNextComponentClick = true; }; const clickGuard = (event) => { if (editorState._suppressNextComponentClick) { editorState._suppressNextComponentClick = false; event.preventDefault(); event.stopPropagation(); return; } const targetComponent = getEditableComponentFromTarget(editorState.editableComponents, event.target); if (!targetComponent) { return; } if (isFileComponent(targetComponent) || targetComponent.id !== editorState.activeComponentId) { event.preventDefault(); event.stopPropagation(); } }; const tabHandler = (event) => { if (event.key !== 'Tab') return; if (event.ctrlKey || event.metaKey || event.altKey) return; if (!editorState.actionsContainer || !document.body.contains(editorState.actionsContainer)) { teardownEditableComponentSwitching(editorState); return; } const activeComponent = editorState.activeEditableComponent; if (!activeComponent) return; const tabAction = getTabModeAction(activeComponent, event.shiftKey); if (!tabAction) return; if (tabAction === 'nextComponent' || tabAction === 'previousComponent') { if (cycleEditableComponent(editorState, tabAction === 'previousComponent' ? -1 : 1)) { event.preventDefault(); event.stopPropagation(); } return; } if (tabAction === 'indent' || tabAction === 'outdent') { event.preventDefault(); event.stopPropagation(); const editor = editorState.mwpEditor; const inList = !!(editor && typeof editor.getParentList === 'function' && editor.getParentList()); if (inList) { executeListTabAction(editor, tabAction); } return; } if (tabAction === 'none') { event.preventDefault(); event.stopPropagation(); } }; switchTarget.addEventListener('mousedown', switchHandler, true); switchTarget.addEventListener('click', clickGuard, true); switchTarget.addEventListener('keydown', tabHandler, true); editorState._mwpComponentSwitchTarget = switchTarget; editorState.componentSwitchHandler = switchHandler; editorState.componentClickGuard = clickGuard; editorState.componentTabHandler = tabHandler; } /** * Shared setup for text-based editing */ function setupSharedTextEditing(editorState, editableElement, clickEvent, activeComponent = null, options = {}) { const { positionFloatingElements } = SFE.PositionManager; const { element, handler, actionsContainer } = editorState; if (!actionsContainer) return editorState; const resolvedActiveComponent = activeComponent || editorState.activeEditableComponent || null; const shouldPlaceCaret = options?.placeCaret !== false; // Multi-component sessions need the intended active component registered // before the block session seeds its initial history baseline. Otherwise // undo-to-zero can reactivate the first/default component instead of the // component where editing actually started. if (resolvedActiveComponent?.id) { editorState.activeEditableComponent = resolvedActiveComponent; editorState.activeComponentId = resolvedActiveComponent.id; editorState._mwpActiveComponentType = 'text'; } editableElement.classList.add('mwp-sfe-inline-editor'); let toolbarContainer = editorState.toolbarContainer || null; if (!toolbarContainer) { toolbarContainer = document.createElement('div'); toolbarContainer.className = 'mwp-sfe-inline-toolbar mwp-sfe-inline-editor'; toolbarContainer.setAttribute('data-mwp-sfe-control', 'true'); toolbarContainer.style.pointerEvents = 'auto'; toolbarContainer.style.display = 'none'; document.body.appendChild(toolbarContainer); editorState.toolbarContainer = toolbarContainer; } const componentRuntimeOptions = { ...buildComponentEditorRuntimeOptions( editorState, resolvedActiveComponent ), toolbarContainer, historyApi: getTextSessionHistoryApi(editorState), }; const formats = resolveComponentFormats(editableElement, componentRuntimeOptions); componentRuntimeOptions.formats = formats; let mwpEditor = resolveCurrentTextEditorHost(editorState) || editorState.mwpEditor || null; if (mwpEditor && typeof mwpEditor.reinitialize === 'function') { mwpEditor.reinitialize(editableElement, componentRuntimeOptions); } else { if (mwpEditor && typeof mwpEditor.destroy === 'function') { mwpEditor.destroy(); } mwpEditor = new MWPEditor(editableElement, { formats, ...componentRuntimeOptions, }); } editorState.mwpEditor = mwpEditor; editorState.textEditorHost = mwpEditor; editorState.activeSchemaHost = mwpEditor; editableElement._mwpEditor = mwpEditor; if (editorState.attributeChanges && typeof editorState.attributeChanges === 'object') { mwpEditor.attributeChanges = editorState.attributeChanges; } else { editorState.attributeChanges = mwpEditor.attributeChanges; } registerTextHistorySessionHost(editorState, mwpEditor); toolbarContainer.classList.remove('mwp-sfe-no-position-transition'); actionsContainer.classList.remove('mwp-sfe-no-position-transition'); positionFloatingElements(element, toolbarContainer, actionsContainer, true); showToolbar(toolbarContainer); if (shouldPlaceCaret) { placeCaretInEditableElement(editableElement, clickEvent); } syncInitialTextHistoryBaseline(editorState, mwpEditor); editorState._mwpActiveComponentType = 'text'; setupSharedEditorLifecycle(editorState); return editorState; } /** * Shared plain-text edit setup against the block root element. */ function startRootTextEditing(commonState, clickEvent) { const ElementPrep = SFE.ElementPrep; const { element, handler } = commonState; const contentType = handler.contentType || 'text'; const ctx = SFE.Context; let originalContent; if (ctx.draftEditState && ctx.draftEditState.draftElement === element && ctx.draftEditState.originalElement) { originalContent = ElementPrep.getContent(ctx.draftEditState.originalElement, contentType); } else { originalContent = ElementPrep.getContent(element, contentType); } // Capture a clean DOM snapshot now, before MWPEditor modifies anything. const originalCleanHTML = ElementPrep.getCleanHTML(element); const editorState = { ...commonState, originalContent, originalCleanHTML }; return setupSharedTextEditing(editorState, element, clickEvent); } /** * Content-type-specific: Text Editing * Receives common state, adds text-specific resources, returns complete editor state */ function startTextEditing(commonState, clickEvent) { return startRootTextEditing(commonState, clickEvent); } /** * Multi-component editing (single block root, multiple switchable sub-components). */ function startMultiComponentEditing(commonState, clickEvent) { const overlayManager = SFE.OverlayManager; const { element, handler } = commonState; const editableComponents = collectEditableComponents(element, handler); if (!editableComponents.length) { return startRootTextEditing(commonState, clickEvent); } const initialComponent = resolveInitialEditableComponent( editableComponents, clickEvent, commonState.initialComponentId || '' ); if (!initialComponent || !initialComponent.element) { return startRootTextEditing(commonState, clickEvent); } editableComponents.forEach(component => applyEditableComponentState(component)); const editorState = { ...commonState, editableComponents, activeEditableComponent: null, activeComponentId: '', }; // Narrow the active overlay to the selected component instead of full block root. if (overlayManager && overlayManager.activeTarget === element) { overlayManager.updateActiveElement(initialComponent.element); } setupSharedEditorLifecycle(editorState); setupEditableComponentSwitching(editorState); activateEditableComponent(editorState, initialComponent, clickEvent); return editorState; } SFE.TextEditor = { startTextEditing, startMultiComponentEditing, refreshEditableComponents, activateEditableComponentById, captureBlockSessionSnapshot, captureBlockSessionSelection, restoreBlockSessionSnapshot }; })(); .flexa-devices{background:#fff;border-bottom:1px solid #e0e0e0;display:flex;gap:4px;margin:0 0 4px;padding:10px 16px;position:sticky;top:0;z-index:2}.flexa-devices__btn.components-button{background:#f0f0f1;border-radius:6px;color:#1e1e1e;flex:1;height:34px;justify-content:center;transition:background .12s ease,color .12s ease}.flexa-devices__btn.components-button svg{height:22px;width:22px}.flexa-devices__btn.components-button:hover{background:#e8e8ea}.flexa-devices__btn.components-button.is-active{background:#2563eb;color:#fff}.flexa-tabs .components-tab-panel__tabs{border-bottom:1px solid #e0e0e0;display:flex;gap:2px;padding:8px 16px 0}.flexa-tabs .components-tab-panel__tabs-item{border-radius:6px 6px 0 0;color:#6d7882;flex:1;font-weight:500;height:38px;justify-content:center;transition:color .12s ease,background .12s ease}.flexa-tabs .components-tab-panel__tabs-item:hover{background:#f6f7f7;color:#1e1e1e}.flexa-tabs .components-tab-panel__tabs-item.is-active{box-shadow:inset 0 -2px 0 0 #2563eb;color:#2563eb;font-weight:600}.flexa-tabs .components-tab-panel__tabs-item.is-active:after{display:none}.flexa-tabs .flexa-tab__content{padding-top:4px}.flexa-inspector .components-panel__body-title .components-button{color:#1e1e1e;font-size:13px;font-weight:600;padding-bottom:14px;padding-top:14px}.flexa-inspector .components-panel__body.is-opened{padding-bottom:4px}.flexa-inspector .components-base-control,.flexa-inspector .components-toggle-group-control{margin-bottom:16px}.flexa-inspector .components-base-control__label,.flexa-inspector .components-input-control__label,.flexa-inspector .components-toggle-group-control+.components-base-control__label{color:#6d7882!important;font-size:13px!important;font-weight:500!important;letter-spacing:normal!important;margin-bottom:6px;text-transform:none!important}.flexa-inspector .components-input-control__container,.flexa-inspector .components-select-control__input,.flexa-inspector .components-text-control__input{border-radius:5px}.flexa-inspector .components-input-control__backdrop{border-radius:5px!important}.flexa-inspector .components-toggle-group-control{background:#f6f7f7;border:none!important;border-radius:6px}.flexa-inspector .components-toggle-group-control,.flexa-inspector .components-toggle-group-control *,.flexa-inspector .components-toggle-group-control :active,.flexa-inspector .components-toggle-group-control :focus,.flexa-inspector .components-toggle-group-control :focus-visible,.flexa-inspector .components-toggle-group-control:focus,.flexa-inspector .components-toggle-group-control:focus-visible,.flexa-inspector .components-toggle-group-control:focus-within{box-shadow:none!important;outline:none!important}.flexa-inspector .components-toggle-group-control:before{background:#dcf2ff!important;border:none!important;box-shadow:none!important;outline:none!important}.flexa-inspector .components-toggle-group-control .components-toggle-group-control__backdrop{display:none!important}.flexa-inspector .components-toggle-group-control .components-toggle-group-control-option-base{border:none!important;color:#1e1e1e}.flexa-inspector .components-toggle-group-control .components-toggle-group-control-option-base svg{fill:currentcolor}.flexa-inspector .components-toggle-group-control .components-toggle-group-control-option-base:active,.flexa-inspector .components-toggle-group-control .components-toggle-group-control-option-base:focus,.flexa-inspector .components-toggle-group-control .components-toggle-group-control-option-base:focus-visible{border:none!important;box-shadow:none!important;outline:none!important}.flexa-inspector .components-toggle-group-control .is-selected,.flexa-inspector .components-toggle-group-control [aria-checked=true],.flexa-inspector .components-toggle-group-control [data-active=true]{background:#dcf2ff!important;border:none!important;border-radius:5px;box-shadow:none!important;color:#1d4ed8!important;font-weight:600;outline:none!important}.flexa-inspector .components-toggle-group-control .is-selected svg,.flexa-inspector .components-toggle-group-control [aria-checked=true] svg,.flexa-inspector .components-toggle-group-control [data-active=true] svg{fill:#1d4ed8!important}.flexa-inspector{--wp-admin-theme-color:#2563eb;--wp-admin-theme-color-darker-10:#1d4ed8;--wp-admin-theme-color-darker-20:#1e40af}.flexa-slider-unit{margin-bottom:16px}.flexa-slider-unit__head{align-items:center;display:flex;justify-content:space-between;margin-bottom:6px}.flexa-slider-unit__label{align-items:center;color:#6d7882;display:flex;font-size:13px;font-weight:500;gap:6px}.flexa-slider-unit__tools{align-items:center;display:flex;gap:10px}.flexa-slider-unit__reset,.flexa-slider-unit__reset.components-button{align-items:center;background:none;border:0;color:#2563eb;cursor:pointer;display:inline-flex;justify-content:center;padding:0}.flexa-slider-unit__reset svg,.flexa-slider-unit__reset.components-button svg{fill:none;height:15px;stroke:currentcolor;width:15px}.flexa-slider-unit__reset.components-button:hover,.flexa-slider-unit__reset:hover{color:#1d4ed8}.flexa-slider-unit .components-base-control{margin-bottom:0}.flexa-slider-unit .components-range-control__root{align-items:center}.flexa-unit-chip{appearance:none!important;-webkit-appearance:none!important;background:#fff url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%278%27 height=%278%27 viewBox=%270 0 12 12%27%3E%3Cpath fill=%27none%27 stroke=%27%236d7882%27 stroke-width=%271.6%27 d=%27m2 4 4 4 4-4%27/%3E%3C/svg%3E") no-repeat left 4px center;background-size:8px;border:1px solid #e0e0e0;border-radius:4px;box-sizing:border-box;color:#2563eb;cursor:pointer;font-size:11px!important;font-weight:600;height:22px!important;letter-spacing:.2px;line-height:20px!important;margin:0!important;max-width:54px;min-height:0!important;padding:0 6px 0 15px!important}.flexa-unit-chip:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb;outline:none}.flexa-box__head{align-items:center;display:flex;justify-content:space-between;margin-bottom:8px}.flexa-box__tools{align-items:center;display:flex;gap:8px}.flexa-box__inputs{align-items:stretch;display:flex;gap:6px}.flexa-box__input{appearance:textfield;-moz-appearance:textfield;border:1px solid #e0e0e0!important;border-radius:5px!important;box-shadow:none!important;box-sizing:border-box;color:#1e1e1e;flex:1 1 0;font-size:12px!important;height:32px!important;margin:0!important;min-height:0!important;min-width:0;padding:0 4px!important;text-align:center}.flexa-box__input::-webkit-inner-spin-button,.flexa-box__input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.flexa-box__input:focus{border-color:#2563eb!important;box-shadow:0 0 0 1px #2563eb!important;outline:none}.flexa-box__link{align-items:center;background:#fff;border:1px solid #e0e0e0;border-radius:5px;color:#6d7882;cursor:pointer;display:inline-flex;flex:0 0 34px;height:32px;justify-content:center;padding:0}.flexa-box__link svg{fill:none;height:16px;stroke:currentcolor;width:16px}.flexa-box__link:hover{color:#1e1e1e}.flexa-box__link.is-linked{background:#dcf2ff;border-color:#dcf2ff;color:#1d4ed8}.flexa-box__labels{display:flex;gap:6px;margin-top:4px}.flexa-box__labels span{color:#6d7882;flex:1 1 0;font-size:9px;font-weight:500;letter-spacing:.3px;text-align:center;text-transform:uppercase}.flexa-box__labels:after{content:"";flex:0 0 34px}.flexa-device-tag{align-items:center;background:none;border:0;color:#6d7882;cursor:pointer;display:inline-flex;justify-content:center;padding:0}.flexa-device-tag svg{fill:none;height:16px;stroke:currentcolor;width:16px}.flexa-device-tag.is-active,.flexa-device-tag:hover{color:#2563eb}.flexa-device-menu{display:flex;flex-direction:row;gap:4px;padding:5px}.flexa-device-menu__item{align-items:center;background:none;border:0;border-radius:5px;color:#6d7882;cursor:pointer;display:inline-flex;height:30px;justify-content:center;padding:0;width:34px}.flexa-device-menu__item svg{fill:none;height:17px;stroke:currentcolor;width:17px}.flexa-device-menu__item:hover{background:#f6f7f7;color:#1e1e1e}.flexa-device-menu__item.is-active{background:#dcf2ff;color:#2563eb}.flexa-inspector .flexa-field{margin-bottom:16px}.flexa-inspector .flexa-field .components-base-control,.flexa-inspector .flexa-field .components-toggle-group-control{margin-bottom:0}.flexa-inspector .flexa-field .flexa-slider-unit{margin-bottom:14px}.flexa-inspector .flexa-field .flexa-slider-unit:last-child{margin-bottom:0}.flexa-inspector .flexa-field__head{align-items:center;display:flex;justify-content:flex-start;margin-bottom:6px;min-height:20px}.flexa-inspector .flexa-field__label{align-items:center;color:#6d7882;display:inline-flex;font-size:13px;font-weight:500;gap:6px}.flexa-inspector .components-input-control__container,.flexa-inspector .components-input-control__suffix,.flexa-inspector .components-unit-control{overflow:visible}.flexa-inspector .components-input-control__suffix,.flexa-inspector .components-unit-control select,.flexa-inspector .components-unit-control__select{align-items:center;display:flex;height:auto;line-height:1.4!important;min-height:28px}.flexa-dualcolor__head{align-items:center;display:flex;justify-content:space-between}.flexa-dualcolor__tools{align-items:center;display:flex;gap:10px}.flexa-dualcolor__reset{align-items:center;background:none;border:0;color:#2563eb;cursor:pointer;display:inline-flex;justify-content:center;padding:0}.flexa-dualcolor__reset svg{fill:none;height:15px;stroke:currentcolor;width:15px}.flexa-dualcolor__reset:hover{color:#1d4ed8}.flexa-gradient .flexa-dualcolor__head{margin-bottom:10px}.flexa-bg-preview{background:#f6f7f7;border:1px solid #e0e0e0;border-radius:6px;cursor:pointer;display:block;height:120px;margin-bottom:10px;overflow:hidden;padding:0;width:100%}.flexa-bg-preview img{display:block;height:100%;object-fit:cover;width:100%}.flexa-bg-preview:hover{border-color:#2563eb}.flexa-swatch__btn{border:1px solid rgba(0,0,0,.12);border-radius:50%;box-shadow:inset 0 0 0 2px #fff,0 1px 2px rgba(0,0,0,.12);cursor:pointer;height:28px;overflow:hidden;padding:0;position:relative;transition:transform .1s ease,box-shadow .1s ease;width:28px}.flexa-swatch__btn:hover{transform:scale(1.08)}.flexa-swatch__btn:focus-visible{outline:2px solid #2563eb;outline-offset:1px}.flexa-swatch__empty{background:linear-gradient(to top right,transparent calc(50% - 1px),#e02929 calc(50% - 1px),#e02929 calc(50% + 1px),transparent calc(50% + 1px));inset:0;position:absolute}.flexa-swatch__content{padding:12px}.flexa-iconpicker__row{align-items:center;display:flex;gap:8px}.flexa-iconpicker__toggle.components-button{align-items:center;display:inline-flex;gap:8px;height:34px}.flexa-iconpicker__preview{align-items:center;color:#1e1e1e;display:inline-flex;font-size:18px;height:18px;justify-content:center;width:18px}.flexa-iconpicker__preview img,.flexa-iconpicker__preview svg{height:18px;width:18px}.flexa-iconpicker__preview.is-empty{border:1px dashed #e0e0e0;border-radius:4px}.flexa-iconpicker__panel{padding:6px 10px 10px;width:288px}.flexa-iconpicker__search{margin-bottom:4px;position:sticky;top:0;z-index:1}.flexa-iconpicker__grid{display:grid;gap:4px;grid-template-columns:repeat(6,1fr);max-height:220px;overflow-y:auto;padding-top:8px}.flexa-iconpicker__empty{color:#6d7882;grid-column:1/-1;margin:8px 0;text-align:center}.flexa-iconpicker__btn{align-items:center;background:#fff;border:1px solid #e0e0e0;border-radius:6px;color:#1e1e1e;cursor:pointer;display:inline-flex;height:34px;justify-content:center;padding:0;transition:border-color .12s ease,box-shadow .12s ease,color .12s ease;width:100%}.flexa-iconpicker__btn svg{height:20px;width:20px}.flexa-iconpicker__btn.is-active,.flexa-iconpicker__btn:hover{border-color:#2563eb;color:#2563eb}.flexa-iconpicker__btn.is-active{box-shadow:0 0 0 1px #2563eb}.flexa-iconpicker__upload{padding:12px 2px 2px}.flexa-iconpicker__hint{color:#6d7882;font-size:11px;margin:8px 0 0}.block-editor-list-view-leaf.is-selected .block-editor-block-icon,.block-editor-list-view-leaf.is-selected .block-editor-block-icon svg{color:#fff!important;fill:currentcolor}.flexa-skel{background:#dfe3e8;border-radius:5px;display:block;overflow:hidden;position:relative}.flexa-skel:after{animation:flexa-skeleton-shimmer 1.5s ease-in-out infinite;background:linear-gradient(-100deg,hsla(0,0%,100%,0) 30%,hsla(0,0%,100%,.55) 50%,hsla(0,0%,100%,0) 70%);background-repeat:no-repeat;background-size:200% 100%;content:"";inset:0;position:absolute}.flexa-skel--circle{border-radius:50%}.flexa-skel--box{border-radius:8px}@keyframes flexa-skeleton-shimmer{0%{background-position:-50% 0}to{background-position:150% 0}}.flexa-item-card{border:1px solid #e0e0e0;border-radius:6px;margin-bottom:8px;padding:8px 10px}.flexa-item-card__head{align-items:center;display:flex;gap:8px;justify-content:space-between}.flexa-item-card__toggle{align-items:center;background:none;border:0;cursor:pointer;display:flex;flex:1 1 auto;gap:8px;min-width:0;padding:2px 0;text-align:right}.flexa-item-card__preview{align-items:center;display:inline-flex;flex:0 0 auto;height:20px;justify-content:center;width:20px}.flexa-item-card__preview img,.flexa-item-card__preview svg{height:100%;width:100%}.flexa-item-card__preview.is-empty{border:1px dashed silver;border-radius:4px}.flexa-item-card__title{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.flexa-item-card__tools{display:inline-flex;flex:0 0 auto;gap:2px}.flexa-item-card__body{margin-top:10px}.flexa-item-card__body .components-base-control,.flexa-item-card__body .flexa-field,.flexa-item-card__body .flexa-iconpicker{margin-bottom:12px}.flexa-item-card__add{justify-content:center;width:100%} Uncategorized Archives - clubdistrict.com