PageSourceSearch

https://www.comb.cat/js/page-scroll.js

js comb.cat collected 2026-09-24 09:29:45 UTC 26,186 bytes, 772 lines download raw bytes

1//
2// SmoothScroll for websites v1.4.9 (Balazs Galambosi)
3// http://www.smoothscroll.net/
4//
5// Licensed under the terms of the MIT license.
6//
7// You may use it in your theme if you credit me.
8// It is also free to use on any individual website.
9//
10// Exception:
11// The only restriction is to not publish any
12// extension for browsers or native application
13// without getting a written permission first.
14//
15
16(function () {
17    // Scroll Variables (tweakable)
18    var defaultOptions = {
19        // Scrolling Core
20        frameRate: 150, // [Hz]
21        animationTime: 400, // [ms]
22        stepSize: 100, // [px]
23
24        // Pulse (less tweakable)
25        // ratio of "tail" to "acceleration"
26        pulseAlgorithm: true,
27        pulseScale: 4,
28        pulseNormalize: 1,
29
30        // Acceleration
31        accelerationDelta: 50,  // 50
32        accelerationMax: 3,   // 3
33
34        // Keyboard Settings
35        keyboardSupport: true,  // option
36        arrowScroll: 50,    // [px]
37
38        // Other
39        fixedBackground: true,
40        excluded: ''
41    };
42
43    var options = defaultOptions;
44
45    // Other Variables
46    var isExcluded = false;
47    var isFrame = false;
48    var direction = { x: 0, y: 0 };
49    var initDone = false;
50    var root = document.documentElement;
51    var activeElement;
52    var observer;
53    var refreshSize;
54    var deltaBuffer = [];
55    var deltaBufferTimer;
56    var isMac = /^Mac/.test(navigator.platform);
57
58    var key = {
59        left: 37, up: 38, right: 39, down: 40, spacebar: 32,
60        pageup: 33, pagedown: 34, end: 35, home: 36
61    };
62    var arrowKeys = { 37: 1, 38: 1, 39: 1, 40: 1 };
63
64    /***********************************************
65     * INITIALIZE
66     ***********************************************/
67
68    /**
69     * Tests if smooth scrolling is allowed. Shuts down everything if not.
70     */
71    function initTest() {
72        if (options.keyboardSupport) {
73            addEvent('keydown', keydown);
74        }
75    }
76
77    /**
78     * Sets up scrolls array, determines if frames are involved.
79     */
80    function init() {
81        if (initDone || !document.body) return;
82
83        initDone = true;
84
85        var body = document.body;
86        var html = document.documentElement;
87        var windowHeight = window.innerHeight;
88        var scrollHeight = body.scrollHeight;
89
90        // check compat mode for root element
91        root = (document.compatMode.indexOf('css') >= 0) ? html : body;
92        activeElement = body;
93
94        initTest();
95
96        // Checks if this script is running in a frame
97        if (top != self) {
98            isFrame = true;
99        }
100
101        /**
102         * Safari 10 fixed it, Chrome fixed it in v45:
103         * This fixes a bug where the areas left and right to
104         * the content does not trigger the onmousewheel event
105         * on some pages. e.g.: html, body { height: 100% }
106         */
107        else if (isOldSafari &&
108            scrollHeight > windowHeight &&
109            (body.offsetHeight <= windowHeight ||
110                html.offsetHeight <= windowHeight)) {
111            var fullPageElem = document.createElement('div');
112            fullPageElem.style.cssText = 'position:absolute; z-index:-10000; ' +
113                'top:0; left:0; right:0; height:' +
114                root.scrollHeight + 'px';
115            document.body.appendChild(fullPageElem);
116
117            // DOM changed (throttled) to fix height
118            var pendingRefresh;
119            refreshSize = function () {
120                if (pendingRefresh) return; // could also be: clearTimeout(pendingRefresh);
121                pendingRefresh = setTimeout(function () {
122                    if (isExcluded) return; // could be running after cleanup
123                    fullPageElem.style.height = '0';
124                    fullPageElem.style.height = root.scrollHeight + 'px';
125                    pendingRefresh = null;
126                }, 500); // act rarely to stay fast
127            };
128
129            setTimeout(refreshSize, 10);
130
131            addEvent('resize', refreshSize);
132
133            // TODO: attributeFilter?
134            var config = {
135                attributes: true,
136                childList: true,
137                characterData: false
138                // subtree: true
139            };
140
141            observer = new MutationObserver(refreshSize);
142            observer.observe(body, config);
143
144            if (root.offsetHeight <= windowHeight) {
145                var clearfix = document.createElement('div');
146                clearfix.style.clear = 'both';
147                body.appendChild(clearfix);
148            }
149        }
150
151        // disable fixed background
152        if (!options.fixedBackground && !isExcluded) {
153            body.style.backgroundAttachment = 'scroll';
154            html.style.backgroundAttachment = 'scroll';
155        }
156    }
157
158    /**
159     * Removes event listeners and other traces left on the page.
160     */
161    function cleanup() {
162        observer && observer.disconnect();
163        removeEvent(wheelEvent, wheel);
164        removeEvent('mousedown', mousedown);
165        removeEvent('keydown', keydown);
166        removeEvent('resize', refreshSize);
167        removeEvent('load', init);
168    }
169
170    /************************************************
171     * SCROLLING
172     ************************************************/
173
174    var que = [];
175    var pending = false;
176    var lastScroll = Date.now();
177
178    /**
179     * Pushes scroll actions to the scrolling queue.
180     */
181    function scrollArray(elem, left, top) {
182        directionCheck(left, top);
183
184        if (options.accelerationMax != 1) {
185            var now = Date.now();
186            var elapsed = now - lastScroll;
187            if (elapsed < options.accelerationDelta) {
188                var factor = (1 + (50 / elapsed)) / 2;
189                if (factor > 1) {
190                    factor = Math.min(factor, options.accelerationMax);
191                    left *= factor;
192                    top *= factor;
193                }
194            }
195            lastScroll = Date.now();
196        }
197
198        // push a scroll command
199        que.push({
200            x: left,
201            y: top,
202            lastX: (left < 0) ? 0.99 : -0.99,
203            lastY: (top < 0) ? 0.99 : -0.99,
204            start: Date.now()
205        });
206
207        // don't act if there's a pending queue
208        if (pending) {
209            return;
210        }
211
212        var scrollRoot = getScrollRoot();
213        var isWindowScroll = (elem === scrollRoot || elem === document.body);
214
215        // if we haven't already fixed the behavior,
216        // and it needs fixing for this sesh
217        if (elem.$scrollBehavior == null && isScrollBehaviorSmooth(elem)) {
218            elem.$scrollBehavior = elem.style.scrollBehavior;
219            elem.style.scrollBehavior = 'auto';
220        }
221
222        var step = function (time) {
223            var now = Date.now();
224            var scrollX = 0;
225            var scrollY = 0;
226
227            for (var i = 0; i < que.length; i++) {
228                var item = que[i];
229                var elapsed = now - item.start;
230                var finished = (elapsed >= options.animationTime);
231
232                // scroll position: [0, 1]
233                var position = (finished) ? 1 : elapsed / options.animationTime;
234
235                // easing [optional]
236                if (options.pulseAlgorithm) {
237                    position = pulse(position);
238                }
239
240                // only need the difference
241                var x = (item.x * position - item.lastX) >> 0;
242                var y = (item.y * position - item.lastY) >> 0;
243
244                // add this to the total scrolling
245                scrollX += x;
246                scrollY += y;
247
248                // update last values
249                item.lastX += x;
250                item.lastY += y;
251
252                // delete and step back if it's over
253                if (finished) {
254                    que.splice(i, 1); i--;
255                }
256            }
257
258            // scroll left and top
259            if (isWindowScroll) {
260                window.scrollBy(scrollX, scrollY);
261            }
262            else {
263                if (scrollX) elem.scrollLeft += scrollX;
264                if (scrollY) elem.scrollTop += scrollY;
265            }
266
267            // clean up if there's nothing left to do
268            if (!left && !top) {
269                que = [];
270            }
271
272            if (que.length) {
273                requestFrame(step, elem, (1000 / options.frameRate + 1));
274            } else {
275                pending = false;
276                // restore default behavior at the end of scrolling sesh
277                if (elem.$scrollBehavior != null) {
278                    elem.style.scrollBehavior = elem.$scrollBehavior;
279                    elem.$scrollBehavior = null;
280                }
281            }
282        };
283
284        // start a new queue of actions
285        requestFrame(step, elem, 0);
286        pending = true;
287    }
288
289    /***********************************************
290     * EVENTS
291     ***********************************************/
292
293    /**
294     * Mouse wheel handler.
295     * @param {Object} event
296     */
297    function wheel(event) {
298        if (!initDone) {
299            init();
300        }
301
302        var target = event.target;
303
304        // leave early if default action is prevented
305        // or it's a zooming event with CTRL
306        if (event.defaultPrevented || event.ctrlKey) {
307            return true;
308        }
309
310        // leave embedded content alone (flash & pdf)
311        if (isNodeName(activeElement, 'embed') ||
312            (isNodeName(target, 'embed') && /\.pdf/i.test(target.src)) ||
313            isNodeName(activeElement, 'object') ||
314            target.shadowRoot) {
315            return true;
316        }
317
318        var deltaX = -event.wheelDeltaX || event.deltaX || 0;
319        var deltaY = -event.wheelDeltaY || event.deltaY || 0;
320
321        if (isMac) {
322            if (event.wheelDeltaX && isDivisible(event.wheelDeltaX, 120)) {
323                deltaX = -120 * (event.wheelDeltaX / Math.abs(event.wheelDeltaX));
324            }
325            if (event.wheelDeltaY && isDivisible(event.wheelDeltaY, 120)) {
326                deltaY = -120 * (event.wheelDeltaY / Math.abs(event.wheelDeltaY));
327            }
328        }
329
330        // use wheelDelta if deltaX/Y is not available
331        if (!deltaX && !deltaY) {
332            deltaY = -event.wheelDelta || 0;
333        }
334
335        // line based scrolling (Firefox mostly)
336        if (event.deltaMode === 1) {
337            deltaX *= 40;
338            deltaY *= 40;
339        }
340
341        var overflowing = overflowingAncestor(target);
342
343        // nothing to do if there's no element that's scrollable
344        if (!overflowing) {
345            // except Chrome iframes seem to eat wheel events, which we need to
346            // propagate up, if the iframe has nothing overflowing to scroll
347            if (isFrame && isChrome) {
348                // change target to iframe element itself for the parent frame
349                Object.defineProperty(event, "target", { value: window.frameElement });
350                return parent.wheel(event);
351            }
352            return true;
353        }
354
355        // check if it's a touchpad scroll that should be ignored
356        if (isTouchpad(deltaY)) {
357            return true;
358        }
359
360        // scale by step size
361        // delta is 120 most of the time
362        // synaptics seems to send 1 sometimes
363        if (Math.abs(deltaX) > 1.2) {
364            deltaX *= options.stepSize / 120;
365        }
366        if (Math.abs(deltaY) > 1.2) {
367            deltaY *= options.stepSize / 120;
368        }
369
370        scrollArray(overflowing, deltaX, deltaY);
371        event.preventDefault();
372        scheduleClearCache();
373    }
374
375    /**
376     * Keydown event handler.
377     * @param {Object} event
378     */
379    function keydown(event) {
380        var target = event.target;
381        var modifier = event.ctrlKey || event.altKey || event.metaKey ||
382            (event.shiftKey && event.keyCode !== key.spacebar);
383
384        // our own tracked active element could've been removed from the DOM
385        if (!document.body.contains(activeElement)) {
386            activeElement = document.activeElement;
387        }
388
389        // do nothing if user is editing text
390        // or using a modifier key (except shift)
391        // or in a dropdown
392        // or inside interactive elements
393        var inputNodeNames = /^(textarea|select|embed|object)$/i;
394        var buttonTypes = /^(button|submit|radio|checkbox|file|color|image)$/i;
395        if (event.defaultPrevented ||
396            inputNodeNames.test(target.nodeName) ||
397            isNodeName(target, 'input') && !buttonTypes.test(target.type) ||
398            isNodeName(activeElement, 'video') ||
399            isInsideYoutubeVideo(event) ||
400            target.isContentEditable ||
401            modifier) {
402            return true;
403        }
404
405        // [spacebar] should trigger button press, leave it alone
406        if ((isNodeName(target, 'button') ||
407            isNodeName(target, 'input') && buttonTypes.test(target.type)) &&
408            event.keyCode === key.spacebar) {
409            return true;
410        }
411
412        // [arrwow keys] on radio buttons should be left alone
413        if (isNodeName(target, 'input') && target.type == 'radio' &&
414            arrowKeys[event.keyCode]) {
415            return true;
416        }
417
418        var shift, x = 0, y = 0;
419        var overflowing = overflowingAncestor(activeElement);
420
421        if (!overflowing) {
422            // Chrome iframes seem to eat key events, which we need to
423            // propagate up, if the iframe has nothing overflowing to scroll
424            return (isFrame && isChrome) ? parent.keydown(event) : true;
425        }
426
427        var clientHeight = overflowing.clientHeight;
428
429        if (overflowing == document.body) {
430            clientHeight = window.innerHeight;
431        }
432
433        switch (event.keyCode) {
434            case key.up:
435                y = -options.arrowScroll;
436                break;
437            case key.down:
438                y = options.arrowScroll;
439                break;
440            case key.spacebar: // (+ shift)
441                shift = event.shiftKey ? 1 : -1;
442                y = -shift * clientHeight * 0.9;
443                break;
444            case key.pageup:
445                y = -clientHeight * 0.9;
446                break;
447            case key.pagedown:
448                y = clientHeight * 0.9;
449                break;
450            case key.home:
451                if (overflowing == document.body && document.scrollingElement)
452                    overflowing = document.scrollingElement;
453                y = -overflowing.scrollTop;
454                break;
455            case key.end:
456                var scroll = overflowing.scrollHeight - overflowing.scrollTop;
457                var scrollRemaining = scroll - clientHeight;
458                y = (scrollRemaining > 0) ? scrollRemaining + 10 : 0;
459                break;
460            case key.left:
461                x = -options.arrowScroll;
462                break;
463            case key.right:
464                x = options.arrowScroll;
465                break;
466            default:
467                return true; // a key we don't care about
468        }
469
470        scrollArray(overflowing, x, y);
471        event.preventDefault();
472        scheduleClearCache();
473    }
474
475    /**
476     * Mousedown event only for updating activeElement
477     */
478    function mousedown(event) {
479        activeElement = event.target;
480    }
481
482    /***********************************************
483     * OVERFLOW
484     ***********************************************/
485
486    var uniqueID = (function () {
487        var i = 0;
488        return function (el) {
489            return el.uniqueID || (el.uniqueID = i++);
490        };
491    })();
492
493    var cacheX = {}; // cleared out after a scrolling session
494    var cacheY = {}; // cleared out after a scrolling session
495    var clearCacheTimer;
496    var smoothBehaviorForElement = {};
497
498    //setInterval(function () { cache = {}; }, 10 * 1000);
499
500    function scheduleClearCache() {
501        clearTimeout(clearCacheTimer);
502        clearCacheTimer = setInterval(function () {
503            cacheX = cacheY = smoothBehaviorForElement = {};
504        }, 1 * 1000);
505    }
506
507    function setCache(elems, overflowing, x) {
508        var cache = x ? cacheX : cacheY;
509        for (var i = elems.length; i--;)
510            cache[uniqueID(elems[i])] = overflowing;
511        return overflowing;
512    }
513
514    function getCache(el, x) {
515        return (x ? cacheX : cacheY)[uniqueID(el)];
516    }
517
518    //  (body)                (root)
519    //         | hidden | visible | scroll |  auto  |
520    // hidden  |   no   |    no   |   YES  |   YES  |
521    // visible |   no   |   YES   |   YES  |   YES  |
522    // scroll  |   no   |   YES   |   YES  |   YES  |
523    // auto    |   no   |   YES   |   YES  |   YES  |
524
525    function overflowingAncestor(el) {
526        var elems = [];
527        var body = document.body;
528        var rootScrollHeight = root.scrollHeight;
529        do {
530            var cached = getCache(el, false);
531            if (cached) {
532                return setCache(elems, cached);
533            }
534            elems.push(el);
535            if (rootScrollHeight === el.scrollHeight) {
536                var topOverflowsNotHidden = overflowNotHidden(root) && overflowNotHidden(body);
537                var isOverflowcss = topOverflowsNotHidden || overflowAutoOrScroll(root);
538                if (isFrame && isContentOverflowing(root) ||
539                    !isFrame && isOverflowcss) {
540                    return setCache(elems, getScrollRoot());
541                }
542            } else if (isContentOverflowing(el) && overflowAutoOrScroll(el)) {
543                return setCache(elems, el);
544            }
545        } while ((el = el.parentElement));
546    }
547
548    function isContentOverflowing(el) {
549        return (el.clientHeight + 10 < el.scrollHeight);
550    }
551
552    // typically for <body> and <html>
553    function overflowNotHidden(el) {
554        var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
555        return (overflow !== 'hidden');
556    }
557
558    // for all other elements
559    function overflowAutoOrScroll(el) {
560        var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
561        return (overflow === 'scroll' || overflow === 'auto');
562    }
563
564    // for all other elements
565    function isScrollBehaviorSmooth(el) {
566        var id = uniqueID(el);
567        if (smoothBehaviorForElement[id] == null) {
568            var scrollBehavior = getComputedStyle(el, '')['scroll-behavior'];
569            smoothBehaviorForElement[id] = ('smooth' == scrollBehavior);
570        }
571        return smoothBehaviorForElement[id];
572    }
573
574    /***********************************************
575     * HELPERS
576     ***********************************************/
577
578    function addEvent(type, fn, arg) {
579        window.addEventListener(type, fn, arg || false);
580    }
581
582    function removeEvent(type, fn, arg) {
583        window.removeEventListener(type, fn, arg || false);
584    }
585
586    function isNodeName(el, tag) {
587        return el && (el.nodeName || '').toLowerCase() === tag.toLowerCase();
588    }
589
590    function directionCheck(x, y) {
591        x = (x > 0) ? 1 : -1;
592        y = (y > 0) ? 1 : -1;
593        if (direction.x !== x || direction.y !== y) {
594            direction.x = x;
595            direction.y = y;
596            que = [];
597            lastScroll = 0;
598        }
599    }
600
601    if (window.localStorage && localStorage.SS_deltaBuffer) {
602        try { // #46 Safari throws in private browsing for localStorage
603            deltaBuffer = localStorage.SS_deltaBuffer.split(',');
604        } catch (e) { }
605    }
606
607    function isTouchpad(deltaY) {
608        if (!deltaY) return;
609        if (!deltaBuffer.length) {
610            deltaBuffer = [deltaY, deltaY, deltaY];
611        }
612        deltaY = Math.abs(deltaY);
613        deltaBuffer.push(deltaY);
614        deltaBuffer.shift();
615        clearTimeout(deltaBufferTimer);
616        deltaBufferTimer = setTimeout(function () {
617            try { // #46 Safari throws in private browsing for localStorage
618                localStorage.SS_deltaBuffer = deltaBuffer.join(',');
619            } catch (e) { }
620        }, 1000);
621        var dpiScaledWheelDelta = deltaY > 120 && allDeltasDivisableBy(deltaY); // win64
622        return !allDeltasDivisableBy(120) && !allDeltasDivisableBy(100) && !dpiScaledWheelDelta;
623    }
624
625    function isDivisible(n, divisor) {
626        return (Math.floor(n / divisor) == n / divisor);
627    }
628
629    function allDeltasDivisableBy(divisor) {
630        return (isDivisible(deltaBuffer[0], divisor) &&
631            isDivisible(deltaBuffer[1], divisor) &&
632            isDivisible(deltaBuffer[2], divisor));
633    }
634
635    function isInsideYoutubeVideo(event) {
636        var elem = event.target;
637        var isControl = false;
638        if (document.URL.indexOf('www.youtube.com/watch') != -1) {
639            do {
640                isControl = (elem.classList &&
641                    elem.classList.contains('html5-video-controls'));
642                if (isControl) break;
643            } while ((elem = elem.parentNode));
644        }
645        return isControl;
646    }
647
648    var requestFrame = (function () {
649        return (window.requestAnimationFrame ||
650            window.webkitRequestAnimationFrame ||
651            window.mozRequestAnimationFrame ||
652            function (callback, element, delay) {
653                window.setTimeout(callback, delay || (1000 / 60));
654            });
655    })();
656
657    var MutationObserver = (window.MutationObserver ||
658        window.WebKitMutationObserver ||
659        window.MozMutationObserver);
660
661    var getScrollRoot = (function () {
662        var SCROLL_ROOT = document.scrollingElement;
663        return function () {
664            if (!SCROLL_ROOT) {
665                var dummy = document.createElement('div');
666                dummy.style.cssText = 'height:10000px;width:1px;';
667                document.body.appendChild(dummy);
668                var bodyScrollTop = document.body.scrollTop;
669                var docElScrollTop = document.documentElement.scrollTop;
670                window.scrollBy(0, 3);
671                if (document.body.scrollTop != bodyScrollTop)
672                    (SCROLL_ROOT = document.body);
673                else
674                    (SCROLL_ROOT = document.documentElement);
675                window.scrollBy(0, -3);
676                document.body.removeChild(dummy);
677            }
678            return SCROLL_ROOT;
679        };
680    })();
681
682    /***********************************************
683     * PULSE (by Michael Herf)
684     ***********************************************/
685
686    /**
687     * Viscous fluid with a pulse for part and decay for the rest.
688     * - Applies a fixed force over an interval (a damped acceleration), and
689     * - Lets the exponential bleed away the velocity over a longer interval
690     * - Michael Herf, http://stereopsis.com/stopping/
691     */
692    function pulse_(x) {
693        var val, start, expx;
694        // test
695        x = x * options.pulseScale;
696        if (x < 1) { // acceleartion
697            val = x - (1 - Math.exp(-x));
698        } else {     // tail
699            // the previous animation ended here:
700            start = Math.exp(-1);
701            // simple viscous drag
702            x -= 1;
703            expx = 1 - Math.exp(-x);
704            val = start + (expx * (1 - start));
705        }
706        return val * options.pulseNormalize;
707    }
708
709    function pulse(x) {
710        if (x >= 1) return 1;
711        if (x <= 0) return 0;
712
713        if (options.pulseNormalize == 1) {
714            options.pulseNormalize /= pulse_(1);
715        }
716        return pulse_(x);
717    }
718
719    /***********************************************
720     * FIRST RUN
721     ***********************************************/
722
723    var userAgent = window.navigator.userAgent;
724    var isEdge = /Edge/.test(userAgent); // thank you MS
725    var isChrome = /chrome/i.test(userAgent) && !isEdge;
726    var isSafari = /safari/i.test(userAgent) && !isEdge;
727    var isMobile = /mobile/i.test(userAgent);
728    var isIEWin7 = /Windows NT 6.1/i.test(userAgent) && /rv:11/i.test(userAgent);
729    var isOldSafari = isSafari && (/Version\/8/i.test(userAgent) || /Version\/9/i.test(userAgent));
730    var isEnabledForBrowser = (isChrome || isSafari || isIEWin7) && !isMobile;
731
732    var supportsPassive = false;
733    try {
734        window.addEventListener("test", null, Object.defineProperty({}, 'passive', {
735            get: function () {
736                supportsPassive = true;
737            }
738        }));
739    } catch (e) { }
740
741    var wheelOpt = supportsPassive ? { passive: false } : false;
742    var wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel' : 'mousewheel';
743
744    if (wheelEvent && isEnabledForBrowser) {
745        addEvent(wheelEvent, wheel, wheelOpt);
746        addEvent('mousedown', mousedown);
747        addEvent('load', init);
748    }
749
750    /***********************************************
751     * PUBLIC INTERFACE
752     ***********************************************/
753
754    function SmoothScroll(optionsToSet) {
755        for (var key in optionsToSet)
756            if (defaultOptions.hasOwnProperty(key))
757                options[key] = optionsToSet[key];
758    }
759    SmoothScroll.destroy = cleanup;
760
761    if (window.SmoothScrollOptions) // async API
762        SmoothScroll(window.SmoothScrollOptions);
763
764    if (typeof define === 'function' && define.amd)
765        define(function () {
766            return SmoothScroll;
767        });
768    else if ('object' == typeof exports)
769        module.exports = SmoothScroll;
770    else
771        window.SmoothScroll = SmoothScroll;
772})();

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.