/** * 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%} Legendary DJs Who Shaped Dance: Icons
Headlines

Legendary DJs who shaped dance

The dance music we know today didn’t appear overnight. It was shaped, layer by layer, by artists who dared to do things differently. These legendary DJs who shaped dance didn’t just play tracks—they created movements, influenced cultures, and changed how people experience music on the dance floor.

From vinyl turntables to digital decks, their impact can still be felt in clubs, festivals, and playlists all over the world. Whether spinning house, techno, trance, or hip-hop, these DJs helped build the foundation of modern electronic music. Their stories remind us where dance music came from—and why it continues to matter.

Legendary DJs who shaped dance
Legendary DJs who shaped dance

The Early Pioneers Who Started It All

In the 1970s and early ’80s, dance music culture was still underground. That’s when DJs like Larry Levan and Frankie Knuckles began to make waves in New York and Chicago. Their mixes combined soul, disco, and electronic sounds, laying the groundwork for house music. Frankie Knuckles, often called the “Godfather of House,” played long, emotional sets that inspired people to dance all night. Meanwhile, Larry Levan’s legendary nights at the Paradise Garage showed how music could bring people together, no matter their background.

At the same time, across the Atlantic, DJs like David Mancuso and Nicky Siano were helping shape New York’s early club scene. Their parties weren’t just about dancing—they were about freedom, expression, and unity. These early DJs didn’t just play records—they told stories, built communities, and created a culture that would grow worldwide.

The Rise of House, Techno, and Global Dance Music

As the ’80s moved into the ’90s, dance music exploded. In Detroit, Juan Atkins, Derrick May, and Kevin Saunderson—known as the Belleville Three—created techno, a futuristic sound that combined funk, synth, and machine-like rhythm. Their work brought a new kind of energy to the scene and inspired generations of producers and DJs.

In the UK, DJs like Carl Cox and Paul Oakenfold helped bring house and trance to huge audiences. Carl Cox’s high-energy sets made him one of the most respected names in the world. His love for the music and his deep knowledge of mixing helped him build a global career that still thrives today.

Meanwhile, Sasha & John Digweed became known for their seamless progressive house sets, changing how DJs approached long-form storytelling. In Germany, Sven Väth helped grow the techno scene, leading clubs like Cocoon into international fame.

Breaking Rules and Crossing Borders

Legendary DJs didn’t always follow trends—they created them. The Chemical Brothers and Fatboy Slim pushed big beat sounds into the mainstream. They mixed rock, funk, and rave, helping electronic music reach new listeners. Daft Punk, though more known as producers, also shaped the DJ culture with their iconic performances and unforgettable live shows.

Hip-hop DJs like Grandmaster Flash played a major role in DJ history too. His turntable tricks and beat-matching techniques shaped not only hip-hop but all forms of DJing. These skills are still used today by DJs of every genre.

Each of these legends opened new doors. They proved that DJs weren’t just background performers—they were artists, innovators, and leaders in the world of music.

Why Their Legacy Still Matters

Even though music technology has changed a lot since the early days, the influence of these legendary DJs remains strong. Many of today’s rising stars still study old vinyl mixes, classic club sets, and original radio shows for inspiration. You can hear echoes of their work in modern tracks—through deep basslines, layered loops, and emotional builds.

They also helped shape the culture of respect and connection that dance music is built on. Their focus on community, creativity, and inclusion helped make dance floors places where everyone belongs. This spirit continues to guide the global dance scene today.

Looking Back to Move Forward

Understanding the history of dance music gives us a deeper appreciation for where it’s headed. The legendary DJs who shaped dance didn’t just create beats—they created spaces for freedom, identity, and joy. They showed us how music can move more than just bodies—it can move hearts and minds.

As dance music keeps evolving, their legacy lights the way. Whether you’re just starting to explore electronic music or you’ve been dancing for years, taking time to learn about these icons connects you to something bigger—a global rhythm that’s been growing for decades.