1 | import Component, { ComponentEvent } from "@egjs/component";
|
2 | import { CONTAINER_CLASS_NAME, IS_IOS } from "./consts";
|
3 | import { OnChangeScroll } from "./types";
|
4 | import { isWindow, toArray } from "./utils";
|
5 |
|
6 | export interface ScrollManagerOptions {
|
7 | container?: HTMLElement | boolean | string;
|
8 | containerTag?: string;
|
9 | horizontal?: boolean;
|
10 | }
|
11 |
|
12 | export interface ScrollManagerStatus {
|
13 | contentSize: number;
|
14 | scrollOffset: number;
|
15 | prevScrollPos: number;
|
16 | }
|
17 |
|
18 |
|
19 | export interface ScrollManagerEvents {
|
20 | scroll: OnChangeScroll;
|
21 | }
|
22 |
|
23 | export 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 |
|
186 | container = containerOption;
|
187 | } else if (containerOption === true) {
|
188 |
|
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 |
|
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 | }
|