refresher.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { Directive, EventEmitter, Host, Input, NgZone, Output } from '@angular/core';
  2. import { Content } from '../content/content';
  3. import { GESTURE_PRIORITY_REFRESHER, GESTURE_REFRESHER, GestureController } from '../../gestures/gesture-controller';
  4. import { isTrueProperty } from '../../util/util';
  5. import { Platform } from '../../platform/platform';
  6. import { pointerCoord } from '../../util/dom';
  7. import { UIEventManager } from '../../gestures/ui-event-manager';
  8. /**
  9. * @name Refresher
  10. * @description
  11. * The Refresher provides pull-to-refresh functionality on a content component.
  12. * Place the `ion-refresher` as the first child of your `ion-content` element.
  13. *
  14. * Pages can then listen to the refresher's various output events. The
  15. * `refresh` output event is fired when the user has pulled down far
  16. * enough to kick off the refreshing process. Once the async operation
  17. * has completed and the refreshing should end, call `complete()`.
  18. *
  19. * Note: Do not wrap the `ion-refresher` in a `*ngIf`. It will not render
  20. * properly this way. Please use the `enabled` property instead to
  21. * display or hide the refresher.
  22. *
  23. * @usage
  24. * ```html
  25. * <ion-content>
  26. *
  27. * <ion-refresher (ionRefresh)="doRefresh($event)">
  28. * <ion-refresher-content></ion-refresher-content>
  29. * </ion-refresher>
  30. *
  31. * </ion-content>
  32. * ```
  33. *
  34. * ```ts
  35. * @Component({...})
  36. * export class NewsFeedPage {
  37. *
  38. * doRefresh(refresher) {
  39. * console.log('Begin async operation', refresher);
  40. *
  41. * setTimeout(() => {
  42. * console.log('Async operation has ended');
  43. * refresher.complete();
  44. * }, 2000);
  45. * }
  46. *
  47. * }
  48. * ```
  49. *
  50. *
  51. * ## Refresher Content
  52. *
  53. * By default, Ionic provides the pulling icon and refreshing spinner that
  54. * looks best for the platform the user is on. However, you can change the
  55. * default icon and spinner, along with adding text for each state by
  56. * adding properties to the child `ion-refresher-content` component.
  57. *
  58. * ```html
  59. * <ion-content>
  60. *
  61. * <ion-refresher (ionRefresh)="doRefresh($event)">
  62. * <ion-refresher-content
  63. * pullingIcon="arrow-dropdown"
  64. * pullingText="Pull to refresh"
  65. * refreshingSpinner="circles"
  66. * refreshingText="Refreshing...">
  67. * </ion-refresher-content>
  68. * </ion-refresher>
  69. *
  70. * </ion-content>
  71. * ```
  72. *
  73. *
  74. * ## Further Customizing Refresher Content
  75. *
  76. * The `ion-refresher` component holds the refresh logic.
  77. * It requires a child component in order to display the content.
  78. * Ionic uses `ion-refresher-content` by default. This component
  79. * displays the refresher and changes the look depending
  80. * on the refresher's state. Separating these components
  81. * allows developers to create their own refresher content
  82. * components. You could replace our default content with
  83. * custom SVG or CSS animations.
  84. *
  85. * @demo /docs/demos/src/refresher/
  86. *
  87. */
  88. export class Refresher {
  89. constructor(_plt, _content, _zone, gestureCtrl) {
  90. this._plt = _plt;
  91. this._content = _content;
  92. this._zone = _zone;
  93. this._appliedStyles = false;
  94. this._lastCheck = 0;
  95. this._isEnabled = true;
  96. this._top = '';
  97. /**
  98. * The current state which the refresher is in. The refresher's states include:
  99. *
  100. * - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden.
  101. * - `pulling` - The user is actively pulling down the refresher, but has not reached the point yet that if the user lets go, it'll refresh.
  102. * - `cancelling` - The user pulled down the refresher and let go, but did not pull down far enough to kick off the `refreshing` state. After letting go, the refresher is in the `cancelling` state while it is closing, and will go back to the `inactive` state once closed.
  103. * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state.
  104. * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state.
  105. * - `completing` - The `refreshing` state has finished and the refresher is in the process of closing itself. Once closed, the refresher will go back to the `inactive` state.
  106. */
  107. this.state = STATE_INACTIVE;
  108. /**
  109. * The Y coordinate of where the user started to the pull down the content.
  110. */
  111. this.startY = null;
  112. /**
  113. * The current touch or mouse event's Y coordinate.
  114. */
  115. this.currentY = null;
  116. /**
  117. * The distance between the start of the pull and the current touch or
  118. * mouse event's Y coordinate.
  119. */
  120. this.deltaY = null;
  121. /**
  122. * A number representing how far down the user has pulled.
  123. * The number `0` represents the user hasn't pulled down at all. The
  124. * number `1`, and anything greater than `1`, represents that the user
  125. * has pulled far enough down that when they let go then the refresh will
  126. * happen. If they let go and the number is less than `1`, then the
  127. * refresh will not happen, and the content will return to it's original
  128. * position.
  129. */
  130. this.progress = 0;
  131. /**
  132. * @input {number} The min distance the user must pull down until the
  133. * refresher can go into the `refreshing` state. Default is `60`.
  134. */
  135. this.pullMin = 60;
  136. /**
  137. * @input {number} The maximum distance of the pull until the refresher
  138. * will automatically go into the `refreshing` state. By default, the pull
  139. * maximum will be the result of `pullMin + 60`.
  140. */
  141. this.pullMax = this.pullMin + 60;
  142. /**
  143. * @input {number} How many milliseconds it takes to close the refresher. Default is `280`.
  144. */
  145. this.closeDuration = 280;
  146. /**
  147. * @input {number} How many milliseconds it takes the refresher to to snap back to the `refreshing` state. Default is `280`.
  148. */
  149. this.snapbackDuration = 280;
  150. /**
  151. * @output {event} Emitted when the user lets go and has pulled down
  152. * far enough, which would be farther than the `pullMin`, then your refresh hander if
  153. * fired and the state is updated to `refreshing`. From within your refresh handler,
  154. * you must call the `complete()` method when your async operation has completed.
  155. */
  156. this.ionRefresh = new EventEmitter();
  157. /**
  158. * @output {event} Emitted while the user is pulling down the content and exposing the refresher.
  159. */
  160. this.ionPull = new EventEmitter();
  161. /**
  162. * @output {event} Emitted when the user begins to start pulling down.
  163. */
  164. this.ionStart = new EventEmitter();
  165. this._events = new UIEventManager(_plt);
  166. _content._hasRefresher = true;
  167. this._gesture = gestureCtrl.createGesture({
  168. name: GESTURE_REFRESHER,
  169. priority: GESTURE_PRIORITY_REFRESHER
  170. });
  171. }
  172. /**
  173. * @input {boolean} If the refresher is enabled or not. This should be used in place of an `ngIf`. Default is `true`.
  174. */
  175. get enabled() {
  176. return this._isEnabled;
  177. }
  178. set enabled(val) {
  179. this._isEnabled = isTrueProperty(val);
  180. this._setListeners(this._isEnabled);
  181. }
  182. _onStart(ev) {
  183. // if multitouch then get out immediately
  184. if (ev.touches && ev.touches.length > 1) {
  185. return false;
  186. }
  187. if (this.state !== STATE_INACTIVE) {
  188. return false;
  189. }
  190. let scrollHostScrollTop = this._content.getContentDimensions().scrollTop;
  191. // if the scrollTop is greater than zero then it's
  192. // not possible to pull the content down yet
  193. if (scrollHostScrollTop > 0) {
  194. return false;
  195. }
  196. if (!this._gesture.canStart()) {
  197. return false;
  198. }
  199. let coord = pointerCoord(ev);
  200. (void 0) /* console.debug */;
  201. if (this._content.contentTop > 0) {
  202. let newTop = this._content.contentTop + 'px';
  203. if (this._top !== newTop) {
  204. this._top = newTop;
  205. }
  206. }
  207. this.startY = this.currentY = coord.y;
  208. this.progress = 0;
  209. this.state = STATE_INACTIVE;
  210. return true;
  211. }
  212. _onMove(ev) {
  213. // this method can get called like a bazillion times per second,
  214. // so it's built to be as efficient as possible, and does its
  215. // best to do any DOM read/writes only when absolutely necessary
  216. // if multitouch then get out immediately
  217. if (ev.touches && ev.touches.length > 1) {
  218. return 1;
  219. }
  220. if (!this._gesture.canStart()) {
  221. return 0;
  222. }
  223. // do nothing if it's actively refreshing
  224. // or it's in the process of closing
  225. // or this was never a startY
  226. if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_COMPLETING) {
  227. return 2;
  228. }
  229. // if we just updated stuff less than 16ms ago
  230. // then don't check again, just chillout plz
  231. let now = Date.now();
  232. if (this._lastCheck + 16 > now) {
  233. return 3;
  234. }
  235. // remember the last time we checked all this
  236. this._lastCheck = now;
  237. // get the current pointer coordinates
  238. let coord = pointerCoord(ev);
  239. this.currentY = coord.y;
  240. // it's now possible they could be pulling down the content
  241. // how far have they pulled so far?
  242. this.deltaY = (coord.y - this.startY);
  243. // don't bother if they're scrolling up
  244. // and have not already started dragging
  245. if (this.deltaY <= 0) {
  246. // the current Y is higher than the starting Y
  247. // so they scrolled up enough to be ignored
  248. this.progress = 0;
  249. if (this.state !== STATE_INACTIVE) {
  250. this._zone.run(() => {
  251. this.state = STATE_INACTIVE;
  252. });
  253. }
  254. if (this._appliedStyles) {
  255. // reset the styles only if they were applied
  256. this._setCss(0, '', false, '');
  257. return 5;
  258. }
  259. return 6;
  260. }
  261. if (this.state === STATE_INACTIVE) {
  262. // this refresh is not already actively pulling down
  263. // get the content's scrollTop
  264. let scrollHostScrollTop = this._content.getContentDimensions().scrollTop;
  265. // if the scrollTop is greater than zero then it's
  266. // not possible to pull the content down yet
  267. if (scrollHostScrollTop > 0) {
  268. this.progress = 0;
  269. this.startY = null;
  270. return 7;
  271. }
  272. // content scrolled all the way to the top, and dragging down
  273. this.state = STATE_PULLING;
  274. }
  275. // prevent native scroll events
  276. ev.preventDefault();
  277. // the refresher is actively pulling at this point
  278. // move the scroll element within the content element
  279. this._setCss(this.deltaY, '0ms', true, '');
  280. if (!this.deltaY) {
  281. // don't continue if there's no delta yet
  282. this.progress = 0;
  283. return 8;
  284. }
  285. // so far so good, let's run this all back within zone now
  286. this._zone.run(() => {
  287. this._onMoveInZone();
  288. });
  289. }
  290. _onMoveInZone() {
  291. // set pull progress
  292. this.progress = (this.deltaY / this.pullMin);
  293. // emit "start" if it hasn't started yet
  294. if (!this._didStart) {
  295. this._didStart = true;
  296. this.ionStart.emit(this);
  297. }
  298. // emit "pulling" on every move
  299. this.ionPull.emit(this);
  300. // do nothing if the delta is less than the pull threshold
  301. if (this.deltaY < this.pullMin) {
  302. // ensure it stays in the pulling state, cuz its not ready yet
  303. this.state = STATE_PULLING;
  304. return 2;
  305. }
  306. if (this.deltaY > this.pullMax) {
  307. // they pulled farther than the max, so kick off the refresh
  308. this._beginRefresh();
  309. return 3;
  310. }
  311. // pulled farther than the pull min!!
  312. // it is now in the `ready` state!!
  313. // if they let go then it'll refresh, kerpow!!
  314. this.state = STATE_READY;
  315. return 4;
  316. }
  317. _onEnd() {
  318. // only run in a zone when absolutely necessary
  319. if (this.state === STATE_READY) {
  320. this._zone.run(() => {
  321. // they pulled down far enough, so it's ready to refresh
  322. this._beginRefresh();
  323. });
  324. }
  325. else if (this.state === STATE_PULLING) {
  326. this._zone.run(() => {
  327. // they were pulling down, but didn't pull down far enough
  328. // set the content back to it's original location
  329. // and close the refresher
  330. // set that the refresh is actively cancelling
  331. this.cancel();
  332. });
  333. }
  334. // reset on any touchend/mouseup
  335. this.startY = null;
  336. }
  337. _beginRefresh() {
  338. // assumes we're already back in a zone
  339. // they pulled down far enough, so it's ready to refresh
  340. this.state = STATE_REFRESHING;
  341. // place the content in a hangout position while it thinks
  342. this._setCss(this.pullMin, (this.snapbackDuration + 'ms'), true, '');
  343. // emit "refresh" because it was pulled down far enough
  344. // and they let go to begin refreshing
  345. this.ionRefresh.emit(this);
  346. }
  347. /**
  348. * Call `complete()` when your async operation has completed.
  349. * For example, the `refreshing` state is while the app is performing
  350. * an asynchronous operation, such as receiving more data from an
  351. * AJAX request. Once the data has been received, you then call this
  352. * method to signify that the refreshing has completed and to close
  353. * the refresher. This method also changes the refresher's state from
  354. * `refreshing` to `completing`.
  355. */
  356. complete() {
  357. this._close(STATE_COMPLETING, '120ms');
  358. }
  359. /**
  360. * Changes the refresher's state from `refreshing` to `cancelling`.
  361. */
  362. cancel() {
  363. this._close(STATE_CANCELLING, '');
  364. }
  365. _close(state, delay) {
  366. var timer;
  367. function close(ev) {
  368. // closing is done, return to inactive state
  369. if (ev) {
  370. clearTimeout(timer);
  371. }
  372. this.state = STATE_INACTIVE;
  373. this.progress = 0;
  374. this._didStart = this.startY = this.currentY = this.deltaY = null;
  375. this._setCss(0, '0ms', false, '');
  376. }
  377. // create fallback timer incase something goes wrong with transitionEnd event
  378. timer = setTimeout(close.bind(this), 600);
  379. // create transition end event on the content's scroll element
  380. this._content.onScrollElementTransitionEnd(close.bind(this));
  381. // reset set the styles on the scroll element
  382. // set that the refresh is actively cancelling/completing
  383. this.state = state;
  384. this._setCss(0, '', true, delay);
  385. if (this._pointerEvents) {
  386. this._pointerEvents.stop();
  387. }
  388. }
  389. _setCss(y, duration, overflowVisible, delay) {
  390. this._appliedStyles = (y > 0);
  391. const content = this._content;
  392. const Css = this._plt.Css;
  393. content.setScrollElementStyle(Css.transform, ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)'));
  394. content.setScrollElementStyle(Css.transitionDuration, duration);
  395. content.setScrollElementStyle(Css.transitionDelay, delay);
  396. content.setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : ''));
  397. }
  398. _setListeners(shouldListen) {
  399. this._events.unlistenAll();
  400. this._pointerEvents = null;
  401. if (shouldListen) {
  402. this._pointerEvents = this._events.pointerEvents({
  403. element: this._content.getScrollElement(),
  404. pointerDown: this._onStart.bind(this),
  405. pointerMove: this._onMove.bind(this),
  406. pointerUp: this._onEnd.bind(this),
  407. zone: false
  408. });
  409. }
  410. }
  411. /**
  412. * @hidden
  413. */
  414. ngOnInit() {
  415. // bind event listeners
  416. // save the unregister listener functions to use onDestroy
  417. this._setListeners(this._isEnabled);
  418. }
  419. /**
  420. * @hidden
  421. */
  422. ngOnDestroy() {
  423. this._setListeners(false);
  424. this._events.destroy();
  425. this._gesture.destroy();
  426. }
  427. }
  428. Refresher.decorators = [
  429. { type: Directive, args: [{
  430. selector: 'ion-refresher',
  431. host: {
  432. '[class.refresher-active]': 'state !== "inactive"',
  433. '[style.top]': '_top'
  434. }
  435. },] },
  436. ];
  437. /** @nocollapse */
  438. Refresher.ctorParameters = () => [
  439. { type: Platform, },
  440. { type: Content, decorators: [{ type: Host },] },
  441. { type: NgZone, },
  442. { type: GestureController, },
  443. ];
  444. Refresher.propDecorators = {
  445. 'pullMin': [{ type: Input },],
  446. 'pullMax': [{ type: Input },],
  447. 'closeDuration': [{ type: Input },],
  448. 'snapbackDuration': [{ type: Input },],
  449. 'enabled': [{ type: Input },],
  450. 'ionRefresh': [{ type: Output },],
  451. 'ionPull': [{ type: Output },],
  452. 'ionStart': [{ type: Output },],
  453. };
  454. const STATE_INACTIVE = 'inactive';
  455. const STATE_PULLING = 'pulling';
  456. const STATE_READY = 'ready';
  457. const STATE_REFRESHING = 'refreshing';
  458. const STATE_CANCELLING = 'cancelling';
  459. const STATE_COMPLETING = 'completing';
  460. //# sourceMappingURL=refresher.js.map