1/*1789636316000*/ 2/** 3 * Módulo para detectar cuando el usuario ha hecho scroll más allá de un umbral. 4 * Dispara callbacks personalizados al cruzar los umbrales en ambas direcciones. 5 * Comportamiento configurable por instancia: una sola vez o continuo. 6 * Umbrales configurables por instancia. 7 * Permite activación manual de estados además de detección por scroll. 8 * 9 * @module ScrollThresholdDetector 10 */ 11(() => { 12 const ScrollThresholdDetector = { 13 // Configuración global por defecto (puede ser sobrescrita por instancia) 14 config: { 15 thresholdEnter: 120, 16 thresholdExit: 50, 17 debounceDelay: 300, // Milisegundos de espera antes de permitir otro cambio 18 }, 19 20 // Estado interno para evitar múltiples instancias 21 instances: new Map(), 22 scrollListener: null, 23 isScrollListenerActive: false, 24 lastChangeTime: 0, // Timestamp del último cambio de estado 25 26 /** 27 * Inicializa una nueva instancia del detector de scroll. 28 * 29 * @param {string} instanceId - Identificador único para la instancia 30 * @param {Object} options - Configuración de la instancia 31 * @param {Function} options.onEnter - Callback cuando se cruza thresholdEnter (descendente) 32 * @param {Function} options.onExit - Callback cuando se cruza thresholdExit (ascendente) 33 * @param {HTMLElement} [options.element] - Elemento sobre el que aplicar cambios (opcional, para contexto) 34 * @param {Boolean} [options.oneTimeOnly] - Si true, una vez scrolled permanece asÃ. Si false, comportamiento bidireccional (default: false) 35 * @param {Number} [options.thresholdEnter] - Umbral para entrar en estado scrolled (default: 120) 36 * @param {Number} [options.thresholdExit] - Umbral para salir del estado scrolled (default: 50) 37 * @returns {Object} Objeto con métodos de control de la instancia 38 */ 39 init(instanceId, options = {}) { 40 // Valida que la instancia no exista ya 41 if (this.instances.has(instanceId)) { 42 console.warn(`[ScrollThresholdDetector] La instancia "${instanceId}" ya existe.`); 43 return this.instances.get(instanceId); 44 } 45 46 // Crea la nueva instancia con umbrales configurables 47 const instance = { 48 id: instanceId, 49 onEnter: typeof options.onEnter === 'function' ? options.onEnter : null, 50 onExit: typeof options.onExit === 'function' ? options.onExit : null, 51 element: options.element || null, 52 lastState: null, // null = inicial, true = scrolled, false = no scrolled 53 oneTimeOnly: options.oneTimeOnly === true, // Comportamiento de una sola vez 54 hasScrolledOnce: false, // Bandera por instancia 55 // Umbrales por instancia (usa valores globales por defecto si no se especifican) 56 thresholdEnter: typeof options.thresholdEnter === 'number' ? options.thresholdEnter : this.config.thresholdEnter, 57 thresholdExit: typeof options.thresholdExit === 'number' ? options.thresholdExit : this.config.thresholdExit, 58 }; 59 60 // Almacena la instancia 61 this.instances.set(instanceId, instance); 62 63 // Activa el listener global si no lo está 64 this._activateGlobalListener(); 65 66 // Comprueba la posición inicial del scroll 67 this._checkInitialScroll(instance); 68 69 // Retorna objeto con métodos de control 70 return { 71 destroy: () => this._destroyInstance(instanceId), 72 getState: () => instance.lastState, 73 setScrolledState: (isScrolled) => this._setScrolledState(instanceId, isScrolled), 74 }; 75 }, 76 77 /** 78 * Activa el listener global de scroll si no está ya activo. 79 * Usa requestAnimationFrame para optimizar el rendimiento. 80 * @private 81 */ 82 _activateGlobalListener() { 83 if (this.isScrollListenerActive) return; 84 85 this.scrollListener = () => { 86 window.requestAnimationFrame(() => { 87 this._checkAllInstances(); 88 }); 89 }; 90 91 window.addEventListener('scroll', this.scrollListener, { passive: true }); 92 this.isScrollListenerActive = true; 93 }, 94 95 /** 96 * Desactiva el listener global cuando no hay instancias activas. 97 * @private 98 */ 99 _deactivateGlobalListener() { 100 if (this.instances.size === 0 && this.isScrollListenerActive && this.scrollListener) { 101 window.removeEventListener('scroll', this.scrollListener); 102 this.scrollListener = null; 103 this.isScrollListenerActive = false;
104 } 105 }, 106 107 /** 108 * Comprueba si está dentro del perÃodo de debounce. 109 * @private 110 */ 111 _isDebounced() { 112 const now = Date.now(); 113 const timeSinceLastChange = now - this.lastChangeTime; 114 return timeSinceLastChange < this.config.debounceDelay; 115 }, 116 117 /** 118 * Marca el momento del último cambio de estado. 119 * @private 120 */ 121 _recordStateChange() { 122 this.lastChangeTime = Date.now(); 123 }, 124 125 /** 126 * Comprueba la posición de scroll inicial para cada instancia. 127 * @private 128 */ 129 _checkInitialScroll(instance) { 130 const y = window.scrollY; 131 132 if (y > instance.thresholdEnter) { 133 // El usuario comienza con scroll pasado el umbral 134 instance.lastState = true; 135 if (instance.oneTimeOnly) { 136 instance.hasScrolledOnce = true; 137 } 138 this._executeCallback(instance, 'onEnter', { scrollY: y, direction: 'initial' }); 139 this._recordStateChange(); 140 } else { 141 // El usuario comienza sin scroll o bajo el umbral 142 instance.lastState = false; 143 // No dispara callback en este caso, solo establece el estado 144 } 145 }, 146 147 /** 148 * Comprueba el scroll para todas las instancias activas. 149 * @private 150 */ 151 _checkAllInstances() { 152 const y = window.scrollY; 153 154 this.instances.forEach((instance) => { 155 this._checkInstance(instance, y); 156 }); 157 }, 158 159 /** 160 * Comprueba si se ha cruzado un umbral para una instancia especÃfica. 161 * Comportamiento depende de la configuración oneTimeOnly de cada instancia. 162 * Usa los umbrales configurados por instancia. 163 * @private 164 */ 165 _checkInstance(instance, scrollY) { 166 const thresholdEnter = instance.thresholdEnter; 167 const thresholdExit = instance.thresholdExit; 168 169 // Si está en debounce, no procesa cambios de estado 170 if (this._isDebounced()) { 171 return; 172 } 173 174 // Comportamiento si oneTimeOnly está activado 175 if (instance.oneTimeOnly) { 176 // Si ya se ha hecho scroll una vez, solo permite pasar a scrolled, nunca volver atrás 177 if (instance.hasScrolledOnce) { 178 // Solo permite transición de no-scrolled a scrolled 179 if (instance.lastState === false && scrollY > thresholdEnter) { 180 instance.lastState = true; 181 this._executeCallback(instance, 'onEnter', { scrollY, direction: 'down' }); 182 this._recordStateChange(); 183 } 184 // Ignora intentos de volver a no-scrolled 185 return; 186 } 187 188 // Si aún no se ha scrolleado, comportamiento normal (bidireccional) 189 // Transición: de no-scrolled a scrolled (cruzar thresholdEnter hacia abajo) 190 if (instance.lastState === false && scrollY > thresholdEnter) { 191 instance.lastState = true; 192 instance.hasScrolledOnce = true; // Marca que se ha scrolleado por primera vez 193 this._executeCallback(instance, 'onEnter', { scrollY, direction: 'down' }); 194 this._recordStateChange(); 195 } 196 // Transición: de scrolled a no-scrolled (cruzar thresholdExit hacia arriba) 197 // Solo se permite si aún no se ha scrolleado (before hasScrolledOnce) 198 else if (instance.lastState === true && scrollY < thresholdExit) { 199 instance.lastState = false; 200 this._executeCallback(instance, 'onExit', { scrollY, direction: 'up' }); 201 this._recordStateChange(); 202 } 203 } else { 204 // Comportamiento continuo (bidireccional) - sin restricciones de una sola vez 205 // Transición: de no-scrolled a scrolled (cruzar thresholdEnter hacia abajo) 206 if (instance.lastState === false && scrollY > thresholdEnter) { 207 instance.lastState = true; 208 this._executeCallback(instance, 'onEnter', { scrollY, direction: 'down' }); 209 this._recordStateChange(); 210 } 211 // Transición: de scrolled a no-scrolled (cruzar thresholdExit hacia arriba) 212 else if (instance.lastState === true && scrollY < thresholdExit) { 213 instance.lastState = false; 214 this._executeCallback(instance, 'onExit', { scrollY, direction: 'up' }); 215 this._recordStateChange(); 216 } 217 } 218 }, 219 220 /** 221 * Establece manualmente el estado scrolled de una instancia. 222 * Permite activación manual sin depender del scroll real. 223 * Respeta la configuración oneTimeOnly: si está activada, no permite desactivar scrolled. 224 * Retorna una Promise que se resuelve cuando la transición está completa. 225 * 226 * @param {string} instanceId - ID de la instancia 227 * @param {boolean} isScrolled - true para activar scrolled, false para desactivar 228 * @returns {Promise} Se resuelve cuando el callback se ha ejecutado 229 */ 230 _setScrolledState(instanceId, isScrolled) { 231 return new Promise((resolve) => { 232 const instance = this.instances.get(instanceId); 233 if (!instance) { 234 console.warn(`[ScrollThresholdDetector] La instancia "${instanceId}" no existe.`); 235 resolve(); 236 return; 237 } 238 239 const targetState = isScrolled === true; 240 241 // Si está en debounce, espera a que termine 242 if (this._isDebounced()) { 243 const waitTime = this.config.debounceDelay - (Date.now() - this.lastChangeTime); 244 setTimeout(() => { 245 this._setScrolledState(instanceId, isScrolled).then(resolve); 246 }, waitTime); 247 return; 248 } 249 250 // Si oneTimeOnly está activo y hasScrolledOnce es true, NO permite desactivar 251 if (instance.oneTimeOnly && instance.hasScrolledOnce && !target
251State) { 252 resolve(); 253 return; 254 } 255 256 // Si ya está en el estado deseado, no hace nada 257 if (instance.lastState === targetState) { 258 resolve(); 259 return; 260 } 261 262 // Ejecuta la transición 263 instance.lastState = targetState; 264 265 // Si es activación y oneTimeOnly está activo, marca como scrolleado 266 if (targetState && instance.oneTimeOnly) { 267 instance.hasScrolledOnce = true; 268 } 269 270 if (targetState) { 271 // Activar scrolled (onEnter) 272 this._executeCallback(instance, 'onEnter', { scrollY: window.scrollY, direction: 'manual' }); 273 } else { 274 // Desactivar scrolled (onExit) 275 this._executeCallback(instance, 'onExit', { scrollY: window.scrollY, direction: 'manual' }); 276 } 277 278 this._recordStateChange(); 279 280 // Resuelve después de que el callback se ejecute (asÃncrono) 281 Promise.resolve().then(resolve); 282 }); 283 }, 284 285 /** 286 * Ejecuta un callback de forma asÃncrona si existe. 287 * @private 288 */ 289 _executeCallback(instance, callbackName, data) { 290 const callback = instance[callbackName]; 291 if (!callback) return; 292 293 // Ejecuta de forma asÃncrona usando Promise 294 Promise.resolve().then(() => { 295 try { 296 callback.call(instance, data); 297 } catch (error) { 298 console.error( 299 `[ScrollThresholdDetector] Error en callback "${callbackName}" de la instancia "${instance.id}":`, 300 error 301 ); 302 } 303 }); 304 }, 305 306 /** 307 * Destruye una instancia y limpia sus recursos. 308 * @private 309 */ 310 _destroyInstance(instanceId) { 311 if (!this.instances.has(instanceId)) { 312 console.warn(`[ScrollThresholdDetector] La instancia "${instanceId}" no existe.`); 313 return; 314 } 315 316 this.instances.delete(instanceId); 317 this._deactivateGlobalListener(); 318 }, 319 }; 320 321 // Expone el módulo globalmente 322 window.ScrollThresholdDetector = ScrollThresholdDetector; 323})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.