123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  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. var Refresher = (function () {
  89. function Refresher(_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. Object.defineProperty(Refresher.prototype, "enabled", {
  173. /**
  174. * @input {boolean} If the refresher is enabled or not. This should be used in place of an `ngIf`. Default is `true`.
  175. */
  176. get: function () {
  177. return this._isEnabled;
  178. },
  179. set: function (val) {
  180. this._isEnabled = isTrueProperty(val);
  181. this._setListeners(this._isEnabled);
  182. },
  183. enumerable: true,
  184. configurable: true
  185. });
  186. Refresher.prototype._onStart = function (ev) {
  187. // if multitouch then get out immediately
  188. if (ev.touches && ev.touches.length > 1) {
  189. return false;
  190. }
  191. if (this.state !== STATE_INACTIVE) {
  192. return false;
  193. }
  194. var scrollHostScrollTop = this._content.getContentDimensions().scrollTop;
  195. // if the scrollTop is greater than zero then it's
  196. // not possible to pull the content down yet
  197. if (scrollHostScrollTop > 0) {
  198. return false;
  199. }
  200. if (!this._gesture.canStart()) {
  201. return false;
  202. }
  203. var coord = pointerCoord(ev);
  204. (void 0) /* console.debug */;
  205. if (this._content.contentTop > 0) {
  206. var newTop = this._content.contentTop + 'px';
  207. if (this._top !== newTop) {
  208. this._top = newTop;
  209. }
  210. }
  211. this.startY = this.currentY = coord.y;
  212. this.progress = 0;
  213. this.state = STATE_INACTIVE;
  214. return true;
  215. };
  216. Refresher.prototype._onMove = function (ev) {
  217. // this method can get called like a bazillion times per second,
  218. // so it's built to be as efficient as possible, and does its
  219. // best to do any DOM read/writes only when absolutely necessary
  220. var _this = this;
  221. // if multitouch then get out immediately
  222. if (ev.touches && ev.touches.length > 1) {
  223. return 1;
  224. }
  225. if (!this._gesture.canStart()) {
  226. return 0;
  227. }
  228. // do nothing if it's actively refreshing
  229. // or it's in the process of closing
  230. // or this was never a startY
  231. if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_COMPLETING) {
  232. return 2;
  233. }
  234. // if we just updated stuff less than 16ms ago
  235. // then don't check again, just chillout plz
  236. var now = Date.now();
  237. if (this._lastCheck + 16 > now) {
  238. return 3;
  239. }
  240. // remember the last time we checked all this
  241. this._lastCheck = now;
  242. // get the current pointer coordinates
  243. var coord = pointerCoord(ev);
  244. this.currentY = coord.y;
  245. // it's now possible they could be pulling down the content
  246. // how far have they pulled so far?
  247. this.deltaY = (coord.y - this.startY);
  248. // don't bother if they're scrolling up
  249. // and have not already started dragging
  250. if (this.deltaY <= 0) {
  251. // the current Y is higher than the starting Y
  252. // so they scrolled up enough to be ignored
  253. this.progress = 0;
  254. if (this.state !== STATE_INACTIVE) {
  255. this._zone.run(function () {
  256. _this.state = STATE_INACTIVE;
  257. });
  258. }
  259. if (this._appliedStyles) {
  260. // reset the styles only if they were applied
  261. this._setCss(0, '', false, '');
  262. return 5;
  263. }
  264. return 6;
  265. }
  266. if (this.state === STATE_INACTIVE) {
  267. // this refresh is not already actively pulling down
  268. // get the content's scrollTop
  269. var scrollHostScrollTop = this._content.getContentDimensions().scrollTop;
  270. // if the scrollTop is greater than zero then it's
  271. // not possible to pull the content down yet
  272. if (scrollHostScrollTop > 0) {
  273. this.progress = 0;
  274. this.startY = null;
  275. return 7;
  276. }
  277. // content scrolled all the way to the top, and dragging down
  278. this.state = STATE_PULLING;
  279. }
  280. // prevent native scroll events
  281. ev.preventDefault();
  282. // the refresher is actively pulling at this point
  283. // move the scroll element within the content element
  284. this._setCss(this.deltaY, '0ms', true, '');
  285. if (!this.deltaY) {
  286. // don't continue if there's no delta yet
  287. this.progress = 0;
  288. return 8;
  289. }
  290. // so far so good, let's run this all back within zone now
  291. this._zone.run(function () {
  292. _this._onMoveInZone();
  293. });
  294. };
  295. Refresher.prototype._onMoveInZone = function () {
  296. // set pull progress
  297. this.progress = (this.deltaY / this.pullMin);
  298. // emit "start" if it hasn't started yet
  299. if (!this._didStart) {
  300. this._didStart = true;
  301. this.ionStart.emit(this);
  302. }
  303. // emit "pulling" on every move
  304. this.ionPull.emit(this);
  305. // do nothing if the delta is less than the pull threshold
  306. if (this.deltaY < this.pullMin) {
  307. // ensure it stays in the pulling state, cuz its not ready yet
  308. this.state = STATE_PULLING;
  309. return 2;
  310. }
  311. if (this.deltaY > this.pullMax) {
  312. // they pulled farther than the max, so kick off the refresh
  313. this._beginRefresh();
  314. return 3;
  315. }
  316. // pulled farther than the pull min!!
  317. // it is now in the `ready` state!!
  318. // if they let go then it'll refresh, kerpow!!
  319. this.state = STATE_READY;
  320. return 4;
  321. };
  322. Refresher.prototype._onEnd = function () {
  323. // only run in a zone when absolutely necessary
  324. var _this = this;
  325. if (this.state === STATE_READY) {
  326. this._zone.run(function () {
  327. // they pulled down far enough, so it's ready to refresh
  328. _this._beginRefresh();
  329. });
  330. }
  331. else if (this.state === STATE_PULLING) {
  332. this._zone.run(function () {
  333. // they were pulling down, but didn't pull down far enough
  334. // set the content back to it's original location
  335. // and close the refresher
  336. // set that the refresh is actively cancelling
  337. _this.cancel();
  338. });
  339. }
  340. // reset on any touchend/mouseup
  341. this.startY = null;
  342. };
  343. Refresher.prototype._beginRefresh = function () {
  344. // assumes we're already back in a zone
  345. // they pulled down far enough, so it's ready to refresh
  346. this.state = STATE_REFRESHING;
  347. // place the content in a hangout position while it thinks
  348. this._setCss(this.pullMin, (this.snapbackDuration + 'ms'), true, '');
  349. // emit "refresh" because it was pulled down far enough
  350. // and they let go to begin refreshing
  351. this.ionRefresh.emit(this);
  352. };
  353. /**
  354. * Call `complete()` when your async operation has completed.
  355. * For example, the `refreshing` state is while the app is performing
  356. * an asynchronous operation, such as receiving more data from an
  357. * AJAX request. Once the data has been received, you then call this
  358. * method to signify that the refreshing has completed and to close
  359. * the refresher. This method also changes the refresher's state from
  360. * `refreshing` to `completing`.
  361. */
  362. Refresher.prototype.complete = function () {
  363. this._close(STATE_COMPLETING, '120ms');
  364. };
  365. /**
  366. * Changes the refresher's state from `refreshing` to `cancelling`.
  367. */
  368. Refresher.prototype.cancel = function () {
  369. this._close(STATE_CANCELLING, '');
  370. };
  371. Refresher.prototype._close = function (state, delay) {
  372. var timer;
  373. function close(ev) {
  374. // closing is done, return to inactive state
  375. if (ev) {
  376. clearTimeout(timer);
  377. }
  378. this.state = STATE_INACTIVE;
  379. this.progress = 0;
  380. this._didStart = this.startY = this.currentY = this.deltaY = null;
  381. this._setCss(0, '0ms', false, '');
  382. }
  383. // create fallback timer incase something goes wrong with transitionEnd event
  384. timer = setTimeout(close.bind(this), 600);
  385. // create transition end event on the content's scroll element
  386. this._content.onScrollElementTransitionEnd(close.bind(this));
  387. // reset set the styles on the scroll element
  388. // set that the refresh is actively cancelling/completing
  389. this.state = state;
  390. this._setCss(0, '', true, delay);
  391. if (this._pointerEvents) {
  392. this._pointerEvents.stop();
  393. }
  394. };
  395. Refresher.prototype._setCss = function (y, duration, overflowVisible, delay) {
  396. this._appliedStyles = (y > 0);
  397. var content = this._content;
  398. var Css = this._plt.Css;
  399. content.setScrollElementStyle(Css.transform, ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)'));
  400. content.setScrollElementStyle(Css.transitionDuration, duration);
  401. content.setScrollElementStyle(Css.transitionDelay, delay);
  402. content.setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : ''));
  403. };
  404. Refresher.prototype._setListeners = function (shouldListen) {
  405. this._events.unlistenAll();
  406. this._pointerEvents = null;
  407. if (shouldListen) {
  408. this._pointerEvents = this._events.pointerEvents({
  409. element: this._content.getScrollElement(),
  410. pointerDown: this._onStart.bind(this),
  411. pointerMove: this._onMove.bind(this),
  412. pointerUp: this._onEnd.bind(this),
  413. zone: false
  414. });
  415. }
  416. };
  417. /**
  418. * @hidden
  419. */
  420. Refresher.prototype.ngOnInit = function () {
  421. // bind event listeners
  422. // save the unregister listener functions to use onDestroy
  423. this._setListeners(this._isEnabled);
  424. };
  425. /**
  426. * @hidden
  427. */
  428. Refresher.prototype.ngOnDestroy = function () {
  429. this._setListeners(false);
  430. this._events.destroy();
  431. this._gesture.destroy();
  432. };
  433. Refresher.decorators = [
  434. { type: Directive, args: [{
  435. selector: 'ion-refresher',
  436. host: {
  437. '[class.refresher-active]': 'state !== "inactive"',
  438. '[style.top]': '_top'
  439. }
  440. },] },
  441. ];
  442. /** @nocollapse */
  443. Refresher.ctorParameters = function () { return [
  444. { type: Platform, },
  445. { type: Content, decorators: [{ type: Host },] },
  446. { type: NgZone, },
  447. { type: GestureController, },
  448. ]; };
  449. Refresher.propDecorators = {
  450. 'pullMin': [{ type: Input },],
  451. 'pullMax': [{ type: Input },],
  452. 'closeDuration': [{ type: Input },],
  453. 'snapbackDuration': [{ type: Input },],
  454. 'enabled': [{ type: Input },],
  455. 'ionRefresh': [{ type: Output },],
  456. 'ionPull': [{ type: Output },],
  457. 'ionStart': [{ type: Output },],
  458. };
  459. return Refresher;
  460. }());
  461. export { Refresher };
  462. var STATE_INACTIVE = 'inactive';
  463. var STATE_PULLING = 'pulling';
  464. var STATE_READY = 'ready';
  465. var STATE_REFRESHING = 'refreshing';
  466. var STATE_CANCELLING = 'cancelling';
  467. var STATE_COMPLETING = 'completing';
  468. //# sourceMappingURL=refresher.js.map