aboutsummaryrefslogtreecommitdiff
path: root/src/content/presenters/ScrollPresenter.ts
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2019-05-19 15:59:05 +0900
committerGitHub <noreply@github.com>2019-05-19 15:59:05 +0900
commit3f4bc62ed515f1c5da90ee1c3e42f3d435ea6e39 (patch)
tree8af9f8e5b12d007ce9628b40f3046b73f18e29f8 /src/content/presenters/ScrollPresenter.ts
parent6ec560bca33e774ff7e363270c423c919fdcf4ce (diff)
parentc4dcdff9844e2404e3bc035f4cea9fce2f7770ab (diff)
Merge pull request #587 from ueokande/refactor-content
Refactor content scripts
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..9286fb0
--- /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);
+ }
+}
+
+export 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);
+ }
+}