2017-10-28 19:08:07 +05:30
|
|
|
/**
|
|
|
|
* Implements scroll to animation with momentum effect
|
|
|
|
*
|
|
|
|
* @see http://ariya.ofilabs.com/2013/11/javascript-kinetic-scrolling-part-2.html
|
|
|
|
*/
|
|
|
|
|
|
|
|
const TIME_CONSTANT = 100; // higher numbers - slower animation
|
|
|
|
const SCROLL_ANCHOR_OFFSET = 80; // 50 + 30 (header height + some spacing)
|
|
|
|
// Первый скролл выполняется сразу после загрузки страницы, так что чтобы снизить
|
|
|
|
// нагрузку на рендеринг мы откладываем первый скрол на 200ms
|
|
|
|
let isFirstScroll = true;
|
2019-12-07 16:58:52 +05:30
|
|
|
let scrollJob: {
|
|
|
|
hasAmplitude: boolean;
|
|
|
|
start: number;
|
|
|
|
y: number;
|
|
|
|
amplitude: number;
|
|
|
|
} | null = null;
|
2017-10-28 19:08:07 +05:30
|
|
|
|
|
|
|
export function scrollTo(y: number) {
|
2019-11-27 14:33:32 +05:30
|
|
|
if (scrollJob) {
|
|
|
|
// we already scrolling, so simply change the coordinates we are scrolling to
|
|
|
|
if (scrollJob.hasAmplitude) {
|
|
|
|
const delta = y - scrollJob.y;
|
|
|
|
scrollJob.amplitude += delta;
|
|
|
|
}
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
scrollJob.y = y;
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const start = Date.now();
|
|
|
|
let scrollWasTouched = false;
|
|
|
|
scrollJob = {
|
|
|
|
// NOTE: we may use some sort of debounce to wait till we catch all the
|
|
|
|
// scroll requests after app state changes, but the way with hasAmplitude
|
|
|
|
// seems to be more reliable
|
|
|
|
hasAmplitude: false,
|
|
|
|
start,
|
|
|
|
y,
|
|
|
|
amplitude: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
// wrap in requestAnimationFrame to optimize initial reading of scrollTop
|
|
|
|
if (!scrollJob) {
|
|
|
|
return;
|
2017-10-28 19:08:07 +05:30
|
|
|
}
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
const y = normalizeScrollPosition(scrollJob.y);
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
scrollJob.hasAmplitude = true;
|
|
|
|
scrollJob.y = y;
|
|
|
|
scrollJob.amplitude = y - getScrollTop();
|
|
|
|
|
|
|
|
(function animateScroll() {
|
|
|
|
if (!scrollJob) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { start, y, amplitude } = scrollJob;
|
|
|
|
const elapsed = Date.now() - start;
|
|
|
|
|
|
|
|
let delta = -amplitude * Math.exp(-elapsed / TIME_CONSTANT);
|
|
|
|
|
|
|
|
if (Math.abs(delta) > 0.5 && !scrollWasTouched) {
|
|
|
|
requestAnimationFrame(animateScroll);
|
|
|
|
} else {
|
|
|
|
// the last animation frame
|
|
|
|
delta = 0;
|
|
|
|
scrollJob = null;
|
|
|
|
document.removeEventListener('mousewheel', markScrollTouched);
|
|
|
|
document.removeEventListener('touchstart', markScrollTouched);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (scrollWasTouched) {
|
|
|
|
// block any animation visualisation in case, when user touched scroll
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newScrollTop = y + delta;
|
|
|
|
window.scrollTo(0, newScrollTop);
|
|
|
|
})();
|
|
|
|
});
|
|
|
|
|
|
|
|
document.addEventListener('mousewheel', markScrollTouched);
|
|
|
|
document.addEventListener('touchstart', markScrollTouched);
|
|
|
|
function markScrollTouched() {
|
|
|
|
scrollWasTouched = true;
|
|
|
|
}
|
2017-10-28 19:08:07 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensures, that `y` is the coordinate, that can be physically scrolled to
|
|
|
|
*
|
|
|
|
* @param {number} y
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {number}
|
2017-10-28 19:08:07 +05:30
|
|
|
*/
|
|
|
|
function normalizeScrollPosition(y: number): number {
|
2019-11-27 14:33:32 +05:30
|
|
|
const contentHeight =
|
|
|
|
(document.documentElement && document.documentElement.scrollHeight) || 0;
|
|
|
|
const windowHeight: number = window.innerHeight;
|
|
|
|
const maxY = contentHeight - windowHeight;
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return Math.min(y, maxY);
|
2017-10-28 19:08:07 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Scrolls to page's top or #anchor link, if any
|
|
|
|
*
|
|
|
|
* @param {?HTMLElement} targetEl - the element to scroll to
|
|
|
|
*/
|
2019-12-07 16:58:52 +05:30
|
|
|
export function restoreScroll(targetEl: HTMLElement | null = null) {
|
2019-11-27 14:33:32 +05:30
|
|
|
const { hash } = window.location;
|
2018-05-14 01:02:53 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
setTimeout(
|
|
|
|
() => {
|
|
|
|
isFirstScroll = false;
|
2018-05-14 01:02:53 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (targetEl === null) {
|
|
|
|
const id = hash.substr(1);
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (!id) {
|
|
|
|
return;
|
2018-03-26 00:52:43 +05:30
|
|
|
}
|
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
targetEl = document.getElementById(id);
|
|
|
|
}
|
2018-05-14 01:02:53 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
const viewPort = document.body;
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (!viewPort) {
|
|
|
|
console.log('Can not find viewPort element'); // eslint-disable-line
|
2018-05-14 01:02:53 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let y = 0;
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (targetEl) {
|
|
|
|
const { top } = targetEl.getBoundingClientRect();
|
|
|
|
y = getScrollTop() + top - SCROLL_ANCHOR_OFFSET;
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollTo(y);
|
|
|
|
},
|
|
|
|
isFirstScroll ? 200 : 0,
|
|
|
|
);
|
2017-10-28 19:08:07 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* http://stackoverflow.com/a/3464890/5184751
|
|
|
|
*
|
2019-11-27 14:33:32 +05:30
|
|
|
* @returns {number}
|
2017-10-28 19:08:07 +05:30
|
|
|
*/
|
|
|
|
export function getScrollTop(): number {
|
2019-11-27 14:33:32 +05:30
|
|
|
const doc = document.documentElement;
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
if (doc) {
|
|
|
|
return (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
|
|
|
|
}
|
2017-10-28 19:08:07 +05:30
|
|
|
|
2019-11-27 14:33:32 +05:30
|
|
|
return 0;
|
2017-10-28 19:08:07 +05:30
|
|
|
}
|