1/** 2 * æ»å¨ææå°é¨ä»¶ JavaScript 3 */ 4 5(function() { 6 'use strict'; 7 8 class GentiatechScrollEffect { 9 constructor(element) { 10 this.element = element; 11 this.imageContainer = element.querySelector('.mygica-scroll-image-container'); 12 this.contentContainer = element.querySelector('.mygica-scroll-content-container'); 13 this.contentArea = element.querySelector('.mygica-scroll-content'); 14 this.images = element.querySelectorAll('.mygica-scroll-image'); 15 this.items = element.querySelectorAll('.mygica-scroll-item'); 16 this.fadeBottom = element.querySelector('.mygica-scroll-fade-bottom'); 17 18 // è·åé ç½® 19 this.visibleItems = parseFloat(element.dataset.visibleItems) || 1.2; 20 this.maskHeight = parseInt(element.dataset.maskHeight) || 50; 21 22 this.currentActiveIndex = 0; 23 this.isScrolling = false; 24 this.scrollTimeout = null; 25 this.rafId = null; // RequestAnimationFrame ID 26 this.lastScrollTime = 0; 27 this.scrollThrottle = 16; // 约60fps 28 29 this.init(); 30 } 31 32 init() { 33 this.setupLayout(); 34 this.bindEvents(); 35 this.updateActiveItem(0); 36 this.updateFadeEffect(); 37 38 // æ·»å å è½½å¨ç» 39 this.element.classList.add('loading'); 40 setTimeout(() => { 41 this.element.classList.remove('loading'); 42 }, 100); 43 } 44 45 setupLayout() { 46 // 设置å 容åºåé«åº¦ä»¥æ¯ææ»å¨ 47 const containerHeight = this.contentArea ? this.contentArea.offsetHeight : this.element.offsetHeight; 48 const itemHeight = containerHeight / this.visibleItems; 49 50 // 设置æ¯ä¸ªé¡¹ç®çæå°é«åº¦ 51 this.items.forEach((item, index) => { 52 item.style.minHeight = itemHeight + 'px'; 53 item.style.display = 'flex'; 54 item.style.flexDirection = 'column'; 55 item.style.justifyContent = 'center'; 56 57 // 为æåä¸ä¸ªé¡¹ç®æ·»å é¢å¤çåºé¨è¾¹è·ï¼ç¡®ä¿å¯ä»¥æ»å¨å°ä¸å¿ 58 if (index === this.items.length - 1) { 59 item.style.marginBottom = (containerHeight / 2) + 'px'; 60 } 61 }); 62 63 // æ¸éé®ç½©é«åº¦ç°å¨ç±CSSæ§å¶ï¼ä¸éè¦å¨JSä¸è®¾ç½® 64 // ä¿çmaskHeightåéç¨äºéæåº¦è®¡ç® 65 } 66 67 bindEvents() { 68 // çå¬å 容åºåæ»å¨ï¼ä½¿ç¨èæµä¼å 69 this.contentArea.addEventListener('scroll', () => { 70 this.throttledHandleScroll(); 71 }); 72 73 // çå¬çªå£å¤§å°åå 74 window.addEventListener('resize', () => { 75 this.handleResize(); 76 }); 77 78 // çå¬é¡¹ç®ç¹å» 79 this.items.forEach((item, index) => { 80 item.addEventListener('click', () => { 81 this.scrollToItem(index); 82 }); 83 }); 84 85 // é®çå¯¼èªæ¯æ 86 this.element.addEventListener('keydown', (e) => { 87 this.handleKeydown(e); 88 }); 89 90 // è§¦æ¸æ¯æ 91 this.setupTouchEvents(); 92 } 93 94 // èæµå¤çæ»å¨äºä»¶ 95 throttledHandleScroll() { 96 const currentTime = Date.now(); 97 98 // 妿è·ç¦»ä¸æ¬¡æ§è¡æ¶é´å°äºèæµé´éï¼åè·³è¿ 99 if (currentTime - this.lastScrollTime < this.scrollThrottle) { 100 // åæ¶ä¹åçå¨ç»å¸§ 101 if (this.rafId) { 102 cancelAnimationFrame(this.rafId); 103 } 104 105 // 宿ä¸ä¸å¸§æ§è¡ 106 this.rafId = requestAnimationFrame(() => { 107 this.handleScroll(); 108 this.lastScrollTime = Date.now(); 109 }); 110 return; 111 } 112 113 // ç«å³æ§è¡ 114 this.handleScroll(); 115 this.lastScrollTime = currentTime; 116 } 117 118 handleScroll() { 119 // 使ç¨requestAnimationFrameç¡®ä¿å¨ä¸ä¸å¸§æ§è¡ 120 if (this.rafId) { 121 cancelAnimationFrame(this.rafId); 122 } 123 124 this.rafId = requestAnimationFrame(() => { 125 this.performScrollCalculations(); 126 }); 127 } 128 129 performScrollCalculations() { 130 // ç¼åDOMæ¥è¯¢ç»æ 131 const scrollTop = this.contentArea.scrollTop; 132 const containerHeight = this.contentArea.offsetHeight; 133 const scrollHeight = this.contentArea.scrollHeight; 134 const centerPoint = scrollTop + containerHeight / 2; 135 136 let activeIndex = 0; 137 let minDistance = Infinity; 138 139 // æ£æ¥æ¯å¦æ»å¨å°åºé¨ï¼å¦ææ¯åæ¿æ´»æåä¸ä¸ªé¡¹ç® 140 const isAtBottom = scrollTop + containerHeight >= scrollHeight - 10; // 10px容差 141 142 if (isAtBottom && this.items.length > 0) { 143 activeIndex = this.items.length - 1; 144 } else { 145 // 使ç¨ç¼åçä½ç½®ä¿¡æ¯ï¼é¿å é夿¥è¯¢DOM 146 for (let index = 0; index < this.items.length; index++) { 147 const item = this.items[index]; 148 const itemTop = item.offsetTop; 149 const itemHeight = item.offsetHeight; 150 const itemCenter = itemTop + itemHeight / 2; 151 const distance = Math.abs(centerPoint - itemCenter); 152 153 if (distance < minDistance) { 154 minDistance = distance; 155 activeIndex = index; 156 } 157 } 158 } 159 160 // åªå¨æ¿æ´»é¡¹ç®çæ£æ¹åæ¶ææ´æ°DOM 161 if (activeIndex !== this.currentActiveIndex) { 162 this.updateActiveItem(activeIndex); 163 } 164 165 // 使ç¨èæµæ´æ°æ¸éææ 166 this.throttledUpdateFadeEffect(scrollTop, containerHeight, scrollHeight); 167 } 168 169 // èæµæ´æ°æ¸éææ 170 throttledUpdateFadeEffect(scrollTop, containerHeight, scrollHeight) { 171 if (!this.fadeBottom) return; 172 173 // 计ç®åºé¨æ¸ééæåº¦ 174 const maxScroll = scrollHeight - containerHeight; 175 const bottomOpacity = Math.min(1, (maxScroll - scrollTop) / this.maskHeight); 176 177 // 使ç¨CSSåéé¿å é¢ç¹çæ ·å¼è®¡ç® 178 this.fadeBottom.style.setProperty('--dynamic-opacity', bottomOpacity); 179 } 180 181 // åå§åä¸çªå£ååæ¶æ´æ°åºé¨æ¸éææ 182 updateFadeEffect() { 183 if (!this.contentArea) return; 184 const scrollTop = this.contentArea.scrollTop; 185 const containerHeight = this.contentArea.offsetHeight; 186 const scrollHeight = this.contentArea.scrollHeight; 187 this.throttledUpdateFadeEffect(scrollTop, containerHeight, scrollHeight); 188 } 189 190 updateActiveItem(index) { 191 // ç§»é¤æææ¿æ´»ç¶æ
192 this.items.forEach(item => item.classList.remove('active')); 193 this.images.forEach(image => image.classList.remove('active')); 194 195 // æ·»å æ°çæ¿æ´»ç¶æ 196 if (this.items[index]) { 197 this.items[index].classList.add('active'); 198 } 199 if (this.images[index]) { 200 this.images[index].classList.add('active'); 201 } 202 203 this.currentActiveIndex = index; 204 205 // 触åèªå®ä¹äºä»¶ 206 this.element.dispatchEvent(new CustomEvent('activeItemChanged', { 207 detail: { index, item: this.items[index], image: this.images[index] } 208 })); 209 } 210 211 212 scrollToItem(index) { 213 if (index < 0 || index >= this.items.length) return; 214 215 const item = this.items[index]; 216 const containerHeight = this.contentArea.offsetHeight; 217 const scrollHeight = this.contentArea.scrollHeight; 218 const itemTop = item.offsetTop; 219 const itemHeight = item.offsetHeight; 220 221 // è®¡ç®æ»å¨ä½ç½®ï¼ä½¿é¡¹ç®å¨å®¹å¨ä¸å¿ 222 let scrollTop = itemTop - (containerHeight / 2) + (itemHeight / 2); 223 224 // å¯¹äºæåä¸ä¸ªé¡¹ç®ï¼ç¡®ä¿å¯ä»¥æ»å¨å°åºé¨ 225 if (index === this.items.length - 1) { 226 const maxScrollTop = scrollHeight - containerHeight; 227 scrollTop = Math.min(scrollTop, maxScrollTop); 228 } 229 230 this.contentArea.scrollTo({ 231 top: Math.max(0, scrollTop), 232 behavior: 'smooth' 233 }); 234 } 235 236 handleKeydown(e) { 237 switch (e.key) { 238 case 'ArrowUp': 239 e.preventDefault(); 240 this.scrollToItem(Math.max(0, this.currentActiveIndex - 1)); 241 break; 242 case 'ArrowDown': 243 e.preventDefault(); 244 this.scrollToItem(Math.min(this.items.length - 1, this.currentActiveIndex + 1)); 245 break; 246 case 'Home': 247 e.preventDefault(); 248 this.scrollToItem(0); 249 break; 250 case 'End': 251 e.preventDefault(); 252 this.scrollToItem(this.items.length - 1); 253 break; 254 } 255 } 256 257 setupTouchEvents() { 258 let startY = 0; 259 let startTime = 0; 260 261 // 鲿¢å¨iOS䏿»å¨å¡å¨å®¹å¨é¡¶é¨/åºé¨ 262 this.contentArea.addEventListener('touchstart', () => { 263 const el = this.contentArea; 264 if (!el) return; 265 if (el.scrollTop <= 0) { 266 el.scrollTop = 1; // è½»æ¨ä¸ä¸ï¼é¿å å¡å¨é¡¶é¨ 267 } else if (el.scrollTop + el.offsetHeight >= el.scrollHeight) { 268 el.scrollTop -= 1; // è½»æ¨ä¸ä¸ï¼é¿å å¡å¨åºé¨ 269 } 270 }, { passive: true }); 271 272 this.contentArea.addEventListener('touchstart', (e) => { 273 startY = e.touches[0].clientY; 274 startTime = Date.now(); 275 }, { passive: true }); 276 277 this.contentArea.addEventListener('touchend', (e) => { 278 const endY = e.changedTouches[0].clientY; 279 const endTime = Date.now(); 280 const deltaY = startY - endY; 281 const deltaTime = endTime - startTime; 282 283 // æ£æµå¿«éæ»å¨ 284 if (Math.abs(deltaY) > 50 && deltaTime < 300) { 285 const direction = deltaY > 0 ? 1 : -1; 286 const targetIndex = Math.max(0, Math.min(this.items.length - 1, 287 this.currentActiveIndex + direction)); 288 this.scrollToItem(targetIndex); 289 } 290 }, { passive: true }); 291 } 292 293 handleResize() { 294 // éæ°è®¾ç½®å¸å± 295 this.setupLayout(); 296 297 // éæ°è®¡ç®æ¿æ´»é¡¹ç® 298 setTimeout(() => { 299 this.handleScroll(); 300 }, 100); 301 } 302 303 // å ¬å ±æ¹æ³ï¼è·åå½åæ¿æ´»é¡¹ç® 304 getCurrentActiveIndex() { 305 return this.currentActiveIndex; 306 } 307 308 // å ¬å ±æ¹æ³ï¼è·åå½åæ¿æ´»é¡¹ç®å ç´ 309 getCurrentActiveItem() { 310 return this.items[this.currentActiveIndex]; 311 } 312 313 // å ¬å ±æ¹æ³ï¼è·åå½åæ¿æ´»å¾ç 314 getCurrentActiveImage() { 315 return this.images[this.currentActiveIndex]; 316 } 317 318 // å ¬å ±æ¹æ³ï¼æ»å¨å°æå®é¡¹ç® 319 goToItem(index) { 320 this.scrollToItem(index); 321 } 322 323 // å ¬å ±æ¹æ³ï¼ä¸ä¸ä¸ªé¡¹ç® 324 nextItem() { 325 this.scrollToItem(Math.min(this.items.length - 1, this.currentActiveIndex + 1)); 326 } 327 328 // å ¬å ±æ¹æ³ï¼ä¸ä¸ä¸ªé¡¹ç® 329 prevItem() { 330 this.scrollToItem(Math.max(0, this.currentActiveIndex - 1)); 331 } 332 333 // å ¬å ±æ¹æ³ï¼å·æ° 334 refresh() { 335 this.images = this.element.querySelectorAll('.mygica-scroll-image'); 336 this.items = this.element.querySelectorAll('.mygica-scroll-item'); 337 this.setupLayout(); 338 this.updateActiveItem(0); 339 } 340 341 // å ¬å ±æ¹æ³ï¼éæ¯ 342 destroy() { 343 // åæ¶å¨ç»å¸§ 344 if (this.rafId) { 345 cancelAnimationFrame(this.rafId); 346 this.rafId = null; 347 } 348 349 // æ¸ é¤è¶ æ¶ 350 if (this.scrollTimeout) { 351 clearTimeout(this.scrollTimeout); 352 this.scrollTimeout = null; 353 } 354 355 // ç§»é¤äºä»¶çå¬å¨ 356 this.contentArea.removeEventListener('scroll', this.throttledHandleScroll); 357 window.removeEventListener('resize', this.handleResize); 358 this.element.removeEventListener('keydown', this.handleKeydown); 359 360 // æ¸ çå¼ç¨ 361 this.element = null; 362 this.imageContainer = null; 363 this.contentContainer = null; 364 this.contentArea = null; 365 this.images = null; 366 this.items = null; 367 this.fadeBottom = null; 368 } 369 } 370 371 // åå§åæææ»å¨ææç»ä»¶ 372 function initScrollEffects() { 373 const scrollEffects = document.querySelectorAll('.mygica-scroll-effect-container');
374 scrollEffects.forEach(scrollEffect => { 375 if (!scrollEffect.gentiatechScrollEffect) { 376 scrollEffect.gentiatechScrollEffect = new GentiatechScrollEffect(scrollEffect); 377 } 378 }); 379 } 380 381 // DOMå è½½å®æååå§å 382 if (document.readyState === 'loading') { 383 document.addEventListener('DOMContentLoaded', initScrollEffects); 384 } else { 385 initScrollEffects(); 386 } 387 388 // 页é¢å®å ¨å è½½åä¹åå§å䏿¬¡ 389 if (document.readyState === 'complete') { 390 initScrollEffects(); 391 } else { 392 window.addEventListener('load', initScrollEffects); 393 } 394 395 // Elementorå端åå§å 396 if (typeof elementorFrontend !== 'undefined') { 397 elementorFrontend.hooks.addAction('frontend/element_ready/mygica_scroll_effect.default', function($scope) { 398 const scrollEffect = $scope[0].querySelector('.mygica-scroll-effect-container'); 399 if (scrollEffect) { 400 // 鿝æ§å®ä¾ 401 if (scrollEffect.gentiatechScrollEffect) { 402 scrollEffect.gentiatechScrollEffect.destroy(); 403 } 404 // å建æ°å®ä¾ 405 scrollEffect.gentiatechScrollEffect = new GentiatechScrollEffect(scrollEffect); 406 } 407 }); 408 } 409 410 // Elementorç¼è¾å¨åå§å 411 if (typeof elementor !== 'undefined') { 412 elementor.hooks.addAction('panel/open_editor/widget/mygica_scroll_effect', function(panel, model, view) { 413 // ç¼è¾å¨æå¼æ¶å·æ°æ»å¨ææ 414 setTimeout(() => { 415 const scrollEffect = view.$el[0].querySelector('.mygica-scroll-effect-container'); 416 if (scrollEffect && scrollEffect.gentiatechScrollEffect) { 417 scrollEffect.gentiatechScrollEffect.refresh(); 418 } 419 }, 100); 420 }); 421 } 422 423 // å ¨å±æ´é²ç±»ï¼ç¨äºè°è¯åæ©å±ï¼ 424 window.GentiatechScrollEffect = GentiatechScrollEffect; 425 426 // å·¥å ·å½æ°ï¼è·åæ»å¨ææå®ä¾ 427 window.getScrollEffectInstance = function(element) { 428 const container = element.closest('.mygica-scroll-effect-container'); 429 return container ? container.gentiatechScrollEffect : null; 430 }; 431 432 // å·¥å ·å½æ°ï¼æ¹éåå§å 433 window.initAllScrollEffects = initScrollEffects; 434 435})();
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.