UNPKG

6.96 kBPlain TextView Raw
1import Component, { ComponentEvent } from "@egjs/component";
2import { CONTAINER_CLASS_NAME, IS_IOS } from "./consts";
3import { OnChangeScroll } from "./types";
4import { isWindow, toArray } from "./utils";
5
6export interface ScrollManagerOptions {
7 container?: HTMLElement | boolean | string;
8 containerTag?: string;
9 horizontal?: boolean;
10}
11
12export interface ScrollManagerStatus {
13 contentSize: number;
14 scrollOffset: number;
15 prevScrollPos: number;
16}
17
18
19export interface ScrollManagerEvents {
20 scroll: OnChangeScroll;
21}
22
23export class ScrollManager extends Component<ScrollManagerEvents> {
24 public options: Required<ScrollManagerOptions>;
25 protected prevScrollPos: number | null = null;
26 protected eventTarget: HTMLElement | Window;
27 protected scrollOffset = 0;
28 protected contentSize = 0;
29 protected container: HTMLElement;
30 protected scrollContainer: HTMLElement;
31 private _orgCSSText: string;
32 private _isScrollIssue = IS_IOS;
33 private _isCreateElement: boolean;
34
35 constructor(
36 protected wrapper: HTMLElement,
37 options: ScrollManagerOptions,
38 ) {
39 super();
40 this.options = {
41 container: false,
42 containerTag: "div",
43 horizontal: false,
44 ...options,
45 };
46
47 this._init();
48 }
49 public getWrapper() {
50 return this.wrapper;
51 }
52 public getContainer() {
53 return this.container;
54 }
55 public getScrollContainer() {
56 return this.scrollContainer;
57 }
58 public getScrollOffset() {
59 return this.scrollOffset;
60 }
61 public getContentSize() {
62 return this.contentSize;
63 }
64 public getRelativeScrollPos() {
65 return (this.prevScrollPos || 0) - this.scrollOffset;
66 }
67 public getScrollPos() {
68 return this.prevScrollPos;
69 }
70 public setScrollPos(pos: number) {
71 this.prevScrollPos = pos;
72 }
73 public getOrgScrollPos() {
74 const eventTarget = this.eventTarget;
75 const horizontal = this.options.horizontal;
76
77 const prop = `scroll${horizontal ? "Left" : "Top"}` as "scrollLeft" | "scrollTop";
78
79 if (isWindow(eventTarget)) {
80 return window[horizontal ? "pageXOffset" : "pageYOffset"]
81 || document.documentElement[prop] || document.body[prop];
82 } else {
83 return eventTarget[prop];
84 }
85 }
86 public setStatus(status: ScrollManagerStatus) {
87 this.contentSize = status.contentSize;
88 this.scrollOffset = status.scrollOffset;
89 this.prevScrollPos = status.prevScrollPos;
90
91 this.scrollTo(this.prevScrollPos);
92 }
93 public getStatus(): ScrollManagerStatus {
94 return {
95 contentSize: this.contentSize,
96 scrollOffset: this.scrollOffset,
97 prevScrollPos: this.prevScrollPos!,
98 };
99 }
100 public scrollTo(pos: number) {
101 const eventTarget = this.eventTarget;
102 const horizontal = this.options.horizontal;
103 const [x, y] = horizontal ? [pos, 0] : [0, pos];
104
105 if (isWindow(eventTarget)) {
106 eventTarget.scroll(x, y);
107 } else {
108 eventTarget.scrollLeft = x;
109 eventTarget.scrollTop = y;
110 }
111 }
112 public scrollBy(pos: number) {
113 if (!pos) {
114 return;
115 }
116 const eventTarget = this.eventTarget;
117 const horizontal = this.options.horizontal;
118 const [x, y] = horizontal ? [pos, 0] : [0, pos];
119
120
121 this.prevScrollPos! += pos;
122
123 if (isWindow(eventTarget)) {
124 eventTarget.scrollBy(x, y);
125 } else {
126 eventTarget.scrollLeft += x;
127 eventTarget.scrollTop += y;
128 }
129 }
130 public resize() {
131 const scrollContainer = this.scrollContainer;
132 const horizontal = this.options.horizontal;
133 const scrollContainerRect = scrollContainer === document.body
134 ? { top: 0, left: 0 }
135 : scrollContainer.getBoundingClientRect();
136 const containerRect = this.container.getBoundingClientRect();
137
138 this.scrollOffset = (this.prevScrollPos! || 0) + (horizontal
139 ? containerRect.left - scrollContainerRect.left
140 : containerRect.top - scrollContainerRect.top);
141 this.contentSize = horizontal ? scrollContainer.offsetWidth : scrollContainer.offsetHeight;
142 }
143 public destroy() {
144 const container = this.container;
145
146 this.eventTarget.removeEventListener("scroll", this._onCheck);
147
148 if (this._isCreateElement) {
149 const scrollContainer = this.scrollContainer;
150
151 const fragment = document.createDocumentFragment();
152 const childNodes = toArray(container.childNodes);
153
154 scrollContainer.removeChild(container);
155 childNodes.forEach((childNode) => {
156 fragment.appendChild(childNode);
157 });
158 scrollContainer.appendChild(fragment);
159 } else if (this.options.container) {
160 container.style.cssText = this._orgCSSText;
161 }
162 }
163 private _init() {
164 const {
165 container: containerOption,
166 containerTag,
167 horizontal,
168 } = this.options;
169 const wrapper = this.wrapper;
170 let scrollContainer = wrapper;
171 let container = wrapper;
172 let containerCSSText = "";
173
174 if (!containerOption) {
175 scrollContainer = document.body;
176 containerCSSText = container.style.cssText;
177 } else {
178 if (containerOption instanceof HTMLElement) {
179 // Container that already exists
180 container = containerOption;
181 } else if (containerOption === true) {
182 // Create Container
183 container = document.createElement(containerTag) as HTMLElement;
184
185 container.style.position = "relative";
186 container.className = CONTAINER_CLASS_NAME;
187 const childNodes = toArray(scrollContainer.childNodes);
188
189 childNodes.forEach((childNode) => {
190 container.appendChild(childNode);
191 });
192 scrollContainer.appendChild(container);
193
194 this._isCreateElement = true;
195 } else {
196 // Find Container by Selector
197 container = scrollContainer.querySelector(containerOption) as HTMLElement;
198 }
199 containerCSSText = container.style.cssText;
200
201 const style = scrollContainer.style;
202
203 [style.overflowX, style.overflowY] = horizontal ? ["scroll", "hidden"] : ["hidden", "scroll"];
204
205 if (horizontal) {
206 container.style.height = "100%";
207 }
208 }
209 const eventTarget = scrollContainer === document.body ? window : scrollContainer;
210
211 eventTarget.addEventListener("scroll", this._onCheck);
212 this._orgCSSText = containerCSSText;
213 this.container = container;
214 this.scrollContainer = scrollContainer;
215 this.eventTarget = eventTarget;
216 this.resize();
217 this.setScrollPos(this.getOrgScrollPos());
218 }
219 private _onCheck = () => {
220 const prevScrollPos = this.getScrollPos();
221 const nextScrollPos = this.getOrgScrollPos();
222
223 this.setScrollPos(nextScrollPos);
224
225 if (prevScrollPos === null || (this._isScrollIssue && nextScrollPos === 0) || prevScrollPos === nextScrollPos) {
226 nextScrollPos && (this._isScrollIssue = false);
227 return;
228 }
229 this._isScrollIssue = false;
230 this.trigger(new ComponentEvent("scroll", {
231 direction: prevScrollPos < nextScrollPos ? "end" : "start",
232 scrollPos: nextScrollPos,
233 relativeScrollPos: this.getRelativeScrollPos(),
234 }));
235 }
236}