PageSourceSearch

https://www.mygica.com/wp-content/themes/Mygica/elementor-widgets/…/scroll-effect-widget.js?ver=1.0.0

js mygica.com collected 2026-10-03 22:29:58 UTC 16,068 bytes, 435 lines download raw bytes

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.