· 6 years ago · Oct 17, 2019, 11:40 AM
1//
2// SmoothScroll for websites v1.4.6 (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 // 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
45var options = defaultOptions;
46
47
48// Other Variables
49var isExcluded = false;
50var isFrame = false;
51var direction = { x: 0, y: 0 };
52var initDone = false;
53var root = document.documentElement;
54var activeElement;
55var observer;
56var refreshSize;
57var deltaBuffer = [];
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 scrollWindow = (elem === document.body);
217
218 var step = function (time) {
219
220 var now = Date.now();
221 var scrollX = 0;
222 var scrollY = 0;
223
224 for (var i = 0; i < que.length; i++) {
225
226 var item = que[i];
227 var elapsed = now - item.start;
228 var finished = (elapsed >= options.animationTime);
229
230 // scroll position: [0, 1]
231 var position = (finished) ? 1 : elapsed / options.animationTime;
232
233 // easing [optional]
234 if (options.pulseAlgorithm) {
235 position = pulse(position);
236 }
237
238 // only need the difference
239 var x = (item.x * position - item.lastX) >> 0;
240 var y = (item.y * position - item.lastY) >> 0;
241
242 // add this to the total scrolling
243 scrollX += x;
244 scrollY += y;
245
246 // update last values
247 item.lastX += x;
248 item.lastY += y;
249
250 // delete and step back if it's over
251 if (finished) {
252 que.splice(i, 1); i--;
253 }
254 }
255
256 // scroll left and top
257 if (scrollWindow) {
258 window.scrollBy(scrollX, scrollY);
259 }
260 else {
261 if (scrollX) elem.scrollLeft += scrollX;
262 if (scrollY) elem.scrollTop += scrollY;
263 }
264
265 // clean up if there's nothing left to do
266 if (!left && !top) {
267 que = [];
268 }
269
270 if (que.length) {
271 requestFrame(step, elem, (1000 / options.frameRate + 1));
272 } else {
273 pending = false;
274 }
275 };
276
277 // start a new queue of actions
278 requestFrame(step, elem, 0);
279 pending = true;
280}
281
282
283/***********************************************
284 * EVENTS
285 ***********************************************/
286
287/**
288 * Mouse wheel handler.
289 * @param {Object} event
290 */
291function wheel(event) {
292
293 if (!initDone) {
294 init();
295 }
296
297 var target = event.target;
298
299 // leave early if default action is prevented
300 // or it's a zooming event with CTRL
301 if (event.defaultPrevented || event.ctrlKey) {
302 return true;
303 }
304
305 // leave embedded content alone (flash & pdf)
306 if (isNodeName(activeElement, 'embed') ||
307 (isNodeName(target, 'embed') && /\.pdf/i.test(target.src)) ||
308 isNodeName(activeElement, 'object') ||
309 target.shadowRoot) {
310 return true;
311 }
312
313 var deltaX = -event.wheelDeltaX || event.deltaX || 0;
314 var deltaY = -event.wheelDeltaY || event.deltaY || 0;
315
316 if (isMac) {
317 if (event.wheelDeltaX && isDivisible(event.wheelDeltaX, 120)) {
318 deltaX = -120 * (event.wheelDeltaX / Math.abs(event.wheelDeltaX));
319 }
320 if (event.wheelDeltaY && isDivisible(event.wheelDeltaY, 120)) {
321 deltaY = -120 * (event.wheelDeltaY / Math.abs(event.wheelDeltaY));
322 }
323 }
324
325 // use wheelDelta if deltaX/Y is not available
326 if (!deltaX && !deltaY) {
327 deltaY = -event.wheelDelta || 0;
328 }
329
330 // line based scrolling (Firefox mostly)
331 if (event.deltaMode === 1) {
332 deltaX *= 40;
333 deltaY *= 40;
334 }
335
336 var overflowing = overflowingAncestor(target);
337
338 // nothing to do if there's no element that's scrollable
339 if (!overflowing) {
340 // except Chrome iframes seem to eat wheel events, which we need to
341 // propagate up, if the iframe has nothing overflowing to scroll
342 if (isFrame && isChrome) {
343 // change target to iframe element itself for the parent frame
344 Object.defineProperty(event, "target", {value: window.frameElement});
345 return parent.wheel(event);
346 }
347 return true;
348 }
349
350 // check if it's a touchpad scroll that should be ignored
351 if (isTouchpad(deltaY)) {
352 return true;
353 }
354
355 // scale by step size
356 // delta is 120 most of the time
357 // synaptics seems to send 1 sometimes
358 if (Math.abs(deltaX) > 1.2) {
359 deltaX *= options.stepSize / 120;
360 }
361 if (Math.abs(deltaY) > 1.2) {
362 deltaY *= options.stepSize / 120;
363 }
364
365 scrollArray(overflowing, deltaX, deltaY);
366 scheduleClearCache();
367}
368
369/**
370 * Keydown event handler.
371 * @param {Object} event
372 */
373function keydown(event) {
374
375 var target = event.target;
376 var modifier = event.ctrlKey || event.altKey || event.metaKey ||
377 (event.shiftKey && event.keyCode !== key.spacebar);
378
379 // our own tracked active element could've been removed from the DOM
380 if (!document.body.contains(activeElement)) {
381 activeElement = document.activeElement;
382 }
383
384 // do nothing if user is editing text
385 // or using a modifier key (except shift)
386 // or in a dropdown
387 // or inside interactive elements
388 var inputNodeNames = /^(textarea|select|embed|object)$/i;
389 var buttonTypes = /^(button|submit|radio|checkbox|file|color|image)$/i;
390 if ( event.defaultPrevented ||
391 inputNodeNames.test(target.nodeName) ||
392 isNodeName(target, 'input') && !buttonTypes.test(target.type) ||
393 isNodeName(activeElement, 'video') ||
394 isInsideYoutubeVideo(event) ||
395 target.isContentEditable ||
396 modifier ) {
397 return true;
398 }
399
400 // [spacebar] should trigger button press, leave it alone
401 if ((isNodeName(target, 'button') ||
402 isNodeName(target, 'input') && buttonTypes.test(target.type)) &&
403 event.keyCode === key.spacebar) {
404 return true;
405 }
406
407 // [arrwow keys] on radio buttons should be left alone
408 if (isNodeName(target, 'input') && target.type == 'radio' &&
409 arrowKeys[event.keyCode]) {
410 return true;
411 }
412
413 var shift, x = 0, y = 0;
414 var overflowing = overflowingAncestor(activeElement);
415
416 if (!overflowing) {
417 // Chrome iframes seem to eat key events, which we need to
418 // propagate up, if the iframe has nothing overflowing to scroll
419 return (isFrame && isChrome) ? parent.keydown(event) : true;
420 }
421
422 var clientHeight = overflowing.clientHeight;
423
424 if (overflowing == document.body) {
425 clientHeight = window.innerHeight;
426 }
427
428 switch (event.keyCode) {
429 case key.up:
430 y = -options.arrowScroll;
431 break;
432 case key.down:
433 y = options.arrowScroll;
434 break;
435 case key.spacebar: // (+ shift)
436 shift = event.shiftKey ? 1 : -1;
437 y = -shift * clientHeight * 0.9;
438 break;
439 case key.pageup:
440 y = -clientHeight * 0.9;
441 break;
442 case key.pagedown:
443 y = clientHeight * 0.9;
444 break;
445 case key.home:
446 y = -overflowing.scrollTop;
447 break;
448 case key.end:
449 var scroll = overflowing.scrollHeight - overflowing.scrollTop;
450 var scrollRemaining = scroll - clientHeight;
451 y = (scrollRemaining > 0) ? scrollRemaining + 10 : 0;
452 break;
453 case key.left:
454 x = -options.arrowScroll;
455 break;
456 case key.right:
457 x = options.arrowScroll;
458 break;
459 default:
460 return true; // a key we don't care about
461 }
462
463 scrollArray(overflowing, x, y);
464 event.preventDefault();
465 scheduleClearCache();
466}
467
468/**
469 * Mousedown event only for updating activeElement
470 */
471function mousedown(event) {
472 activeElement = event.target;
473}
474
475
476/***********************************************
477 * OVERFLOW
478 ***********************************************/
479
480var uniqueID = (function () {
481 var i = 0;
482 return function (el) {
483 return el.uniqueID || (el.uniqueID = i++);
484 };
485})();
486
487var cache = {}; // cleared out after a scrolling session
488var clearCacheTimer;
489
490//setInterval(function () { cache = {}; }, 10 * 1000);
491
492function scheduleClearCache() {
493 clearTimeout(clearCacheTimer);
494 clearCacheTimer = setInterval(function () { cache = {}; }, 1*1000);
495}
496
497function setCache(elems, overflowing) {
498 for (var i = elems.length; i--;)
499 cache[uniqueID(elems[i])] = overflowing;
500 return overflowing;
501}
502
503// (body) (root)
504// | hidden | visible | scroll | auto |
505// hidden | no | no | YES | YES |
506// visible | no | YES | YES | YES |
507// scroll | no | YES | YES | YES |
508// auto | no | YES | YES | YES |
509
510function overflowingAncestor(el) {
511 var elems = [];
512 var body = document.body;
513 var rootScrollHeight = root.scrollHeight;
514 do {
515 var cached = cache[uniqueID(el)];
516 if (cached) {
517 return setCache(elems, cached);
518 }
519 elems.push(el);
520 if (rootScrollHeight === el.scrollHeight) {
521 var topOverflowsNotHidden = overflowNotHidden(root) && overflowNotHidden(body);
522 var isOverflowCSS = topOverflowsNotHidden || overflowAutoOrScroll(root);
523 if (isFrame && isContentOverflowing(root) ||
524 !isFrame && isOverflowCSS) {
525 return setCache(elems, getScrollRoot());
526 }
527 } else if (isContentOverflowing(el) && overflowAutoOrScroll(el)) {
528 return setCache(elems, el);
529 }
530 } while (el = el.parentElement);
531}
532
533function isContentOverflowing(el) {
534 return (el.clientHeight + 10 < el.scrollHeight);
535}
536
537// typically for <body> and <html>
538function overflowNotHidden(el) {
539 var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
540 return (overflow !== 'hidden');
541}
542
543// for all other elements
544function overflowAutoOrScroll(el) {
545 var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
546 return (overflow === 'scroll' || overflow === 'auto');
547}
548
549
550/***********************************************
551 * HELPERS
552 ***********************************************/
553
554function addEvent(type, fn) {
555 window.addEventListener(type, fn, false);
556}
557
558function removeEvent(type, fn) {
559 window.removeEventListener(type, fn, false);
560}
561
562function isNodeName(el, tag) {
563 return (el.nodeName||'').toLowerCase() === tag.toLowerCase();
564}
565
566function directionCheck(x, y) {
567 x = (x > 0) ? 1 : -1;
568 y = (y > 0) ? 1 : -1;
569 if (direction.x !== x || direction.y !== y) {
570 direction.x = x;
571 direction.y = y;
572 que = [];
573 lastScroll = 0;
574 }
575}
576
577var deltaBufferTimer;
578
579if (window.localStorage && localStorage.SS_deltaBuffer) {
580 try { // #46 Safari throws in private browsing for localStorage
581 deltaBuffer = localStorage.SS_deltaBuffer.split(',');
582 } catch (e) { }
583}
584
585function isTouchpad(deltaY) {
586 if (!deltaY) return;
587 if (!deltaBuffer.length) {
588 deltaBuffer = [deltaY, deltaY, deltaY];
589 }
590 deltaY = Math.abs(deltaY);
591 deltaBuffer.push(deltaY);
592 deltaBuffer.shift();
593 clearTimeout(deltaBufferTimer);
594 deltaBufferTimer = setTimeout(function () {
595 try { // #46 Safari throws in private browsing for localStorage
596 localStorage.SS_deltaBuffer = deltaBuffer.join(',');
597 } catch (e) { }
598 }, 1000);
599 return !allDeltasDivisableBy(120) && !allDeltasDivisableBy(100);
600}
601
602function isDivisible(n, divisor) {
603 return (Math.floor(n / divisor) == n / divisor);
604}
605
606function allDeltasDivisableBy(divisor) {
607 return (isDivisible(deltaBuffer[0], divisor) &&
608 isDivisible(deltaBuffer[1], divisor) &&
609 isDivisible(deltaBuffer[2], divisor));
610}
611
612function isInsideYoutubeVideo(event) {
613 var elem = event.target;
614 var isControl = false;
615 if (document.URL.indexOf ('www.youtube.com/watch') != -1) {
616 do {
617 isControl = (elem.classList &&
618 elem.classList.contains('html5-video-controls'));
619 if (isControl) break;
620 } while (elem = elem.parentNode);
621 }
622 return isControl;
623}
624
625var requestFrame = (function () {
626 return (window.requestAnimationFrame ||
627 window.webkitRequestAnimationFrame ||
628 window.mozRequestAnimationFrame ||
629 function (callback, element, delay) {
630 window.setTimeout(callback, delay || (1000/60));
631 });
632})();
633
634var MutationObserver = (window.MutationObserver ||
635 window.WebKitMutationObserver ||
636 window.MozMutationObserver);
637
638var getScrollRoot = (function() {
639 var SCROLL_ROOT;
640 return function() {
641 if (!SCROLL_ROOT) {
642 var dummy = document.createElement('div');
643 dummy.style.cssText = 'height:10000px;width:1px;';
644 document.body.appendChild(dummy);
645 var bodyScrollTop = document.body.scrollTop;
646 var docElScrollTop = document.documentElement.scrollTop;
647 window.scrollBy(0, 3);
648 if (document.body.scrollTop != bodyScrollTop)
649 (SCROLL_ROOT = document.body);
650 else
651 (SCROLL_ROOT = document.documentElement);
652 window.scrollBy(0, -3);
653 document.body.removeChild(dummy);
654 }
655 return SCROLL_ROOT;
656 };
657})();
658
659
660/***********************************************
661 * PULSE (by Michael Herf)
662 ***********************************************/
663
664/**
665 * Viscous fluid with a pulse for part and decay for the rest.
666 * - Applies a fixed force over an interval (a damped acceleration), and
667 * - Lets the exponential bleed away the velocity over a longer interval
668 * - Michael Herf, http://stereopsis.com/stopping/
669 */
670function pulse_(x) {
671 var val, start, expx;
672 // test
673 x = x * options.pulseScale;
674 if (x < 1) { // acceleartion
675 val = x - (1 - Math.exp(-x));
676 } else { // tail
677 // the previous animation ended here:
678 start = Math.exp(-1);
679 // simple viscous drag
680 x -= 1;
681 expx = 1 - Math.exp(-x);
682 val = start + (expx * (1 - start));
683 }
684 return val * options.pulseNormalize;
685}
686
687function pulse(x) {
688 if (x >= 1) return 1;
689 if (x <= 0) return 0;
690
691 if (options.pulseNormalize == 1) {
692 options.pulseNormalize /= pulse_(1);
693 }
694 return pulse_(x);
695}
696
697
698/***********************************************
699 * FIRST RUN
700 ***********************************************/
701
702var userAgent = window.navigator.userAgent;
703var isEdge = /Edge/.test(userAgent); // thank you MS
704var isChrome = /chrome/i.test(userAgent) && !isEdge;
705var isSafari = /safari/i.test(userAgent) && !isEdge;
706var isMobile = /mobile/i.test(userAgent);
707var isIEWin7 = /Windows NT 6.1/i.test(userAgent) && /rv:11/i.test(userAgent);
708var isOldSafari = isSafari && (/Version\/8/i.test(userAgent) || /Version\/9/i.test(userAgent));
709var isEnabledForBrowser = (isChrome || isSafari || isIEWin7) && !isMobile;
710
711var wheelEvent;
712if ('onwheel' in document.createElement('div'))
713 wheelEvent = 'wheel';
714else if ('onmousewheel' in document.createElement('div'))
715 wheelEvent = 'mousewheel';
716
717if (wheelEvent && isEnabledForBrowser) {
718 addEvent(wheelEvent, wheel);
719 addEvent('mousedown', mousedown);
720 addEvent('load', init);
721}
722
723
724/***********************************************
725 * PUBLIC INTERFACE
726 ***********************************************/
727
728function SmoothScroll(optionsToSet) {
729 for (var key in optionsToSet)
730 if (defaultOptions.hasOwnProperty(key))
731 options[key] = optionsToSet[key];
732}
733SmoothScroll.destroy = cleanup;
734
735if (window.SmoothScrollOptions) // async API
736 SmoothScroll(window.SmoothScrollOptions);
737
738if (typeof define === 'function' && define.amd)
739 define(function() {
740 return SmoothScroll;
741 });
742else if ('object' == typeof exports)
743 module.exports = SmoothScroll;
744else
745 window.SmoothScroll = SmoothScroll;
746
747})();