/** * Bottom-right ABE launcher owned by the base frontend editor. * * Exposes: SFE.ABELauncher { init } */ (function() { 'use strict'; window.MWP = window.MWP || {}; window.MWP.SFE = window.MWP.SFE || {}; const SFE = window.MWP.SFE; SFE.ManagerData = SFE.ManagerData || {}; const LAUNCHER_VISIBILITY_EVENT = 'mwp-abe-launcher-visibility'; const SPARKLE_GRADIENT_ID = 'mwp-abe-sparkle-gradient'; const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; let visibilityListenerInstalled = false; let lifecycleRecoveryInstalled = false; let shadowHost = null; let shadowRootRef = null; let shadowShell = null; let teaserUi = null; /** * @returns {HTMLButtonElement|null} Current launcher button when mounted. */ function getLauncherButton() { const button = shadowShell ? shadowShell.querySelector('.mwp-abe-launcher') : null; return button instanceof HTMLButtonElement ? button : null; } /** * @returns {HTMLDivElement|null} Shared shadow shell that owns all base ABE UI. */ function getUiShell() { return shadowShell instanceof HTMLDivElement ? shadowShell : null; } /** * Ensure the shared sparkle gradient defs exist inside the launcher's shadow * shell so the SVG icon can resolve `url(#mwp-abe-sparkle-gradient)`. * * @param {ParentNode|null} container Launcher-owned rendering root. * @returns {SVGSVGElement|null} Hidden defs host when available. */ function ensureSparkleGradientDefs(container) { if ( !container || typeof container.querySelector !== 'function' || typeof container.insertBefore !== 'function' ) { return null; } const existingDefs = container.querySelector('.mwp-abe-global-gradients'); if (existingDefs instanceof SVGSVGElement) { return existingDefs; } const svg = document.createElementNS(SVG_NAMESPACE, 'svg'); svg.setAttribute('class', 'mwp-abe-global-gradients'); svg.setAttribute('width', '0'); svg.setAttribute('height', '0'); svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false'); svg.style.position = 'absolute'; svg.style.pointerEvents = 'none'; const defs = document.createElementNS(SVG_NAMESPACE, 'defs'); const gradient = document.createElementNS(SVG_NAMESPACE, 'linearGradient'); gradient.setAttribute('id', SPARKLE_GRADIENT_ID); gradient.setAttribute('x1', '0'); gradient.setAttribute('y1', '0'); gradient.setAttribute('x2', '0'); gradient.setAttribute('y2', '1'); const startStop = document.createElementNS(SVG_NAMESPACE, 'stop'); startStop.setAttribute('offset', '25%'); startStop.setAttribute('stop-color', 'var(--mwp-abe-sparkle-gradient-start)'); const endStop = document.createElementNS(SVG_NAMESPACE, 'stop'); endStop.setAttribute('offset', '75%'); endStop.setAttribute('stop-color', 'var(--mwp-abe-sparkle-gradient-end)'); gradient.appendChild(startStop); gradient.appendChild(endStop); defs.appendChild(gradient); svg.appendChild(defs); container.insertBefore(svg, container.firstChild || null); return svg; } /** * Build or return the shared Shadow DOM host for the launcher-owned ABE UI. * * @returns {HTMLDivElement} Shadow shell container. */ function ensureUiShell() { if (getUiShell()) { return shadowShell; } const config = getConfig(); shadowHost = document.createElement('div'); shadowHost.className = 'mwp-abe-shadow-host'; shadowHost.setAttribute('data-mwp-sfe-control', 'true'); shadowHost.setAttribute('aria-hidden', 'true'); shadowHost.style.pointerEvents = 'auto'; document.body.appendChild(shadowHost); shadowRootRef = typeof shadowHost.attachShadow === 'function' ? shadowHost.attachShadow({ mode: 'open' }) : shadowHost; shadowShell = document.createElement('div'); shadowShell.className = 'mwp-abe-shadow-shell'; shadowRootRef.appendChild(shadowShell); ensureSparkleGradientDefs(shadowShell); (config.styleUrls || []).forEach(styleUrl => { if (!styleUrl) { return; } const stylesheet = document.createElement('link'); stylesheet.rel = 'stylesheet'; stylesheet.href = styleUrl; shadowShell.appendChild(stylesheet); }); return shadowShell; } /** * @returns {Object} Launcher configuration. */ function getConfig() { const defaults = { enabled: false, isAvailable: false, eventName: 'mwp-abe-launch-request', label: 'ABE', downloadUrl: '#', unavailableTitle: 'AI Assisted Block Editor Coming Soon', unavailableSubtitle: 'Sign up for updates.', unavailableMessage: 'AI Automated Block Editor is coming soon. Join our waitlist to get early updates, sneak peeks, and be among the first to know when the plugin officially launches. We will let you know as soon as it is available.', unavailableCta: 'Join the Waitlist', previewCta: 'Preview ABE', styleUrls: [], }; return Object.assign({}, defaults, SFE.ManagerData.abeLauncher || {}); } /** * @returns {string} Sparkle icon markup. */ function buildSparkleIcon() { return [ '', ].join(''); } /** * @returns {string} Close icon markup. */ function buildCloseIcon() { return [ '', ].join(''); } /** * @returns {string} Plus icon markup. */ function buildPlusIcon() { return [ '', ].join(''); } /** * @returns {string} Arrow icon markup. */ function buildArrowUpIcon() { return [ '', ].join(''); } /** * @param {boolean} isHidden Whether the launcher should be hidden. * @returns {void} */ function setLauncherVisibility(isHidden) { const button = getLauncherButton(); if (!button) { return; } button.classList.toggle('mwp-abe-launcher-hidden', !!isHidden); button.setAttribute('aria-hidden', isHidden ? 'true' : 'false'); button.tabIndex = isHidden ? -1 : 0; } /** * Install the visibility event listener only once even if init() reruns. * * @returns {void} */ function ensureVisibilityListener() { if (visibilityListenerInstalled) { return; } document.addEventListener(LAUNCHER_VISIBILITY_EVENT, event => { const detail = event && event.detail ? event.detail : {}; setLauncherVisibility(!!detail.isHidden); }); visibilityListenerInstalled = true; } /** * Re-run launcher initialization after browser lifecycle changes when the * button should exist but no longer does. * * @returns {void} */ function recoverLauncherIfMissing() { const config = getConfig(); if (!config.enabled || getLauncherButton()) { return; } init(); } /** * Register lifecycle listeners once so the FrontEdit-owned launcher can restore * itself if a browser/tab transition leaves it detached. * * @returns {void} */ function ensureLifecycleRecovery() { if (lifecycleRecoveryInstalled) { return; } window.addEventListener('pageshow', recoverLauncherIfMissing); window.addEventListener('focus', recoverLauncherIfMissing); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { recoverLauncherIfMissing(); } }); lifecycleRecoveryInstalled = true; } /** * @param {HTMLElement} element Surface element. * @returns {number} Transition duration in milliseconds. */ function getTransitionDuration(element) { const styles = window.getComputedStyle(element); const durations = String(styles.transitionDuration || '0s').split(','); const delays = String(styles.transitionDelay || '0s').split(','); let longest = 0; durations.forEach((duration, index) => { const delay = delays[index] || delays[delays.length - 1] || '0s'; const total = toMilliseconds(duration.trim()) + toMilliseconds(delay.trim()); longest = Math.max(longest, total); }); return longest; } /** * @param {string} value CSS duration string. * @returns {number} Milliseconds. */ function toMilliseconds(value) { if (!value) { return 0; } if (value.endsWith('ms')) { return Number.parseFloat(value) || 0; } if (value.endsWith('s')) { return (Number.parseFloat(value) || 0) * 1000; } return Number.parseFloat(value) || 0; } /** * @param {HTMLElement} element Surface element. * @returns {void} */ function showSurface(element) { if (!element) { return; } if (element._mwpAbeHideTimeoutId) { window.clearTimeout(element._mwpAbeHideTimeoutId); element._mwpAbeHideTimeoutId = 0; } element.hidden = false; element.setAttribute('aria-hidden', 'false'); if (shadowHost) { shadowHost.setAttribute('aria-hidden', 'false'); } element.classList.remove('is-open'); void element.offsetWidth; window.requestAnimationFrame(() => { element.classList.add('is-open'); }); } /** * @param {HTMLElement} element Surface element. * @param {Function|null} [callback=null] Optional callback after hide completes. * @returns {void} */ function hideSurface(element, callback = null) { if (!element) { if (typeof callback === 'function') { callback(); } return; } if (element._mwpAbeHideTimeoutId) { window.clearTimeout(element._mwpAbeHideTimeoutId); element._mwpAbeHideTimeoutId = 0; } element.classList.remove('is-open'); element.setAttribute('aria-hidden', 'true'); const finalize = () => { element.hidden = true; element._mwpAbeHideTimeoutId = 0; if (typeof callback === 'function') { callback(); } }; const duration = getTransitionDuration(element); if (duration <= 0) { finalize(); return; } element._mwpAbeHideTimeoutId = window.setTimeout(finalize, duration + 50); } /** * @returns {void} */ function syncShellVisibility() { if (!shadowHost || !teaserUi) { return; } const hasOpenSurface = !teaserUi.modal.hidden || !teaserUi.chat.hidden; shadowHost.setAttribute('aria-hidden', hasOpenSurface ? 'false' : 'true'); setLauncherVisibility(hasOpenSurface); } /** * @param {Object} options Header settings. * @returns {string} Branded header markup. */ function buildBrandedHeaderMarkup(options) { const settings = options && typeof options === 'object' ? options : {}; const title = String(settings.title || ''); const subtitle = String(settings.subtitle || ''); const titleId = String(settings.titleId || '').trim(); const actionsMarkup = String(settings.actionsMarkup || ''); const titleAttribute = titleId ? ` id="${titleId}"` : ''; return `
${subtitle}
${config.unavailableMessage || 'AI Automated Block Editor is coming soon. Join our waitlist to get early updates, sneak peeks, and be among the first to know when the plugin officially launches. We will let you know as soon as it is available.'}