UNPKG

17 kBJavaScriptView Raw
1import { pointerCoord } from './dom';
2var ScrollView = (function () {
3 function ScrollView(_app, _plt, _dom) {
4 this._app = _app;
5 this._plt = _plt;
6 this._dom = _dom;
7 this.isScrolling = false;
8 this.initialized = false;
9 this._eventsEnabled = false;
10 this._t = 0;
11 this._l = 0;
12 this.ev = {
13 timeStamp: 0,
14 scrollTop: 0,
15 scrollLeft: 0,
16 scrollHeight: 0,
17 scrollWidth: 0,
18 contentHeight: 0,
19 contentWidth: 0,
20 contentTop: 0,
21 contentBottom: 0,
22 startY: 0,
23 startX: 0,
24 deltaY: 0,
25 deltaX: 0,
26 velocityY: 0,
27 velocityX: 0,
28 directionY: 'down',
29 directionX: null,
30 domWrite: _dom.write.bind(_dom)
31 };
32 }
33 ScrollView.prototype.init = function (ele, contentTop, contentBottom) {
34 (void 0) /* assert */;
35 this._el = ele;
36 if (!this.initialized) {
37 this.initialized = true;
38 if (this._js) {
39 this.enableJsScroll(contentTop, contentBottom);
40 }
41 else {
42 this.enableNativeScrolling();
43 }
44 }
45 };
46 ScrollView.prototype.enableEvents = function () {
47 this._eventsEnabled = true;
48 };
49 ScrollView.prototype.setScrolling = function (isScrolling, ev) {
50 if (this.isScrolling) {
51 if (isScrolling) {
52 this.onScroll && this.onScroll(ev);
53 }
54 else {
55 this.isScrolling = false;
56 this.onScrollEnd && this.onScrollEnd(ev);
57 }
58 }
59 else if (isScrolling) {
60 this.isScrolling = true;
61 this.onScrollStart && this.onScrollStart(ev);
62 }
63 };
64 ScrollView.prototype.enableNativeScrolling = function () {
65 (void 0) /* assert */;
66 (void 0) /* assert */;
67 (void 0) /* assert */;
68 this._js = false;
69 if (!this._el) {
70 return;
71 }
72 (void 0) /* console.debug */;
73 var self = this;
74 var ev = self.ev;
75 var positions = [];
76 function scrollCallback(scrollEvent) {
77 // remind the app that it's currently scrolling
78 self._app.setScrolling();
79 // if events are disabled, we do nothing
80 if (!self._eventsEnabled) {
81 return;
82 }
83 ev.timeStamp = scrollEvent.timeStamp;
84 // Event.timeStamp is 0 in firefox
85 if (!ev.timeStamp) {
86 ev.timeStamp = Date.now();
87 }
88 // get the current scrollTop
89 // ******** DOM READ ****************
90 ev.scrollTop = self.getTop();
91 // get the current scrollLeft
92 // ******** DOM READ ****************
93 ev.scrollLeft = self.getLeft();
94 if (!self.isScrolling) {
95 // remember the start positions
96 ev.startY = ev.scrollTop;
97 ev.startX = ev.scrollLeft;
98 // new scroll, so do some resets
99 ev.velocityY = ev.velocityX = 0;
100 ev.deltaY = ev.deltaX = 0;
101 positions.length = 0;
102 }
103 // actively scrolling
104 positions.push(ev.scrollTop, ev.scrollLeft, ev.timeStamp);
105 if (positions.length > 3) {
106 // we've gotten at least 2 scroll events so far
107 ev.deltaY = (ev.scrollTop - ev.startY);
108 ev.deltaX = (ev.scrollLeft - ev.startX);
109 var endPos = (positions.length - 1);
110 var startPos = endPos;
111 var timeRange = (ev.timeStamp - 100);
112 // move pointer to position measured 100ms ago
113 for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 3) {
114 startPos = i;
115 }
116 if (startPos !== endPos) {
117 // compute relative movement between these two points
118 var movedTop = (positions[startPos - 2] - positions[endPos - 2]);
119 var movedLeft = (positions[startPos - 1] - positions[endPos - 1]);
120 var factor = FRAME_MS / (positions[endPos] - positions[startPos]);
121 // based on XXms compute the movement to apply for each render step
122 ev.velocityY = movedTop * factor;
123 ev.velocityX = movedLeft * factor;
124 // figure out which direction we're scrolling
125 ev.directionY = (movedTop > 0 ? 'up' : 'down');
126 ev.directionX = (movedLeft > 0 ? 'left' : 'right');
127 }
128 }
129 function scrollEnd() {
130 // reset velocity, do not reset the directions or deltas
131 ev.velocityY = ev.velocityX = 0;
132 // emit that the scroll has ended
133 self.setScrolling(false, ev);
134 self._endTmr = null;
135 }
136 // emit on each scroll event
137 self.setScrolling(true, ev);
138 // debounce for a moment after the last scroll event
139 self._dom.cancel(self._endTmr);
140 self._endTmr = self._dom.read(scrollEnd, SCROLL_END_DEBOUNCE_MS);
141 }
142 // clear out any existing listeners (just to be safe)
143 self._lsn && self._lsn();
144 // assign the raw scroll listener
145 // note that it does not have a wrapping requestAnimationFrame on purpose
146 // a scroll event callback will always be right before the raf callback
147 // so there's little to no value of using raf here since it'll all ways immediately
148 // call the raf if it was set within the scroll event, so this will save us some time
149 self._lsn = self._plt.registerListener(self._el, 'scroll', scrollCallback, EVENT_OPTS);
150 };
151 /**
152 * @hidden
153 * JS Scrolling has been provided only as a temporary solution
154 * until iOS apps can take advantage of scroll events at all times.
155 * The goal is to eventually remove JS scrolling entirely. When we
156 * no longer have to worry about iOS not firing scroll events during
157 * inertia then this can be burned to the ground. iOS's more modern
158 * WKWebView does not have this issue, only UIWebView does.
159 */
160 ScrollView.prototype.enableJsScroll = function (contentTop, contentBottom) {
161 var self = this;
162 self._js = true;
163 var ele = self._el;
164 if (!ele) {
165 return;
166 }
167 (void 0) /* console.debug */;
168 var ev = self.ev;
169 var positions = [];
170 var rafCancel;
171 var max;
172 function setMax() {
173 if (!max) {
174 // ******** DOM READ ****************
175 max = ele.scrollHeight - ele.parentElement.offsetHeight + contentTop + contentBottom;
176 }
177 }
178 function jsScrollDecelerate(timeStamp) {
179 ev.timeStamp = timeStamp;
180 (void 0) /* console.debug */;
181 if (ev.velocityY) {
182 ev.velocityY *= DECELERATION_FRICTION;
183 // update top with updated velocity
184 // clamp top within scroll limits
185 // ******** DOM READ ****************
186 setMax();
187 self._t = Math.min(Math.max(self._t + ev.velocityY, 0), max);
188 ev.scrollTop = self._t;
189 // emit on each scroll event
190 self.onScroll(ev);
191 self._dom.write(function () {
192 // ******** DOM WRITE ****************
193 self.setTop(self._t);
194 if (self._t > 0 && self._t < max && Math.abs(ev.velocityY) > MIN_VELOCITY_CONTINUE_DECELERATION) {
195 rafCancel = self._dom.read(function (rafTimeStamp) {
196 jsScrollDecelerate(rafTimeStamp);
197 });
198 }
199 else {
200 // haven't scrolled in a while, so it's a scrollend
201 self.isScrolling = false;
202 // reset velocity, do not reset the directions or deltas
203 ev.velocityY = ev.velocityX = 0;
204 // emit that the scroll has ended
205 self.onScrollEnd(ev);
206 }
207 });
208 }
209 }
210 function jsScrollTouchStart(touchEvent) {
211 positions.length = 0;
212 max = null;
213 self._dom.cancel(rafCancel);
214 positions.push(pointerCoord(touchEvent).y, touchEvent.timeStamp);
215 }
216 function jsScrollTouchMove(touchEvent) {
217 if (!positions.length) {
218 return;
219 }
220 ev.timeStamp = touchEvent.timeStamp;
221 var y = pointerCoord(touchEvent).y;
222 // ******** DOM READ ****************
223 setMax();
224 self._t -= (y - positions[positions.length - 2]);
225 self._t = Math.min(Math.max(self._t, 0), max);
226 positions.push(y, ev.timeStamp);
227 if (!self.isScrolling) {
228 // remember the start position
229 ev.startY = self._t;
230 // new scroll, so do some resets
231 ev.velocityY = ev.deltaY = 0;
232 self.isScrolling = true;
233 // emit only on the first scroll event
234 self.onScrollStart(ev);
235 }
236 self._dom.write(function () {
237 // ******** DOM WRITE ****************
238 self.setTop(self._t);
239 });
240 }
241 function jsScrollTouchEnd(touchEvent) {
242 // figure out what the scroll position was about 100ms ago
243 self._dom.cancel(rafCancel);
244 if (!positions.length && self.isScrolling) {
245 self.isScrolling = false;
246 ev.velocityY = ev.velocityX = 0;
247 self.onScrollEnd(ev);
248 return;
249 }
250 var y = pointerCoord(touchEvent).y;
251 positions.push(y, touchEvent.timeStamp);
252 var endPos = (positions.length - 1);
253 var startPos = endPos;
254 var timeRange = (touchEvent.timeStamp - 100);
255 // move pointer to position measured 100ms ago
256 for (var i = endPos; i > 0 && positions[i] > timeRange; i -= 2) {
257 startPos = i;
258 }
259 if (startPos !== endPos) {
260 // compute relative movement between these two points
261 var timeOffset = (positions[endPos] - positions[startPos]);
262 var movedTop = (positions[startPos - 1] - positions[endPos - 1]);
263 // based on XXms compute the movement to apply for each render step
264 ev.velocityY = ((movedTop / timeOffset) * FRAME_MS);
265 // verify that we have enough velocity to start deceleration
266 if (Math.abs(ev.velocityY) > MIN_VELOCITY_START_DECELERATION) {
267 // ******** DOM READ ****************
268 setMax();
269 rafCancel = self._dom.read(function (rafTimeStamp) {
270 jsScrollDecelerate(rafTimeStamp);
271 });
272 }
273 }
274 else {
275 self.isScrolling = false;
276 ev.velocityY = 0;
277 self.onScrollEnd(ev);
278 }
279 positions.length = 0;
280 }
281 var plt = self._plt;
282 var unRegStart = plt.registerListener(ele, 'touchstart', jsScrollTouchStart, EVENT_OPTS);
283 var unRegMove = plt.registerListener(ele, 'touchmove', jsScrollTouchMove, EVENT_OPTS);
284 var unRegEnd = plt.registerListener(ele, 'touchend', jsScrollTouchEnd, EVENT_OPTS);
285 ele.parentElement.classList.add('js-scroll');
286 // stop listening for actual scroll events
287 self._lsn && self._lsn();
288 // create an unregister for all of these events
289 self._lsn = function () {
290 unRegStart();
291 unRegMove();
292 unRegEnd();
293 ele.parentElement.classList.remove('js-scroll');
294 };
295 };
296 /**
297 * DOM READ
298 */
299 ScrollView.prototype.getTop = function () {
300 if (this._js) {
301 return this._t;
302 }
303 return this._t = this._el.scrollTop;
304 };
305 /**
306 * DOM READ
307 */
308 ScrollView.prototype.getLeft = function () {
309 if (this._js) {
310 return 0;
311 }
312 return this._l = this._el.scrollLeft;
313 };
314 /**
315 * DOM WRITE
316 */
317 ScrollView.prototype.setTop = function (top) {
318 this._t = top;
319 if (this._js) {
320 this._el.style[this._plt.Css.transform] = "translate3d(" + this._l * -1 + "px," + top * -1 + "px,0px)";
321 }
322 else {
323 this._el.scrollTop = top;
324 }
325 };
326 /**
327 * DOM WRITE
328 */
329 ScrollView.prototype.setLeft = function (left) {
330 this._l = left;
331 if (this._js) {
332 this._el.style[this._plt.Css.transform] = "translate3d(" + left * -1 + "px," + this._t * -1 + "px,0px)";
333 }
334 else {
335 this._el.scrollLeft = left;
336 }
337 };
338 ScrollView.prototype.scrollTo = function (x, y, duration, done) {
339 // scroll animation loop w/ easing
340 // credit https://gist.github.com/dezinezync/5487119
341 var promise;
342 if (done === undefined) {
343 // only create a promise if a done callback wasn't provided
344 // done can be a null, which avoids any functions
345 promise = new Promise(function (resolve) {
346 done = resolve;
347 });
348 }
349 var self = this;
350 var el = self._el;
351 if (!el) {
352 // invalid element
353 done();
354 return promise;
355 }
356 if (duration < 32) {
357 self.setTop(y);
358 self.setLeft(x);
359 done();
360 return promise;
361 }
362 var fromY = el.scrollTop;
363 var fromX = el.scrollLeft;
364 var maxAttempts = (duration / 16) + 100;
365 var transform = self._plt.Css.transform;
366 var startTime;
367 var attempts = 0;
368 var stopScroll = false;
369 // scroll loop
370 function step(timeStamp) {
371 attempts++;
372 if (!self._el || stopScroll || attempts > maxAttempts) {
373 self.setScrolling(false, null);
374 el.style[transform] = '';
375 done();
376 return;
377 }
378 var time = Math.min(1, ((timeStamp - startTime) / duration));
379 // where .5 would be 50% of time on a linear scale easedT gives a
380 // fraction based on the easing method
381 var easedT = (--time) * time * time + 1;
382 if (fromY !== y) {
383 self.setTop((easedT * (y - fromY)) + fromY);
384 }
385 if (fromX !== x) {
386 self.setLeft(Math.floor((easedT * (x - fromX)) + fromX));
387 }
388 if (easedT < 1) {
389 // do not use DomController here
390 // must use nativeRaf in order to fire in the next frame
391 self._plt.raf(step);
392 }
393 else {
394 stopScroll = true;
395 self.setScrolling(false, null);
396 el.style[transform] = '';
397 done();
398 }
399 }
400 // start scroll loop
401 self.setScrolling(true, null);
402 self.isScrolling = true;
403 // chill out for a frame first
404 self._dom.write(function (timeStamp) {
405 startTime = timeStamp;
406 step(timeStamp);
407 }, 16);
408 return promise;
409 };
410 ScrollView.prototype.scrollToTop = function (duration) {
411 return this.scrollTo(0, 0, duration);
412 };
413 ScrollView.prototype.scrollToBottom = function (duration) {
414 var y = 0;
415 if (this._el) {
416 y = this._el.scrollHeight - this._el.clientHeight;
417 }
418 return this.scrollTo(0, y, duration);
419 };
420 ScrollView.prototype.stop = function () {
421 this.setScrolling(false, null);
422 };
423 /**
424 * @hidden
425 */
426 ScrollView.prototype.destroy = function () {
427 this.stop();
428 this._endTmr && this._dom.cancel(this._endTmr);
429 this._lsn && this._lsn();
430 var ev = this.ev;
431 ev.domWrite = ev.contentElement = ev.fixedElement = ev.scrollElement = ev.headerElement = null;
432 this._lsn = this._el = this._dom = this.ev = ev = null;
433 this.onScrollStart = this.onScroll = this.onScrollEnd = null;
434 };
435 return ScrollView;
436}());
437export { ScrollView };
438var SCROLL_END_DEBOUNCE_MS = 80;
439var MIN_VELOCITY_START_DECELERATION = 4;
440var MIN_VELOCITY_CONTINUE_DECELERATION = 0.12;
441var DECELERATION_FRICTION = 0.97;
442var FRAME_MS = (1000 / 60);
443var EVENT_OPTS = {
444 passive: true,
445 zone: false
446};
447//# sourceMappingURL=scroll-view.js.map
\No newline at end of file