UI for Zipcoin Blue

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import { pointerCoord } from './dom';
  2. var 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. }());
  437. export { ScrollView };
  438. var SCROLL_END_DEBOUNCE_MS = 80;
  439. var MIN_VELOCITY_START_DECELERATION = 4;
  440. var MIN_VELOCITY_CONTINUE_DECELERATION = 0.12;
  441. var DECELERATION_FRICTION = 0.97;
  442. var FRAME_MS = (1000 / 60);
  443. var EVENT_OPTS = {
  444. passive: true,
  445. zone: false
  446. };
  447. //# sourceMappingURL=scroll-view.js.map