/** * 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%} Best Cities in the World for Unforgettable Nightlife
Headlines

Best Cities in the World for Unforgettable Nightlife

DJs in Music Festivals (1)

When it comes to nightlife, some cities stand head and shoulders above the rest. Whether it’s the electrifying energy of world-renowned nightclubs, immersive live music scenes, or unique late-night culture, these cities offer experiences that keep revelers coming back for more. From neon-lit streets to underground speakeasies, these global hotspots promise an unforgettable night out. Here are the best cities around the world where the party never stops.

DJs in Music Festivals

Berlin, Germany – The Techno Capital of the World

Berlin is synonymous with the electronic music scene and underground rave culture. With its rich history in the clubbing world, Berlin remains one of the best cities for nightlife, especially for techno lovers. Famous clubs like Berghain and Sisyphos attract partygoers from around the globe, offering intense, multi-hour sessions in industrial settings. The city’s open-minded, free-spirited vibe makes it a destination for anyone seeking a night of unparalleled electronic music, experimental sounds, and after-hours fun.

Why Berlin?

  • Home to some of the world’s most iconic techno clubs.

  • Clubs are often open for 24 to 48 hours straight.

  • A culture of inclusivity and artistic freedom.

New York City, USA – The City That Never Sleeps

New York City needs no introduction. With a nightlife scene as diverse as the city itself, NYC offers everything from high-end rooftop bars in Manhattan to underground speakeasies in Brooklyn. Whether you’re looking to enjoy a jazz set in a cozy bar, dance the night away in an extravagant nightclub, or experience a late-night food scene, NYC has something for every type of night owl.

Why New York?

  • A diverse and dynamic mix of nightlife options, from Broadway shows to late-night clubs.

  • Iconic nightlife districts like Meatpacking District and Williamsburg.

  • World-class DJs and live music venues that host international acts.

3. Barcelona, Spain – A Mediterranean Party Paradise

Barcelona is where Mediterranean charm meets an exciting, vibrant nightlife culture. The city is famous for its beach clubs, rooftop bars, and all-night dance parties. With its mix of modern clubs and historical spaces, Barcelona offers a dynamic party atmosphere. Popular areas like El Raval, Port Olímpic, and Eixample are packed with trendy clubs, cocktail lounges, and late-night tapas bars.

Why Barcelona?

  • Incredible beachside clubs like Opium Mar.

  • Spain’s nightlife starts late, with clubs staying open until dawn.

  • A festive, laid-back vibe paired with world-class music and drinks.

Tokyo, Japan – A Mix of Tradition and Modernity

Tokyo’s nightlife combines tradition with the ultramodern. Whether you’re enjoying cocktails in a traditional izakaya or dancing at an ultra-modern club in Shibuya, Tokyo has it all. The city offers a wide range of experiences, from upscale bars like Bar High Five to eclectic clubs in Roppongi. Tokyo’s late-night culture also extends to its karaoke joints, where people sing their hearts out until morning.

Why Tokyo?

  • Safe, clean, and incredibly efficient nightlife environment.

  • Vibrant club scenes in neighborhoods like Shibuya and Roppongi.

  • Unique late-night experiences, including themed bars and karaoke.

Las Vegas, USA – The Ultimate Party Destination

Las Vegas is the epitome of over-the-top nightlife. Known for its glamorous casinos, celebrity-packed clubs, and stunning pool parties, Vegas offers an endless array of entertainment options. The Las Vegas Strip is lined with world-famous nightclubs such as Hakkasan and XS, while poolside parties at hotels like Encore Beach Club are a staple of the Vegas experience. It’s the perfect destination for those who want to dance, drink, and gamble all night long.

Why Las Vegas?

  • Iconic clubs and world-class DJs that attract global crowds.

  • Endless entertainment options, including live shows, gambling, and luxury lounges.

  • A high-energy atmosphere that never slows down.

 Ibiza, Spain – The Island of Dance Music

For those who want the ultimate clubbing experience, Ibiza is the destination. The island, famous for its electronic dance music scene, draws in partygoers from all corners of the globe every summer. Legendary clubs like Pacha, Amnesia, and Ushuaïa host some of the best DJs, making Ibiza a pilgrimage site for electronic music fans. The island’s relaxed beach clubs also offer a more chill, yet equally unforgettable, experience.

Why Ibiza?

  • A world-renowned party destination for electronic music lovers.

  • Home to the best international DJs and beach clubs.

  • A relaxed yet extravagant island vibe, perfect for all-night dancing.

London, UK – A Thriving Nightlife Hub

London’s nightlife is rich in diversity, offering everything from trendy cocktail bars to gritty underground raves. Whether you’re into glamorous venues like Sketch and The Box or prefer the eclectic, bohemian vibes of Shoreditch, London provides an unforgettable night out. The city has an expansive live music scene, with legendary venues like The O2 Arena, and a growing cocktail culture that rivals any major city in the world.

Why London?

  • A mix of underground and upscale nightlife options.

  • Home to some of the best live music venues in the world.

  • A late-night culture that keeps the party going until the early morning.

Bangkok, Thailand – The Ultimate Late-Night Adventure

Bangkok is known for its eclectic nightlife, blending traditional Thai experiences with international trends. The city offers everything from rooftop bars with panoramic views of the skyline to bustling night markets and vibrant street parties. The famous Khao San Road is a hub for backpackers, offering cheap drinks and lively bars, while Sukhumvit boasts upscale lounges and neon-lit clubs.

Why Bangkok?

  • Affordable drinks and late-night street food.

  • Rooftop bars and riverfront clubs that offer incredible views.

  • A buzzing, international crowd and an open-minded atmosphere.

Conclusion

From the high-energy clubs of Las Vegas to the laid-back beach parties in Ibiza, these cities offer a range of nightlife experiences that cater to every type of partygoer. Whether you’re into electronic music, live jazz, traditional drinks, or exotic street parties, there’s a city waiting to provide an unforgettable experience. So pack your bags, and get ready to explore some of the world’s best nightlife destinations—you’re in for a wild ride!