UNPKG

7.11 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 isBody = scrollContainer === document.body;
134 const scrollContainerRect = isBody
135 ? { top: 0, left: 0 }
136 : scrollContainer.getBoundingClientRect();
137 const containerRect = this.container.getBoundingClientRect();
138
139 this.scrollOffset = (this.prevScrollPos! || 0) + (horizontal
140 ? containerRect.left - scrollContainerRect.left
141 : containerRect.top - scrollContainerRect.top);
142
143 if (isBody) {
144 this.contentSize = horizontal ? window.innerWidth : window.innerHeight;
145 } else {
146 this.contentSize = horizontal ? scrollContainer.offsetWidth : scrollContainer.offsetHeight;
147 }
148 }
149 public destroy() {
150 const container = this.container;
151
152 this.eventTarget.removeEventListener("scroll", this._onCheck);
153
154 if (this._isCreateElement) {
155 const scrollContainer = this.scrollContainer;
156
157 const fragment = document.createDocumentFragment();
158 const childNodes = toArray(container.childNodes);
159
160 scrollContainer.removeChild(container);
161 childNodes.forEach((childNode) => {
162 fragment.appendChild(childNode);
163 });
164 scrollContainer.appendChild(fragment);
165 } else if (this.options.container) {
166 container.style.cssText = this._orgCSSText;
167 }
168 }
169 private _init() {
170 const {
171 container: containerOption,
172 containerTag,
173 horizontal,
174 } = this.options;
175 const wrapper = this.wrapper;
176 let scrollContainer = wrapper;
177 let container = wrapper;
178 let containerCSSText = "";
179
180 if (!containerOption) {
181 scrollContainer = document.body;
182 containerCSSText = container.style.cssText;
183 } else {
184 if (containerOption instanceof HTMLElement) {
185 // Container that already exists
186 container = containerOption;
187 } else if (containerOption === true) {
188 // Create Container
189 container = document.createElement(containerTag) as HTMLElement;
190
191 container.style.position = "relative";
192 container.className = CONTAINER_CLASS_NAME;
193 const childNodes = toArray(scrollContainer.childNodes);
194
195 childNodes.forEach((childNode) => {
196 container.appendChild(childNode);
197 });
198 scrollContainer.appendChild(container);
199
200 this._isCreateElement = true;
201 } else {
202 // Find Container by Selector
203 container = scrollContainer.querySelector(containerOption) as HTMLElement;
204 }
205 containerCSSText = container.style.cssText;
206
207 const style = scrollContainer.style;
208
209 [style.overflowX, style.overflowY] = horizontal ? ["scroll", "hidden"] : ["hidden", "scroll"];
210
211 if (horizontal) {
212 container.style.height = "100%";
213 }
214 }
215 const eventTarget = scrollContainer === document.body ? window : scrollContainer;
216
217 eventTarget.addEventListener("scroll", this._onCheck);
218 this._orgCSSText = containerCSSText;
219 this.container = container;
220 this.scrollContainer = scrollContainer;
221 this.eventTarget = eventTarget;
222 this.resize();
223 this.setScrollPos(this.getOrgScrollPos());
224 }
225 private _onCheck = () => {
226 const prevScrollPos = this.getScrollPos();
227 const nextScrollPos = this.getOrgScrollPos();
228
229 this.setScrollPos(nextScrollPos);
230
231 if (prevScrollPos === null || (this._isScrollIssue && nextScrollPos === 0) || prevScrollPos === nextScrollPos) {
232 nextScrollPos && (this._isScrollIssue = false);
233 return;
234 }
235 this._isScrollIssue = false;
236 this.trigger(new ComponentEvent("scroll", {
237 direction: prevScrollPos < nextScrollPos ? "end" : "start",
238 scrollPos: nextScrollPos,
239 relativeScrollPos: this.getRelativeScrollPos(),
240 }));
241 }
242}