UNPKG

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