181 lines
4.2 KiB
JavaScript
181 lines
4.2 KiB
JavaScript
import * as doms from 'shared/utils/dom';
|
|
|
|
const SCROLL_DELTA_X = 64;
|
|
const SCROLL_DELTA_Y = 64;
|
|
|
|
// dirty way to store scrolling state on globally
|
|
let scrolling = false;
|
|
let lastTimeoutId = null;
|
|
|
|
const isScrollableStyle = (element) => {
|
|
let { overflowX, overflowY } = window.getComputedStyle(element);
|
|
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
|
|
overflowY !== 'scroll' && overflowY !== 'auto');
|
|
};
|
|
|
|
const isOverflowed = (element) => {
|
|
return element.scrollWidth > element.clientWidth ||
|
|
element.scrollHeight > element.clientHeight;
|
|
};
|
|
|
|
// Find a visiable and scrollable element by depth-first search. Currently
|
|
// this method is called by each scrolling, and the returned value of this
|
|
// method is not cached. That does not cause performance issue because in the
|
|
// most pages, the window is root element i,e, documentElement.
|
|
const findScrollable = (element) => {
|
|
if (isScrollableStyle(element) && isOverflowed(element)) {
|
|
return element;
|
|
}
|
|
|
|
let children = Array.from(element.children).filter(doms.isVisible);
|
|
for (let child of children) {
|
|
let scrollable = findScrollable(child);
|
|
if (scrollable) {
|
|
return scrollable;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const scrollTarget = () => {
|
|
if (isOverflowed(window.document.documentElement)) {
|
|
return window.document.documentElement;
|
|
}
|
|
if (isOverflowed(window.document.body)) {
|
|
return window.document.body;
|
|
}
|
|
let target = findScrollable(window.document.documentElement);
|
|
if (target) {
|
|
return target;
|
|
}
|
|
return window.document.documentElement;
|
|
};
|
|
|
|
const resetScrolling = () => {
|
|
scrolling = false;
|
|
};
|
|
|
|
class Scroller {
|
|
constructor(element, smooth) {
|
|
this.element = element;
|
|
this.smooth = smooth;
|
|
}
|
|
|
|
scrollTo(x, y) {
|
|
let behavior = this.smooth ? 'smooth' : 'auto';
|
|
window.scrollTo({
|
|
left: x,
|
|
top: y,
|
|
behavior: behavior,
|
|
});
|
|
if (!this.smooth) {
|
|
return;
|
|
}
|
|
this.prepareReset();
|
|
}
|
|
|
|
scrollBy(x, y) {
|
|
let behavior = this.smooth ? 'smooth' : 'auto';
|
|
window.scrollBy({
|
|
left: x,
|
|
top: y,
|
|
behavior: behavior,
|
|
});
|
|
if (!this.smooth) {
|
|
return;
|
|
}
|
|
this.prepareReset();
|
|
}
|
|
|
|
prepareReset() {
|
|
scrolling = true;
|
|
if (lastTimeoutId) {
|
|
clearTimeout(lastTimeoutId);
|
|
lastTimeoutId = null;
|
|
}
|
|
lastTimeoutId = setTimeout(resetScrolling, 100);
|
|
}
|
|
}
|
|
|
|
class RoughtScroller {
|
|
constructor(element) {
|
|
this.element = element;
|
|
}
|
|
|
|
scroll(x, y) {
|
|
this.element.scrollTo(x, y);
|
|
}
|
|
}
|
|
|
|
const getScroll = () => {
|
|
let target = scrollTarget();
|
|
return { x: target.scrollLeft, y: target.scrollTop };
|
|
};
|
|
|
|
const scrollVertically = (count, smooth) => {
|
|
let target = scrollTarget();
|
|
let delta = SCROLL_DELTA_Y * count;
|
|
if (scrolling) {
|
|
delta = SCROLL_DELTA_Y * count * 4;
|
|
}
|
|
new Scroller(target, smooth).scrollBy(0, delta);
|
|
};
|
|
|
|
const scrollHorizonally = (count, smooth) => {
|
|
let target = scrollTarget();
|
|
let delta = SCROLL_DELTA_X * count;
|
|
if (scrolling) {
|
|
delta = SCROLL_DELTA_X * count * 4;
|
|
}
|
|
new Scroller(target, smooth).scrollBy(delta, 0);
|
|
};
|
|
|
|
const scrollPages = (count, smooth) => {
|
|
let target = scrollTarget();
|
|
let height = target.clientHeight;
|
|
let delta = height * count;
|
|
if (scrolling) {
|
|
delta = height * count;
|
|
}
|
|
new Scroller(target, smooth).scrollBy(0, delta);
|
|
};
|
|
|
|
const scrollTo = (x, y, smooth) => {
|
|
let target = scrollTarget();
|
|
new Scroller(target, smooth).scrollTo(x, y);
|
|
};
|
|
|
|
const scrollToTop = (smooth) => {
|
|
let target = scrollTarget();
|
|
let x = target.scrollLeft;
|
|
let y = 0;
|
|
new Scroller(target, smooth).scrollTo(x, y);
|
|
};
|
|
|
|
const scrollToBottom = (smooth) => {
|
|
let target = scrollTarget();
|
|
let x = target.scrollLeft;
|
|
let y = target.scrollHeight;
|
|
new Scroller(target, smooth).scrollTo(x, y);
|
|
};
|
|
|
|
const scrollToHome = (smooth) => {
|
|
let target = scrollTarget();
|
|
let x = 0;
|
|
let y = target.scrollTop;
|
|
new Scroller(target, smooth).scrollTo(x, y);
|
|
};
|
|
|
|
const scrollToEnd = (smooth) => {
|
|
let target = scrollTarget();
|
|
let x = target.scrollWidth;
|
|
let y = target.scrollTop;
|
|
new Scroller(target, smooth).scrollTo(x, y);
|
|
};
|
|
|
|
export {
|
|
getScroll,
|
|
scrollVertically, scrollHorizonally, scrollPages,
|
|
scrollTo,
|
|
scrollToTop, scrollToBottom, scrollToHome, scrollToEnd
|
|
};
|