PageSourceSearch

https://moxoai.com/assets/js/plugins/smooth-scroll.js

js moxoai.com collected 2026-09-24 14:16:14 UTC 23,018 bytes, 789 lines download raw bytes

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

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.