| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- 'use strict';
- // polyfill
- function polyfill() {
- // aliases
- var w = window;
- var d = document;
- // return if scroll behavior is supported and polyfill is not forced
- if (
- 'scrollBehavior' in d.documentElement.style &&
- w.__forceSmoothScrollPolyfill__ !== true
- ) {
- return;
- }
- // globals
- var Element = w.HTMLElement || w.Element;
- var SCROLL_TIME = 468;
- // object gathering original scroll methods
- var original = {
- scroll: w.scroll || w.scrollTo,
- scrollBy: w.scrollBy,
- elementScroll: Element.prototype.scroll || scrollElement,
- scrollIntoView: Element.prototype.scrollIntoView
- };
- // define timing method
- var now =
- w.performance && w.performance.now
- ? w.performance.now.bind(w.performance)
- : Date.now;
- /**
- * indicates if a the current browser is made by Microsoft
- * @method isMicrosoftBrowser
- * @param {String} userAgent
- * @returns {Boolean}
- */
- function isMicrosoftBrowser(userAgent) {
- var userAgentPatterns = ['MSIE ', 'Trident/', 'Edge/'];
- return new RegExp(userAgentPatterns.join('|')).test(userAgent);
- }
- /*
- * IE has rounding bug rounding down clientHeight and clientWidth and
- * rounding up scrollHeight and scrollWidth causing false positives
- * on hasScrollableSpace
- */
- var ROUNDING_TOLERANCE = isMicrosoftBrowser(w.navigator.userAgent) ? 1 : 0;
- /**
- * changes scroll position inside an element
- * @method scrollElement
- * @param {Number} x
- * @param {Number} y
- * @returns {undefined}
- */
- function scrollElement(x, y) {
- this.scrollLeft = x;
- this.scrollTop = y;
- }
- /**
- * returns result of applying ease math function to a number
- * @method ease
- * @param {Number} k
- * @returns {Number}
- */
- function ease(k) {
- return 0.5 * (1 - Math.cos(Math.PI * k));
- }
- /**
- * indicates if a smooth behavior should be applied
- * @method shouldBailOut
- * @param {Number|Object} firstArg
- * @returns {Boolean}
- */
- function shouldBailOut(firstArg) {
- if (
- firstArg === null ||
- typeof firstArg !== 'object' ||
- firstArg.behavior === undefined ||
- firstArg.behavior === 'auto' ||
- firstArg.behavior === 'instant'
- ) {
- // first argument is not an object/null
- // or behavior is auto, instant or undefined
- return true;
- }
- if (typeof firstArg === 'object' && firstArg.behavior === 'smooth') {
- // first argument is an object and behavior is smooth
- return false;
- }
- // throw error when behavior is not supported
- throw new TypeError(
- 'behavior member of ScrollOptions ' +
- firstArg.behavior +
- ' is not a valid value for enumeration ScrollBehavior.'
- );
- }
- /**
- * indicates if an element has scrollable space in the provided axis
- * @method hasScrollableSpace
- * @param {Node} el
- * @param {String} axis
- * @returns {Boolean}
- */
- function hasScrollableSpace(el, axis) {
- if (axis === 'Y') {
- return el.clientHeight + ROUNDING_TOLERANCE < el.scrollHeight;
- }
- if (axis === 'X') {
- return el.clientWidth + ROUNDING_TOLERANCE < el.scrollWidth;
- }
- }
- /**
- * indicates if an element has a scrollable overflow property in the axis
- * @method canOverflow
- * @param {Node} el
- * @param {String} axis
- * @returns {Boolean}
- */
- function canOverflow(el, axis) {
- var overflowValue = w.getComputedStyle(el, null)['overflow' + axis];
- return overflowValue === 'auto' || overflowValue === 'scroll';
- }
- /**
- * indicates if an element can be scrolled in either axis
- * @method isScrollable
- * @param {Node} el
- * @param {String} axis
- * @returns {Boolean}
- */
- function isScrollable(el) {
- var isScrollableY = hasScrollableSpace(el, 'Y') && canOverflow(el, 'Y');
- var isScrollableX = hasScrollableSpace(el, 'X') && canOverflow(el, 'X');
- return isScrollableY || isScrollableX;
- }
- /**
- * finds scrollable parent of an element
- * @method findScrollableParent
- * @param {Node} el
- * @returns {Node} el
- */
- function findScrollableParent(el) {
- var isBody;
- do {
- el = el.parentNode;
- isBody = el === d.body;
- } while (isBody === false && isScrollable(el) === false);
- isBody = null;
- return el;
- }
- /**
- * self invoked function that, given a context, steps through scrolling
- * @method step
- * @param {Object} context
- * @returns {undefined}
- */
- function step(context) {
- var time = now();
- var value;
- var currentX;
- var currentY;
- var elapsed = (time - context.startTime) / SCROLL_TIME;
- // avoid elapsed times higher than one
- elapsed = elapsed > 1 ? 1 : elapsed;
- // apply easing to elapsed time
- value = ease(elapsed);
- currentX = context.startX + (context.x - context.startX) * value;
- currentY = context.startY + (context.y - context.startY) * value;
- context.method.call(context.scrollable, currentX, currentY);
- // scroll more if we have not reached our destination
- if (currentX !== context.x || currentY !== context.y) {
- w.requestAnimationFrame(step.bind(w, context));
- }
- }
- /**
- * scrolls window or element with a smooth behavior
- * @method smoothScroll
- * @param {Object|Node} el
- * @param {Number} x
- * @param {Number} y
- * @returns {undefined}
- */
- function smoothScroll(el, x, y) {
- var scrollable;
- var startX;
- var startY;
- var method;
- var startTime = now();
- // define scroll context
- if (el === d.body) {
- scrollable = w;
- startX = w.scrollX || w.pageXOffset;
- startY = w.scrollY || w.pageYOffset;
- method = original.scroll;
- } else {
- scrollable = el;
- startX = el.scrollLeft;
- startY = el.scrollTop;
- method = scrollElement;
- }
- // scroll looping over a frame
- step({
- scrollable: scrollable,
- method: method,
- startTime: startTime,
- startX: startX,
- startY: startY,
- x: x,
- y: y
- });
- }
- // ORIGINAL METHODS OVERRIDES
- // w.scroll and w.scrollTo
- w.scroll = w.scrollTo = function() {
- // avoid action when no arguments are passed
- if (arguments[0] === undefined) {
- return;
- }
- // avoid smooth behavior if not required
- if (shouldBailOut(arguments[0]) === true) {
- original.scroll.call(
- w,
- arguments[0].left !== undefined
- ? arguments[0].left
- : typeof arguments[0] !== 'object'
- ? arguments[0]
- : w.scrollX || w.pageXOffset,
- // use top prop, second argument if present or fallback to scrollY
- arguments[0].top !== undefined
- ? arguments[0].top
- : arguments[1] !== undefined
- ? arguments[1]
- : w.scrollY || w.pageYOffset
- );
- return;
- }
- // LET THE SMOOTHNESS BEGIN!
- smoothScroll.call(
- w,
- d.body,
- arguments[0].left !== undefined
- ? ~~arguments[0].left
- : w.scrollX || w.pageXOffset,
- arguments[0].top !== undefined
- ? ~~arguments[0].top
- : w.scrollY || w.pageYOffset
- );
- };
- // w.scrollBy
- w.scrollBy = function() {
- // avoid action when no arguments are passed
- if (arguments[0] === undefined) {
- return;
- }
- // avoid smooth behavior if not required
- if (shouldBailOut(arguments[0])) {
- original.scrollBy.call(
- w,
- arguments[0].left !== undefined
- ? arguments[0].left
- : typeof arguments[0] !== 'object' ? arguments[0] : 0,
- arguments[0].top !== undefined
- ? arguments[0].top
- : arguments[1] !== undefined ? arguments[1] : 0
- );
- return;
- }
- // LET THE SMOOTHNESS BEGIN!
- smoothScroll.call(
- w,
- d.body,
- ~~arguments[0].left + (w.scrollX || w.pageXOffset),
- ~~arguments[0].top + (w.scrollY || w.pageYOffset)
- );
- };
- // Element.prototype.scroll and Element.prototype.scrollTo
- Element.prototype.scroll = Element.prototype.scrollTo = function() {
- // avoid action when no arguments are passed
- if (arguments[0] === undefined) {
- return;
- }
- // avoid smooth behavior if not required
- if (shouldBailOut(arguments[0]) === true) {
- // if one number is passed, throw error to match Firefox implementation
- if (typeof arguments[0] === 'number' && arguments[1] === undefined) {
- throw new SyntaxError('Value could not be converted');
- }
- original.elementScroll.call(
- this,
- // use left prop, first number argument or fallback to scrollLeft
- arguments[0].left !== undefined
- ? ~~arguments[0].left
- : typeof arguments[0] !== 'object' ? ~~arguments[0] : this.scrollLeft,
- // use top prop, second argument or fallback to scrollTop
- arguments[0].top !== undefined
- ? ~~arguments[0].top
- : arguments[1] !== undefined ? ~~arguments[1] : this.scrollTop
- );
- return;
- }
- var left = arguments[0].left;
- var top = arguments[0].top;
- // LET THE SMOOTHNESS BEGIN!
- smoothScroll.call(
- this,
- this,
- typeof left === 'undefined' ? this.scrollLeft : ~~left,
- typeof top === 'undefined' ? this.scrollTop : ~~top
- );
- };
- // Element.prototype.scrollBy
- Element.prototype.scrollBy = function() {
- // avoid action when no arguments are passed
- if (arguments[0] === undefined) {
- return;
- }
- // avoid smooth behavior if not required
- if (shouldBailOut(arguments[0]) === true) {
- original.elementScroll.call(
- this,
- arguments[0].left !== undefined
- ? ~~arguments[0].left + this.scrollLeft
- : ~~arguments[0] + this.scrollLeft,
- arguments[0].top !== undefined
- ? ~~arguments[0].top + this.scrollTop
- : ~~arguments[1] + this.scrollTop
- );
- return;
- }
- this.scroll({
- left: ~~arguments[0].left + this.scrollLeft,
- top: ~~arguments[0].top + this.scrollTop,
- behavior: arguments[0].behavior
- });
- };
- // Element.prototype.scrollIntoView
- Element.prototype.scrollIntoView = function() {
- // avoid smooth behavior if not required
- if (shouldBailOut(arguments[0]) === true) {
- original.scrollIntoView.call(
- this,
- arguments[0] === undefined ? true : arguments[0]
- );
- return;
- }
- // LET THE SMOOTHNESS BEGIN!
- var scrollableParent = findScrollableParent(this);
- var parentRects = scrollableParent.getBoundingClientRect();
- var clientRects = this.getBoundingClientRect();
- if (scrollableParent !== d.body) {
- // reveal element inside parent
- smoothScroll.call(
- this,
- scrollableParent,
- scrollableParent.scrollLeft + clientRects.left - parentRects.left,
- scrollableParent.scrollTop + clientRects.top - parentRects.top
- );
- // reveal parent in viewport unless is fixed
- if (w.getComputedStyle(scrollableParent).position !== 'fixed') {
- w.scrollBy({
- left: parentRects.left,
- top: parentRects.top,
- behavior: 'smooth'
- });
- }
- } else {
- // reveal element in viewport
- w.scrollBy({
- left: clientRects.left,
- top: clientRects.top,
- behavior: 'smooth'
- });
- }
- };
- }
- if (typeof exports === 'object' && typeof module !== 'undefined') {
- // commonjs
- module.exports = { polyfill: polyfill };
- } else {
- // global
- polyfill();
- }
|