1/*! 2 * wf_scrollanim.js 3 * Reemplazo moderno de kissui.scrollanim 4 * Compatible con: 5 * data-kui-anim="fadeInUp" 6 * data-kui-event="in" 7 */ 8 9class wf_scrollanim 10{ 11 constructor(opciones = {}) 12 { 13 this.opciones = { 14 selector : `[data-kui-anim]`, 15 prefijoAnimado : `animated`, 16 atributoAnim : `data-kui-anim`, 17 atributoEvento : `data-kui-event`, 18 atributoDelay : `data-kui-delay`, 19 atributoDuration : `data-kui-duration`, 20 autoReset : true, 21 triggerOnInit : true, 22 debug : false, 23 ...opciones 24 }; 25 26 this.items = []; 27 this.observer = null; 28 this._init(); 29 } 30 31 log(...args) 32 { 33 if (this.opciones.debug) 34 console.log(`[wf_scrollanim]`, ...args); 35 } 36 37 _init() 38 { 39 const elementos = document.querySelectorAll(this.opciones.selector); 40 41 if (!elementos.length) 42 { 43 this.log(`No se encontraron elementos.`); 44 return; 45 } 46 47 this.items = Array.from(elementos).map((elemento) => 48 { 49 const animacion = (elemento.getAttribute(this.opciones.atributoAnim) || ``).trim(); 50 const evento = (elemento.getAttribute(this.opciones.atributoEvento) || `in`).trim(); 51 const delay = (elemento.getAttribute(this.opciones.atributoDelay) || ``).trim(); 52 const duration = (elemento.getAttribute(this.opciones.atributoDuration) || ``).trim(); 53 54 if (!animacion) 55 return null; 56 57 elemento.style.opacity = `0`; 58 59 if (delay !== ``) 60 elemento.style.animationDelay = delay; 61 62 if (duration !== ``) 63 elemento.style.animationDuration = duration; 64 65 return { 66 elemento, 67 animacion, 68 evento, 69 activa : false, // animación en curso 70 animada : false, // ya se animó en esta entrada al viewport 71 visible : false // está actualmente visible 72 }; 73 }).filter(Boolean); 74 75 this._crearObserver(); 76 77 this.items.forEach((item) => 78 { 79 this.observer.observe(item.elemento); 80 }); 81 82 if (this.opciones.triggerOnInit) 83 this._comprobarManual(); 84 } 85 86 _crearObserver() 87 { 88 this.observer = new IntersectionObserver((entradas) => 89 { 90 entradas.forEach((entrada) => 91 { 92 const item = this.items.find((it) => it.elemento === entrada.target); 93 if (!item) return; 94 95 const rect = entrada.boundingClientRect; 96 const viewportH = window.innerHeight || document.documentElement.clientHeight; 97 const viewportW = window.innerWidth || document.documentElement.clientWidth; 98 99 const visibleReal = 100 rect.bottom > 0 && 101 rect.right > 0 && 102 rect.top < viewportH && 103 rect.left < viewportW && 104 entrada.isIntersecting; 105 106 item.visible = visibleReal; 107 108 // Mientras siga visible, jamás se reanima. 109 if (visibleReal) 110 { 111 if (!item.animada) 112 { 113 this._animar(item); 114 item.animada = true; 115 } 116 } 117 else 118 { 119 // Sólo al salir completamente de pantalla se rearma. 120 this._reset(item); 121 } 122 }); 123 }, { 124 threshold : 0 125 }); 126 } 127 128 _comprobarManual() 129 { 130 this.items.forEach((item) => 131 { 132 const rect = item.elemento.getBoundingClientRect(); 133 const viewportH = window.innerHeight || document.documentElement.clientHeight; 134 const viewportW = window.innerWidth || document.documentElement.clientWidth; 135 136 const visible = 137 rect.bottom > 0 && 138 rect.right > 0 && 139 rect.top < viewportH && 140 rect.left < viewportW; 141 142 item.visible = visible; 143 144 if (visible && !item.animada) 145 { 146 this._animar(item); 147 item.animada = true; 148 } 149 }); 150 } 151 152 _cumpleEvento(evento, estado) 153 { 154 const eventos = evento.split(/\s+/).filter(Boolean); 155 156 return eventos.every((ev) => 157 { 158 switch (ev) 159 { 160 case `in`: 161 return estado.visible; 162 163 case `out`: 164 return !estado.visible; 165 166 case `middle`: 167 return this._puntoEnRango( 168 estado.viewportH / 2, 169 estado.rect.top, 170 estado.rect.bottom 171 ); 172 173 case `center`: 174 return this._puntoEnRango( 175 estado.viewportW / 2, 176 estado.rect.left, 177 estado.rect.right 178 ); 179 180 case `top`: 181 return this._puntoEnRango(0, estado.rect.top, estado.rect.bottom); 182 183 case `bottom`: 184 return this._puntoEnRango( 185 estado.viewportH, 186 estado.rect.top, 187 estado.rect.bottom 188 ); 189 190 case `left`: 191 return this._puntoEnRango(0, estado.rect.left, estado.rect.right); 192 193 case `right`: 194 return this._puntoEnRango( 195 estado.viewportW, 196 estado.rect.left, 197 estado.rect.right 198 ); 199 200 default: 201 this.log(`Evento no soportado:`, ev); 202 return false; 203 } 204 }); 205 } 206 207 _puntoEnRango(punto, inicio, fin) 208 { 209 return punto >= inicio && punto <= fin; 210 } 211 212 _clasesAnimacion(animacion) 213 { 214 return String(animacion || ``) 215 .trim() 216 .split(/\s+/) 217 .filter(Boolean)
218 .map((clase) => 219 { 220 if (clase.startsWith(`animate__`)) 221 return clase; 222 223 return `animate__${clase}`; 224 }); 225 } 226 227 _animar(item) 228 { 229 if (item.activa) 230 return; 231 232 const el = item.elemento; 233 const claseBase = `animate__animated`; 234 const clasesAnim = this._clasesAnimacion(item.animacion); 235 236 el.style.opacity = `1`; 237 el.classList.remove(claseBase, ...clasesAnim); 238 239 void el.offsetWidth; 240 241 el.classList.add(claseBase, ...clasesAnim); 242 item.activa = true; 243 244 const finAnim = () => 245 { 246 el.classList.remove(claseBase, ...clasesAnim); 247 el.removeEventListener(`animationend`, finAnim); 248 249 item.activa = false; 250 }; 251 252 el.addEventListener(`animationend`, finAnim); 253 } 254 255 _reset(item) 256 { 257 const clasesAnim = this._clasesAnimacion(item.animacion); 258 259 item.activa = false; 260 item.animada = false; 261 item.visible = false; 262 item.elemento.style.opacity = `0`; 263 item.elemento.classList.remove(`animate__animated`, ...clasesAnim); 264 } 265 266 refresh() 267 { 268 if (this.observer) 269 this.observer.disconnect(); 270 271 this.items = []; 272 this._init(); 273 } 274 275 destroy() 276 { 277 if (this.observer) 278 this.observer.disconnect(); 279 280 this.items.forEach((item) => 281 { 282 const clasesAnim = this._clasesAnimacion(item.animacion); 283 284 item.elemento.classList.remove(`animate__animated`, ...clasesAnim); 285 item.elemento.style.opacity = ``; 286 item.elemento.style.animationDelay = ``; 287 item.elemento.style.animationDuration = ``; 288 }); 289 290 this.items = []; 291 this.observer = null; 292 } 293}
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.