import {Directive, Input, Output, EventEmitter, Host, NgZone, ElementRef} from 'angular2/core'; import {Content} from '../content/content'; /** * @name InfiniteScroll * @description * The infinite scroll allows you to call a method whenever the user * gets to the bottom of the page or near the bottom of the page. * * The expression you add to the `infinite` output event is called when * the user scrolls greater than distance away from the bottom of the * content. Once your `infinite` handler is done loading new data, it * should call the `endLoading()` method on the infinite scroll instance. * * @usage * ```html * * * * {{i}} * * * * * * * * ``` * * ```ts * @Page({...}) * export class NewsFeedPage { * * constructor() { * this.items = []; * for (var i = 0; i < 30; i++) { * this.items.push( this.items.length ); * } * } * * doInfinite(infiniteScroll) { * console.log('Begin async operation'); * * setTimeout(() => { * for (var i = 0; i < 30; i++) { * this.items.push( this.items.length ); * } * * console.log('Async operation has ended'); * infiniteScroll.endLoading(); * }, 500); * } * * } * ``` * * * ## Infinite Scroll Content * * By default, Ionic provides the infinite scroll spinner that looks * best for the platform the user is on. However, you can change the * default spinner, along with adding text by adding properties to * the child `ion-infinite-content` component. * * ```html * * * * * * * * * ``` * * * ## Further Customizing Infinite Scroll Content * * The `ion-infinite` component holds the infinite scroll logic, and it * requires a child infinite scroll content component for its display. * The `ion-infinite-content` component is Ionic's default that shows * the actual display of the infinite scroll and changes its look depending * on the infinite scroll's state. With this separation, it also allows * developers to create their own infinite scroll content components. * Ideas include having some cool SVG or CSS animations that are * customized to your app and animates to your liking. * */ @Directive({ selector: 'ion-infinite' }) export class InfiniteScroll { private _lastCheck: number = 0; private _highestY: number = 0; private _scLsn: Function; private _thr: string = '15%'; private _thrPx: number = 0; private _thrPc: number = 0.15; private _init: boolean = false; state: string = STATE_ENABLED; /** * @input {string} The threshold distance from the bottom * of the content to call the `infinite` output event when scrolled. * The threshold input value can be either a percent, or * in pixels. For example, use the value of `10%` for the `infinite` * output event to get called when the scroll has 10% of the scroll * left until it reaches the bottom. Use the value `100px` when the * scroll is within 100 pixels from the bottom of the content. * Default is `15%`. */ @Input() get threshold(): string { return this._thr; } set threshold(val: string) { this._thr = val; if (val.indexOf('%') > -1) { this._thrPx = 0; this._thrPc = (parseFloat(val) / 100); } else { this._thrPx = parseFloat(val); this._thrPc = 0; } } /** * @output {event} The expression to call when the scroll reaches * the threshold input distance. From within your infinite handler, * you must call the infinite scroll's `endLoading()` method when * your async operation has completed. */ @Output() infinite: EventEmitter = new EventEmitter(); constructor( @Host() private _content: Content, private _zone: NgZone, private _elementRef: ElementRef ) { _content.addCssClass('has-infinite-scroll'); } private _onScroll(ev) { if (this.state === STATE_LOADING || this.state === STATE_DISABLED) { return 1; } let now = Date.now(); if (this._lastCheck + 32 > now) { // no need to check less than every XXms return 2; } this._lastCheck = now; let infiniteHeight = this._elementRef.nativeElement.scrollHeight; if (!infiniteHeight) { // if there is no height of this element then do nothing return 3; } let d = this._content.getContentDimensions(); if (d.scrollTop <= this._highestY) { // don't bother if scrollY is less than the highest Y seen return 4; } this._highestY = d.scrollTop; let reloadY = d.contentHeight; if (this._thrPc) { reloadY += (reloadY * this._thrPc); } else { reloadY += this._thrPx } let distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - reloadY; if (distanceFromInfinite < 0) { this._zone.run(() => { console.debug('infinite scroll'); this.state = STATE_LOADING; this.infinite.emit(this); }); return 5; } return 6; } /** * Call `endLoading()` within the `infinite` output event handler when * your async operation has completed. For example, the `loading` * state is while the app is performing an asynchronous operation, * such as receiving more data from an AJAX request to add more items * to a data list. Once the data has been received and UI updated, you * then call this method to signify that the loading has completed. * This method will change the infinite scroll's state from `loading` * to `enabled`. */ endLoading() { this.state = STATE_ENABLED; } /** * Call `enable(false)` to disable the infinite scroll from actively * trying to receive new data while scrolling. This method is useful * when it is known that there is no more data that can be added, and * the infinite scroll is no longer needed. * @param {boolean} shouldEnable If the infinite scroll should be enabled or not. Setting to `false` will remove scroll event listeners and hide the display. */ enable(shouldEnable: boolean) { this.state = (shouldEnable ? STATE_ENABLED : STATE_DISABLED); this._setListeners(shouldEnable); } private _setListeners(shouldListen: boolean) { if (this._init) { if (shouldListen) { if (!this._scLsn) { this._zone.runOutsideAngular(() => { this._scLsn = this._content.addScrollListener( this._onScroll.bind(this) ); }); } } else { this._scLsn && this._scLsn(); this._scLsn = null; } } } /** * @private */ ngAfterContentInit() { this._init = true; this._setListeners(this.state !== STATE_DISABLED); } /** * @private */ ngOnDestroy() { this._setListeners(false); } } const STATE_ENABLED = 'enabled'; const STATE_DISABLED = 'disabled'; const STATE_LOADING = 'loading';