From cb29453294033cf4bcc5cfdaf4892f858c4607ec Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 21 Nov 2025 12:30:07 -0500 Subject: [PATCH] Refactor iframe selection script to improve structure and performance This commit enhances the iframe selection script by consolidating functionality under a namespace, improving the organization of the code. It introduces a more modular approach by defining functions within the namespace for better encapsulation. Additionally, it optimizes the event handling and style application processes, ensuring that the script operates efficiently within iframes. Unused comments and excessive logging have been removed to streamline the codebase. --- next-env.d.ts | 2 +- .../utils/iframe-selection-script.js | 23 ++- .../utils/iframe-selection-script.ts | 2 +- .../iframe-selection-script/compatibility.js | 7 - .../iframe-selection-script/constants.js | 147 ++++++++++++++---- .../iframe-selection-script/domain-links.js | 19 +-- .../iframe-selection-script/event-handlers.js | 123 +++++++++------ .../utils/iframe-selection-script/init.js | 87 +++++++++-- .../iframe-selection-script/selection.js | 100 ++++++++---- .../utils/iframe-selection-script/utils.js | 25 +-- 10 files changed, 378 insertions(+), 157 deletions(-) diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script.js index 02574987..03578cfb 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script.js @@ -65,27 +65,40 @@ export function iframeSelectionScript(storeDomain) { const compatibility = createCompatibilityCode(); const init = createInitCode(); - // Combinar todos los módulos en un IIFE - // El módulo de compatibilidad se ejecuta primero para agregar atributos antes de la inicialización - // Agregar verificación para evitar ejecución múltiple return `(function() { - if (window.__FASTTIFY_THEME_STUDIO_SCRIPT_LOADED__) { + 'use strict'; + var NS_KEY = '__FASTTIFY_THEME_STUDIO_NS__'; + if (window[NS_KEY]) { return; } - window.__FASTTIFY_THEME_STUDIO_SCRIPT_LOADED__ = true; + var $ = window[NS_KEY] = {}; +(function() { ${constants} +})(); +(function() { ${utils} +})(); +(function() { ${selection} +})(); +(function() { ${eventHandlers} +})(); +(function() { ${domainLinks} +})(); +(function() { ${compatibility} +})(); +(function() { ${init} +})(); })();`; } diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script.ts b/packages/theme-studio/src/presentation/utils/iframe-selection-script.ts index 4d1093c3..06b1e27d 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script.ts +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -// @ts-ignore - Importing JS file directly +// @ts-ignore import { iframeSelectionScript as iframeSelectionScriptImpl } from './iframe-selection-script.js'; /** diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/compatibility.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/compatibility.js index e1ae657c..66ede763 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/compatibility.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/compatibility.js @@ -146,15 +146,12 @@ function compatibilityModule() { } } - // Detectar comentario de inicio de bloque (para futura implementación) const blockStartMatch = commentText.match(/^FASTTIFY_BLOCK_START:(.+)$/); if (blockStartMatch) { const blockId = blockStartMatch[1]; const blockElement = findNextVisibleElement(comment); if (blockElement && !blockElement.hasAttribute('data-block-id')) { - // Necesitamos obtener el sectionId del contexto - // Por ahora, buscar el sectionId más cercano subiendo en el árbol let blockParent = blockElement.parentElement; let sectionId = null; @@ -177,19 +174,15 @@ function compatibilityModule() { * Se ejecuta después de que el DOM esté completamente cargado */ function initCompatibility() { - // Ejecutar inmediatamente si el DOM ya está listo if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', processCompatibilityMarkers); } else { - // Usar setTimeout para asegurar que el contenido renderizado esté disponible setTimeout(processCompatibilityMarkers, 0); } - // También procesar después de un pequeño delay para capturar contenido dinámico setTimeout(processCompatibilityMarkers, 100); } - // Inicializar cuando el módulo se carga initCompatibility(); } diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/constants.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/constants.js index 94947bee..094a98e3 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/constants.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/constants.js @@ -31,26 +31,27 @@ function constantsModule(storeDomain) { if (window.self === window.top) { return; } - const SELECTED_CLASS = 'fasttify-theme-studio-selected'; - const HOVER_CLASS = 'fasttify-theme-studio-hover'; - const STORE_DOMAIN = storeDomain; - const IS_LOCALHOST = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; - const style = document.createElement('style'); - style.textContent = + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; + $.SELECTED_CLASS = 'fasttify-theme-studio-selected'; + $.HOVER_CLASS = 'fasttify-theme-studio-hover'; + $.STORE_DOMAIN = storeDomain; + $.IS_LOCALHOST = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; + $.style = document.createElement('style'); + $.style.textContent = '[data-section-id].' + - SELECTED_CLASS + + $.SELECTED_CLASS + ',' + '[data-block-id].' + - SELECTED_CLASS + + $.SELECTED_CLASS + ' {' + ' position: relative !important;' + ' box-shadow: inset 0 0 0 2px #006fbb !important;' + '}' + '[data-section-id].' + - HOVER_CLASS + + $.HOVER_CLASS + ',' + '[data-block-id].' + - HOVER_CLASS + + $.HOVER_CLASS + ' {' + ' position: relative !important;' + ' box-shadow: inset 0 0 0 2px rgba(0, 111, 187, 0.6) !important;' + @@ -68,39 +69,116 @@ function constantsModule(storeDomain) { ' pointer-events: none;' + ' border-radius: 4px;' + '}'; - document.head.appendChild(style); - let inspectorEnabled = true; - let currentSelectedElement = null; - let hoveredElement = null; - let currentLabelElement = null; - let hoverLabelElement = null; - let lastSelectionTimestamp = 0; - let scrollAnimationFrame = null; + document.head.appendChild($.style); + + /** + * Aplica estilos directamente al elemento como fallback si el CSS no funciona + * @param {Element} element - El elemento al que aplicar los estilos + * @param {string} type - Tipo de estilo: 'hover' o 'selected' + */ + $.applyStyles = function (element, type) { + if (!element) return; + + // Guardar estilos originales solo la primera vez + if (!element._fasttifyOriginalStyles) { + element._fasttifyOriginalStyles = { + boxShadow: element.style.boxShadow || '', + position: element.style.position || '', + }; + } + + if (type === 'hover') { + element.style.setProperty('box-shadow', 'inset 0 0 0 2px rgba(0, 111, 187, 0.6)', 'important'); + element.style.setProperty('position', 'relative', 'important'); + } else if (type === 'selected') { + element.style.setProperty('box-shadow', 'inset 0 0 0 2px #006fbb', 'important'); + element.style.setProperty('position', 'relative', 'important'); + } + }; + + /** + * Remueve los estilos aplicados y restaura los originales + * @param {Element} element - El elemento del que remover los estilos + */ + $.removeStyles = function (element) { + if (!element) return; + + if (element._fasttifyOriginalStyles) { + // Restaurar estilos originales + if (element._fasttifyOriginalStyles.boxShadow) { + element.style.boxShadow = element._fasttifyOriginalStyles.boxShadow; + } else { + element.style.removeProperty('box-shadow'); + } + + if (element._fasttifyOriginalStyles.position) { + element.style.position = element._fasttifyOriginalStyles.position; + } else { + element.style.removeProperty('position'); + } + + delete element._fasttifyOriginalStyles; + } else { + // Si no hay estilos originales guardados, remover los que agregamos + element.style.removeProperty('box-shadow'); + element.style.removeProperty('position'); + } + }; + + /** + * Verifica si los estilos CSS se están aplicando correctamente + * @param {Element} element - El elemento a verificar + * @param {string} type - Tipo de estilo: 'hover' o 'selected' + * @returns {boolean} true si los estilos se están aplicando, false si no + */ + $.verifyStylesApplied = function (element, type) { + if (!element) return false; + + var computedStyle = window.getComputedStyle(element); + var boxShadow = computedStyle.boxShadow; + var hasBoxShadow = boxShadow && boxShadow !== 'none' && boxShadow.indexOf('inset') !== -1; + + return hasBoxShadow; + }; + + $.inspectorEnabled = true; + $.currentSelectedElement = null; + $.hoveredElement = null; + $.currentLabelElement = null; + $.hoverLabelElement = null; + $.lastSelectionTimestamp = 0; + $.scrollAnimationFrame = null; /** * Función global para toggle el inspector * @param {boolean} enabled - Si el inspector está habilitado */ window.toggleInspector = function (enabled) { - inspectorEnabled = enabled !== undefined ? enabled : !inspectorEnabled; - if (style) { - style.disabled = !inspectorEnabled; + $.inspectorEnabled = enabled !== undefined ? enabled : !$.inspectorEnabled; + if ($.style) { + $.style.disabled = !$.inspectorEnabled; } // Limpiar selección cuando se desactiva - if (!inspectorEnabled) { - if (currentSelectedElement) { - currentSelectedElement.classList.remove(SELECTED_CLASS); + if (!$.inspectorEnabled) { + if ($.currentSelectedElement) { + $.currentSelectedElement.classList.remove($.SELECTED_CLASS); + if (typeof $.removeStyles === 'function') { + $.removeStyles($.currentSelectedElement); + } if (typeof window.removeLabel === 'function') { window.removeLabel(false); } - currentSelectedElement = null; + $.currentSelectedElement = null; } - if (hoveredElement) { - hoveredElement.classList.remove(HOVER_CLASS); + if ($.hoveredElement) { + $.hoveredElement.classList.remove($.HOVER_CLASS); + if (typeof $.removeStyles === 'function') { + $.removeStyles($.hoveredElement); + } if (typeof window.removeLabel === 'function') { window.removeLabel(true); } - hoveredElement = null; + $.hoveredElement = null; } } }; @@ -114,6 +192,17 @@ function constantsModule(storeDomain) { export function createConstantsCode(storeDomain) { let functionBody = extractFunctionBody(constantsModule, 'constants'); const domainValue = storeDomain ? JSON.stringify(storeDomain) : 'null'; - functionBody = functionBody.replace(/const STORE_DOMAIN = storeDomain;/, `const STORE_DOMAIN = ${domainValue};`); + + functionBody = functionBody.replace( + /[a-zA-Z_$][a-zA-Z0-9_$]*\.STORE_DOMAIN\s*=\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*[;,]/g, + (match) => { + const varMatch = match.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.STORE_DOMAIN/); + if (varMatch) { + return `${varMatch[1]}.STORE_DOMAIN = ${domainValue};`; + } + return match; + } + ); + return functionBody; } diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/domain-links.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/domain-links.js index e3e35b65..ee6152f3 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/domain-links.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/domain-links.js @@ -26,20 +26,21 @@ import { extractFunctionBody } from './extract-function-body.js'; * Esta función no se ejecuta directamente, se convierte a string para inyectar en el iframe */ function domainLinksModule() { + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; /** * Configura los enlaces relativos para mostrar el dominio completo de la tienda * En localhost solo agrega tooltips, en producción modifica los hrefs */ - function setupDomainLinks() { - if (!STORE_DOMAIN) return; + $.setupDomainLinks = function () { + if (!$.STORE_DOMAIN) return; // Modificar todos los enlaces para que muestren el dominio de la tienda en el tooltip // En localhost, los enlaces relativos funcionan bien, solo agregamos el tooltip // En producción, modificamos el href pero interceptamos los clicks - const updateLinks = function () { - const links = document.querySelectorAll('a[href]'); + var updateLinks = function () { + var links = document.querySelectorAll('a[href]'); links.forEach(function (link) { - const href = link.getAttribute('href'); + var href = link.getAttribute('href'); if ( href && !href.startsWith('http://') && @@ -54,11 +55,11 @@ function domainLinksModule() { } // Construir la URL completa con el dominio de la tienda para el tooltip - const fullUrl = 'https://' + STORE_DOMAIN + (href.startsWith('/') ? href : '/' + href); + var fullUrl = 'https://' + $.STORE_DOMAIN + (href.startsWith('/') ? href : '/' + href); // En localhost, solo agregar title para el tooltip sin modificar href // En producción, modificar href para que el navegador muestre la URL completa - if (IS_LOCALHOST) { + if ($.IS_LOCALHOST) { // Solo agregar title si no tiene uno personalizado if (!link.hasAttribute('title')) { link.setAttribute('title', fullUrl); @@ -76,7 +77,7 @@ function domainLinksModule() { // Observar cambios en el DOM para actualizar nuevos enlaces if (typeof MutationObserver !== 'undefined') { - const observer = new MutationObserver(function (mutations) { + var observer = new MutationObserver(function (mutations) { updateLinks(); }); observer.observe(document.body, { @@ -84,7 +85,7 @@ function domainLinksModule() { subtree: true, }); } - } + }; } /** diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/event-handlers.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/event-handlers.js index 5ad8c844..f80e1ebf 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/event-handlers.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/event-handlers.js @@ -26,24 +26,25 @@ import { extractFunctionBody } from './extract-function-body.js'; * Esta función no se ejecuta directamente, se convierte a string para inyectar en el iframe */ function eventHandlersModule() { + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; /** * Maneja los clicks en elementos seleccionables * Permite que elementos interactivos (enlaces, botones) funcionen normalmente * mientras envía mensajes de selección al padre * @param {MouseEvent} event - El evento de click */ - function handleClick(event) { - if (inspectorEnabled === false) return; - const target = event.target; - const selectableElement = findSelectableElement(target); + $.handleClick = function (event) { + if ($.inspectorEnabled === false) return; + var target = event.target; + var selectableElement = $.findSelectableElement(target); if (selectableElement) { // Solo prevenir el comportamiento por defecto si el elemento clickeado es directamente el elemento seleccionable // Esto permite que enlaces, botones y otros elementos interactivos funcionen normalmente - const isDirectSelectable = target === selectableElement; + var isDirectSelectable = target === selectableElement; // Si es un elemento interactivo (enlace, botón, input, etc.), no prevenir el comportamiento - const isInteractiveElement = + var isInteractiveElement = target.tagName === 'A' || target.tagName === 'BUTTON' || target.tagName === 'INPUT' || @@ -60,23 +61,23 @@ function eventHandlersModule() { } // Siempre enviar el mensaje de selección, pero no interferir con elementos interactivos - const { sectionId, blockId, subBlockId } = getElementIds(selectableElement); + var ids = $.getElementIds(selectableElement); if (window.parent) { window.parent.postMessage( { type: 'FASTTIFY_THEME_STUDIO_ELEMENT_CLICKED', - sectionId, - blockId, - subBlockId, + sectionId: ids.sectionId, + blockId: ids.blockId, + subBlockId: ids.subBlockId, }, '*' ); } } - } + }; - let hoverTimeout = null; - let leaveTimeout = null; + $.hoverTimeout = null; + $.leaveTimeout = null; /** * Verifica si un elemento o su elemento relacionado están dentro del elemento seleccionable @@ -93,88 +94,110 @@ function eventHandlersModule() { * Maneja el evento mouseenter para mostrar el estado hover * @param {MouseEvent} event - El evento mouseenter */ - function handleMouseEnter(event) { - if (inspectorEnabled === false) return; + $.handleMouseEnter = function (event) { + if ($.inspectorEnabled === false) return; // Cancelar cualquier timeout de leave pendiente - if (leaveTimeout) { - clearTimeout(leaveTimeout); - leaveTimeout = null; + if ($.leaveTimeout) { + clearTimeout($.leaveTimeout); + $.leaveTimeout = null; } - const target = event.target; - const selectableElement = findSelectableElement(target); + var target = event.target; + var selectableElement = $.findSelectableElement(target); // Si ya está en hover este elemento, no hacer nada - if (hoveredElement === selectableElement) { + if ($.hoveredElement === selectableElement) { return; } // Solo mostrar hover si no es el elemento seleccionado actualmente - if (selectableElement && selectableElement !== currentSelectedElement) { + if (selectableElement && selectableElement !== $.currentSelectedElement) { // Cancelar timeout anterior si existe - if (hoverTimeout) { - clearTimeout(hoverTimeout); + if ($.hoverTimeout) { + clearTimeout($.hoverTimeout); } // Pequeño delay para evitar parpadeos rápidos - hoverTimeout = setTimeout(function () { - if (selectableElement && selectableElement !== currentSelectedElement) { - selectableElement.classList.add(HOVER_CLASS); - hoveredElement = selectableElement; - const elementName = getElementName(selectableElement); + $.hoverTimeout = setTimeout(function () { + if (selectableElement && selectableElement !== $.currentSelectedElement) { + selectableElement.classList.add($.HOVER_CLASS); + + // Aplicar estilos directamente como fallback + if (typeof $.applyStyles === 'function') { + $.applyStyles(selectableElement, 'hover'); + } + + // Verificar después de un frame si los estilos se aplicaron correctamente + requestAnimationFrame(function () { + if (typeof $.verifyStylesApplied === 'function' && !$.verifyStylesApplied(selectableElement, 'hover')) { + // Si los estilos CSS no se aplicaron, aplicar directamente + if (typeof $.applyStyles === 'function') { + $.applyStyles(selectableElement, 'hover'); + } + } + }); + + $.hoveredElement = selectableElement; + var elementName = $.getElementName(selectableElement); if (elementName && typeof window.updateLabel === 'function') { window.updateLabel(selectableElement, elementName, true); } } - hoverTimeout = null; + $.hoverTimeout = null; }, 50); } - } + }; /** * Maneja el evento mouseleave para remover el estado hover * @param {MouseEvent} event - El evento mouseleave */ - function handleMouseLeave(event) { + $.handleMouseLeave = function (event) { // Cancelar cualquier timeout de hover pendiente - if (hoverTimeout) { - clearTimeout(hoverTimeout); - hoverTimeout = null; + if ($.hoverTimeout) { + clearTimeout($.hoverTimeout); + $.hoverTimeout = null; } - if (!hoveredElement) return; + if (!$.hoveredElement) return; - const relatedTarget = event.relatedTarget; + var relatedTarget = event.relatedTarget; // Verificar si realmente estamos saliendo del elemento seleccionable // Si el relatedTarget (elemento hacia donde va el mouse) está dentro del elemento seleccionable, // entonces no removemos el hover (el mouse sigue dentro del elemento, solo pasó a un hijo) - if (relatedTarget && isWithinSelectable(relatedTarget, hoveredElement)) { + if (relatedTarget && isWithinSelectable(relatedTarget, $.hoveredElement)) { return; } // Pequeño delay para evitar parpadeos cuando el mouse pasa rápidamente entre elementos // Esto da tiempo para que se dispare handleMouseEnter si el mouse entró a otro elemento - leaveTimeout = setTimeout(function () { + $.leaveTimeout = setTimeout(function () { // Verificar nuevamente que realmente salimos del elemento // Si durante el delay el mouse entró a otro elemento, hoveredElement podría haber cambiado - if (hoveredElement) { - hoveredElement.classList.remove(HOVER_CLASS); + if ($.hoveredElement) { + $.hoveredElement.classList.remove($.HOVER_CLASS); + + // Remover estilos aplicados y restaurar originales + if (typeof $.removeStyles === 'function') { + $.removeStyles($.hoveredElement); + } + if (typeof window.removeLabel === 'function') { window.removeLabel(true); } - hoveredElement = null; + $.hoveredElement = null; } - leaveTimeout = null; + $.leaveTimeout = null; }, 150); - } + }; /** * Maneja los mensajes recibidos del window padre * Escucha comandos de selección y limpieza de selección * @param {MessageEvent} event - El evento de mensaje */ - function handleMessage(event) { + $.handleMessage = function (event) { // Solo procesar mensajes de nuestra aplicación // Validamos el tipo del mensaje en lugar del origen para funcionar en producción // cuando el iframe está en un dominio diferente al parent window @@ -187,8 +210,8 @@ function eventHandlersModule() { window.toggleInspector(event.data.enabled); } } else if (event.data.type === 'FASTTIFY_THEME_STUDIO_SELECT_ELEMENT') { - if (inspectorEnabled !== false) { - selectElement( + if ($.inspectorEnabled !== false) { + $.selectElement( event.data.sectionId, event.data.blockId, event.data.subBlockId, @@ -197,10 +220,10 @@ function eventHandlersModule() { ); } } else if (event.data.type === 'FASTTIFY_THEME_STUDIO_CLEAR_SELECTION') { - clearSelection(); - lastSelectionTimestamp = 0; + $.clearSelection(); + $.lastSelectionTimestamp = 0; } - } + }; } /** diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/init.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/init.js index e3024e5c..26d1f15f 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/init.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/init.js @@ -26,26 +26,93 @@ import { extractFunctionBody } from './extract-function-body.js'; * Esta función no se ejecuta directamente, se convierte a string para inyectar en el iframe */ function initModule() { + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; + var listenersRegistered = false; + + /** + * Verifica si hay elementos seleccionables en el DOM + */ + function hasSelectableElements() { + return ( + document.querySelectorAll('[data-section-id]').length > 0 || + document.querySelectorAll('[data-block-id]').length > 0 || + document.querySelectorAll('[data-sub-block-id]').length > 0 + ); + } + + /** + * Registra los event listeners + */ + function registerEventListeners() { + if (listenersRegistered) { + return; + } + document.addEventListener('click', $.handleClick, true); + document.addEventListener('mouseenter', $.handleMouseEnter, true); + document.addEventListener('mouseleave', $.handleMouseLeave, true); + window.addEventListener('message', $.handleMessage); + listenersRegistered = true; + } + /** * Inicializa los event listeners y configura los enlaces de dominio + * Espera a que haya elementos seleccionables antes de registrar listeners */ function init() { - document.addEventListener('click', handleClick, true); - document.addEventListener('mouseenter', handleMouseEnter, true); - document.addEventListener('mouseleave', handleMouseLeave, true); - window.addEventListener('message', handleMessage); - setupDomainLinks(); + // Configurar enlaces de dominio inmediatamente (no depende de elementos) + $.setupDomainLinks(); + + // Intentar registrar listeners inmediatamente si hay elementos + if (hasSelectableElements()) { + registerEventListeners(); + return; + } + + // Si no hay elementos, esperar y verificar periódicamente + var attempts = 0; + var maxAttempts = 50; // 5 segundos máximo (50 * 100ms) + + var checkInterval = setInterval(function () { + attempts++; + if (hasSelectableElements()) { + registerEventListeners(); + clearInterval(checkInterval); + } else if (attempts >= maxAttempts) { + registerEventListeners(); + clearInterval(checkInterval); + } + }, 100); + + // También usar MutationObserver para detectar cuando se agregan elementos + if (typeof MutationObserver !== 'undefined') { + var observer = new MutationObserver(function (mutations) { + if (!listenersRegistered && hasSelectableElements()) { + registerEventListeners(); + observer.disconnect(); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['data-section-id', 'data-block-id', 'data-sub-block-id'], + }); + } } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } - window.addEventListener('beforeunload', () => { - document.removeEventListener('click', handleClick, true); - document.removeEventListener('mouseenter', handleMouseEnter, true); - document.removeEventListener('mouseleave', handleMouseLeave, true); - window.removeEventListener('message', handleMessage); + + window.addEventListener('beforeunload', function () { + if (listenersRegistered) { + document.removeEventListener('click', $.handleClick, true); + document.removeEventListener('mouseenter', $.handleMouseEnter, true); + document.removeEventListener('mouseleave', $.handleMouseLeave, true); + window.removeEventListener('message', $.handleMessage); + } }); } diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/selection.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/selection.js index 386a0e8a..f5209cf1 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/selection.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/selection.js @@ -26,6 +26,7 @@ import { extractFunctionBody } from './extract-function-body.js'; * Esta función no se ejecuta directamente, se convierte a string para inyectar en el iframe */ function selectionModule() { + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; /** * Crea o actualiza la etiqueta visual del selector * @param {Element} element - El elemento al que agregar la etiqueta @@ -36,24 +37,35 @@ function selectionModule() { if (!element || !labelText) return; // Remover etiqueta anterior si existe - const labelVar = isHover ? 'hoverLabelElement' : 'currentLabelElement'; - const existingLabel = isHover ? hoverLabelElement : currentLabelElement; + var existingLabel = isHover ? $.hoverLabelElement : $.currentLabelElement; if (existingLabel && existingLabel.parentNode) { existingLabel.parentNode.removeChild(existingLabel); } // Crear nueva etiqueta - const label = document.createElement('div'); + var label = document.createElement('div'); label.className = 'fasttify-selector-label'; label.textContent = labelText; + // Aplicar todos los estilos del label directamente para asegurar que siempre se muestren + label.style.position = 'fixed'; + label.style.backgroundColor = '#005cd4'; + label.style.color = 'white'; + label.style.fontSize = '12px'; + label.style.fontWeight = '600'; + label.style.padding = '4px 10px'; + label.style.lineHeight = '1.4'; + label.style.whiteSpace = 'nowrap'; + label.style.zIndex = '999999'; + label.style.pointerEvents = 'none'; + label.style.borderRadius = '4px'; + // Calcular posición relativa al viewport visible del elemento // getBoundingClientRect() ya devuelve coordenadas relativas al viewport - const rect = element.getBoundingClientRect(); + var rect = element.getBoundingClientRect(); // Posicionar la etiqueta en la esquina superior izquierda visible // Como usamos position: fixed, solo necesitamos las coordenadas del viewport - label.style.position = 'fixed'; label.style.top = Math.max(0, rect.top - 2) + 'px'; label.style.left = Math.max(0, rect.left - 2) + 'px'; @@ -61,15 +73,15 @@ function selectionModule() { // Guardar referencia if (isHover) { - hoverLabelElement = label; + $.hoverLabelElement = label; } else { - currentLabelElement = label; + $.currentLabelElement = label; } // Actualizar posición en scroll y resize // getBoundingClientRect() ya devuelve coordenadas relativas al viewport - const updatePosition = function () { - const newRect = element.getBoundingClientRect(); + var updatePosition = function () { + var newRect = element.getBoundingClientRect(); label.style.top = Math.max(0, newRect.top - 2) + 'px'; label.style.left = Math.max(0, newRect.left - 2) + 'px'; }; @@ -90,7 +102,7 @@ function selectionModule() { * @param {boolean} isHover - Si es true, remueve la etiqueta de hover; si es false, remueve la de selección */ window.removeLabel = function (isHover) { - const label = isHover ? hoverLabelElement : currentLabelElement; + var label = isHover ? $.hoverLabelElement : $.currentLabelElement; if (label && label.parentNode) { if (label._cleanup) { label._cleanup(); @@ -98,24 +110,30 @@ function selectionModule() { label.parentNode.removeChild(label); } if (isHover) { - hoverLabelElement = null; + $.hoverLabelElement = null; } else { - currentLabelElement = null; + $.currentLabelElement = null; } }; /** * Limpia la selección actual removiendo la clase de selección del elemento */ - function clearSelection() { - if (currentSelectedElement) { - currentSelectedElement.classList.remove(SELECTED_CLASS); + $.clearSelection = function () { + if ($.currentSelectedElement) { + $.currentSelectedElement.classList.remove($.SELECTED_CLASS); + + // Remover estilos aplicados y restaurar originales + if (typeof $.removeStyles === 'function') { + $.removeStyles($.currentSelectedElement); + } + if (typeof window.removeLabel === 'function') { window.removeLabel(false); } - currentSelectedElement = null; + $.currentSelectedElement = null; } - } + }; /** * Selecciona un elemento por su sectionId, blockId o subBlockId y hace scroll suave hacia él @@ -125,23 +143,23 @@ function selectionModule() { * @param {number} [timestamp] - Timestamp para ignorar mensajes obsoletos * @param {string} [elementName] - Nombre del elemento a mostrar en la etiqueta */ - function selectElement(sectionId, blockId, subBlockId, timestamp, elementName) { + $.selectElement = function (sectionId, blockId, subBlockId, timestamp, elementName) { // Ignorar mensajes obsoletos - if (timestamp && timestamp < lastSelectionTimestamp) { + if (timestamp && timestamp < $.lastSelectionTimestamp) { return; } if (timestamp) { - lastSelectionTimestamp = timestamp; + $.lastSelectionTimestamp = timestamp; } else { - lastSelectionTimestamp = Date.now(); + $.lastSelectionTimestamp = Date.now(); } - clearSelection(); + $.clearSelection(); if (!sectionId && !blockId && !subBlockId) { return; } - let selector = ''; + var selector = ''; if (subBlockId) { // Si hay subBlockId, buscar por data-sub-block-id selector = '[data-sub-block-id="' + subBlockId + '"]'; @@ -153,10 +171,26 @@ function selectionModule() { selector = '[data-section-id="' + sectionId + '"]:not([data-block-id])'; } if (selector) { - const element = document.querySelector(selector); + var element = document.querySelector(selector); if (element) { - element.classList.add(SELECTED_CLASS); - currentSelectedElement = element; + element.classList.add($.SELECTED_CLASS); + + // Aplicar estilos directamente como fallback + if (typeof $.applyStyles === 'function') { + $.applyStyles(element, 'selected'); + } + + // Verificar después de un frame si los estilos se aplicaron correctamente + requestAnimationFrame(function () { + if (typeof $.verifyStylesApplied === 'function' && !$.verifyStylesApplied(element, 'selected')) { + // Si los estilos CSS no se aplicaron, aplicar directamente + if (typeof $.applyStyles === 'function') { + $.applyStyles(element, 'selected'); + } + } + }); + + $.currentSelectedElement = element; // Mostrar etiqueta con el nombre del elemento if (elementName && typeof window.updateLabel === 'function') { @@ -164,21 +198,21 @@ function selectionModule() { } // Cancelar scroll anterior si hay uno pendiente - if (scrollAnimationFrame !== null) { - cancelAnimationFrame(scrollAnimationFrame); - scrollAnimationFrame = null; + if ($.scrollAnimationFrame !== null) { + cancelAnimationFrame($.scrollAnimationFrame); + $.scrollAnimationFrame = null; } // Usar requestAnimationFrame para mejor sincronización con el navegador // y asegurar scroll suave incluso con selecciones rápidas - scrollAnimationFrame = requestAnimationFrame(function () { - scrollAnimationFrame = null; + $.scrollAnimationFrame = requestAnimationFrame(function () { + $.scrollAnimationFrame = null; element.scrollIntoView({ behavior: 'smooth', block: 'center', }); // Actualizar posición de la etiqueta después del scroll - if (elementName && currentLabelElement && typeof window.updateLabel === 'function') { + if (elementName && $.currentLabelElement && typeof window.updateLabel === 'function') { setTimeout(function () { window.updateLabel(element, elementName, false); }, 100); @@ -186,7 +220,7 @@ function selectionModule() { }); } } - } + }; } /** diff --git a/packages/theme-studio/src/presentation/utils/iframe-selection-script/utils.js b/packages/theme-studio/src/presentation/utils/iframe-selection-script/utils.js index ea1f4aa8..e2d82b2b 100644 --- a/packages/theme-studio/src/presentation/utils/iframe-selection-script/utils.js +++ b/packages/theme-studio/src/presentation/utils/iframe-selection-script/utils.js @@ -26,15 +26,16 @@ import { extractFunctionBody } from './extract-function-body.js'; * Esta función no se ejecuta directamente, se convierte a string para inyectar en el iframe */ function utilsModule() { + var $ = window.__FASTTIFY_THEME_STUDIO_NS__; /** * Busca el elemento seleccionable más cercano (con data-section-id, data-block-id o data-sub-block-id) * subiendo en el árbol DOM desde el elemento dado * @param {Element|null} element - El elemento desde donde comenzar la búsqueda * @returns {Element|null} El elemento seleccionable encontrado o null */ - function findSelectableElement(element) { + $.findSelectableElement = function (element) { if (!element) return null; - let current = element; + var current = element; while (current && current.nodeType === 1) { if ( current.hasAttribute && @@ -47,38 +48,38 @@ function utilsModule() { current = current.parentElement; } return null; - } + }; /** * Extrae los IDs de sección, bloque y sub-bloque de un elemento * @param {Element} element - El elemento del cual extraer los IDs * @returns {{sectionId: string|null, blockId: string|null, subBlockId: string|null}} Objeto con sectionId, blockId y subBlockId */ - function getElementIds(element) { + $.getElementIds = function (element) { return { sectionId: element.getAttribute('data-section-id'), blockId: element.getAttribute('data-block-id'), subBlockId: element.getAttribute('data-sub-block-id'), }; - } + }; /** * Extrae el nombre del elemento desde los atributos data * @param {Element} element - El elemento del cual extraer el nombre * @returns {string|null} El nombre del elemento o null */ - function getElementName(element) { + $.getElementName = function (element) { if (!element) return null; // Prioridad: data-sub-block-name > data-block-name > data-section-name > subBlockId > blockId > sectionId - const subBlockName = element.getAttribute('data-sub-block-name'); + var subBlockName = element.getAttribute('data-sub-block-name'); if (subBlockName) return subBlockName; - const blockName = element.getAttribute('data-block-name'); + var blockName = element.getAttribute('data-block-name'); if (blockName) return blockName; - const sectionName = element.getAttribute('data-section-name'); + var sectionName = element.getAttribute('data-section-name'); if (sectionName) return sectionName; - const { subBlockId, blockId, sectionId } = getElementIds(element); - return subBlockId || blockId || sectionId || null; - } + var ids = $.getElementIds(element); + return ids.subBlockId || ids.blockId || ids.sectionId || null; + }; } /**