aboutsummaryrefslogtreecommitdiff
path: root/src/content/presenters/ScrollPresenter.ts
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-11 11:51:29 +0900
committerShin'ya Ueoka <ueokande@i-beam.org>2019-05-11 11:51:29 +0900
commitad1f3c07fbb90c4e69cc2374d74a7373e4da70f2 (patch)
treeae73b962860b334a760137a557dfa9b3c0cd285c /src/content/presenters/ScrollPresenter.ts
parent1ba1660269b24446e9df7df0016de8c3e5596c8f (diff)
Make scroller as a presenter
Diffstat (limited to 'src/content/presenters/ScrollPresenter.ts')
-rw-r--r--src/content/presenters/ScrollPresenter.ts179
1 files changed, 179 insertions, 0 deletions
diff --git a/src/content/presenters/ScrollPresenter.ts b/src/content/presenters/ScrollPresenter.ts
new file mode 100644
index 0000000..9f47394
--- /dev/null
+++ b/src/content/presenters/ScrollPresenter.ts
@@ -0,0 +1,179 @@
+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: number | null = null;
+
+const isScrollableStyle = (element: Element): boolean => {
+ let { overflowX, overflowY } = window.getComputedStyle(element);
+ return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
+ overflowY !== 'scroll' && overflowY !== 'auto');
+};
+
+const isOverflowed = (element: Element): boolean => {
+ 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: Element): Element | null => {
+ 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 {
+ private element: Element;
+
+ private smooth: boolean;
+
+ constructor(element: Element, smooth: boolean) {
+ this.element = element;
+ this.smooth = smooth;
+ }
+
+ scrollTo(x: number, y: number): void {
+ if (!this.smooth) {
+ this.element.scrollTo(x, y);
+ return;
+ }
+ this.element.scrollTo({
+ left: x,
+ top: y,
+ behavior: 'smooth',
+ });
+ this.prepareReset();
+ }
+
+ scrollBy(x: number, y: number): void {
+ let left = this.element.scrollLeft + x;
+ let top = this.element.scrollTop + y;
+ this.scrollTo(left, top);
+ }
+
+ prepareReset(): void {
+ scrolling = true;
+ if (lastTimeoutId) {
+ clearTimeout(lastTimeoutId);
+ lastTimeoutId = null;
+ }
+ lastTimeoutId = setTimeout(resetScrolling, 100);
+ }
+}
+
+type Point = { x: number, y: number };
+
+export default interface ScrollPresenter {
+ getScroll(): Point;
+ scrollVertically(amount: number, smooth: boolean): void;
+ scrollHorizonally(amount: number, smooth: boolean): void;
+ scrollPages(amount: number, smooth: boolean): void;
+ scrollTo(x: number, y: number, smooth: boolean): void;
+ scrollToTop(smooth: boolean): void;
+ scrollToBottom(smooth: boolean): void;
+ scrollToHome(smooth: boolean): void;
+ scrollToEnd(smooth: boolean): void;
+
+ // eslint-disable-next-line semi
+}
+
+export class ScrollPresenterImpl {
+ getScroll(): Point {
+ let target = scrollTarget();
+ return { x: target.scrollLeft, y: target.scrollTop };
+ }
+
+ scrollVertically(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let delta = SCROLL_DELTA_Y * count;
+ if (scrolling) {
+ delta = SCROLL_DELTA_Y * count * 4;
+ }
+ new Scroller(target, smooth).scrollBy(0, delta);
+ }
+
+ scrollHorizonally(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let delta = SCROLL_DELTA_X * count;
+ if (scrolling) {
+ delta = SCROLL_DELTA_X * count * 4;
+ }
+ new Scroller(target, smooth).scrollBy(delta, 0);
+ }
+
+ scrollPages(count: number, smooth: boolean): void {
+ let target = scrollTarget();
+ let height = target.clientHeight;
+ let delta = height * count;
+ if (scrolling) {
+ delta = height * count;
+ }
+ new Scroller(target, smooth).scrollBy(0, delta);
+ }
+
+ scrollTo(x: number, y: number, smooth: boolean): void {
+ let target = scrollTarget();
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+
+ scrollToTop(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollLeft;
+ let y = 0;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+
+ scrollToBottom(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollLeft;
+ let y = target.scrollHeight;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+
+ scrollToHome(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = 0;
+ let y = target.scrollTop;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+
+ scrollToEnd(smooth: boolean): void {
+ let target = scrollTarget();
+ let x = target.scrollWidth;
+ let y = target.scrollTop;
+ new Scroller(target, smooth).scrollTo(x, y);
+ }
+}