|
|
|
// credits to https://github.com/iamdustan/smoothscroll
|
|
|
|
|
|
|
|
type ScrollableElement = (Window & typeof globalThis) | Element;
|
|
|
|
export type SmoothScrollToOptions = Partial<{
|
|
|
|
top: number,
|
|
|
|
left: number,
|
|
|
|
behavior: 'smooth' | 'auto' | 'instant',
|
|
|
|
scrollTime: number
|
|
|
|
}>;
|
|
|
|
|
|
|
|
export const SCROLL_TIME = 468;
|
|
|
|
|
|
|
|
// polyfill
|
|
|
|
export default 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 as any).__forceSmoothScrollPolyfill__ !== true
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// globals
|
|
|
|
var Element = w.HTMLElement || w.Element;
|
|
|
|
|
|
|
|
// 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: string) {
|
|
|
|
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(this: Element, x: number, y: number) {
|
|
|
|
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: number) {
|
|
|
|
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: SmoothScrollToOptions) {
|
|
|
|
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: Element, axis: 'X' | 'Y') {
|
|
|
|
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: Element, axis: string) {
|
|
|
|
// @ts-ignore
|
|
|
|
var overflowValue: string = 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: Element) {
|
|
|
|
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: Element) {
|
|
|
|
while(el !== d.body && isScrollable(el) === false) {
|
|
|
|
// @ts-ignore
|
|
|
|
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: {
|
|
|
|
startTime: number,
|
|
|
|
scrollTime: number,
|
|
|
|
startX: number,
|
|
|
|
startY: number,
|
|
|
|
x: number,
|
|
|
|
y: number,
|
|
|
|
scrollable: ScrollableElement,
|
|
|
|
method: (this: ScrollableElement, currentX: number, currentY: number) => any
|
|
|
|
}) {
|
|
|
|
var time = now();
|
|
|
|
var value: number;
|
|
|
|
var currentX: number;
|
|
|
|
var currentY: number;
|
|
|
|
var elapsed = (time - context.startTime) / context.scrollTime;
|
|
|
|
|
|
|
|
// 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: Element, x: number, y: number, scrollTime = SCROLL_TIME) {
|
|
|
|
var scrollable: ScrollableElement;
|
|
|
|
var startX: number;
|
|
|
|
var startY: number;
|
|
|
|
var method: any;
|
|
|
|
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,
|
|
|
|
scrollTime,
|
|
|
|
startTime: startTime,
|
|
|
|
startX: startX,
|
|
|
|
startY: startY,
|
|
|
|
x: x,
|
|
|
|
y: y
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// ORIGINAL METHODS OVERRIDES
|
|
|
|
// w.scroll and w.scrollTo
|
|
|
|
w.scroll = w.scrollTo = function() {
|
|
|
|
const options = arguments[0] as SmoothScrollToOptions;
|
|
|
|
// avoid action when no arguments are passed
|
|
|
|
if(options === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// avoid smooth behavior if not required
|
|
|
|
if(shouldBailOut(options) === true) {
|
|
|
|
original.scroll.call(
|
|
|
|
w,
|
|
|
|
options.left !== undefined
|
|
|
|
? options.left
|
|
|
|
: typeof options !== 'object'
|
|
|
|
? options
|
|
|
|
: w.scrollX || w.pageXOffset,
|
|
|
|
// use top prop, second argument if present or fallback to scrollY
|
|
|
|
options.top !== undefined
|
|
|
|
? options.top
|
|
|
|
: arguments[1] !== undefined
|
|
|
|
? arguments[1]
|
|
|
|
: w.scrollY || w.pageYOffset
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// LET THE SMOOTHNESS BEGIN!
|
|
|
|
smoothScroll.call(
|
|
|
|
w,
|
|
|
|
d.body,
|
|
|
|
options.left !== undefined
|
|
|
|
? ~~options.left
|
|
|
|
: w.scrollX || w.pageXOffset,
|
|
|
|
options.top !== undefined
|
|
|
|
? ~~options.top
|
|
|
|
: w.scrollY || w.pageYOffset,
|
|
|
|
options.scrollTime
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
// w.scrollBy
|
|
|
|
w.scrollBy = function() {
|
|
|
|
const options = arguments[0] as SmoothScrollToOptions;
|
|
|
|
// avoid action when no arguments are passed
|
|
|
|
if(options === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// avoid smooth behavior if not required
|
|
|
|
if(shouldBailOut(options)) {
|
|
|
|
original.scrollBy.call(
|
|
|
|
w,
|
|
|
|
options.left !== undefined
|
|
|
|
? options.left
|
|
|
|
: typeof options !== 'object' ? options : 0,
|
|
|
|
options.top !== undefined
|
|
|
|
? options.top
|
|
|
|
: arguments[1] !== undefined ? arguments[1] : 0
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// LET THE SMOOTHNESS BEGIN!
|
|
|
|
smoothScroll.call(
|
|
|
|
w,
|
|
|
|
d.body,
|
|
|
|
~~options.left + (w.scrollX || w.pageXOffset),
|
|
|
|
~~options.top + (w.scrollY || w.pageYOffset),
|
|
|
|
options.scrollTime
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Element.prototype.scroll and Element.prototype.scrollTo
|
|
|
|
Element.prototype.scroll = Element.prototype.scrollTo = function() {
|
|
|
|
const options = arguments[0] as SmoothScrollToOptions;
|
|
|
|
// avoid action when no arguments are passed
|
|
|
|
if(options === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// avoid smooth behavior if not required
|
|
|
|
if(shouldBailOut(options) === true) {
|
|
|
|
// if one number is passed, throw error to match Firefox implementation
|
|
|
|
if(typeof options === '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
|
|
|
|
options.left !== undefined
|
|
|
|
? ~~options.left
|
|
|
|
: typeof options !== 'object' ? ~~options : this.scrollLeft,
|
|
|
|
// use top prop, second argument or fallback to scrollTop
|
|
|
|
options.top !== undefined
|
|
|
|
? ~~options.top
|
|
|
|
: arguments[1] !== undefined ? ~~arguments[1] : this.scrollTop
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var left = options.left;
|
|
|
|
var top = options.top;
|
|
|
|
|
|
|
|
// LET THE SMOOTHNESS BEGIN!
|
|
|
|
smoothScroll.call(
|
|
|
|
this,
|
|
|
|
this,
|
|
|
|
typeof left === 'undefined' ? this.scrollLeft : ~~left,
|
|
|
|
typeof top === 'undefined' ? this.scrollTop : ~~top,
|
|
|
|
options.scrollTime
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Element.prototype.scrollBy
|
|
|
|
Element.prototype.scrollBy = function() {
|
|
|
|
const options = arguments[0] as SmoothScrollToOptions;
|
|
|
|
// avoid action when no arguments are passed
|
|
|
|
if(options === undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// avoid smooth behavior if not required
|
|
|
|
if(shouldBailOut(options) === true) {
|
|
|
|
original.elementScroll.call(
|
|
|
|
this,
|
|
|
|
options.left !== undefined
|
|
|
|
? ~~options.left + this.scrollLeft
|
|
|
|
: ~~options + this.scrollLeft,
|
|
|
|
options.top !== undefined
|
|
|
|
? ~~options.top + this.scrollTop
|
|
|
|
: ~~arguments[1] + this.scrollTop
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.scroll({
|
|
|
|
left: ~~options.left + this.scrollLeft,
|
|
|
|
top: ~~options.top + this.scrollTop,
|
|
|
|
behavior: options.behavior as any,
|
|
|
|
scrollTime: options.scrollTime
|
|
|
|
} as any);
|
|
|
|
};
|
|
|
|
|
|
|
|
// Element.prototype.scrollIntoView
|
|
|
|
Element.prototype.scrollIntoView = function() {
|
|
|
|
const options = arguments[0] as SmoothScrollToOptions;
|
|
|
|
// avoid smooth behavior if not required
|
|
|
|
if(shouldBailOut(options) === true) {
|
|
|
|
original.scrollIntoView.call(
|
|
|
|
this,
|
|
|
|
(options === undefined ? true : options) as any
|
|
|
|
);
|
|
|
|
|
|
|
|
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,
|
|
|
|
options.scrollTime
|
|
|
|
);
|
|
|
|
|
|
|
|
// reveal parent in viewport unless is fixed
|
|
|
|
if(w.getComputedStyle(scrollableParent).position !== 'fixed') {
|
|
|
|
w.scrollBy({
|
|
|
|
left: parentRects.left,
|
|
|
|
top: parentRects.top,
|
|
|
|
behavior: 'smooth',
|
|
|
|
scrollTime: options.scrollTime
|
|
|
|
} as any);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// reveal element in viewport
|
|
|
|
w.scrollBy({
|
|
|
|
left: clientRects.left,
|
|
|
|
top: clientRects.top,
|
|
|
|
behavior: 'smooth',
|
|
|
|
scrollTime: options.scrollTime
|
|
|
|
} as any);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/* if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
|
|
// commonjs
|
|
|
|
module.exports = { polyfill: polyfill };
|
|
|
|
} else {
|
|
|
|
// global
|
|
|
|
polyfill();
|
|
|
|
} */
|