· 6 years ago · Jan 23, 2020, 12:08 PM
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 : 1200, // [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) {
359deltaX *= options.stepSize / 120;
360}
361if (Math.abs(deltaY) > 1.2) {
362deltaY *= options.stepSize / 120;
363}
364
365scrollArray(overflowing, deltaX, deltaY);
366event.preventDefault();
367scheduleClearCache();
368}
369
370/**
371 * Keydown event handler.
372 * @param {Object} event
373 */
374function keydown(event) {
375
376 var target = event.target;
377 var modifier = event.ctrlKey || event.altKey || event.metaKey ||
378 (event.shiftKey && event.keyCode !== key.spacebar);
379
380 // our own tracked active element could've been removed from the DOM
381 if (!document.body.contains(activeElement)) {
382 activeElement = document.activeElement;
383 }
384
385 // do nothing if user is editing text
386 // or using a modifier key (except shift)
387 // or in a dropdown
388 // or inside interactive elements
389 var inputNodeNames = /^(textarea|select|embed|object)$/i;
390 var buttonTypes = /^(button|submit|radio|checkbox|file|color|image)$/i;
391 if ( event.defaultPrevented ||
392 inputNodeNames.test(target.nodeName) ||
393 isNodeName(target, 'input') && !buttonTypes.test(target.type) ||
394 isNodeName(activeElement, 'video') ||
395 isInsideYoutubeVideo(event) ||
396 target.isContentEditable ||
397 modifier ) {
398 return true;
399 }
400
401 // [spacebar] should trigger button press, leave it alone
402 if ((isNodeName(target, 'button') ||
403 isNodeName(target, 'input') && buttonTypes.test(target.type)) &&
404 event.keyCode === key.spacebar) {
405 return true;
406 }
407
408 // [arrwow keys] on radio buttons should be left alone
409 if (isNodeName(target, 'input') && target.type == 'radio' &&
410 arrowKeys[event.keyCode]) {
411 return true;
412 }
413
414 var shift, x = 0, y = 0;
415 var overflowing = overflowingAncestor(activeElement);
416
417 if (!overflowing) {
418 // Chrome iframes seem to eat key events, which we need to
419 // propagate up, if the iframe has nothing overflowing to scroll
420 return (isFrame && isChrome) ? parent.keydown(event) : true;
421 }
422
423 var clientHeight = overflowing.clientHeight;
424
425 if (overflowing == document.body) {
426 clientHeight = window.innerHeight;
427 }
428
429 switch (event.keyCode) {
430 case key.up:
431 y = -options.arrowScroll;
432 break;
433 case key.down:
434 y = options.arrowScroll;
435 break;
436 case key.spacebar: // (+ shift)
437 shift = event.shiftKey ? 1 : -1;
438 y = -shift * clientHeight * 0.9;
439 break;
440 case key.pageup:
441 y = -clientHeight * 0.9;
442 break;
443 case key.pagedown:
444 y = clientHeight * 0.9;
445 break;
446 case key.home:
447 y = -overflowing.scrollTop;
448 break;
449 case key.end:
450 var scroll = overflowing.scrollHeight - overflowing.scrollTop;
451 var scrollRemaining = scroll - clientHeight;
452 y = (scrollRemaining > 0) ? scrollRemaining + 10 : 0;
453 break;
454 case key.left:
455 x = -options.arrowScroll;
456 break;
457 case key.right:
458 x = options.arrowScroll;
459 break;
460 default:
461 return true; // a key we don't care about
462 }
463
464 scrollArray(overflowing, x, y);
465 event.preventDefault();
466 scheduleClearCache();
467}
468
469/**
470 * Mousedown event only for updating activeElement
471 */
472function mousedown(event) {
473 activeElement = event.target;
474}
475
476
477/***********************************************
478 * OVERFLOW
479 ***********************************************/
480
481var uniqueID = (function () {
482 var i = 0;
483 return function (el) {
484 return el.uniqueID || (el.uniqueID = i++);
485 };
486})();
487
488var cache = {}; // cleared out after a scrolling session
489var clearCacheTimer;
490
491//setInterval(function () { cache = {}; }, 10 * 1000);
492
493function scheduleClearCache() {
494 clearTimeout(clearCacheTimer);
495 clearCacheTimer = setInterval(function () { cache = {}; }, 1*1000);
496}
497
498function setCache(elems, overflowing) {
499 for (var i = elems.length; i--;)
500 cache[uniqueID(elems[i])] = overflowing;
501 return overflowing;
502}
503
504// (body) (root)
505// | hidden | visible | scroll | auto |
506// hidden | no | no | YES | YES |
507// visible | no | YES | YES | YES |
508// scroll | no | YES | YES | YES |
509// auto | no | YES | YES | YES |
510
511function overflowingAncestor(el) {
512 var elems = [];
513 var body = document.body;
514 var rootScrollHeight = root.scrollHeight;
515 do {
516 var cached = cache[uniqueID(el)];
517 if (cached) {
518 return setCache(elems, cached);
519 }
520 elems.push(el);
521 if (rootScrollHeight === el.scrollHeight) {
522 var topOverflowsNotHidden = overflowNotHidden(root) && overflowNotHidden(body);
523 var isOverflowCSS = topOverflowsNotHidden || overflowAutoOrScroll(root);
524 if (isFrame && isContentOverflowing(root) ||
525 !isFrame && isOverflowCSS) {
526 return setCache(elems, getScrollRoot());
527 }
528 } else if (isContentOverflowing(el) && overflowAutoOrScroll(el)) {
529 return setCache(elems, el);
530 }
531 } while (el = el.parentElement);
532}
533
534function isContentOverflowing(el) {
535 return (el.clientHeight + 10 < el.scrollHeight);
536}
537
538// typically for <body> and <html>
539function overflowNotHidden(el) {
540 var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
541 return (overflow !== 'hidden');
542}
543
544// for all other elements
545function overflowAutoOrScroll(el) {
546 var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y');
547 return (overflow === 'scroll' || overflow === 'auto');
548}
549
550
551/***********************************************
552 * HELPERS
553 ***********************************************/
554
555function addEvent(type, fn) {
556 window.addEventListener(type, fn, false);
557}
558
559function removeEvent(type, fn) {
560 window.removeEventListener(type, fn, false);
561}
562
563function isNodeName(el, tag) {
564 return (el.nodeName||'').toLowerCase() === tag.toLowerCase();
565}
566
567function directionCheck(x, y) {
568 x = (x > 0) ? 1 : -1;
569 y = (y > 0) ? 1 : -1;
570 if (direction.x !== x || direction.y !== y) {
571 direction.x = x;
572 direction.y = y;
573 que = [];
574 lastScroll = 0;
575 }
576}
577
578var deltaBufferTimer;
579
580if (window.localStorage && localStorage.SS_deltaBuffer) {
581 try { // #46 Safari throws in private browsing for localStorage
582 deltaBuffer = localStorage.SS_deltaBuffer.split(',');
583 } catch (e) { }
584}
585
586function isTouchpad(deltaY) {
587 if (!deltaY) return;
588 if (!deltaBuffer.length) {
589 deltaBuffer = [deltaY, deltaY, deltaY];
590 }
591 deltaY = Math.abs(deltaY);
592 deltaBuffer.push(deltaY);
593 deltaBuffer.shift();
594 clearTimeout(deltaBufferTimer);
595 deltaBufferTimer = setTimeout(function () {
596 try { // #46 Safari throws in private browsing for localStorage
597 localStorage.SS_deltaBuffer = deltaBuffer.join(',');
598 } catch (e) { }
599 }, 1000);
600 return !allDeltasDivisableBy(120) && !allDeltasDivisableBy(100);
601}
602
603function isDivisible(n, divisor) {
604 return (Math.floor(n / divisor) == n / divisor);
605}
606
607function allDeltasDivisableBy(divisor) {
608 return (isDivisible(deltaBuffer[0], divisor) &&
609 isDivisible(deltaBuffer[1], divisor) &&
610 isDivisible(deltaBuffer[2], divisor));
611}
612
613function isInsideYoutubeVideo(event) {
614 var elem = event.target;
615 var isControl = false;
616 if (document.URL.indexOf ('www.youtube.com/watch') != -1) {
617 do {
618 isControl = (elem.classList &&
619 elem.classList.contains('html5-video-controls'));
620 if (isControl) break;
621 } while (elem = elem.parentNode);
622 }
623 return isControl;
624}
625
626var requestFrame = (function () {
627 return (window.requestAnimationFrame ||
628 window.webkitRequestAnimationFrame ||
629 window.mozRequestAnimationFrame ||
630 function (callback, element, delay) {
631 window.setTimeout(callback, delay || (1000/60));
632 });
633})();
634
635var MutationObserver = (window.MutationObserver ||
636 window.WebKitMutationObserver ||
637 window.MozMutationObserver);
638
639var getScrollRoot = (function() {
640 var SCROLL_ROOT;
641 return function() {
642 if (!SCROLL_ROOT) {
643 var dummy = document.createElement('div');
644 dummy.style.cssText = 'height:10000px;width:1px;';
645 document.body.appendChild(dummy);
646 var bodyScrollTop = document.body.scrollTop;
647 var docElScrollTop = document.documentElement.scrollTop;
648 window.scrollBy(0, 3);
649 if (document.body.scrollTop != bodyScrollTop)
650 (SCROLL_ROOT = document.body);
651 else
652 (SCROLL_ROOT = document.documentElement);
653 window.scrollBy(0, -3);
654 document.body.removeChild(dummy);
655 }
656 return SCROLL_ROOT;
657 };
658})();
659
660
661/***********************************************
662 * PULSE (by Michael Herf)
663 ***********************************************/
664
665/**
666 * Viscous fluid with a pulse for part and decay for the rest.
667 * - Applies a fixed force over an interval (a damped acceleration), and
668 * - Lets the exponential bleed away the velocity over a longer interval
669 * - Michael Herf, http://stereopsis.com/stopping/
670 */
671function pulse_(x) {
672 var val, start, expx;
673 // test
674 x = x * options.pulseScale;
675 if (x < 1) { // acceleartion
676 val = x - (1 - Math.exp(-x));
677 } else { // tail
678 // the previous animation ended here:
679 start = Math.exp(-1);
680 // simple viscous drag
681 x -= 1;
682 expx = 1 - Math.exp(-x);
683 val = start + (expx * (1 - start));
684 }
685 return val * options.pulseNormalize;
686}
687
688function pulse(x) {
689 if (x >= 1) return 1;
690 if (x <= 0) return 0;
691
692 if (options.pulseNormalize == 1) {
693 options.pulseNormalize /= pulse_(1);
694 }
695 return pulse_(x);
696}
697
698
699/***********************************************
700 * FIRST RUN
701 ***********************************************/
702
703var userAgent = window.navigator.userAgent;
704var isEdge = /Edge/.test(userAgent); // thank you MS
705var isChrome = /chrome/i.test(userAgent) && !isEdge;
706var isSafari = /safari/i.test(userAgent) && !isEdge;
707var isMobile = /mobile/i.test(userAgent);
708var isIEWin7 = /Windows NT 6.1/i.test(userAgent) && /rv:11/i.test(userAgent);
709var isOldSafari = isSafari && (/Version\/8/i.test(userAgent) || /Version\/9/i.test(userAgent));
710var isEnabledForBrowser = (isChrome || isSafari || isIEWin7) && !isMobile;
711
712var wheelEvent;
713if ('onwheel' in document.createElement('div'))
714 wheelEvent = 'wheel';
715else if ('onmousewheel' in document.createElement('div'))
716 wheelEvent = 'mousewheel';
717
718if (wheelEvent && isEnabledForBrowser) {
719 addEvent(wheelEvent, wheel);
720 addEvent('mousedown', mousedown);
721 addEvent('load', init);
722}
723
724
725/***********************************************
726 * PUBLIC INTERFACE
727 ***********************************************/
728
729function SmoothScroll(optionsToSet) {
730 for (var key in optionsToSet)
731 if (defaultOptions.hasOwnProperty(key))
732 options[key] = optionsToSet[key];
733}
734SmoothScroll.destroy = cleanup;
735
736if (window.SmoothScrollOptions) // async API
737 SmoothScroll(window.SmoothScrollOptions);
738
739if (typeof define === 'function' && define.amd)
740 define(function() {
741 return SmoothScroll;
742 });
743else if ('object' == typeof exports)
744 module.exports = SmoothScroll;
745else
746 window.SmoothScroll = SmoothScroll;
747
748})();