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