'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) { while (el !== d.body && isScrollable(el) === false) { el = el.parentNode || el.host; } 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(); }