PageSourceSearch

https://www.stadttheater-aschaffenburg.de/wp-content/plugins/lu-sm…/assets/lu-smooth-scroll.js?ver=1.26.04.12

js stadttheater-aschaffenburg.de collected 2026-10-04 13:13:37 UTC 8,640 bytes, 287 lines download raw bytes

1/**
2 * LU Smooth Scroll
3 * High-performance, vanilla JavaScript smooth scrolling with requestAnimationFrame.
4 */
5const luSmoothScroll = (function () {
6    let settings = {
7        duration: 1200,
8        easing: 'easeOutQuint'
9    };
10
11    if (typeof luSmoothScrollSettings !== 'undefined') {
12        settings.duration = parseFloat(luSmoothScrollSettings.duration) * 1000;
13        settings.easing = luSmoothScrollSettings.easing || 'easeOutQuint';
14    }
15
16    const easings = {
17        linear: t => t,
18        easeInQuad: t => t * t,
19        easeOutQuad: t => t * (2 - t),
20        easeInOutQuad: t => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
21        easeInCubic: t => t * t * t,
22        easeOutCubic: t => (--t) * t * t + 1,
23        easeInOutCubic: t => t < .5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
24        easeInQuart: t => t * t * t * t,
25        easeOutQuart: t => 1 - (--t) * t * t * t,
26        easeInOutQuart: t => t < .5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
27        easeInQuint: t => t * t * t * t * t,
28        easeOutQuint: t => 1 + (--t) * t * t * t * t,
29        easeInOutQuint: t => t < .5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t
30    };
31
32    let baseY = 0;
33    let animations = [];
34    let isAnimating = false;
35    let animationFrameId = null;
36    let lastRenderedY = -1;
37
38    /**
39     * Checks if the user is on a mobile device or touch environment.
40     * @returns {boolean} True if mobile, false otherwise.
41     */
42    function isMobile() {
43        return window.innerWidth < 768 || ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
44    }
45
46    /**
47     * Determines if a DOM element or its ancestors have their own scrollbar.
48     * @param {HTMLElement} node The target node.
49     * @returns {boolean} True if a scrollable parent exists.
50     */
51    function hasScrollableParent(node) {
52        let parent = node;
53        while (parent && parent !== document.body && parent !== document.documentElement) {
54            const style = window.getComputedStyle(parent);
55            if (['auto', 'scroll'].includes(style.overflowY)) {
56                if (parent.scrollHeight > parent.clientHeight) {
57                    return true;
58                }
59            }
60            parent = parent.parentNode;
61        }
62        return false;
63    }
64
65    /**
66     * The main animation loop executed via requestAnimationFrame.
67     * @param {number} timestamp The current time elapsed.
68     */
69    function scrollLoop(timestamp) {
70        if (Math.abs(window.scrollY - lastRenderedY) > 5) {
71            isAnimating = false;
72            animationFrameId = null;
73            animations = [];
74            baseY = window.scrollY;
75            return;
76        }
77
78        let totalOffset = 0;
79        let stillAnimating = [];
80
81        for (let i = 0; i < animations.length; i++) {
82            let anim = animations[i];
83            if (!anim.startTime) {
84                anim.startTime = timestamp;
85            }
86
87            const elapsed = timestamp - anim.startTime;
88            let progress = Math.min(elapsed / settings.duration, 1);
89
90            if (progress < 1) {
91                let easingFunc = easings[settings.easing] || easings['easeOutQuint'];
92                const easedProgress = easingFunc(progress);
93                totalOffset += anim.delta * easedProgress;
94                stillAnimating.push(anim);
95            } else {
96                baseY += anim.delta;
97            }
98        }
99
100        animations = stillAnimating;
101
102        let nextY = baseY + totalOffset;
103
104        const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
105        nextY = Math.max(0, Math.min(nextY, maxScroll));
106
107        window.scrollTo(0, nextY);
108
109        lastRenderedY = window.scrollY;
110
111        if (animations.length > 0) {
112            animationFrameId = requestAnimationFrame(scrollLoop);
113        } else {
114            isAnimating = false;
115            animationFrameId = null;
116            baseY = window.scrollY;
117        }
118    }
119
120    /**
121     * Updates the target scroll destination.
122     * @param {number} deltaY The scroll offset.
123     * @returns {boolean} True if the target was updated and animation triggered.
124     */
125    function updateTarget(deltaY) {
126        if (isMobile()) return false;
127
128        const maxScroll = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
129
130        if (!isAnimating) {
131            baseY = window.scrollY;
132            lastRenderedY = window.scrollY;
133            animations = [];
134        }
135
136        let currentTarget = baseY;
137        for (let i = 0; i < animations.length; i++) {
138            currentTarget += animations[i].delta;
139        }
140
141        let newTarget = currentTarget + deltaY;
142        newTarget = Math.max(0, Math.min(newTarget, maxScroll));
143
144        let clampedDelta = newTarget - currentTarget;
145
146        if (clampedDelta !== 0) {
147            animations.push({
148                delta: clampedDelta,
149                startTime: null
150            });
151
152            if (!isAnimating) {
153                isAnimating = true;
154                animationFrameId = requestAnimationFrame(scrollLoop);
155            }
156            return true;
157        }
158        return false;
159    }
160
161    /**
162     * Handles the native wheel event.
163     * @param {WheelEvent} e The triggered event.
164     */
165    function onWheel(e) {
166        if (isMobile()) return;
167
168        if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
169
170        if (hasScrollableParent(e.target)) return;
171
172        e.preventDefault();
173
174        let deltaY = e.deltaY;
175        if (e.deltaMode === 1) deltaY *= 40;
176        else if (e.deltaMode === 2) deltaY *= window.innerHeight;
177
178        updateTarget(deltaY);
179    }
180
181    /**
182     * Handles keyboard events for page navigation.
183     * @param {KeyboardEvent} e The triggered event.
184     */
185    function onKeyDown(e) {
186        if (isMobile()) return;
187
188        const activeEl = document.activeElement;
189        if (activeEl && (['INPUT', 'TEXTAREA', 'SELECT'].includes(activeEl.tagName) || activeEl.isContentEditable)) {
190            return;
191        }
192
193        let deltaY = 0;
194        const vh = window.innerHeight;
195
196        switch (e.keyCode) {
197            case 33:
198                deltaY = -vh + 40;
199                break;
200            case 34:
201                deltaY = vh - 40;
202                break;
203            case 38:
204                deltaY = -60;
205                break;
206            case 40:
207                deltaY = 60;
208                break;
209            case 32:
210                deltaY = e.shiftKey ? -vh + 40 : vh - 40;
211                break;
212            default:
213                return;
214        }
215
216        if (updateTarget(deltaY)) {
217            e.preventDefault();
218        }
219    }
220
221    /**
222     * Instantly aborts the currently running scroll animation.
223     */
224    function stopAnimation() {
225        if (isAnimating) {
226            cancelAnimationFrame(animationFrameId);
227            isAnimating = false;
228            animationFrameId = null;
229            animations = [];
230            baseY = window.scrollY;
231            lastRenderedY = window.scrollY;
232        }
233    }
234
235    /**
236     * Cancels the animation if an anchor link is clicked.
237     * @param {MouseEvent} e The triggered event.
238     */
239    function onHashClick(e) {
240        const target = e.target.closest('a');
241        if (target && target.hash && target.hash.startsWith('#')) {
242            stopAnimation();
243            setTimeout(() => {
244                baseY = window.scrollY;
245                lastRenderedY = window.scrollY;
246            }, 50);
247        }
248    }
249
250    /**
251     * Syncs the target position upon passive native scroll.
252     */
253    function onPassiveScroll() {
254        if (!isAnimating) {
255            baseY = window.scrollY;
256        }
257    }
258
259    /**
260     * Initializes the smooth scroll functionality by adding event listeners.
261     */
262    function init() {
263        if (isMobile()) return;
264
265        baseY = window.scrollY;
266
267        window.addEventListener('wheel', onWheel, { passive: false });
268        window.addEventListener('keydown', onKeyDown, { passive: false });
269
270        document.addEventListener('click', onHashClick);
271        window.addEventListener('scroll', onPassiveScroll, { passive: true });
272        window.addEventListener('resize', onPassiveScroll, { passive: true });
273
274        window.addEventListener('mousedown', stopAnimation);
275        window.addEventListener('touchstart', stopAnimation, { passive: true });
276    }
277
278    return {
279        init: init
280    };
281})();
282
283if (document.readyState === 'loading') {
284    document.addEventListener('DOMContentLoaded', luSmoothScroll.init);
285} else {
286    luSmoothScroll.init();
287}

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.