lib/scrolls.js
import { delay, waitForPredicate } from './delays';
/** @ignore */
let isBadFF;
/**
* Scrolls the supplied element into view.
*
* Scroll behavior:
* - behavior: smooth
* - block: center
* - inline: center
* @param {Element} elem - The element to be scrolled into view
* @param {Object} [opts] - Optional scroll behavior to be
* used rather than the default
* @return {void}
*/
export function scrollIntoView(elem, opts) {
if (elem == null) return;
if (isBadFF == null) {
isBadFF = /Firefox\/57(?:\.[0-9]+)?/i.test(window.navigator.userAgent);
}
const defaults = isBadFF
? { behavior: 'smooth', inline: 'center' }
: {
behavior: 'smooth',
block: 'center',
inline: 'center',
};
elem.scrollIntoView(
opts && typeof opts === 'object' ? Object.assign(defaults, opts) : defaults
);
}
/**
* Scrolls the supplied element into view, afterwards waits for the specified delay time
*
* Scroll behavior:
* - behavior: smooth
* - block: center
* - inline: center
* @param {Element|HTMLElement|Node} elem - The element to be scrolled into view with delay
* @param {number} [delayTime = 1000] - How long is the delay
* @return {Promise<void>}
*/
export function scrollIntoViewWithDelay(elem, delayTime) {
scrollIntoView(elem);
return delay(delayTime || 1000);
}
/**
* Scrolls the supplied element into view and then waits for the supplied predicate function to return true
*
* Scroll behavior:
* - behavior: smooth
* - block: center
* - inline: center
* @param {Element|HTMLElement|Node} elem - The element to be scrolled into view with delay
* @param {function(): boolean} predicate - Function returning T/F to indicate when the
* condition waited for has been satisfied
* @param {{wait: ?WaitForOptions, scrollBehavior: ?Object}} [options] - Options controlling
* the scroll behavior and how the wait will happen
* @return {Promise<{predicate: boolean, maxExceeded: boolean}>}
*/
export function scrollIntoViewAndWaitFor(elem, predicate, options) {
scrollIntoView(elem, options && options.scrollBehavior);
return waitForPredicate(predicate, options && options.wait);
}
/**
* Scrolls the window by the supplied elements offsetTop. If the elements
* offsetTop is zero then {@link scrollIntoView} is used
* @param {Element|HTMLElement|Node} elem - The element who's offsetTop will be used to scroll by
* @param {string} [behavior] - Options controlling the behavior of window.scrollTo
* defaults to auto
* @return {void}
*/
export function scrollToElemOffset(elem, behavior) {
if (elem.offsetTop === 0) {
return scrollIntoView(elem);
}
window.scrollTo({
behavior: behavior || 'auto',
left: 0,
top: elem.offsetTop,
});
}
/**
* Scrolls the window by the supplied elements offsetTop. If the elements
* offsetTop is zero then {@link scrollIntoView} is used
* @param {Element|HTMLElement|Node} elem - The element who's offsetTop will be used to scroll by
* @param {number} [delayTime = 1000] - How long is the delay
* @return {Promise<void>}
*/
export function scrollToElemOffsetWithDelay(elem, delayTime) {
scrollToElemOffset(elem);
return delay(delayTime || 1000);
}
/**
* Scrolls down by the elements height
* @param {Element|HTMLElement|Node} elem - The element who's height
* to scroll down by
* @return {void}
*/
export function scrollDownByElemHeight(elem) {
if (!elem) return;
const rect = elem.getBoundingClientRect();
scrollWindowBy(0, rect.height + elem.offsetHeight);
}
/**
* Scrolls down by supplied elements height and then waits for
* the supplied delay time.
* @param {Element|HTMLElement|Node} elem - The element to be
* @param {number} [delayTime = 1000] - How long is the delay
* @return {Promise<void>}
*/
export function scrollDownByElemHeightWithDelay(elem, delayTime) {
scrollDownByElemHeight(elem);
return delay(delayTime || 1000);
}
/**
* Determines if we can scroll down any more
* @return {boolean} - T/F indicating if we can scroll down some more
*/
export function canScrollDownMore() {
return (
window.scrollY + window.innerHeight <
Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.body.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight,
document.documentElement.clientHeight
)
);
}
/**
* Determines if we can scroll up any more
* @return {boolean} - T/F indicating if we can scroll up some more
*/
export function canScrollUpMore() {
return window.scrollY !== 0;
}
/**
* Scrolls the window by the supplied x and y amount
* @param {number} x - Amount to scroll in the x direction
* @param {number} y - Amount to scroll in the y direction
*/
export function scrollWindowBy(x, y) {
window.scrollBy(x, y);
}
/**
* Scrolls the window by the supplied x and y amount smoothly
* @param {number} x - Amount to scroll in the x direction
* @param {number} y - Amount to scroll in the y direction
*/
export function smoothScrollWindowBy(x, y) {
window.scrollBy({ left: x, top: y, behavior: 'smooth' });
}
/**
* Scrolls the window down by the supplied amount
* @param {number} amount - Amount to scroll the down by
*/
export function scrollWindowDownBy(amount) {
scrollWindowBy(0, amount);
}
/**
* Scrolls the window down by the supplied amount smoothly
* @param {number} amount - Amount to scroll the down by
*/
export function smoothScrollWindowDownBy(amount) {
smoothScrollWindowBy(0, amount);
}
/**
* Scrolls the window by the supplied x and y amount
* @param {number} x - Amount to scroll in the x direction
* @param {number} y - Amount to scroll in the y direction
* @param {number} [delayTime = 1500]
* @return {Promise<void>}
*/
export function scrollWindowByWithDelay(x, y, delayTime) {
scrollWindowBy(x, y);
return delay(delayTime || 1500);
}
/**
* Scrolls the window down by the supplied amount
* @param {number} amount - Amount to scroll the down by
* @param {number} [delayTime = 1500]
* @return {Promise<void>}
*/
export function scrollWindowDownByWithDelay(amount, delayTime) {
scrollWindowBy(0, amount);
return delay(delayTime || 1500);
}
/**
* Creates and returns an object that calculates the scroll amount, up/down or left/right,
* based on dividing the scroll width/height by the supplied desired times to completely
* scroll the page, defaults to 10.
*
* The calculation also takes into account the fact that the height or width of
* the document may change and reacts to those changes such that the returned scroll
* amount is always proportional to the documents current maximum scroll height or width
*
* @param {number} [desiredTimesToScroll = 10] - How many scrolls until the maximum scroll
* position is reached
* @return {{timesToScroll: number, scrollUpDownAmount: number, scrollLeftRightAmount: number}}
*/
export function createScrollAmount(desiredTimesToScroll) {
let docsBoundingCRect = document.documentElement.getBoundingClientRect();
const getMaxUpDown = () =>
Math.max(
document.scrollingElement.scrollHeight,
document.documentElement.scrollHeight,
docsBoundingCRect.bottom
);
const getMaxLeftRight = () =>
Math.max(
document.scrollingElement.scrollWidth,
document.documentElement.scrollWidth,
docsBoundingCRect.right
);
let lastUpDownMax = getMaxUpDown();
let lastLeftRightMax = getMaxLeftRight();
return {
timesToScroll: desiredTimesToScroll || 10,
get scrollUpDownAmount() {
let currentMax = getMaxUpDown();
if (currentMax !== lastUpDownMax) {
docsBoundingCRect = document.documentElement.getBoundingClientRect();
currentMax = getMaxUpDown();
lastUpDownMax = currentMax;
}
return currentMax / this.timesToScroll;
},
get scrollLeftRightAmount() {
let currentMax = getMaxLeftRight();
if (currentMax !== lastLeftRightMax) {
docsBoundingCRect = document.documentElement.getBoundingClientRect();
currentMax = getMaxLeftRight();
lastLeftRightMax = currentMax;
}
return currentMax / this.timesToScroll;
},
};
}
/**
* @typedef {Object} Scroller
* @property {number} timesToScroll - the desired number of times to be scrolled
* @property {number} scrollUpDownAmount - The current amount to be scrolled up or down
* @property {number} scrollLeftRightAmount - The current amount to be scrolled left or right
* @property {function(): void} scrollRight - Scroll the page right
* @property {function(): void} scrollLeft - Scroll the page left
* @property {function(): void} scrollUp - Scroll the page up
* @property {function(): void} scrollDown - Scroll the page down
* @property {function(): boolean} canScrollDownMore - Can the page be scrolled some more down
* @property {function(): boolean} canScrollUpMore - Can the page be scrolled some more up
*/
/**
* Creates and returns an object for scrolling the page up/down and left/right.
* The number of times a respective scroll has occurred is also tracked.
*
* For additional details see the documentation of {@link createScrollAmount}
* as the amount of scroll applied is determined the object it returns.
*
* @param {number} [timesToScroll = 10] - How many scrolls until the maximum scroll
* position is reached
* @return {Scroller}
*/
export function createScroller(timesToScroll) {
const scrollAmount = createScrollAmount(timesToScroll || 10);
return Object.assign(
{
canScrollDownMore,
canScrollUpMore,
scrollDown() {
scrollWindowBy(0, this.scrollUpDownAmount);
},
scrollUp() {
scrollWindowBy(0, -this.scrollUpDownAmount);
},
scrollLeft() {
scrollWindowBy(-this.scrollLeftRightAmount, 0);
},
scrollRight() {
scrollWindowBy(this.scrollLeftRightAmount, 0);
},
},
scrollAmount
);
}