From 21d4cef543dfce35ef4be365f04aa6e8db6c12ec Mon Sep 17 00:00:00 2001 From: Dominique Rau Date: Thu, 25 Feb 2016 23:52:22 +0100 Subject: [PATCH 01/32] (search-bar) Hide cancle button for android Hide cancle button for android as well... Added `hideCancelButton` to example. This helps a lot for guys coming from ng1 as I tried snake-case way too long. --- ionic/components/searchbar/searchbar.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ionic/components/searchbar/searchbar.ts b/ionic/components/searchbar/searchbar.ts index fd201d895b..f5277fafd1 100644 --- a/ionic/components/searchbar/searchbar.ts +++ b/ionic/components/searchbar/searchbar.ts @@ -39,7 +39,7 @@ export class SearchbarInput { * * @usage * ```html - * + * * ``` * * @demo /docs/v2/demos/searchbar/ @@ -49,7 +49,7 @@ export class SearchbarInput { selector: 'ion-searchbar', template: '
' + - '' + '
' + From 6c734466282ae6fa33a7333d50b34d435186be74 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 25 Feb 2016 17:23:48 -0600 Subject: [PATCH 02/32] feat(spinner): SVG spinners --- ionic/components.core.scss | 3 +- ionic/components/spinner/spinner.scss | 119 ++++++++ ionic/components/spinner/spinner.ts | 283 ++++++++++++++++++ ionic/components/spinner/test/basic/index.ts | 13 + ionic/components/spinner/test/basic/main.html | 55 ++++ ionic/config/directives.ts | 3 + ionic/config/modes.ts | 4 + 7 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 ionic/components/spinner/spinner.scss create mode 100644 ionic/components/spinner/spinner.ts create mode 100644 ionic/components/spinner/test/basic/index.ts create mode 100644 ionic/components/spinner/test/basic/main.html diff --git a/ionic/components.core.scss b/ionic/components.core.scss index b34aa092cd..44814293c9 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -20,7 +20,8 @@ "components/modal/modal", "components/scroll/scroll", "components/scroll/pull-to-refresh", - "components/slides/slides"; + "components/slides/slides", + "components/spinner/spinner"; // Ionicons (to be replaced with SVGs) diff --git a/ionic/components/spinner/spinner.scss b/ionic/components/spinner/spinner.scss new file mode 100644 index 0000000000..cce517e8b4 --- /dev/null +++ b/ionic/components/spinner/spinner.scss @@ -0,0 +1,119 @@ + + +// Spinners +// -------------------------------------------------- + +ion-spinner { + display: inline-block; + position: relative; + width: 28px; + height: 28px; +} + +ion-spinner svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform: translateZ(0); +} + +ion-spinner.spinner-paused svg { + animation-play-state: paused; +} + + +// Spinner: ios / ios-small +// -------------------------------------------------- + +.spinner-ios line, +.spinner-ios-small line { + stroke: #69717d;; + stroke-width: 4px; + stroke-linecap: round; +} + +.spinner-ios svg, +.spinner-ios-small svg { + animation: spinner-fade-out 1s linear infinite; +} + + +// Spinner: bubbles +// -------------------------------------------------- + +.spinner-bubbles circle { + fill: black; +} + +.spinner-bubbles svg { + animation: spinner-scale-out 1s linear infinite; +} + + +// Spinner: circles +// -------------------------------------------------- + +.spinner-circles circle { + fill: #69717d; +} + +.spinner-circles svg { + animation: spinner-fade-out 1s linear infinite; +} + + +// Spinner: crescent +// -------------------------------------------------- + +.spinner-crescent circle { + fill: transparent; + stroke: black; + stroke-width: 4px; + stroke-dasharray: 128px; + stroke-dashoffset: 82px; +} + +.spinner-crescent svg { + animation: spinner-rotate 1s linear infinite; +} + + +// Spinner: dots +// -------------------------------------------------- + +.spinner-dots circle { + fill: #444; + stroke-width: 0; +} + +.spinner-dots svg { + animation: spinner-dots 1s linear infinite; + transform-origin: center; +} + + +// Animation Keyframes +// -------------------------------------------------- + +@keyframes spinner-fade-out { + 0% { opacity: 1; } + 100% { opacity: 0; } +} + +@keyframes spinner-scale-out { + 0% { transform: scale(1, 1); } + 100% { transform: scale(0, 0); } +} + +@keyframes spinner-rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes spinner-dots { + 0% { opacity: 0.9; transform: scale(1, 1); } + 50% { opacity: 0.3; transform: scale(0.4, 0.4); } + 100% { opacity: 0.9; transform: scale(1, 1); } +} diff --git a/ionic/components/spinner/spinner.ts b/ionic/components/spinner/spinner.ts new file mode 100644 index 0000000000..1a300d7402 --- /dev/null +++ b/ionic/components/spinner/spinner.ts @@ -0,0 +1,283 @@ +import {Component, Input} from 'angular2/core'; +import {NgStyle} from 'angular2/common'; + +import {Config} from '../../config/config'; + + +/** + * @name Spinner + * @description + * The `ion-spinner` component provides a variety of animated SVG spinners. + * Spinners enables you to give users feedback that the app is actively + * processing/thinking/waiting/chillin’ out, or whatever you’d like it to indicate. + * By default, the `ion-refresher` feature uses this spinner component while it's + * the refresher is in the `refreshing` state. + * + * Ionic offers a handful of spinners out of the box, and by default, it will use + * the appropriate spinner for the platform on which it’s running. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * ios + * + * + *
+ * ios-small + * + * + *
+ * bubbles + * + * + *
+ * circles + * + * + *
+ * crescent + * + * + *
+ * dots + * + * + *
+ * + * @usage + * The following code would use the default spinner for the platform it's + * running from. If it's neither iOS or Android, it'll default to use `ios`. + * + * ```html + * + * ``` + * + * By setting the `name` property, you can specify which predefined spinner to + * use, no matter what the platform is. + * + * ```html + * + * ``` + * + * ## Styling SVG with CSS + * One cool thing about SVG is its ability to be styled with CSS! One thing to note + * is that some of the CSS properties on an SVG element have different names. For + * example, SVG uses the term `stroke` instead of `border`, and `fill` instead + * of `background-color`. + * + * ```css + * ion-spinner svg { + * width: 28px; + * height: 28px; + * stroke: #444; + * fill: #222; + * } + * ``` + */ +@Component({ + selector: 'ion-spinner', + template: + '' + + '' + + '' + + '' + + '' + + '', + directives: [NgStyle], + host: { + '[class]': '_applied', + '[class.spinner-paused]': 'paused' + } +}) +export class Spinner { + private _c: any[]; + private _l: any[]; + private _name: string; + private _dur: number = null; + private _init: boolean; + private _applied: string; + + /** + * @input {string} SVG spinner name. + */ + @Input() + get name(): string { + return this._name; + } + + set name(val: string) { + this._name = val; + this.load(); + } + + /** + * @input {string} How long it takes it to do one loop. + */ + @Input() + get duration(): number { + return this._dur; + } + + set duration(val: number) { + this._dur = val; + this.load(); + } + + /** + * @input {string} If the animation is paused or not. Defaults to `false`. + */ + @Input() paused: boolean = false; + + constructor(private _config: Config) {} + + ngOnInit() { + this._init = true; + this.load(); + } + + load() { + if (this._init) { + this._l = []; + this._c = []; + + var name = this._name || this._config.get('spinner', 'ios'); + + const spinner = SPINNERS[name]; + if (spinner) { + this._applied = 'spinner-' + name; + + if (spinner.lines) { + for (var i = 0, l = spinner.lines; i < l; i++) { + this._l.push( this._loadEle(spinner, i, l) ); + } + + } else if (spinner.circles) { + for (var i = 0, l = spinner.circles; i < l; i++) { + this._c.push( this._loadEle(spinner, i, l) ); + } + } + + } + } + } + + _loadEle(spinner: any, index: number, total: number) { + let duration = this._dur || spinner.dur + let data = spinner.fn(duration, index, total); + data.style.animationDuration = duration + 'ms'; + return data; + } + +} + +const SPINNERS = { + + ios: { + dur: 1000, + lines: 12, + fn: function(dur, index, total) { + return { + y1: 17, + y2: 29, + style: { + transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)', + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + 'ios-small': { + dur: 1000, + lines: 12, + fn: function(dur, index, total) { + return { + y1: 12, + y2: 20, + style: { + transform: 'rotate(' + (30 * index + (index < 6 ? 180 : -180)) + 'deg)', + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + bubbles: { + dur: 1000, + circles: 9, + fn: function(dur, index, total) { + return { + r: 5, + style: { + top: 9 * Math.sin(2 * Math.PI * index / total), + left: 9 * Math.cos(2 * Math.PI * index / total), + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + circles: { + dur: 1000, + circles: 8, + fn: function(dur, index, total) { + return { + r: 5, + style: { + top: 9 * Math.sin(2 * Math.PI * index / total), + left: 9 * Math.cos(2 * Math.PI * index / total), + animationDelay: -(dur - ((dur / total) * index)) + 'ms' + } + } + } + }, + + crescent: { + dur: 750, + circles: 1, + fn: function(dur) { + return { + r: 26, + style: {} + } + } + }, + + dots: { + dur: 750, + circles: 3, + fn: function(dur, index, total) { + return { + r: 6, + style: { + left: (9 - (9 * index)), + animationDelay: -(110 * index) + 'ms' + } + } + } + } + +}; diff --git a/ionic/components/spinner/test/basic/index.ts b/ionic/components/spinner/test/basic/index.ts new file mode 100644 index 0000000000..e578203808 --- /dev/null +++ b/ionic/components/spinner/test/basic/index.ts @@ -0,0 +1,13 @@ +import {App} from 'ionic-angular'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp { + paused: boolean = false; + + toggleState() { + this.paused = !this.paused; + } +} diff --git a/ionic/components/spinner/test/basic/main.html b/ionic/components/spinner/test/basic/main.html new file mode 100644 index 0000000000..af93f162fe --- /dev/null +++ b/ionic/components/spinner/test/basic/main.html @@ -0,0 +1,55 @@ + + + Spinners + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Platform Default + +
ios + +
ios-small + +
bubbles + +
circles + +
crescent + +
dots + +
+ + + +
diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts index 9fc4c72274..dc8da6d53a 100644 --- a/ionic/config/directives.ts +++ b/ionic/config/directives.ts @@ -19,6 +19,7 @@ import {Item} from '../components/item/item'; import {ItemSliding} from '../components/item/item-sliding'; import {Toolbar, ToolbarTitle, ToolbarItem} from '../components/toolbar/toolbar'; import {Icon} from '../components/icon/icon'; +import {Spinner} from '../components/spinner/spinner'; import {Checkbox} from '../components/checkbox/checkbox'; import {Select} from '../components/select/select'; import {Option} from '../components/option/option'; @@ -80,6 +81,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when'; * * **Media** * - Icon + * - Spinner * * **Forms** * - Searchbar @@ -146,6 +148,7 @@ export const IONIC_DIRECTIVES = [ // Media Icon, + Spinner, // Forms Searchbar, diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index 8cba1a0cca..d2dd2355ee 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -25,6 +25,8 @@ Config.setModeConfig('ios', { pageTransition: 'ios-transition', pageTransitionDelay: 16, + spinner: 'ios', + tabbarPlacement: 'bottom', }); @@ -52,6 +54,8 @@ Config.setModeConfig('md', { pageTransition: 'md-transition', pageTransitionDelay: 96, + spinner: 'crescent', + tabbarHighlight: true, tabbarPlacement: 'top', From a0f0004012778eb95dd2bd60650799b59900bb19 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Thu, 25 Feb 2016 18:36:12 -0500 Subject: [PATCH 03/32] refactor(searchbar): add class to searchbar when hideCancel is passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit only hide the search icon when hideCancel isn’t passed --- ionic/components/searchbar/searchbar.md.scss | 2 +- ionic/components/searchbar/searchbar.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ionic/components/searchbar/searchbar.md.scss b/ionic/components/searchbar/searchbar.md.scss index acba47956b..4133ddb09f 100644 --- a/ionic/components/searchbar/searchbar.md.scss +++ b/ionic/components/searchbar/searchbar.md.scss @@ -119,7 +119,7 @@ ion-searchbar { // Searchbar Focused // ----------------------------------------- -.searchbar-focused { +.searchbar-focused:not(.searchbar-hide-cancel) { .searchbar-search-icon { display: none; } diff --git a/ionic/components/searchbar/searchbar.ts b/ionic/components/searchbar/searchbar.ts index f5277fafd1..a050ba2620 100644 --- a/ionic/components/searchbar/searchbar.ts +++ b/ionic/components/searchbar/searchbar.ts @@ -39,7 +39,12 @@ export class SearchbarInput { * * @usage * ```html - * + * + * * ``` * * @demo /docs/v2/demos/searchbar/ @@ -47,6 +52,9 @@ export class SearchbarInput { */ @Component({ selector: 'ion-searchbar', + host: { + '[class.searchbar-hide-cancel]': 'hideCancelButton' + }, template: '
' + ' From 91df5f97eef9ed1d261e6642c2d9ab7e8cc4063c Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Sat, 27 Feb 2016 17:33:55 -0600 Subject: [PATCH 07/32] refactor(refresher): allow refresher content customization Breaking Change: ## Refresher: - `` now takes a child `` component. - Custom refresh content components can now be replaced for Ionic's default refresher content. - Properties `pullingIcon`, `pullingText` and `refreshingText` have been moved to the `` component. - `spinner` property has been renamed to `refreshingSpinner` and now goes on the `` component. - `refreshingIcon` property is no longer an input, but instead `refreshingSpinner` should be used. Was: ``` ``` Now: ``` ``` --- demos/refresher/index.ts | 31 +- demos/refresher/main.html | 31 +- demos/refresher/mock-provider.ts | 61 ++ ionic/components.core.scss | 2 +- ionic/components.ts | 3 +- ionic/components/app/structure.scss | 4 + ionic/components/content/content.ts | 141 +++-- ionic/components/input/input-base.ts | 2 +- ionic/components/ion.ts | 18 +- .../components/refresher/refresher-content.ts | 69 +++ ionic/components/refresher/refresher.scss | 112 ++++ ionic/components/refresher/refresher.ts | 549 ++++++++++++++++++ .../components/refresher/test/basic/index.ts | 85 +++ .../components/refresher/test/basic/main.html | 21 + .../refresher/test/refresher.spec.ts | 257 ++++++++ ionic/components/scroll/pull-to-refresh.scss | 104 ---- ionic/components/scroll/pull-to-refresh.ts | 544 ----------------- .../scroll/test/pull-to-refresh/index.ts | 39 -- .../scroll/test/pull-to-refresh/main.html | 18 - ionic/config/directives.ts | 5 +- ionic/util/dom.ts | 11 +- 21 files changed, 1296 insertions(+), 811 deletions(-) create mode 100644 demos/refresher/mock-provider.ts create mode 100644 ionic/components/refresher/refresher-content.ts create mode 100644 ionic/components/refresher/refresher.scss create mode 100644 ionic/components/refresher/refresher.ts create mode 100644 ionic/components/refresher/test/basic/index.ts create mode 100644 ionic/components/refresher/test/basic/main.html create mode 100644 ionic/components/refresher/test/refresher.spec.ts delete mode 100644 ionic/components/scroll/pull-to-refresh.scss delete mode 100644 ionic/components/scroll/pull-to-refresh.ts delete mode 100644 ionic/components/scroll/test/pull-to-refresh/index.ts delete mode 100644 ionic/components/scroll/test/pull-to-refresh/main.html diff --git a/demos/refresher/index.ts b/demos/refresher/index.ts index 2a5fec1437..b6b48f26b0 100644 --- a/demos/refresher/index.ts +++ b/demos/refresher/index.ts @@ -1,22 +1,31 @@ -import {App, Page, IonicApp} from 'ionic-angular'; +import {App, Page, Refresher} from 'ionic-angular'; +import {MockProvider} from './mock-provider'; + @App({ - templateUrl: 'main.html' + templateUrl: 'main.html', + providers: [MockProvider] }) class ApiDemoApp { - doRefresh(refresher) { - console.log('DOREFRESH', refresher) + items: string[]; - setTimeout(() => { - refresher.complete(); - }) + constructor(private mockProvider: MockProvider) { + this.items = mockProvider.getData(); } - doStarting() { - console.log('DOSTARTING'); + doRefresh(refresher: Refresher) { + console.log('DOREFRESH', refresher); + + this.mockProvider.getAsyncData().then((newData) => { + for (var i = 0; i < newData.length; i++) { + this.items.unshift( newData[i] ); + } + + refresher.endRefreshing(); + }); } - doPulling(amt) { - console.log('DOPULLING', amt); + doPulling(refresher: Refresher) { + console.log('DOPULLING', refresher.progress); } } diff --git a/demos/refresher/main.html b/demos/refresher/main.html index 035ae5a976..186be0ab41 100644 --- a/demos/refresher/main.html +++ b/demos/refresher/main.html @@ -1,33 +1,20 @@ - Refresher + Pull To Refresh - + + + - Item 1 - Item 2 - Item 3 - Item 4 - Item 5 - Item 6 - Item 7 - Item 8 - Item 9 - Item 10 - Item 11 - Item 12 - Item 13 - Item 14 - Item 15 - Item 16 - Item 17 - Item 18 - Item 19 - Item 20 + + {{ item }} + diff --git a/demos/refresher/mock-provider.ts b/demos/refresher/mock-provider.ts new file mode 100644 index 0000000000..515a6d2172 --- /dev/null +++ b/demos/refresher/mock-provider.ts @@ -0,0 +1,61 @@ +import {Injectable} from 'angular2/core'; + +/** + * Mock Data Access Object + **/ +@Injectable() +export class MockProvider { + + getData() { + // return mock data synchronously + let data = []; + for (var i = 0; i < 3; i++) { + data.push( this._getRandomData() ); + } + return data; + } + + getAsyncData() { + // async receive mock data + return new Promise(resolve => { + + setTimeout(() => { + resolve(this.getData()); + }, 1000); + + }); + } + + private _getRandomData() { + let i = Math.floor( Math.random() * this._data.length ); + return this._data[i]; + } + + private _data = [ + 'Fast Times at Ridgemont High', + 'Peggy Sue Got Married', + 'Raising Arizona', + 'Moonstruck', + 'Fire Birds', + 'Honeymoon in Vegas', + 'Amos & Andrew', + 'It Could Happen to You', + 'Trapped in Paradise', + 'Leaving Las Vegas', + 'The Rock', + 'Con Air', + 'Face/Off', + 'City of Angels', + 'Gone in Sixty Seconds', + 'The Family Man', + 'Windtalkers', + 'Matchstick Men', + 'National Treasure', + 'Ghost Rider', + 'Grindhouse', + 'Next', + 'Kick-Ass', + 'Drive Angry', + ]; + +} diff --git a/ionic/components.core.scss b/ionic/components.core.scss index 44814293c9..98d29b3821 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -18,8 +18,8 @@ "components/icon/icon", "components/menu/menu", "components/modal/modal", + "components/refresher/refresher", "components/scroll/scroll", - "components/scroll/pull-to-refresh", "components/slides/slides", "components/spinner/spinner"; diff --git a/ionic/components.ts b/ionic/components.ts index 0c315e0b33..971e535087 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -31,8 +31,9 @@ export * from './components/overlay/overlay' export * from './components/slides/slides' export * from './components/radio/radio-button' export * from './components/radio/radio-group' +export * from './components/refresher/refresher' +export * from './components/refresher/refresher-content' export * from './components/scroll/scroll' -export * from './components/scroll/pull-to-refresh' export * from './components/searchbar/searchbar' export * from './components/segment/segment' export * from './components/select/select' diff --git a/ionic/components/app/structure.scss b/ionic/components/app/structure.scss index 0bb88cfc2a..9eeafaf51b 100644 --- a/ionic/components/app/structure.scss +++ b/ionic/components/app/structure.scss @@ -9,6 +9,9 @@ $z-index-menu-backdrop: 79; $z-index-overlay: 1000; $z-index-click-block: 9999; +$z-index-scroll-content: 1; +$z-index-refresher: 0; + $z-index-navbar-section: 10; $z-index-toolbar: 10; @@ -129,6 +132,7 @@ ion-content { scroll-content { position: absolute; + z-index: $z-index-scroll-content; top: 0; right: 0; bottom: 0; diff --git a/ionic/components/content/content.ts b/ionic/components/content/content.ts index 74b725ade1..0983ba4e01 100644 --- a/ionic/components/content/content.ts +++ b/ionic/components/content/content.ts @@ -3,7 +3,7 @@ import {Component, ElementRef, Optional, NgZone} from 'angular2/core'; import {Ion} from '../ion'; import {IonicApp} from '../app/app'; import {Config} from '../../config/config'; -import {raf} from '../../util/dom'; +import {raf, transitionEnd} from '../../util/dom'; import {ViewController} from '../nav/view-controller'; import {Animation} from '../../animations/animation'; import {ScrollTo} from '../../animations/scroll-to'; @@ -11,15 +11,15 @@ import {ScrollTo} from '../../animations/scroll-to'; /** * @name Content * @description - * The Content component provides an easy to use content area that can be configured to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser. + * The Content component provides an easy to use content area with some useful + * methods to control the scrollable area. * - * While we recommend using the custom Scroll features in Ionic in most cases, sometimes (for performance reasons) only the browser's native overflow scrolling will suffice, and so we've made it easy to toggle between the Ionic scroll implementation and overflow scrolling. - * - * You can implement pull-to-refresh with the [Refresher](../../scroll/Refresher) component. + * The content area can also implement pull-to-refresh with the + * [Refresher](../../scroll/Refresher) component. * * @usage * ```html - * + * * Add your content here! * * ``` @@ -30,7 +30,8 @@ import {ScrollTo} from '../../animations/scroll-to'; template: '' + '' + - '' + '' + + '' }) export class Content extends Ion { private _padding: number = 0; @@ -42,10 +43,6 @@ export class Content extends Ion { */ scrollElement: HTMLElement; - /** - * @param {elementRef} elementRef A reference to the component's DOM element. - * @param {config} config The config object to change content's default settings. - */ constructor( private _elementRef: ElementRef, private _config: Config, @@ -83,7 +80,8 @@ export class Content extends Ion { * @private */ ngOnDestroy() { - this.scrollElement.removeEventListener('scroll', this._onScroll); + this.scrollElement.removeEventListener('scroll', this._onScroll.bind(this)); + this.scrollElement = null; } /** @@ -112,28 +110,70 @@ export class Content extends Ion { * @param {Function} handler The method you want perform when scrolling * @returns {Function} A function that removes the scroll handler. */ - addScrollEventListener(handler) { - if (!this.scrollElement) { - return; - } + addScrollListener(handler) { + return this._addListener('scroll', handler); + } + + /** + * @private + */ + addTouchStartListener(handler) { + return this._addListener('touchstart', handler); + } + + /** + * @private + */ + addTouchMoveListener(handler) { + return this._addListener('touchmove', handler); + } + + /** + * @private + */ + addTouchEndListener(handler) { + return this._addListener('touchend', handler); + } + + /** + * @private + */ + addMouseDownListener(handler) { + return this._addListener('mousedown', handler); + } + + /** + * @private + */ + addMouseUpListener(handler) { + return this._addListener('mouseup', handler); + } + + /** + * @private + */ + addMouseMoveListener(handler) { + return this._addListener('mousemove', handler); + } + + private _addListener(type: string, handler: any): Function { + if (!this.scrollElement) { return; } // ensure we're not creating duplicates - this.scrollElement.removeEventListener('scroll', handler); - - this.scrollElement.addEventListener('scroll', handler); + this.scrollElement.removeEventListener(type, handler); + this.scrollElement.addEventListener(type, handler); return () => { - this.scrollElement.removeEventListener('scroll', handler); + this.scrollElement.removeEventListener(type, handler); } } - /** + * @private * Call a method when scrolling has stopped - * * @param {Function} callback The method you want perform when scrolling has ended */ - onScrollEnd(callback) { + onScrollEnd(callback: Function) { let lastScrollTop = null; let framesUnchanged = 0; let _scrollEle = this.scrollElement; @@ -163,43 +203,8 @@ export class Content extends Ion { setTimeout(next, 100); } - /** - * @private - * Adds the specified touchmove handler to the content's scroll element. - * - * ```ts - * @Page({ - * template: `` - * )} - * export class MyPage{ - * constructor(app: IonicApp){ - * this.app = app; - * } - * // Need to wait until the component has been initialized - * ngAfterViewInit() { - * // Here 'my-content' is the ID of my ion-content - * this.content = this.app.getComponent('my-content'); - * this.content.addTouchMoveListener(this.touchHandler); - * } - * touchHandler() { - * console.log("I'm touching all the magazines!!"); - * } - * } - * ``` - * @param {Function} handler The method you want to perform when touchmove is firing - * @returns {Function} A function that removes the touchmove handler. - */ - addTouchMoveListener(handler) { - if (!this.scrollElement) { return; } - - // ensure we're not creating duplicates - this.scrollElement.removeEventListener('touchmove', handler); - - this.scrollElement.addEventListener('touchmove', handler); - - return () => { - this.scrollElement.removeEventListener('touchmove', handler); - } + onScrollElementTransitionEnd(callback: Function) { + transitionEnd(this.scrollElement, callback); } /** @@ -276,6 +281,22 @@ export class Content extends Ion { return this._scrollTo.start(0, 0, 300, 0); } + getScrollTop(): number { + return this.getNativeElement().scrollTop; + } + + addCssClass(className: string) { + this.getNativeElement().classList.add(className); + } + + removeCssClass(className: string) { + this.getNativeElement().classList.remove(className); + } + + setScrollElementStyle(prop: string, val: any) { + this.scrollElement.style[prop] = val; + } + /** * @private * Returns the content and scroll elements' dimensions. diff --git a/ionic/components/input/input-base.ts b/ionic/components/input/input-base.ts index b9c0d8de08..4f12516721 100644 --- a/ionic/components/input/input-base.ts +++ b/ionic/components/input/input-base.ts @@ -438,7 +438,7 @@ export class InputBase { if (this._useAssist && this._scrollView) { setTimeout(() => { this.deregScrollMove(); - this._deregScroll = this._scrollView.addScrollEventListener(this._scrollMove); + this._deregScroll = this._scrollView.addScrollListener(this._scrollMove); }, 80); } } diff --git a/ionic/components/ion.ts b/ionic/components/ion.ts index e46ad3ed71..ba8826f952 100644 --- a/ionic/components/ion.ts +++ b/ionic/components/ion.ts @@ -1,6 +1,4 @@ import {ElementRef} from 'angular2/core'; -import {Config} from '../config/config'; -import {isArray} from '../util'; import * as dom from '../util/dom'; let ids:number = 0; @@ -17,24 +15,30 @@ export class Ion { this._id = 'i' + ids++; } - getElementRef() { + getElementRef(): ElementRef { return this.elementRef; } - getNativeElement() { + getNativeElement(): any { return this.elementRef.nativeElement; } - getDimensions() { + getDimensions(): { + width: number, height: number, left: number, top: number + } { return dom.getDimensions(this.elementRef.nativeElement, this._id); } - width() { + width(): number { return dom.getDimensions(this.elementRef.nativeElement, this._id).width; } - height() { + height(): number { return dom.getDimensions(this.elementRef.nativeElement, this._id).height; } + ngOnDestroy() { + dom.clearDimensions(this._id); + } + } diff --git a/ionic/components/refresher/refresher-content.ts b/ionic/components/refresher/refresher-content.ts new file mode 100644 index 0000000000..98f93d2dc5 --- /dev/null +++ b/ionic/components/refresher/refresher-content.ts @@ -0,0 +1,69 @@ +import {Component, Input} from 'angular2/core' +import {NgIf} from 'angular2/common'; + +import {Config} from '../../config/config'; +import {Icon} from '../icon/icon'; +import {Refresher} from './refresher'; +import {Spinner} from '../spinner/spinner'; + + +/** + * @private + */ +@Component({ + selector: 'ion-refresher-content', + template: + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '
' + + '
' + + '
', + directives: [NgIf, Icon, Spinner], + host: { + '[attr.state]': 'r.state' + } +}) +export class RefresherContent { + + /** + * @input {string} a static icon to display when you begin to pull down + */ + @Input() pullingIcon: string; + + /** + * @input {string} the text you want to display when you begin to pull down + */ + @Input() pullingText: string; + + /** + * @input {string} An animated SVG spinner that shows when refreshing begins + */ + @Input() refreshingSpinner: string; + + /** + * @input {string} the text you want to display when performing a refresh + */ + @Input() refreshingText: string; + + + constructor(private r: Refresher, private _config: Config) {} + + /** + * @private + */ + ngOnInit() { + if (!this.pullingIcon) { + this.pullingIcon = this._config.get('pullingIcon', 'arrow-down'); + } + if (!this.refreshingSpinner) { + this.refreshingSpinner = this._config.get('refreshingSpinner', 'ios'); + } + } +} diff --git a/ionic/components/refresher/refresher.scss b/ionic/components/refresher/refresher.scss new file mode 100644 index 0000000000..108e8970e3 --- /dev/null +++ b/ionic/components/refresher/refresher.scss @@ -0,0 +1,112 @@ +@import "../../globals.core"; + +// Refresher +// -------------------------------------------------- + +$refresher-height: 60px !default; + +$refresher-icon-color: #000 !default; +$refresher-icon-font-size: 30px !default; + +$refresher-text-color: #000 !default; +$refresher-text-font-size: 16px !default; + + +ion-refresher { + position: absolute; + top: 0; + left: 0; + z-index: $z-index-refresher; + width: 100%; + height: $refresher-height; + display: none; + + &.refresher-active { + display: block; + } +} + +.has-refresher > scroll-content { + // when the refresher is let go or has completed + // this transition is what is used to put the + // scroll content back into it's original location + transition: all 320ms cubic-bezier(0.36,0.66,0.04,1); + border-top: 1px solid #ddd; + margin-top: -1px; +} + + +// Refresher Content +// -------------------------------------------------- + +ion-refresher-content { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + +.refresher-pulling, +.refresher-refreshing { + display: none; + width: 100%; +} + +.refresher-pulling-icon, +.refresher-refreshing-icon { + text-align: center; + color: $refresher-icon-color; + font-size: $refresher-icon-font-size; + transition: 200ms; + transform-origin: center; +} + +.refresher-pulling-text, +.refresher-refreshing-text { + text-align: center; + color: $refresher-text-color; + font-size: $refresher-text-font-size; +} + + +// Refresher Content States +// -------------------------------------------------- + +ion-refresher-content[state=pulling] { + .refresher-pulling { + display: block; + } +} + +ion-refresher-content[state=ready] { + .refresher-pulling { + display: block; + } + .refresher-pulling-icon { + transform: rotate(180deg); + } +} + +ion-refresher-content[state=refreshing] { + .refresher-refreshing { + display: block; + } +} + +ion-refresher-content[state=cancelling] { + .refresher-pulling { + display: block; + } + .refresher-pulling-icon { + transform: scale(0); + } +} + +ion-refresher-content[state=ending] { + .refresher-refreshing { + display: block; + } + .refresher-refreshing-icon { + transform: scale(0); + } +} diff --git a/ionic/components/refresher/refresher.ts b/ionic/components/refresher/refresher.ts new file mode 100644 index 0000000000..0140e2ba1a --- /dev/null +++ b/ionic/components/refresher/refresher.ts @@ -0,0 +1,549 @@ +import {Directive, ElementRef, EventEmitter, Host, Input, Output, NgZone} from 'angular2/core' +import {NgIf, NgClass} from 'angular2/common'; + +import {Content} from '../content/content'; +import {Icon} from '../icon/icon'; +import {isTrueProperty} from '../../util/util'; +import {CSS, pointerCoord, transitionEnd} from '../../util/dom'; + + +/** + * @name Refresher + * @description + * Allows you to add Pull-To-Refresh to an Content component. + * Place `ion-refresher` as the first child of your `ion-content` element. + * + * Pages can then can listen to the refreshers various output events. The + * `refresh` output event is the one that's fired when the user has pulled + * down far enough to kick off the refreshing process. Once the async operation + * has completed and the refreshing should end, call `endRefreshing()`. + * + * @usage + * ```html + * + * + * + * + * + * + * + * ``` + * + * ```ts + * @Page({...}) + * export class NewsFeedPage { + * + * doRefresh(refresher) { + * console.log('Begin async operation', refresher); + * + * setTimeout(() => { + * console.log('Async operation has ended'); + * refresher.endRefreshing(); + * }, 2000); + * } + * + * } + * ``` + * + * + * ## Refresher Content + * + * By default, Ionic provides the pulling icon and refreshing spinner that + * looks best for the platform the user is on. However, you can change the + * default icon and spinner, along with adding text for each state by + * adding properties to the child `ion-refresher-content` component. + * + * ```html + * + * + * + * + * + * + * + * + * ``` + * + * + * ## Further Customizing Refresher Content + * + * The `ion-refresh` component holds the refresh logic, and it requires a + * child refresher content component for its display. The `ion-refresher-content` + * component is Ionic's default that shows the actual display of the refresher + * and changes its look depending on the refresher's state. With this separation, + * it also allows developers to create their own refresher content components. + * Ideas include having some cool SVG or CSS animations that are customized to + * your app and animates the various refresher states to your liking. + * + * @demo /docs/v2/demos/refresher/ + * + */ +@Directive({ + selector: 'ion-refresher', + host: { + '[class.refresher-active]': 'state !== "inactive"' + } +}) +export class Refresher { + private _appliedStyles: boolean = false; + private _didStart: boolean; + private _lastStart: number = 0; + private _lastCheck: number = 0; + private _isEnabled: boolean = true; + private _mDown: Function; + private _mMove: Function; + private _mUp: Function; + private _tStart: Function; + private _tMove: Function; + private _tEnd: Function; + + /** + * The current state which the refresher is in. The refresher's states include: + * + * - `inactive` - The refresher is not being pulled down or refreshing and is currently hidden. + * - `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. + * - `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. + * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state. + * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `endRefreshing()` it will begin the `ending` state. + * - `ending` - 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. + */ + state: string = STATE_INACTIVE; + + /** + * The Y coordinate of where the user started to the pull down the content. + */ + startY: number = null; + + /** + * The current touch or mouse event's Y coordinate. + */ + currentY: number = null; + + /** + * The distance between the start of the pull and the current touch or + * mouse event's Y coordinate. + */ + deltaY: number = null; + + /** + * A number representing how far down the user has pulled. + * The number `0` represents the user hasn't pulled down at all. The + * number `1`, and anything greater than `1`, represents that the user + * has pulled far enough down that when they let go then the refresh will + * happen. If they let go and the number is less than `1`, then the + * refresh will not happen, and the content will return to it's original + * position. + */ + progress: number = 0; + + /** + * @input {number} The min distance the user must pull down until the + * refresher can go into the `refreshing` state. Default is `60`. + */ + @Input() pullMin: number = 60; + + /** + * @input {number} The maximum distance of the pull until the refresher + * will automatically go into the `refreshing` state. By default, the pull + * maximum will be the result of `pullMin + 60`. + */ + @Input() pullMax: number = null; + + /** + * @input {number} How many milliseconds it takes to close the refresher. Default is `280`. + */ + @Input() closeDuration: number = 280; + + /** + * @input {number} How many milliseconds it takes the refresher to to snap back to the `refreshing` state. Default is `280`. + */ + @Input() snapbackDuration: number = 280; + + /** + * @input {boolean} If the refresher is enabled or not. Default is `true`. + */ + @Input() + get enabled(): boolean { + return this._isEnabled; + } + + set enabled(val: boolean) { + this._isEnabled = isTrueProperty(val); + this._setListeners(this._isEnabled); + } + + /** + * @output {event} When the user lets go and has pulled down far enough, which would be + * farther than the `pullMin`, then your refresh hander if fired and the state is + * updated to `refreshing`. From within your refresh handler, you must call the + * `endRefreshing()` method when your async operation has completed. + */ + @Output() refresh: EventEmitter = new EventEmitter(); + + /** + * @output {event} While the user is pulling down the content and exposing the refresher. + */ + @Output() pulling: EventEmitter = new EventEmitter(); + + /** + * @output {event} When the user begins to start pulling down. + */ + @Output() start: EventEmitter = new EventEmitter(); + + + constructor( + @Host() private _content: Content, + private _zone: NgZone, + elementRef: ElementRef + ) { + _content.addCssClass('has-refresher'); + + // deprecated warning + let ele = elementRef.nativeElement; + let deprecatedAttrs = ['pullingIcon', 'pullingText', 'refreshingIcon', 'refreshingText', 'spinner']; + deprecatedAttrs.forEach(attrName => { + if (ele.hasAttribute(attrName)) { + console.warn(' property "' + attrName + '" should now be placed on the inner component instead of . Please review the Refresher docs for API updates.'); + } + }); + } + + private _onStart(ev: TouchEvent): any { + // if multitouch then get out immediately + if (ev.touches && ev.touches.length > 1) { + return 1; + } + + let coord = pointerCoord(ev); + console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y); + + let now = Date.now(); + if (this._lastStart + 100 > now) { + return 2; + } + this._lastStart = now; + + if ( ev.type === 'mousedown' && !this._mMove) { + this._mMove = this._content.addMouseMoveListener( this._onMove.bind(this) ); + } + + this.startY = this.currentY = coord.y; + this.progress = 0; + + if (!this.pullMax) { + this.pullMax = (this.pullMin + 60); + } + } + + private _onMove(ev: TouchEvent): any { + // this method can get called like a bazillion times per second, + // so it's built to be as efficient as possible, and does its + // best to do any DOM read/writes only when absolutely necessary + console.debug('Pull-to-refresh, onMove', ev.type); + + // if multitouch then get out immediately + if (ev.touches && ev.touches.length > 1) { + return 1; + } + + // do nothing if it's actively refreshing + // or it's in the process of closing + // or this was never a startY + if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_ENDING) { + return 2; + } + + // if we just updated stuff less than 16ms ago + // then don't check again, just chillout plz + let now = Date.now(); + if (this._lastCheck + 16 > now) { + return 3; + } + + // remember the last time we checked all this + this._lastCheck = now; + + // get the current pointer coordinates + let coord = pointerCoord(ev); + + this.currentY = coord.y; + + // it's now possible they could be pulling down the content + // how far have they pulled so far? + this.deltaY = (coord.y - this.startY); + + // don't bother if they're scrolling up + // and have not already started dragging + if (this.deltaY <= 0) { + // the current Y is higher than the starting Y + // so they scrolled up enough to be ignored + this.progress = 0; + + if (this.state !== STATE_INACTIVE) { + this._zone.run(() => { + this.state = STATE_INACTIVE; + }); + } + + if (this._appliedStyles) { + // reset the styles only if they were applied + this._setCss(0, '', false, ''); + return 5; + } + + return 6; + } + + if (this.state === STATE_INACTIVE) { + // this refresh is not alreadying actively pulling down + + // get the content's scrollTop + let scrollHostScrollTop = this._content.getScrollTop(); + + // if the scrollTop is greater than zero then it's + // not possible to pull the content down yet + if (scrollHostScrollTop > 0) { + this.progress = 0; + return 7; + } + + // content scrolled all the way to the top, and dragging down + this.state = STATE_PULLING; + } + + // prevent native scroll events + ev.preventDefault(); + + // the refresher is actively pulling at this point + // move the scroll element within the content element + this._setCss(this.deltaY, '0ms', true, ''); + + if (!this.deltaY) { + // don't continue if there's no delta yet + this.progress = 0; + return 8; + } + + // so far so good, let's run this all back within zone now + this._zone.run(() => { + this._onMoveInZone(); + }); + } + + private _onMoveInZone() { + // set pull progress + this.progress = (this.deltaY / this.pullMin); + + // emit "start" if it hasn't started yet + if (!this._didStart) { + this._didStart = true; + this.start.emit(this); + } + + // emit "pulling" on every move + this.pulling.emit(this); + + // do nothing if the delta is less than the pull threshold + if (this.deltaY < this.pullMin) { + // ensure it stays in the pulling state, cuz its not ready yet + this.state = STATE_PULLING; + return 2; + } + + if (this.deltaY > this.pullMax) { + // they pulled farther than the max, so kick off the refresh + this._beginRefresh(); + return 3; + } + + // pulled farther than the pull min!! + // it is now in the `ready` state!! + // if they let go then it'll refresh, kerpow!! + this.state = STATE_READY; + + return 4; + } + + private _onEnd(ev) { + // only run in a zone when absolutely necessary + + if (this.state === STATE_READY) { + this._zone.run(() => { + // they pulled down far enough, so it's ready to refresh + this._beginRefresh(); + }); + + } else if (this.state === STATE_PULLING) { + this._zone.run(() => { + // they were pulling down, but didn't pull down far enough + // set the content back to it's original location + // and close the refresher + // set that the refresh is actively cancelling + this.cancelRefreshing(); + }); + } + + // reset on any touchend/mouseup + this.startY = null; + if (this._mMove) { + // we don't want to always listen to mousemoves + // remove it if we're still listening + this._mMove(); + this._mMove = null; + } + } + + private _beginRefresh() { + // assumes we're already back in a zone + // they pulled down far enough, so it's ready to refresh + this.state = STATE_REFRESHING; + + // place the content in a hangout position while it thinks + this._setCss(this.pullMin,( this.snapbackDuration + 'ms'), true, ''); + + // emit "refresh" because it was pulled down far enough + // and they let go to begin refreshing + this.refresh.emit(this); + } + + /** + * Call `endRefreshing()` when your async operation has completed. + * For example, the `refreshing` state is while the app is performing + * an asynchronous operation, such as receiving more data from an + * AJAX request. Once the data has been received, you then call this + * method to signify that the refreshing has completed and to close + * the refresher. This method also changes the refresher's state from + * `refreshing` to `ending`. + */ + endRefreshing() { + this._close(STATE_ENDING, '120ms'); + } + + /** + * Changes the refresher's state from `refreshing` to `cancelling`. + */ + cancelRefreshing() { + this._close(STATE_CANCELLING, ''); + } + + /** + * @private + */ + private complete() { + // deprecated warning + console.warn('refresher completed() deprecated, please update to endRefreshing()'); + this.endRefreshing(); + } + + private _close(state: string, delay: string) { + var timer; + + function close(ev) { + // closing is done, return to inactive state + if (ev) { + clearTimeout(timer); + } + + this.state = STATE_INACTIVE; + this.progress = 0; + this._didStart = this.startY = this.currentY = this.deltaY = null; + this._setCss(0, '0ms', false, ''); + } + + // create fallback timer incase something goes wrong with transitionEnd event + timer = setTimeout(close.bind(this), 600); + + // create transition end event on the content's scroll element + this._content.onScrollElementTransitionEnd(close.bind(this)); + + // reset set the styles on the scroll element + // set that the refresh is actively cancelling/completing + this.state = state; + this._setCss(0, '', true, delay); + + if (this._mMove) { + // always remove the mousemove event + this._mMove(); + this._mMove = null; + } + } + + private _setCss(y: number, duration: string, overflowVisible: boolean, delay: string) { + this._appliedStyles = (y > 0); + + var content = this._content; + content.setScrollElementStyle(CSS.transform, ((y > 0) ? 'translateY(' + y + 'px) translateZ(0px)' : 'translateZ(0px)')); + content.setScrollElementStyle(CSS.transitionDuration, duration); + content.setScrollElementStyle(CSS.transitionDelay, delay); + content.setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : '')); + } + + private _setListeners(shouldListen: boolean) { + const self = this; + const content = self._content; + + if (shouldListen) { + // add listener outside of zone + // touch handlers + self._zone.runOutsideAngular(function() { + if (!self._tStart) { + self._tStart = content.addTouchStartListener( self._onStart.bind(self) ); + } + if (!self._tMove) { + self._tMove = content.addTouchMoveListener( self._onMove.bind(self) ); + } + if (!self._tEnd) { + self._tEnd = content.addTouchEndListener( self._onEnd.bind(self) ); + } + + // mouse handlers + // mousemove does not get added until mousedown fires + if (!self._mDown) { + self._mDown = content.addMouseDownListener( self._onStart.bind(self) ); + } + if (!self._mUp) { + self._mUp = content.addMouseUpListener( self._onEnd.bind(self) ); + } + }); + + } else { + // unregister event listeners from content element + self._mDown && self._mDown(); + self._mMove && self._mMove(); + self._mUp && self._mUp(); + self._tStart && self._tStart(); + self._tMove && self._tMove(); + self._tEnd && self._tEnd(); + + self._mDown = self._mMove = self._mUp = self._tStart = self._tMove = self._tEnd = null; + } + } + + /** + * @private + */ + ngOnInit() { + // bind event listeners + // save the unregister listener functions to use onDestroy + this._setListeners(this._isEnabled); + } + + /** + * @private + */ + ngOnDestroy() { + this._setListeners(false); + } + +} + +const STATE_INACTIVE = 'inactive'; +const STATE_PULLING = 'pulling'; +const STATE_READY = 'ready'; +const STATE_REFRESHING = 'refreshing'; +const STATE_CANCELLING = 'cancelling'; +const STATE_ENDING = 'ending'; diff --git a/ionic/components/refresher/test/basic/index.ts b/ionic/components/refresher/test/basic/index.ts new file mode 100644 index 0000000000..0dde21fb59 --- /dev/null +++ b/ionic/components/refresher/test/basic/index.ts @@ -0,0 +1,85 @@ +import {App} from 'ionic-angular'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp { + items = []; + + constructor() { + for (var i = 0; i < 3; i++) { + this.items.push( getRandomData() ); + } + } + + doRefresh(refresher) { + console.log('Begin async operation'); + + getAsyncData().then(newData => { + for (var i = 0; i < newData.length; i++) { + this.items.unshift( newData[i] ); + } + + console.log('Finished receiving data, async operation complete'); + refresher.endRefreshing(); + }); + } + + doStart(refresher) { + console.log('Refresher, start'); + } + + doPulling(refresher) { + console.log('Pulling', refresher.progress); + } + +} + +function getAsyncData() { + // async return mock data + return new Promise(resolve => { + + setTimeout(() => { + let data = []; + for (var i = 0; i < 3; i++) { + data.push( getRandomData() ); + } + + resolve(data); + }, 1000); + + }); +} + +function getRandomData() { + let i = Math.floor( Math.random() * data.length ); + return data[i]; +} + +const data = [ + 'Fast Times at Ridgemont High', + 'Peggy Sue Got Married', + 'Raising Arizona', + 'Moonstruck', + 'Fire Birds', + 'Honeymoon in Vegas', + 'Amos & Andrew', + 'It Could Happen to You', + 'Trapped in Paradise', + 'Leaving Las Vegas', + 'The Rock', + 'Con Air', + 'Face/Off', + 'City of Angels', + 'Gone in Sixty Seconds', + 'The Family Man', + 'Windtalkers', + 'Matchstick Men', + 'National Treasure', + 'Ghost Rider', + 'Grindhouse', + 'Next', + 'Kick-Ass', + 'Drive Angry' +]; \ No newline at end of file diff --git a/ionic/components/refresher/test/basic/main.html b/ionic/components/refresher/test/basic/main.html new file mode 100644 index 0000000000..a31eed2f23 --- /dev/null +++ b/ionic/components/refresher/test/basic/main.html @@ -0,0 +1,21 @@ +Pull To Refresh + + + + + + + + + + + + + {{ item }} + + + + diff --git a/ionic/components/refresher/test/refresher.spec.ts b/ionic/components/refresher/test/refresher.spec.ts new file mode 100644 index 0000000000..70ff1c3835 --- /dev/null +++ b/ionic/components/refresher/test/refresher.spec.ts @@ -0,0 +1,257 @@ +import {Refresher, Content, Config, Ion} from 'ionic-angular'; + +export function run() { + +describe('Refresher', () => { + + describe('_onEnd', () => { + + it('should set to refreshing if state=ready', () => { + refresher.state = 'ready'; + refresher._onEnd(); + expect(refresher.state).toEqual('refreshing'); + }); + + it('should set to canelling if state=pulling on release', () => { + refresher.state = 'pulling'; + refresher._onEnd(); + expect(refresher.state).toEqual('cancelling'); + }); + + it('should do nothing if state=cancelling', () => { + refresher.state = 'cancelling'; + var results = refresher._onEnd(); + expect(refresher.state).toEqual('cancelling'); + }); + + it('should do nothing if state=completing', () => { + refresher.state = 'completing'; + var results = refresher._onEnd(); + expect(refresher.state).toEqual('completing'); + }); + + it('should do nothing if state=refreshing', () => { + refresher.state = 'refreshing'; + var results = refresher._onEnd(); + expect(refresher.state).toEqual('refreshing'); + }); + + it('should do nothing if state=inactive', () => { + refresher.state = 'inactive'; + refresher._onEnd(); + expect(refresher.state).toEqual('inactive'); + }); + + }); + + describe('_onMoveInZone', () => { + + it('should set ready state when pulling down and it went past the pull min', () => { + refresher.state = 'inactive'; + + refresher.pullMin = 100; + refresher.pullMax = 200; + refresher.deltaY = 100; + let result = refresher._onMoveInZone(); + + expect(result).toEqual(4); + expect(refresher.state).toEqual('ready'); + expect(refresher.progress).toEqual(1); + }); + + it('should set begin refreshing when pulling down and it went past the pull max', () => { + refresher.state = 'inactive'; + + refresher.pullMin = 100; + refresher.pullMax = 200; + refresher.deltaY = 250; + let result = refresher._onMoveInZone(); + + expect(result).toEqual(3); + expect(refresher.state).toEqual('refreshing'); + expect(refresher.progress).toEqual(2.5); + }); + + it('should set pulling state when pulling down, but not past the pull min', () => { + refresher.state = 'inactive'; + + refresher.pullMin = 100; + refresher.pullMax = 200; + refresher.deltaY = 50; + let result = refresher._onMoveInZone(); + + expect(result).toEqual(2); + expect(refresher.state).toEqual('pulling'); + expect(refresher.progress).toEqual(0.5); + }); + + }); + + describe('_onMove', () => { + + it('should set scrollElement inline styles when pulling down, but not past threshold', () => { + setContentScrollTop(0); + refresher.startY = 100; + refresher.pullMin = 80; + let result = refresher._onMove( touchEv(125) ); + + expect(getScrollElementStyles().transform).toEqual('translateY(25px) translateZ(0px)'); + expect(getScrollElementStyles().transitionDuration).toEqual('0ms'); + expect(getScrollElementStyles().overflow).toEqual('hidden'); + }); + + it('should set scrollElement inline styles when pulling up above startY', () => { + refresher.state = 'inactive'; + refresher._appliedStyles = false; + + setContentScrollTop(1); + refresher.startY = 100; + let result = refresher._onMove( touchEv(95) ); + + expect(result).toEqual(6); + }); + + it('should not pull when scrolling down, state=inactive, deltaY>0, scrollTop>0', () => { + refresher.state = 'inactive'; + + setContentScrollTop(50); + refresher.startY = 100; + let result = refresher._onMove( touchEv(125) ); + + expect(refresher.state).toEqual('inactive'); + expect(result).toEqual(7); + }); + + it('should reset styles when _appliedStyles=true, delta<=0', () => { + refresher._appliedStyles = true; + + refresher.startY = 100; + let result = refresher._onMove( touchEv(85) ); + + expect(refresher.state).toEqual('inactive'); + expect(getScrollElementStyles().transform).toEqual('translateZ(0px)'); + expect(getScrollElementStyles().transitionDuration).toEqual(''); + expect(getScrollElementStyles().overflow).toEqual(''); + expect(result).toEqual(5); + }); + + it('should not run when scrollTop is > 0', () => { + setContentScrollTop(50); + refresher.startY = 100; + + var results = refresher._onMove(touchEv(80)); + expect(results).toEqual(6); + }); + + it('should not run when scrolling up, but isnt actively dragging', () => { + setContentScrollTop(1); + refresher.startY = 100; + refresher._isDragging = false + + var results = refresher._onMove(touchEv(85)); + expect(results).toEqual(6); + }); + + it('should set the deltaY', () => { + refresher.startY = 100; + refresher._onMove( touchEv(133) ); + expect(refresher.deltaY).toEqual(33); + + refresher._lastCheck = 0; // force allow next check + + refresher._onMove( touchEv(50) ); + expect(refresher.deltaY).toEqual(-50); + }); + + it('should not run if it already ran less than 16ms ago', () => { + refresher.startY = 100; + var results = refresher._onMove(touchEv(88)); + expect(results).toEqual(6); + + results = refresher._onMove(touchEv(88)); + expect(results).toEqual(3); + }); + + it('should not run if state=refreshing', () => { + refresher.startY = 100; + refresher.state = 'refreshing'; + var results = refresher._onMove( touchEv(88) ); + expect(results).toEqual(2); + }); + + it('should not run if state=ending', () => { + refresher.startY = 100; + refresher.state = 'ending'; + var results = refresher._onMove( touchEv(88) ); + expect(results).toEqual(2); + }); + + it('should not run if state=cancelling', () => { + refresher.startY = 100; + refresher.state = 'cancelling'; + var results = refresher._onMove( touchEv(88) ); + expect(results).toEqual(2); + }); + + it('should not run if no startY', () => { + refresher.startY = null; + var results = refresher._onMove( touchEv(88) ); + expect(results).toEqual(2); + }); + + it('should not run if multiple touches', () => { + var results = refresher._onMove({ + touches: [{},{}] + }); + expect(results).toEqual(1); + }); + + }); + + + let config = new Config(); + let refresher: Refresher; + let content: Content; + let contentElementRef; + let zone = { + run: function(cb) {cb()}, + runOutsideAngular: function(cb) {cb()} + }; + + beforeEach(() => { + contentElementRef = mockElementRef(); + content = new Content(contentElementRef, config, null, null, null); + content.scrollElement = document.createElement('scroll-content'); + + refresher = new Refresher(content, zone, mockElementRef()); + }); + + function touchEv(y: number) { + return { + type: 'mockTouch', + touches: [{clientY: y}], + preventDefault: function(){} + } + } + + function mockElementRef() { + return { + nativeElement: { + classList: { add: function(){}, remove: function(){} }, + scrollTop: 0, + hasAttribute: function(){} + } + } + } + + function setContentScrollTop(scrollTop) { + contentElementRef.nativeElement.scrollTop = scrollTop; + } + + function getScrollElementStyles() { + return content.scrollElement.style; + } + +}); + +} diff --git a/ionic/components/scroll/pull-to-refresh.scss b/ionic/components/scroll/pull-to-refresh.scss deleted file mode 100644 index 1ee2dcf1ff..0000000000 --- a/ionic/components/scroll/pull-to-refresh.scss +++ /dev/null @@ -1,104 +0,0 @@ -// Scroll refresher (for pull to refresh) -ion-refresher { - position: absolute; - top: -60px; - right: 0; - left: 0; - overflow: hidden; - margin: auto; - height: 60px; - .refresher-content { - position: absolute; - bottom: 15px; - left: 0; - width: 100%; - color: #000;//$scroll-refresh-icon-color; - text-align: center; - - font-size: 30px; - - .text-refreshing, - .text-pulling { - font-size: 16px; - line-height: 16px; - } - &.refresher-with-text { - bottom: 10px; - } - } - - .icon-refreshing, - .icon-pulling { - width: 100%; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-transform-style: preserve-3d; - transform-style: preserve-3d; - } - .icon-pulling { - animation-name: refresh-spin-back; - animation-duration: 200ms; - animation-timing-function: linear; - animation-fill-mode: none; - transform: translate3d(0,0,0) rotate(0deg); - } - .icon-refreshing, - .text-refreshing { - display: none; - } - .icon-refreshing { - animation-duration: 1.5s; - } - - &.active { - .icon-pulling:not(.pulling-rotation-disabled) { - animation-name: refresh-spin; - transform: translate3d(0,0,0) rotate(-180deg); - } - &.refreshing { - transition: -webkit-transform .2s; - transition: transform .2s; - transform: scale(1,1); - - .icon-pulling, - .text-pulling { - display: none; - } - .icon-refreshing, - .text-refreshing { - display: block; - } - &.refreshing-tail { - transform: scale(0,0); - } - } - } -} -scroll-content.overscroll { - overflow: visible; -} -/* - -webkit-overflow-scrolling:touch; - width:100%; -} -*/ - -@-webkit-keyframes refresh-spin { - 0% { -webkit-transform: translate3d(0,0,0) rotate(0); } - 100% { -webkit-transform: translate3d(0,0,0) rotate(180deg); } -} - -@keyframes refresh-spin { - 0% { transform: translate3d(0,0,0) rotate(0); } - 100% { transform: translate3d(0,0,0) rotate(180deg); } -} - -@-webkit-keyframes refresh-spin-back { - 0% { -webkit-transform: translate3d(0,0,0) rotate(180deg); } - 100% { -webkit-transform: translate3d(0,0,0) rotate(0); } -} - -@keyframes refresh-spin-back { - 0% { transform: translate3d(0,0,0) rotate(180deg); } - 100% { transform: translate3d(0,0,0) rotate(0); } -} diff --git a/ionic/components/scroll/pull-to-refresh.ts b/ionic/components/scroll/pull-to-refresh.ts deleted file mode 100644 index fdce69e5bf..0000000000 --- a/ionic/components/scroll/pull-to-refresh.ts +++ /dev/null @@ -1,544 +0,0 @@ -import {Component, ElementRef, EventEmitter, Host, Input, Output} from 'angular2/core' -import {NgIf, NgClass} from 'angular2/common'; - -import {Content} from '../content/content'; -import {Icon} from '../icon/icon'; -import {isDefined, defaults} from '../../util/util'; -import {raf, ready, CSS} from '../../util/dom'; - - -/** - * @name Refresher - * @description - * Allows you to add pull-to-refresh to an Content component. - * Place it as the first child of your Content or Scroll element. - * - * When refreshing is complete, call `refresher.complete()` from your controller. - * - * @usage - * ```html - * - * - * - * - * - - * ``` - * - * ```ts - * export class MyClass { - * - * doRefresh(refresher) { - * console.log('Doing Refresh', refresher) - * - * setTimeout(() => { - * refresher.complete(); - * console.log("Complete"); - * }, 5000); - * } - * - * doStart(refresher) { - * console.log('Doing Start', refresher); - * } - * - * doPulling(refresher) { - * console.log('Pulling', refresher); - * } - * - * } - * ``` - * @demo /docs/v2/demos/refresher/ - * - */ -@Component({ - selector: 'ion-refresher', - host: { - '[class.active]': 'isActive', - '[class.refreshing]': 'isRefreshing', - '[class.refreshingTail]': 'isRefreshingTail' - }, - template: - '
' + - '
' + - '' + - '
' + - '
' + - '
' + - '' + - '
' + - '
' + - '
', - directives: [NgIf, NgClass, Icon] -}) -export class Refresher { - private _ele: HTMLElement; - private _touchMoveListener; - private _touchEndListener; - private _handleScrollListener; - - - /** - * @private - */ - isActive: boolean; - - /** - * @private - */ - isDragging: boolean = false; - - /** - * @private - */ - isOverscrolling: boolean = false; - - /** - * @private - */ - dragOffset: number = 0; - - /** - * @private - */ - lastOverscroll: number = 0; - - /** - * @private - */ - ptrThreshold: number = 0; - - /** - * @private - */ - activated: boolean = false; - - /** - * @private - */ - scrollTime: number = 500; - - /** - * @private - */ - canOverscroll: boolean = true; - - /** - * @private - */ - startY; - - /** - * @private - */ - deltaY; - - /** - * @private - */ - scrollHost; - - /** - * @private - */ - scrollChild; - - /** - * @private - */ - showIcon: boolean; - - /** - * @private - */ - showSpinner: boolean; - - /** - * @private - */ - isRefreshing: boolean; - - /** - * @private - */ - isRefreshingTail: boolean; - - - /** - * @input {string} the icon you want to display when you begin to pull down - */ - @Input() pullingIcon: string; - - /** - * @input {string} the text you want to display when you begin to pull down - */ - @Input() pullingText: string; - - /** - * @input {string} the icon you want to display when performing a refresh - */ - @Input() refreshingIcon: string; - - /** - * @input {string} the text you want to display when performing a refresh - */ - @Input() refreshingText: string; - - /** - * @private - */ - @Input() spinner: string; - - - /** - * @output {event} When you are pulling down - */ - @Output() pulling: EventEmitter = new EventEmitter(); - - /** - * @output {event} When you are refreshing - */ - @Output() refresh: EventEmitter = new EventEmitter(); - - /** - * @output {event} When you start pulling down - */ - @Output() start: EventEmitter = new EventEmitter(); - - - constructor( - @Host() private _content: Content, - _element: ElementRef - ) { - this._ele = _element.nativeElement; - this._ele.classList.add('content'); - } - - /** - * @private - */ - ngOnInit() { - let sp = this._content.getNativeElement(); - let sc = this._content.scrollElement; - - this.startY = null; - this.deltaY = null; - this.scrollHost = sp; - this.scrollChild = sc; - - defaults(this, { - pullingIcon: 'md-arrow-down', - refreshingIcon: 'ionic' - }) - - this.showSpinner = !isDefined(this.refreshingIcon) && this.spinner != 'none'; - this.showIcon = isDefined(this.refreshingIcon); - - this._touchMoveListener = this._handleTouchMove.bind(this); - this._touchEndListener = this._handleTouchEnd.bind(this); - this._handleScrollListener = this._handleScroll.bind(this); - - sc.addEventListener('touchmove', this._touchMoveListener); - sc.addEventListener('touchend', this._touchEndListener); - sc.addEventListener('scroll', this._handleScrollListener); - } - - /** - * @private - */ - ngOnDestroy() { - let sc = this._content.scrollElement; - sc.removeEventListener('touchmove', this._touchMoveListener); - sc.removeEventListener('touchend', this._touchEndListener); - sc.removeEventListener('scroll', this._handleScrollListener); - } - - /** - * @private - * @param {TODO} val TODO - */ - overscroll(val) { - this.scrollChild.style[CSS.transform] = 'translateY(' + val + 'px)'; - this.lastOverscroll = val; - } - - /** - * @private - * @param {TODO} target TODO - * @param {TODO} newScrollTop TODO - */ - nativescroll(target, newScrollTop) { - // creates a scroll event that bubbles, can be cancelled, and with its view - // and detail property initialized to window and 1, respectively - target.scrollTop = newScrollTop; - var e = document.createEvent("UIEvents"); - e.initUIEvent("scroll", true, true, window, 1); - target.dispatchEvent(e); - } - - /** - * @private - * @param {TODO} enabled TODO - */ - setScrollLock(enabled) { - // set the scrollbar to be position:fixed in preparation to overscroll - // or remove it so the app can be natively scrolled - if (enabled) { - raf(() => { - this.scrollChild.classList.add('overscroll'); - this.show(); - }); - - } else { - raf(() => { - this.scrollChild.classList.remove('overscroll'); - this.hide(); - this.deactivate(); - }); - } - } - - /** - * @private - */ - activate() { - //this.ele.classList.add('active'); - this.isActive = true; - this.start.emit(this); - } - - /** - * @private - */ - deactivate() { - // give tail 150ms to finish - setTimeout(() => { - this.isActive = false; - this.isRefreshing = false; - this.isRefreshingTail = false; - // deactivateCallback - if (this.activated) this.activated = false; - }, 150); - } - - /** - * @private - */ - startRefresh() { - // startCallback - this.isRefreshing = true; - this.refresh.emit(this); - } - - /** - * @private - */ - show() { - // showCallback - this._ele.classList.remove('invisible'); - } - - /** - * @private - */ - hide() { - // showCallback - this._ele.classList.add('invisible'); - } - - /** - * @private - */ - tail() { - // tailCallback - this._ele.classList.add('refreshing-tail'); - } - - /** - * @private - */ - complete() { - setTimeout(() => { - raf(this.tail.bind(this)); - - // scroll back to home during tail animation - this.scrollTo(0, this.scrollTime, this.deactivate.bind(this)); - - // return to native scrolling after tail animation has time to finish - setTimeout(() => { - if (this.isOverscrolling) { - this.isOverscrolling = false; - this.setScrollLock(false); - } - }, this.scrollTime); - - }, this.scrollTime); - } - -/** - * @private - * @param {TODO} Y TODO - * @param {TODO} duration TODO - * @param {Function} callback TODO - */ - scrollTo(Y, duration, callback?) { - // scroll animation loop w/ easing - // credit https://gist.github.com/dezinezync/5487119 - var start = Date.now(), - from = this.lastOverscroll; - - if (from === Y) { - callback && callback(); - return; /* Prevent scrolling to the Y point if already there */ - } - - // decelerating to zero velocity - function easeOutCubic(t) { - return (--t) * t * t + 1; - } - - // scroll loop - function scroll() { - var currentTime = Date.now(), - time = Math.min(1, ((currentTime - start) / duration)), - // where .5 would be 50% of time on a linear scale easedT gives a - // fraction based on the easing method - easedT = easeOutCubic(time); - - this.overscroll( Math.round((easedT * (Y - from)) + from) ); - - if (time < 1) { - raf(scroll.bind(this)); - - } else { - - if (Y < 5 && Y > -5) { - this.isOverscrolling = false; - this.setScrollLock(false); - } - - callback && callback(); - } - } - - // start scroll loop - raf(scroll.bind(this)); - } - - /** - * @private - * TODO - * @param {Event} e TODO - */ - _handleTouchMove(e) { - //console.debug('TOUCHMOVE', e); - - // if multitouch or regular scroll event, get out immediately - if (!this.canOverscroll || e.touches.length > 1) { - return; - } - //if this is a new drag, keep track of where we start - if (this.startY === null) { - this.startY = parseInt(e.touches[0].screenY, 10); - } - - // how far have we dragged so far? - this.deltaY = parseInt(e.touches[0].screenY, 10) - this.startY; - - - // if we've dragged up and back down in to native scroll territory - if (this.deltaY - this.dragOffset <= 0 || this.scrollHost.scrollTop !== 0) { - - if (this.isOverscrolling) { - this.isOverscrolling = false; - this.setScrollLock(false); - } - - if (this.isDragging) { - this.nativescroll(this.scrollHost, Math.round(this.deltaY - this.dragOffset) * -1); - } - - // if we're not at overscroll 0 yet, 0 out - if (this.lastOverscroll !== 0) { - this.overscroll(0); - } - return; - - } else if (this.deltaY > 0 && this.scrollHost.scrollTop === 0 && !this.isOverscrolling) { - // starting overscroll, but drag started below scrollTop 0, so we need to offset the position - this.dragOffset = this.deltaY; - } - - // prevent native scroll events while overscrolling - e.preventDefault(); - - // if not overscrolling yet, initiate overscrolling - if (!this.isOverscrolling) { - this.isOverscrolling = true; - this.setScrollLock(true); - } - - this.isDragging = true; - - // overscroll according to the user's drag so far - this.overscroll( Math.round((this.deltaY - this.dragOffset) / 3) ); - - // Pass the refresher to the EventEmitter - this.pulling.emit(this); - - // update the icon accordingly - if (!this.activated && this.lastOverscroll > this.ptrThreshold) { - this.activated = true; - raf(this.activate.bind(this)); - - } else if (this.activated && this.lastOverscroll < this.ptrThreshold) { - this.activated = false; - raf(this.deactivate.bind(this)); - } - } - - /** - * @private - * TODO - * @param {Event} e TODO - */ - _handleTouchEnd(e) { - console.debug('TOUCHEND', e); - // if this wasn't an overscroll, get out immediately - if (!this.canOverscroll && !this.isDragging) { - return; - } - // reset Y - this.startY = null; - // the user has overscrolled but went back to native scrolling - if (!this.isDragging) { - this.dragOffset = 0; - this.isOverscrolling = false; - this.setScrollLock(false); - } else { - this.isDragging = false; - this.dragOffset = 0; - - // the user has scroll far enough to trigger a refresh - if (this.lastOverscroll > this.ptrThreshold) { - this.startRefresh(); - this.scrollTo(this.ptrThreshold, this.scrollTime); - - // the user has overscrolled but not far enough to trigger a refresh - } else { - this.scrollTo(0, this.scrollTime, this.deactivate.bind(this)); - this.isOverscrolling = false; - } - } - } - - /** - * @private - * TODO - * @param {Event} e TODO - */ - _handleScroll(e) { - console.debug('SCROLL', e.target.scrollTop); - } -} diff --git a/ionic/components/scroll/test/pull-to-refresh/index.ts b/ionic/components/scroll/test/pull-to-refresh/index.ts deleted file mode 100644 index fa5d12f25e..0000000000 --- a/ionic/components/scroll/test/pull-to-refresh/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import {App} from 'ionic-angular'; - - -@App({ - templateUrl: 'main.html' -}) -class E2EApp { - items = []; - - constructor() { - for(let i = 0; i < 20; i++) { - this.items.push({ "index": i }); - } - } - - doRefresh(refresher) { - console.log('Doing Refresh', refresher) - - // Add to the top of the list on refresh - let firstIndex = this.items[0].index - 1; - - for(let i = firstIndex; i > firstIndex - 5; i--) { - this.items.unshift({ "index": i }); - } - - setTimeout(() => { - refresher.complete(); - console.log("Complete"); - }, 5000); - } - - doStart(refresher) { - console.log('Doing Start', refresher); - } - - doPulling(refresher) { - console.log('Pulling', refresher); - } -} diff --git a/ionic/components/scroll/test/pull-to-refresh/main.html b/ionic/components/scroll/test/pull-to-refresh/main.html deleted file mode 100644 index f4029d0a4e..0000000000 --- a/ionic/components/scroll/test/pull-to-refresh/main.html +++ /dev/null @@ -1,18 +0,0 @@ -Pull To Refresh - - - - - - - Item {{ item.index }} - - - diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts index dc8da6d53a..d4cc4af272 100644 --- a/ionic/config/directives.ts +++ b/ionic/config/directives.ts @@ -10,7 +10,8 @@ import {Button} from '../components/button/button'; import {Blur} from '../components/blur/blur'; import {Content} from '../components/content/content'; import {Scroll} from '../components/scroll/scroll'; -import {Refresher} from '../components/scroll/pull-to-refresh'; +import {Refresher} from '../components/refresher/refresher'; +import {RefresherContent} from '../components/refresher/refresher-content'; import {Slides, Slide, SlideLazy} from '../components/slides/slides'; import {Tabs} from '../components/tabs/tabs'; import {Tab} from '../components/tabs/tab'; @@ -58,6 +59,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when'; * - Content * - Scroll * - Refresher + * - RefresherContent * * **Lists** * - List @@ -125,6 +127,7 @@ export const IONIC_DIRECTIVES = [ Content, Scroll, Refresher, + RefresherContent, // Lists List, diff --git a/ionic/util/dom.ts b/ionic/util/dom.ts index bc080d4693..93f4a2cf4c 100644 --- a/ionic/util/dom.ts +++ b/ionic/util/dom.ts @@ -45,6 +45,7 @@ export let CSS: { transform?: string, transition?: string, transitionDuration?: string, + transitionDelay?: string, transitionTimingFn?: string, transitionStart?: string, transitionEnd?: string, @@ -80,6 +81,9 @@ export let CSS: { // transition timing function CSS.transitionTimingFn = (isWebkit ? '-webkit-' : '') + 'transition-timing-function'; + // transition delay + CSS.transitionDelay = (isWebkit ? '-webkit-' : '') + 'transition-delay'; + // To be sure transitionend works everywhere, include *both* the webkit and non-webkit events CSS.transitionEnd = (isWebkit ? 'webkitTransitionEnd ' : '') + 'transitionend'; })(); @@ -156,7 +160,7 @@ export function windowLoad(callback?: Function) { return promise; } -export function pointerCoord(ev): {x: number, y: number} { +export function pointerCoord(ev: any): {x: number, y: number} { // get coordinates for either a mouse click // or a touch depending on the given event let c = { x: 0, y: 0 }; @@ -243,7 +247,6 @@ export function closest(ele: HTMLElement, selector: string, checkSelf?: boolean) /** * Get the element offsetWidth and offsetHeight. Values are cached * to reduce DOM reads. Cache is cleared on a window resize. - * @param {TODO} ele TODO */ export function getDimensions(ele: HTMLElement, id: string): { width: number, height: number, left: number, top: number @@ -268,6 +271,10 @@ export function getDimensions(ele: HTMLElement, id: string): { return dimensions; } +export function clearDimensions(id: string) { + delete dimensionCache[id]; +} + export function windowDimensions(): {width: number, height: number} { if (!dimensionCache.win) { // make sure we got good values before caching From 05a0da480a56c640a69922fc49d24064305ae5fe Mon Sep 17 00:00:00 2001 From: chuz93 Date: Sat, 27 Feb 2016 18:12:33 -0600 Subject: [PATCH 08/32] Update input.ts Fix * * - * Website + * Website * * * From 66af6ff3a004fafd65e90029b956b2d79164d09c Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Sat, 27 Feb 2016 20:50:57 -0600 Subject: [PATCH 09/32] test(actionsheet): open modal from actionsheet --- .../action-sheet/test/basic/index.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/ionic/components/action-sheet/test/basic/index.ts b/ionic/components/action-sheet/test/basic/index.ts index 5a745a3bc4..49949d70bd 100644 --- a/ionic/components/action-sheet/test/basic/index.ts +++ b/ionic/components/action-sheet/test/basic/index.ts @@ -1,4 +1,4 @@ -import {App, Page, ActionSheet, NavController} from 'ionic-angular'; +import {App, Page, ActionSheet, Modal, NavController, ViewController} from 'ionic-angular'; @Page({ @@ -31,9 +31,10 @@ class E2EPage { } }, { - text: 'No close', + text: 'Open Modal', handler: () => { - console.log('do not close clicked'); + let modal = Modal.create(ModalPage); + this.nav.present(modal); // returning false does not allow the actionsheet to be closed return false; @@ -90,6 +91,27 @@ class E2EPage { } +@Page({ + template: ` + + + + + Modal + + + Hi, I'm Bob, and I'm a modal. + + ` +}) +class ModalPage { + constructor(private viewCtrl: ViewController) {} + + dismiss() { + this.viewCtrl.dismiss(); + } +} + @App({ template: '' From 406a709aa17d6d4d188d2550ae185f4d7683bfce Mon Sep 17 00:00:00 2001 From: Brandon Brown <7200rpm@gmail.com> Date: Sat, 27 Feb 2016 21:20:15 -0800 Subject: [PATCH 10/32] Update modal.ts Need to import ViewController for modal as well. --- ionic/components/modal/modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ionic/components/modal/modal.ts b/ionic/components/modal/modal.ts index e583baf44b..af51119cfe 100644 --- a/ionic/components/modal/modal.ts +++ b/ionic/components/modal/modal.ts @@ -60,7 +60,7 @@ import {Transition, TransitionOptions} from '../../transitions/transition'; * modal. * * ```ts - * import {Page, Modal, NavController} from 'ionic-angular'; + * import {Page, Modal, NavController, ViewController} from 'ionic-angular'; * * @Page(...) * class HomePage { From d3cdcc5fe288d789025863240def1b00ecad8a37 Mon Sep 17 00:00:00 2001 From: Justin Willis Date: Sun, 28 Feb 2016 17:59:40 -0600 Subject: [PATCH 11/32] demos: remove custom animation on alert and actionsheet for now and add padding to alert --- demos/action-sheet/main.html | 1 - demos/alert/index.ts | 4 ---- demos/alert/main.html | 3 +-- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/demos/action-sheet/main.html b/demos/action-sheet/main.html index b43475c1e4..efb9d9ac3f 100644 --- a/demos/action-sheet/main.html +++ b/demos/action-sheet/main.html @@ -4,5 +4,4 @@ - diff --git a/demos/alert/index.ts b/demos/alert/index.ts index 77cdde6380..cf0000843b 100644 --- a/demos/alert/index.ts +++ b/demos/alert/index.ts @@ -213,8 +213,4 @@ export class InitialPage { this.nav.present(alert); } - doCustomAnimation() { - - } - } diff --git a/demos/alert/main.html b/demos/alert/main.html index 30d0896fd9..88560381e6 100644 --- a/demos/alert/main.html +++ b/demos/alert/main.html @@ -2,11 +2,10 @@ Alert - + - From 3784f47cfa50929aba744ffef14c6cc052fb0419 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 08:53:24 -0600 Subject: [PATCH 12/32] fix(generate): output correct Sass import for pages Closes #5641. --- tooling/generators/page/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tooling/generators/page/index.js b/tooling/generators/page/index.js index cbaa217448..c3dbc6622b 100644 --- a/tooling/generators/page/index.js +++ b/tooling/generators/page/index.js @@ -28,9 +28,9 @@ Generator.prototype.renderTemplates = function renderTemplates() { }, this); console.log(('\nDon\'t forget to add an import for ' + scssName + ' ' + 'in ' + - path.join('app', 'themes', 'app.core.scss') + ':\n\n @import ' + + path.join('app', 'themes', 'app.core.scss') + ':\n\n @import "' + path.relative(path.join(this.appDirectory, 'app', 'themes'), scssPath) + - '\n').green); + '";\n').green); } From 0480fa3b50839dc19e60cd2ba83d934465b2a5de Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 11:13:27 -0600 Subject: [PATCH 13/32] feat(infiniteScroll): add infinite scroll Closes #5415 --- ionic/components.core.scss | 1 + ionic/components.ts | 2 + ionic/components/content/content.ts | 17 +- .../infinite-scroll-content.ts | 48 ++++ .../infinite-scroll/infinite-scroll.scss | 44 +++ .../infinite-scroll/infinite-scroll.ts | 256 ++++++++++++++++++ .../infinite-scroll/test/basic/index.ts | 49 ++++ .../infinite-scroll/test/basic/main.html | 18 ++ .../test/infinite-scroll.spec.ts | 152 +++++++++++ ionic/config/directives.ts | 6 + 10 files changed, 583 insertions(+), 10 deletions(-) create mode 100644 ionic/components/infinite-scroll/infinite-scroll-content.ts create mode 100644 ionic/components/infinite-scroll/infinite-scroll.scss create mode 100644 ionic/components/infinite-scroll/infinite-scroll.ts create mode 100644 ionic/components/infinite-scroll/test/basic/index.ts create mode 100644 ionic/components/infinite-scroll/test/basic/main.html create mode 100644 ionic/components/infinite-scroll/test/infinite-scroll.spec.ts diff --git a/ionic/components.core.scss b/ionic/components.core.scss index 98d29b3821..7d1105d6cb 100644 --- a/ionic/components.core.scss +++ b/ionic/components.core.scss @@ -16,6 +16,7 @@ @import "components/grid/grid", "components/icon/icon", + "components/infinite-scroll/infinite-scroll", "components/menu/menu", "components/modal/modal", "components/refresher/refresher", diff --git a/ionic/components.ts b/ionic/components.ts index 971e535087..362c737a3b 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -7,6 +7,8 @@ export * from './components/button/button' export * from './components/checkbox/checkbox' export * from './components/content/content' export * from './components/icon/icon' +export * from './components/infinite-scroll/infinite-scroll' +export * from './components/infinite-scroll/infinite-scroll-content' export * from './components/input/input' export * from './components/item/item' export * from './components/item/item-sliding' diff --git a/ionic/components/content/content.ts b/ionic/components/content/content.ts index 0983ba4e01..f6b2ad2fb8 100644 --- a/ionic/components/content/content.ts +++ b/ionic/components/content/content.ts @@ -35,8 +35,8 @@ import {ScrollTo} from '../../animations/scroll-to'; }) export class Content extends Ion { private _padding: number = 0; - private _onScroll: any; private _scrollTo: ScrollTo; + private _scLsn: Function; /** * @private @@ -65,13 +65,11 @@ export class Content extends Ion { let self = this; self.scrollElement = self._elementRef.nativeElement.children[0]; - self._onScroll = function(ev) { - self._app.setScrolling(); - }; - if (self._config.get('tapPolyfill') === true) { self._zone.runOutsideAngular(function() { - self.scrollElement.addEventListener('scroll', self._onScroll); + self._scLsn = self.addScrollListener(function() { + self._app.setScrolling(); + }); }); } } @@ -80,8 +78,8 @@ export class Content extends Ion { * @private */ ngOnDestroy() { - this.scrollElement.removeEventListener('scroll', this._onScroll.bind(this)); - this.scrollElement = null; + this._scLsn && this._scLsn(); + this.scrollElement = this._scLsn = null; } /** @@ -298,7 +296,6 @@ export class Content extends Ion { } /** - * @private * Returns the content and scroll elements' dimensions. * @returns {object} dimensions The content and scroll elements' dimensions * {number} dimensions.contentHeight content offsetHeight @@ -334,7 +331,7 @@ export class Content extends Ion { scrollWidth: _scrollEle.scrollWidth, scrollLeft: _scrollEle.scrollLeft, scrollRight: _scrollEle.scrollLeft + _scrollEle.scrollWidth, - } + }; } /** diff --git a/ionic/components/infinite-scroll/infinite-scroll-content.ts b/ionic/components/infinite-scroll/infinite-scroll-content.ts new file mode 100644 index 0000000000..ec938d472e --- /dev/null +++ b/ionic/components/infinite-scroll/infinite-scroll-content.ts @@ -0,0 +1,48 @@ +import {Component, Input} from 'angular2/core' +import {NgIf} from 'angular2/common'; + +import {Config} from '../../config/config'; +import {InfiniteScroll} from './infinite-scroll'; +import {Spinner} from '../spinner/spinner'; + + +/** + * @private + */ +@Component({ + selector: 'ion-infinite-content', + template: + '
' + + '
' + + '' + + '
' + + '
' + + '
', + directives: [NgIf, Spinner], + host: { + '[attr.state]': 'inf.state' + } +}) +export class InfiniteScrollContent { + + /** + * @input {string} An animated SVG spinner that shows while loading. + */ + @Input() loadingSpinner: string; + + /** + * @input {string} Optional text to display while loading. + */ + @Input() loadingText: string; + + constructor(private inf: InfiniteScroll, private _config: Config) {} + + /** + * @private + */ + ngOnInit() { + if (!this.loadingSpinner) { + this.loadingSpinner = this._config.get('infiniteLoadingSpinner', this._config.get('spinner', 'ios')); + } + } +} diff --git a/ionic/components/infinite-scroll/infinite-scroll.scss b/ionic/components/infinite-scroll/infinite-scroll.scss new file mode 100644 index 0000000000..4cca90b588 --- /dev/null +++ b/ionic/components/infinite-scroll/infinite-scroll.scss @@ -0,0 +1,44 @@ +@import "../../globals.core"; + +// Infinite Scroll +// -------------------------------------------------- + +$infinite-scroll-loading-margin: 0px 0px 32px 0px !default; +$infinite-scroll-loading-color: #666 !default; +$infinite-scroll-loading-text-margin: 4px 32px 0 32px !default; + + +ion-infinite { + display: block; + width: 100%; +} + + +// Infinite Scroll Content +// -------------------------------------------------- + +ion-infinite-content { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + text-align: center; +} + +.infinite-loading { + width: 100%; + margin: $infinite-scroll-loading-margin; +} + +.infinite-loading-text { + margin: $infinite-scroll-loading-text-margin; + color: $infinite-scroll-loading-color; +} + + +// Infinite Scroll Content States +// -------------------------------------------------- + +ion-infinite-content[state=disabled] .infinite-loading { + display: none; +} diff --git a/ionic/components/infinite-scroll/infinite-scroll.ts b/ionic/components/infinite-scroll/infinite-scroll.ts new file mode 100644 index 0000000000..94b9e03169 --- /dev/null +++ b/ionic/components/infinite-scroll/infinite-scroll.ts @@ -0,0 +1,256 @@ +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'; diff --git a/ionic/components/infinite-scroll/test/basic/index.ts b/ionic/components/infinite-scroll/test/basic/index.ts new file mode 100644 index 0000000000..ed23a6fa90 --- /dev/null +++ b/ionic/components/infinite-scroll/test/basic/index.ts @@ -0,0 +1,49 @@ +import {App, InfiniteScroll} from 'ionic-angular'; + + +@App({ + templateUrl: 'main.html' +}) +class E2EApp { + items = []; + + constructor() { + for (var i = 0; i < 30; i++) { + this.items.push( this.items.length ); + } + } + + doInfinite(infiniteScroll: InfiniteScroll) { + console.log('Begin async operation'); + + getAsyncData().then(newData => { + for (var i = 0; i < newData.length; i++) { + this.items.push( this.items.length ); + } + + console.log('Finished receiving data, async operation complete'); + infiniteScroll.endLoading(); + + if (this.items.length > 90) { + infiniteScroll.enable(false); + } + }); + } + +} + +function getAsyncData() { + // async return mock data + return new Promise(resolve => { + + setTimeout(() => { + let data = []; + for (var i = 0; i < 30; i++) { + data.push(i); + } + + resolve(data); + }, 500); + + }); +} diff --git a/ionic/components/infinite-scroll/test/basic/main.html b/ionic/components/infinite-scroll/test/basic/main.html new file mode 100644 index 0000000000..6c7a1610c1 --- /dev/null +++ b/ionic/components/infinite-scroll/test/basic/main.html @@ -0,0 +1,18 @@ +Infinite Scroll + + + + + + {{ item }} + + + + + + + + + diff --git a/ionic/components/infinite-scroll/test/infinite-scroll.spec.ts b/ionic/components/infinite-scroll/test/infinite-scroll.spec.ts new file mode 100644 index 0000000000..89bd7c035d --- /dev/null +++ b/ionic/components/infinite-scroll/test/infinite-scroll.spec.ts @@ -0,0 +1,152 @@ +import {InfiniteScroll, Content, Config} from 'ionic-angular'; + +export function run() { + +describe('Infinite Scroll', () => { + + describe('_onScroll', () => { + + it('should not set loading state when does not meet threshold', () => { + setInfiniteScrollHeight(25); + content.getContentDimensions = function() { + return { scrollHeight: 1000, scrollTop: 350, contentHeight: 500 }; + }; + inf._highestY = 0; + inf.threshold = '100px'; + + setInfiniteScrollTop(300); + + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(6); + }); + + it('should set loading state when meets threshold', () => { + setInfiniteScrollHeight(25); + content.getContentDimensions = function() { + return { scrollHeight: 1000, scrollTop: 500, contentHeight: 500 }; + }; + inf._highestY = 0; + inf.threshold = '100px'; + + setInfiniteScrollTop(300); + + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(5); + }); + + it('should not continue if the scrolltop is <= the highest Y', () => { + inf._highestY = 100; + setInfiniteScrollTop(50); + setInfiniteScrollHeight(100); + content.getContentDimensions = function() { + return { scrollTop: 50 }; + }; + + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(4); + }); + + it('should not run if there is not infinite element height', () => { + setInfiniteScrollTop(0); + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(3); + }); + + it('should not run again if ran less than 32ms ago', () => { + inf._lastCheck = Date.now(); + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(2); + }); + + it('should not run if state is disabled', () => { + inf.state = 'disabled'; + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(1); + }); + + it('should not run if state is loading', () => { + inf.state = 'loading'; + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(1); + }); + + it('should not run if not enabled', () => { + inf.state = 'disabled'; + var result = inf._onScroll(scrollEv()); + expect(result).toEqual(1); + }); + + }); + + describe('threshold', () => { + + it('should set by percent', () => { + inf.threshold = '10%'; + expect(inf._thr).toEqual('10%'); + expect(inf._thrPx).toEqual(0); + expect(inf._thrPc).toEqual(0.1); + }); + + it('should set by pixels', () => { + inf.threshold = '10'; + expect(inf._thr).toEqual('10'); + expect(inf._thrPx).toEqual(10); + expect(inf._thrPc).toEqual(0); + + inf.threshold = '10px'; + expect(inf._thr).toEqual('10px'); + expect(inf._thrPx).toEqual(10); + expect(inf._thrPc).toEqual(0); + }); + + }); + + + let config = new Config(); + let inf: InfiniteScroll; + let content: Content; + let contentElementRef; + let infiniteElementRef; + let zone = { + run: function(cb) {cb()}, + runOutsideAngular: function(cb) {cb()} + }; + + beforeEach(() => { + contentElementRef = mockElementRef(); + content = new Content(contentElementRef, config, null, null, null); + content.scrollElement = document.createElement('scroll-content'); + + infiniteElementRef = mockElementRef(); + inf = new InfiniteScroll(content, zone, infiniteElementRef); + }); + + function scrollEv() { + return {} + } + + function mockElementRef() { + return { + nativeElement: { + classList: { add: function(){}, remove: function(){} }, + scrollTop: 0, + hasAttribute: function(){} + } + } + } + + function setInfiniteScrollTop(scrollTop) { + infiniteElementRef.nativeElement.scrollTop = scrollTop; + } + + function setInfiniteScrollHeight(scrollHeight) { + infiniteElementRef.nativeElement.scrollHeight = scrollHeight; + } + + function getScrollElementStyles() { + return content.scrollElement.style; + } + +}); + +} diff --git a/ionic/config/directives.ts b/ionic/config/directives.ts index d4cc4af272..8fd313a238 100644 --- a/ionic/config/directives.ts +++ b/ionic/config/directives.ts @@ -10,6 +10,8 @@ import {Button} from '../components/button/button'; import {Blur} from '../components/blur/blur'; import {Content} from '../components/content/content'; import {Scroll} from '../components/scroll/scroll'; +import {InfiniteScroll} from '../components/infinite-scroll/infinite-scroll'; +import {InfiniteScrollContent} from '../components/infinite-scroll/infinite-scroll-content'; import {Refresher} from '../components/refresher/refresher'; import {RefresherContent} from '../components/refresher/refresher-content'; import {Slides, Slide, SlideLazy} from '../components/slides/slides'; @@ -58,6 +60,8 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when'; * - Blur * - Content * - Scroll + * - InfiniteScroll + * - InfiniteScrollContent * - Refresher * - RefresherContent * @@ -126,6 +130,8 @@ export const IONIC_DIRECTIVES = [ Blur, Content, Scroll, + InfiniteScroll, + InfiniteScrollContent, Refresher, RefresherContent, From 4c93eb06a9a2c9d14918fde304f45e3d4ea2d82e Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 12:24:40 -0500 Subject: [PATCH 14/32] refactor(label): move label css to the correct scss files references #5651 --- ionic/components/card/test/list/main.html | 31 +++++++++-------------- ionic/components/item/item.md.scss | 5 ---- ionic/components/item/item.scss | 12 --------- ionic/components/label/label.ios.scss | 3 ++- ionic/components/label/label.md.scss | 7 ++++- ionic/components/label/label.scss | 9 ++++++- 6 files changed, 28 insertions(+), 39 deletions(-) diff --git a/ionic/components/card/test/list/main.html b/ionic/components/card/test/list/main.html index 11d77a3fb1..b3a7b75c6d 100644 --- a/ionic/components/card/test/list/main.html +++ b/ionic/components/card/test/list/main.html @@ -11,27 +11,20 @@ Card List - - - Wifi - - + - - - Affection - - Very Little - - + - - - Home - - Where the heart is - - + diff --git a/ionic/components/item/item.md.scss b/ionic/components/item/item.md.scss index c892a91ce0..1a3850e98e 100644 --- a/ionic/components/item/item.md.scss +++ b/ionic/components/item/item.md.scss @@ -103,11 +103,6 @@ ion-icon[item-right] { padding: 0 1px; } -[text-wrap] ion-label { - font-size: $item-md-body-text-font-size; - line-height: $item-md-body-text-line-height; -} - ion-icon[item-left] + .item-inner, ion-icon[item-left] + .item-input { margin-left: $item-md-padding-left + ($item-md-padding-left / 2); diff --git a/ionic/components/item/item.scss b/ionic/components/item/item.scss index d75625f8d9..c180386e76 100644 --- a/ionic/components/item/item.scss +++ b/ionic/components/item/item.scss @@ -61,18 +61,6 @@ ion-item-divider { } } -ion-label { - margin: 0; - flex: 1; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -[text-wrap] ion-label { - white-space: normal; -} - [vertical-align-top], ion-input.item { align-items: flex-start; diff --git a/ionic/components/label/label.ios.scss b/ionic/components/label/label.ios.scss index d3a4fb531b..5cf01bbef1 100644 --- a/ionic/components/label/label.ios.scss +++ b/ionic/components/label/label.ios.scss @@ -5,13 +5,14 @@ // -------------------------------------------------- $label-ios-text-color: #7f7f7f !default; +$label-ios-margin: $item-ios-padding-top ($item-ios-padding-right / 2) $item-ios-padding-bottom 0 !default; // iOS Default Label // -------------------------------------------------- ion-label { - margin: $item-ios-padding-top ($item-ios-padding-right / 2) $item-ios-padding-bottom 0; + margin: $label-ios-margin; } diff --git a/ionic/components/label/label.md.scss b/ionic/components/label/label.md.scss index dd5282e835..86d48063e3 100644 --- a/ionic/components/label/label.md.scss +++ b/ionic/components/label/label.md.scss @@ -6,15 +6,20 @@ $label-md-text-color: #999 !default; $label-md-text-color-focused: map-get($colors-md, primary) !default; +$label-md-margin: $item-md-padding-top ($item-md-padding-right / 2) $item-md-padding-bottom 0 !default; // Material Design Default Label // -------------------------------------------------- ion-label { - margin: $item-md-padding-top ($item-md-padding-right / 2) $item-md-padding-bottom 0; + margin: $label-md-margin; } +[text-wrap] ion-label { + font-size: $item-md-body-text-font-size; + line-height: $item-md-body-text-line-height; +} // Material Design Default Label Inside An Input Item // -------------------------------------------------- diff --git a/ionic/components/label/label.scss b/ionic/components/label/label.scss index 475dd0f6a7..3ca9788cbd 100644 --- a/ionic/components/label/label.scss +++ b/ionic/components/label/label.scss @@ -7,15 +7,22 @@ ion-label { display: block; font-size: inherit; white-space: nowrap; + margin: 0; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; } - .item-input ion-label { max-width: 200px; flex: initial; pointer-events: none; } +[text-wrap] ion-label { + white-space: normal; +} + // Stacked & Floating Inputs // -------------------------------------------------- From 7a01234e07a5c8b3a105ef06b6972b229fbe2867 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 11:43:49 -0600 Subject: [PATCH 15/32] chore(): update typescript to 1.8 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index afc4f9f4bc..2cfa55ad1f 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "strip-sourcemap-loader": "0.0.1", "systemjs": "0.19.6", "through2": "^0.6.3", - "typescript": "^1.7.3", + "typescript": "^1.8.2", "vinyl": "^0.4.6", "webpack": "^1.12.2", "yargs": "^3.6.0" @@ -88,4 +88,4 @@ "path": "node_modules/ionic-cz-conventional-changelog" } } -} \ No newline at end of file +} From f905e18a8080c7e7ccfe971131719575f8f4f5be Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 11:49:02 -0600 Subject: [PATCH 16/32] chore(transpile): don't kill stream on TS errors --- gulpfile.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 5a8b5a09d1..33c5006a30 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -251,7 +251,6 @@ function tsCompile(options, cacheName){ .pipe(tsc(options, undefined, tscReporter)) .on('error', function(error) { console.log(error.message); - this.emit('end'); }); } @@ -406,7 +405,6 @@ gulp.task('e2e.build', function() { .pipe(tsc(getTscOptions(), undefined, tscReporter)) .on('error', function(error) { console.log(error.message); - this.emit('end'); }) .pipe(gulpif(/index.js$/, createIndexHTML())) .pipe(gulpif(/e2e.js$/, createPlatformTests())) @@ -590,7 +588,6 @@ gulp.task('build.demos', function() { .pipe(tsc(getTscOptions(), undefined, tscReporter)) .on('error', function(error) { console.log(error.message); - this.emit('end'); }) .pipe(gulpif(/index.js$/, createIndexHTML())) //TSC changes .ts to .js From ea884ded6d1a81b6e533ebcc918b2c8e15d9ea61 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 13:48:36 -0500 Subject: [PATCH 17/32] fix(refresher): get scrollTop from the scroll element to prevent refreshing when dragging up fixes #5207 --- ionic/components/refresher/refresher.ts | 4 ++-- ionic/components/refresher/test/basic/index.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ionic/components/refresher/refresher.ts b/ionic/components/refresher/refresher.ts index 0140e2ba1a..2ffc4a2307 100644 --- a/ionic/components/refresher/refresher.ts +++ b/ionic/components/refresher/refresher.ts @@ -299,10 +299,10 @@ export class Refresher { } if (this.state === STATE_INACTIVE) { - // this refresh is not alreadying actively pulling down + // this refresh is not already actively pulling down // get the content's scrollTop - let scrollHostScrollTop = this._content.getScrollTop(); + let scrollHostScrollTop = this._content.scrollElement.scrollTop; // if the scrollTop is greater than zero then it's // not possible to pull the content down yet diff --git a/ionic/components/refresher/test/basic/index.ts b/ionic/components/refresher/test/basic/index.ts index 0dde21fb59..bad4a02cb7 100644 --- a/ionic/components/refresher/test/basic/index.ts +++ b/ionic/components/refresher/test/basic/index.ts @@ -8,30 +8,30 @@ class E2EApp { items = []; constructor() { - for (var i = 0; i < 3; i++) { + for (var i = 0; i < 15; i++) { this.items.push( getRandomData() ); } } doRefresh(refresher) { - console.log('Begin async operation'); + console.info('Begin async operation'); getAsyncData().then(newData => { for (var i = 0; i < newData.length; i++) { this.items.unshift( newData[i] ); } - console.log('Finished receiving data, async operation complete'); + console.info('Finished receiving data, async operation complete'); refresher.endRefreshing(); }); } doStart(refresher) { - console.log('Refresher, start'); + console.info('Refresher, start'); } doPulling(refresher) { - console.log('Pulling', refresher.progress); + console.info('Pulling', refresher.progress); } } @@ -82,4 +82,4 @@ const data = [ 'Next', 'Kick-Ass', 'Drive Angry' -]; \ No newline at end of file +]; From fdb311d397f17c6a1defced4340bd4a30f7d6926 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 13:02:11 -0600 Subject: [PATCH 18/32] chore(virtualScroll): remove virtualScroll until refactor complete --- ionic/components/list/list.ts | 72 ++--------- ionic/components/list/test/infinite/index.ts | 9 -- ionic/components/list/test/infinite/main.html | 7 -- ionic/components/list/virtual.ts | 119 ------------------ 4 files changed, 11 insertions(+), 196 deletions(-) delete mode 100644 ionic/components/list/test/infinite/index.ts delete mode 100644 ionic/components/list/test/infinite/main.html delete mode 100644 ionic/components/list/virtual.ts diff --git a/ionic/components/list/list.ts b/ionic/components/list/list.ts index 84bf9aee6b..0089a5285f 100644 --- a/ionic/components/list/list.ts +++ b/ionic/components/list/list.ts @@ -1,19 +1,21 @@ -import {Directive, ElementRef, Renderer, Attribute, NgZone, Input} from 'angular2/core'; +import {Directive, ElementRef, Renderer, Attribute, NgZone} from 'angular2/core'; import {Ion} from '../ion'; -import {ListVirtualScroll} from './virtual'; import {ItemSlidingGesture} from '../item/item-sliding-gesture'; import {isDefined} from '../../util'; /** - * The List is a widely used interface element in almost any mobile app, and can include - * content ranging from basic text all the way to buttons, toggles, icons, and thumbnails. + * The List is a widely used interface element in almost any mobile app, + * and can include content ranging from basic text all the way to + * buttons, toggles, icons, and thumbnails. * - * Both the list, which contains items, and the list items themselves can be any HTML - * element. + * Both the list, which contains items, and the list items themselves + * can be any HTML element. * * Using the List and Item components make it easy to support various - * interaction modes such as swipe to edit, drag to reorder, and removing items. + * interaction modes such as swipe to edit, drag to reorder, and + * removing items. + * * @demo /docs/v2/demos/list/ * @see {@link /docs/v2/components#lists List Component Docs} * @@ -23,80 +25,28 @@ import {isDefined} from '../../util'; }) export class List extends Ion { private _enableSliding: boolean = false; - private _virtualScrollingManager: ListVirtualScroll; /** * @private */ ele: HTMLElement; - /** - * @private - */ - itemTemplate: any; - /** * @private */ slidingGesture: ItemSlidingGesture; - - /** - * @private - */ - @Input() items; - - /** - * @private - */ - @Input() virtual; - - /** - * @private - */ - @Input() content; - constructor(elementRef: ElementRef, private _zone: NgZone) { super(elementRef); this.ele = elementRef.nativeElement; } - /** - * @private - */ - ngOnInit() { - if (isDefined(this.virtual)) { - console.debug('Content', this.content); - console.debug('Virtual?', this.virtual); - console.debug('Items?', this.items.length, 'of \'em'); - this._initVirtualScrolling(); - } - } - /** * @private */ ngOnDestroy() { - this.ele = null; - this.slidingGesture && this.slidingGesture.unlisten(); - } - - /** - * @private - */ - _initVirtualScrolling() { - if(!this.content) { - return; - } - - this._virtualScrollingManager = new ListVirtualScroll(this); - } - - /** - * @private - */ - setItemTemplate(item: any) { - this.itemTemplate = item; + this.slidingGesture && this.slidingGesture.destroy(); + this.ele = this.slidingGesture = null; } /** diff --git a/ionic/components/list/test/infinite/index.ts b/ionic/components/list/test/infinite/index.ts deleted file mode 100644 index 709b974f1d..0000000000 --- a/ionic/components/list/test/infinite/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {App} from 'ionic-angular'; - - -@App({ - templateUrl: 'main.html' -}) -class E2EApp { - // TODO -} diff --git a/ionic/components/list/test/infinite/main.html b/ionic/components/list/test/infinite/main.html deleted file mode 100644 index 0b680180a1..0000000000 --- a/ionic/components/list/test/infinite/main.html +++ /dev/null @@ -1,7 +0,0 @@ -Infinite List - - - -TODO - - diff --git a/ionic/components/list/virtual.ts b/ionic/components/list/virtual.ts deleted file mode 100644 index c4d829d4bb..0000000000 --- a/ionic/components/list/virtual.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {List} from './list'; - - -export class ListVirtualScroll { - content; - viewContainer; - viewportHeight; - virtualHeight; - viewportScrollHeight; - itemsPerScreen; - list: List; - itemHeight: number = 60; - shownItems = {}; - enteringItems = []; - leavingItems = []; - - constructor(list: List) { - this.list = list; - this.content = this.list.content; - - this.viewportHeight = this.content.height(); - - this.viewContainer = this.list.itemTemplate.viewContainer; - - // Compute the initial sizes - setTimeout(() => { - this.resize(); - - // Simulate the first event to start layout - this._handleVirtualScroll({ - target: this.content.scrollElement - }); - }) - - this.content.addScrollEventListener((event) => { - this._handleVirtualScroll(event); - }); - } - - resize() { - this.viewportHeight = this.content.height(); - this.viewportScrollHeight = this.content.scrollElement.scrollHeight; - - this.virtualHeight = this.list.items.length * this.itemHeight; - this.itemsPerScreen = this.viewportHeight / this.itemHeight; - - console.debug('VIRTUAL: resize(viewportHeight:', this.viewportHeight, - 'viewportScrollHeight:', this.viewportScrollHeight, 'virtualHeight:', this.virtualHeight, - ', itemsPerScreen:', this.itemsPerScreen, ')'); - } - - _handleVirtualScroll(event) { - let item; - let shownItemRef; - - let st = event.target.scrollTop; - let sh = event.target.scrollHeight; - - let topIndex = Math.floor(st / this.itemHeight); - let bottomIndex = Math.floor((st / this.itemHeight) + this.itemsPerScreen); - - let items = this.list.items; - - // Key iterate the shown items map - // and compare the index to our index range, - // pushing the items to remove to our leaving - // list if they're ouside this range. - for (let i in this.shownItems) { - if (i < topIndex || i > bottomIndex) { - this.leavingItems.push(this.shownItems[i]); - delete this.shownItems[i]; - } - } - - let realIndex = 0; - // Iterate the set of items that will be rendered, using the - // index from the actual items list as the map for the - // virtual items we draw - for (let i = topIndex, realIndex = 0; i < bottomIndex && i < items.length; i++, realIndex++) { - item = items[i]; - console.debug('Drawing item', i, item.title); - - shownItemRef = this.shownItems[i]; - - // Is this a new item? - if (!shownItemRef) { - let itemView = this.viewContainer.create(this.list.itemTemplate.protoViewRef, realIndex); - - itemView.setLocal('\$implicit', item); - itemView.setLocal('\$item', item); - - shownItemRef = new VirtualItemRef(item, i, realIndex, itemView); - - this.shownItems[i] = shownItemRef; - this.enteringItems.push(shownItemRef); - } - - //tuple.view = viewContainer.create(protoViewRef, tuple.record.currentIndex); - } - - while (this.leavingItems.length) { - let itemRef = this.leavingItems.pop(); - console.debug('Removing item', itemRef.item, itemRef.realIndex); - this.viewContainer.remove(itemRef.realIndex); - } - - console.debug('VIRTUAL SCROLL: scroll(scrollTop:', st, 'topIndex:', topIndex, 'bottomIndex:', bottomIndex, ')'); - console.debug('Container has', this.list.getNativeElement().children.length, 'children'); - } - - cellAtIndex(index) { - - } - -} - -class VirtualItemRef { - constructor(public item, public index, public realIndex, public view) {} -} From d39f21f410a69b9ee66e7fb2e5288215c93a2e9a Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 13:27:44 -0600 Subject: [PATCH 19/32] test(refresher): fix refresher unit tests --- ionic/components/refresher/refresher.ts | 3 ++- ionic/components/refresher/test/refresher.spec.ts | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/ionic/components/refresher/refresher.ts b/ionic/components/refresher/refresher.ts index 2ffc4a2307..e0db9ff487 100644 --- a/ionic/components/refresher/refresher.ts +++ b/ionic/components/refresher/refresher.ts @@ -302,12 +302,13 @@ export class Refresher { // this refresh is not already actively pulling down // get the content's scrollTop - let scrollHostScrollTop = this._content.scrollElement.scrollTop; + let scrollHostScrollTop = this._content.getContentDimensions().scrollTop; // if the scrollTop is greater than zero then it's // not possible to pull the content down yet if (scrollHostScrollTop > 0) { this.progress = 0; + this.startY = null; return 7; } diff --git a/ionic/components/refresher/test/refresher.spec.ts b/ionic/components/refresher/test/refresher.spec.ts index 70ff1c3835..10f7d13f28 100644 --- a/ionic/components/refresher/test/refresher.spec.ts +++ b/ionic/components/refresher/test/refresher.spec.ts @@ -119,6 +119,8 @@ describe('Refresher', () => { let result = refresher._onMove( touchEv(125) ); expect(refresher.state).toEqual('inactive'); + expect(refresher.progress).toEqual(0); + expect(refresher.startY).toEqual(null); expect(result).toEqual(7); }); @@ -153,13 +155,16 @@ describe('Refresher', () => { }); it('should set the deltaY', () => { + setContentScrollTop(1); refresher.startY = 100; refresher._onMove( touchEv(133) ); expect(refresher.deltaY).toEqual(33); refresher._lastCheck = 0; // force allow next check + refresher.startY = 100; - refresher._onMove( touchEv(50) ); + var results = refresher._onMove( touchEv(50) ); + expect(results).toEqual(6); expect(refresher.deltaY).toEqual(-50); }); @@ -245,7 +250,11 @@ describe('Refresher', () => { } function setContentScrollTop(scrollTop) { - contentElementRef.nativeElement.scrollTop = scrollTop; + content.getContentDimensions = function() { + return { + scrollTop: scrollTop + }; + }; } function getScrollElementStyles() { From 20fd2c3b8b7ea49595332622753d519bf2cd7ca9 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 13:43:35 -0600 Subject: [PATCH 20/32] chore(refresher): add warning for required ion-refresher-content --- ionic/components/refresher/refresher.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ionic/components/refresher/refresher.ts b/ionic/components/refresher/refresher.ts index e0db9ff487..a96c50340c 100644 --- a/ionic/components/refresher/refresher.ts +++ b/ionic/components/refresher/refresher.ts @@ -210,6 +210,9 @@ export class Refresher { console.warn(' property "' + attrName + '" should now be placed on the inner component instead of . Please review the Refresher docs for API updates.'); } }); + if (!ele.children.length) { + console.warn(' should now have an inner component. Please review the Refresher docs for API updates.'); + } } private _onStart(ev: TouchEvent): any { From fd5c4092bf67852cdb5d48945f8611b399cd2859 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 13:51:42 -0600 Subject: [PATCH 21/32] chore(refresher): update config variable names --- ionic/components/refresher/refresher-content.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ionic/components/refresher/refresher-content.ts b/ionic/components/refresher/refresher-content.ts index 98f93d2dc5..3c021bec2e 100644 --- a/ionic/components/refresher/refresher-content.ts +++ b/ionic/components/refresher/refresher-content.ts @@ -60,10 +60,10 @@ export class RefresherContent { */ ngOnInit() { if (!this.pullingIcon) { - this.pullingIcon = this._config.get('pullingIcon', 'arrow-down'); + this.pullingIcon = this._config.get('refresherPullingIcon', 'arrow-down'); } if (!this.refreshingSpinner) { - this.refreshingSpinner = this._config.get('refreshingSpinner', 'ios'); + this.refreshingSpinner = this._config.get('refresherRefreshingSpinner', this._config.get('spinner', 'ios')); } } } From 1bb51c2e2617e58ebe3af0ea53d8267ebac0fd81 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 29 Feb 2016 14:44:15 -0600 Subject: [PATCH 22/32] chore(refresher): rename "endRefreshing" to "complete" --- demos/refresher/index.ts | 2 +- ionic/components/refresher/refresher.scss | 2 +- ionic/components/refresher/refresher.ts | 35 +++++++------------ .../components/refresher/test/basic/index.ts | 2 +- .../refresher/test/refresher.spec.ts | 7 ++-- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/demos/refresher/index.ts b/demos/refresher/index.ts index b6b48f26b0..12db707b99 100644 --- a/demos/refresher/index.ts +++ b/demos/refresher/index.ts @@ -21,7 +21,7 @@ class ApiDemoApp { this.items.unshift( newData[i] ); } - refresher.endRefreshing(); + refresher.complete(); }); } diff --git a/ionic/components/refresher/refresher.scss b/ionic/components/refresher/refresher.scss index 108e8970e3..e9434e7cb5 100644 --- a/ionic/components/refresher/refresher.scss +++ b/ionic/components/refresher/refresher.scss @@ -102,7 +102,7 @@ ion-refresher-content[state=cancelling] { } } -ion-refresher-content[state=ending] { +ion-refresher-content[state=completing] { .refresher-refreshing { display: block; } diff --git a/ionic/components/refresher/refresher.ts b/ionic/components/refresher/refresher.ts index a96c50340c..94d196159e 100644 --- a/ionic/components/refresher/refresher.ts +++ b/ionic/components/refresher/refresher.ts @@ -16,7 +16,7 @@ import {CSS, pointerCoord, transitionEnd} from '../../util/dom'; * Pages can then can listen to the refreshers various output events. The * `refresh` output event is the one that's fired when the user has pulled * down far enough to kick off the refreshing process. Once the async operation - * has completed and the refreshing should end, call `endRefreshing()`. + * has completed and the refreshing should end, call `complete()`. * * @usage * ```html @@ -38,7 +38,7 @@ import {CSS, pointerCoord, transitionEnd} from '../../util/dom'; * * setTimeout(() => { * console.log('Async operation has ended'); - * refresher.endRefreshing(); + * refresher.complete(); * }, 2000); * } * @@ -108,8 +108,8 @@ export class Refresher { * - `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. * - `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. * - `ready` - The user has pulled down the refresher far enough that if they let go, it'll begin the `refreshing` state. - * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `endRefreshing()` it will begin the `ending` state. - * - `ending` - 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. + * - `refreshing` - The refresher is actively waiting on the async operation to end. Once the refresh handler calls `complete()` it will begin the `completing` state. + * - `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. */ state: string = STATE_INACTIVE; @@ -180,7 +180,7 @@ export class Refresher { * @output {event} When the user lets go and has pulled down far enough, which would be * farther than the `pullMin`, then your refresh hander if fired and the state is * updated to `refreshing`. From within your refresh handler, you must call the - * `endRefreshing()` method when your async operation has completed. + * `complete()` method when your async operation has completed. */ @Output() refresh: EventEmitter = new EventEmitter(); @@ -256,7 +256,7 @@ export class Refresher { // do nothing if it's actively refreshing // or it's in the process of closing // or this was never a startY - if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_ENDING) { + if (this.startY === null || this.state === STATE_REFRESHING || this.state === STATE_CANCELLING || this.state === STATE_COMPLETING) { return 2; } @@ -387,7 +387,7 @@ export class Refresher { // set the content back to it's original location // and close the refresher // set that the refresh is actively cancelling - this.cancelRefreshing(); + this.cancel(); }); } @@ -415,34 +415,25 @@ export class Refresher { } /** - * Call `endRefreshing()` when your async operation has completed. + * Call `complete()` when your async operation has completed. * For example, the `refreshing` state is while the app is performing * an asynchronous operation, such as receiving more data from an * AJAX request. Once the data has been received, you then call this * method to signify that the refreshing has completed and to close * the refresher. This method also changes the refresher's state from - * `refreshing` to `ending`. + * `refreshing` to `completing`. */ - endRefreshing() { - this._close(STATE_ENDING, '120ms'); + complete() { + this._close(STATE_COMPLETING, '120ms'); } /** * Changes the refresher's state from `refreshing` to `cancelling`. */ - cancelRefreshing() { + cancel() { this._close(STATE_CANCELLING, ''); } - /** - * @private - */ - private complete() { - // deprecated warning - console.warn('refresher completed() deprecated, please update to endRefreshing()'); - this.endRefreshing(); - } - private _close(state: string, delay: string) { var timer; @@ -550,4 +541,4 @@ const STATE_PULLING = 'pulling'; const STATE_READY = 'ready'; const STATE_REFRESHING = 'refreshing'; const STATE_CANCELLING = 'cancelling'; -const STATE_ENDING = 'ending'; +const STATE_COMPLETING = 'completing'; diff --git a/ionic/components/refresher/test/basic/index.ts b/ionic/components/refresher/test/basic/index.ts index bad4a02cb7..07f8661bf6 100644 --- a/ionic/components/refresher/test/basic/index.ts +++ b/ionic/components/refresher/test/basic/index.ts @@ -22,7 +22,7 @@ class E2EApp { } console.info('Finished receiving data, async operation complete'); - refresher.endRefreshing(); + refresher.complete(); }); } diff --git a/ionic/components/refresher/test/refresher.spec.ts b/ionic/components/refresher/test/refresher.spec.ts index 10f7d13f28..3cf20416eb 100644 --- a/ionic/components/refresher/test/refresher.spec.ts +++ b/ionic/components/refresher/test/refresher.spec.ts @@ -184,9 +184,9 @@ describe('Refresher', () => { expect(results).toEqual(2); }); - it('should not run if state=ending', () => { + it('should not run if state=completing', () => { refresher.startY = 100; - refresher.state = 'ending'; + refresher.state = 'completing'; var results = refresher._onMove( touchEv(88) ); expect(results).toEqual(2); }); @@ -244,7 +244,8 @@ describe('Refresher', () => { nativeElement: { classList: { add: function(){}, remove: function(){} }, scrollTop: 0, - hasAttribute: function(){} + hasAttribute: function(){}, + children: {length: 1 } } } } From 29d3bb1fcbc502e745818d61ed5268de393da53f Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 15:49:39 -0500 Subject: [PATCH 23/32] refactor(infinite-scroll): rename the elements to include the word scroll update API docs & rename endLoading function to complete. references #5415 --- .../infinite-scroll-content.ts | 2 +- .../infinite-scroll/infinite-scroll.scss | 6 +- .../infinite-scroll/infinite-scroll.ts | 65 +++++++++---------- .../infinite-scroll/test/basic/index.ts | 2 +- .../infinite-scroll/test/basic/main.html | 8 +-- 5 files changed, 41 insertions(+), 42 deletions(-) diff --git a/ionic/components/infinite-scroll/infinite-scroll-content.ts b/ionic/components/infinite-scroll/infinite-scroll-content.ts index ec938d472e..3b00cc0534 100644 --- a/ionic/components/infinite-scroll/infinite-scroll-content.ts +++ b/ionic/components/infinite-scroll/infinite-scroll-content.ts @@ -10,7 +10,7 @@ import {Spinner} from '../spinner/spinner'; * @private */ @Component({ - selector: 'ion-infinite-content', + selector: 'ion-infinite-scroll-content', template: '
' + '
' + diff --git a/ionic/components/infinite-scroll/infinite-scroll.scss b/ionic/components/infinite-scroll/infinite-scroll.scss index 4cca90b588..5b9245ea36 100644 --- a/ionic/components/infinite-scroll/infinite-scroll.scss +++ b/ionic/components/infinite-scroll/infinite-scroll.scss @@ -8,7 +8,7 @@ $infinite-scroll-loading-color: #666 !default; $infinite-scroll-loading-text-margin: 4px 32px 0 32px !default; -ion-infinite { +ion-infinite-scroll { display: block; width: 100%; } @@ -17,7 +17,7 @@ ion-infinite { // Infinite Scroll Content // -------------------------------------------------- -ion-infinite-content { +ion-infinite-scroll-content { display: flex; flex-direction: column; justify-content: center; @@ -39,6 +39,6 @@ ion-infinite-content { // Infinite Scroll Content States // -------------------------------------------------- -ion-infinite-content[state=disabled] .infinite-loading { +ion-infinite-scroll-content[state=disabled] .infinite-loading { display: none; } diff --git a/ionic/components/infinite-scroll/infinite-scroll.ts b/ionic/components/infinite-scroll/infinite-scroll.ts index 94b9e03169..272726f334 100644 --- a/ionic/components/infinite-scroll/infinite-scroll.ts +++ b/ionic/components/infinite-scroll/infinite-scroll.ts @@ -6,13 +6,13 @@ 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 Infinite Scroll allows you to perform an action when the user + * scrolls a specified distance from 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. + * The expression assigned to the `infinite` event is called when + * the user scrolls to the specified distance. When this expression + * has finished its tasks, it should call the `complete()` method + * on the infinite scroll instance. * * @usage * ```html @@ -22,9 +22,9 @@ import {Content} from '../content/content'; * {{i}} * * - * - * - * + * + * + * * * * ``` @@ -49,7 +49,7 @@ import {Content} from '../content/content'; * } * * console.log('Async operation has ended'); - * infiniteScroll.endLoading(); + * infiniteScroll.complete(); * }, 500); * } * @@ -59,20 +59,20 @@ import {Content} from '../content/content'; * * ## Infinite Scroll Content * - * By default, Ionic provides the infinite scroll spinner that looks + * By default, Ionic uses 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. + * default spinner or add text by adding properties to the + * `ion-infinite-scroll-content` component. * * ```html * * - * - * + * - * - * + * + * * * * ``` @@ -80,18 +80,17 @@ import {Content} from '../content/content'; * * ## 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 + * The `ion-infinite-scroll` component holds the infinite scroll logic. + * It requires a child component in order to display the content. + * Ionic uses `ion-infinite-scroll-content` by default. This component + * displays the infinite scroll and changes the look depending + * on the infinite scroll's state. Separating these components 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. + * You could replace our default content with custom SVG or CSS animations. * */ @Directive({ - selector: 'ion-infinite' + selector: 'ion-infinite-scroll' }) export class InfiniteScroll { private _lastCheck: number = 0; @@ -107,11 +106,11 @@ export class InfiniteScroll { /** * @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 + * The threshold 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. + * output event to get called when the user has scrolled 10% + * from the bottom of the page. Use the value `100px` when the + * scroll is within 100 pixels from the bottom of the page. * Default is `15%`. */ @Input() @@ -132,8 +131,8 @@ export class InfiniteScroll { /** * @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 + * the threshold distance. From within your infinite handler, + * you must call the infinite scroll's `complete()` method when * your async operation has completed. */ @Output() infinite: EventEmitter = new EventEmitter(); @@ -194,7 +193,7 @@ export class InfiniteScroll { } /** - * Call `endLoading()` within the `infinite` output event handler when + * Call `complete()` 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 @@ -203,7 +202,7 @@ export class InfiniteScroll { * This method will change the infinite scroll's state from `loading` * to `enabled`. */ - endLoading() { + complete() { this.state = STATE_ENABLED; } diff --git a/ionic/components/infinite-scroll/test/basic/index.ts b/ionic/components/infinite-scroll/test/basic/index.ts index ed23a6fa90..a515f1c747 100644 --- a/ionic/components/infinite-scroll/test/basic/index.ts +++ b/ionic/components/infinite-scroll/test/basic/index.ts @@ -22,7 +22,7 @@ class E2EApp { } console.log('Finished receiving data, async operation complete'); - infiniteScroll.endLoading(); + infiniteScroll.complete(); if (this.items.length > 90) { infiniteScroll.enable(false); diff --git a/ionic/components/infinite-scroll/test/basic/main.html b/ionic/components/infinite-scroll/test/basic/main.html index 6c7a1610c1..f0af47b484 100644 --- a/ionic/components/infinite-scroll/test/basic/main.html +++ b/ionic/components/infinite-scroll/test/basic/main.html @@ -8,11 +8,11 @@ - - + - - + + From 834d6c690e36b7bf1155053aae31d606547d636f Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 16:15:29 -0500 Subject: [PATCH 24/32] docs(demos): remove inline styling from segment API demo references driftyco/ionic-site#453 --- demos/segment/main.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/demos/segment/main.html b/demos/segment/main.html index e332338824..c4d5e5476b 100644 --- a/demos/segment/main.html +++ b/demos/segment/main.html @@ -134,13 +134,3 @@ - - From 17c2fe52b9c68eed822bcea5087dfa1cb7657e5d Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 16:30:16 -0500 Subject: [PATCH 25/32] docs(content): update content functions to private --- ionic/components/content/content.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ionic/components/content/content.ts b/ionic/components/content/content.ts index f6b2ad2fb8..f0ac050390 100644 --- a/ionic/components/content/content.ts +++ b/ionic/components/content/content.ts @@ -279,18 +279,30 @@ export class Content extends Ion { return this._scrollTo.start(0, 0, 300, 0); } + /** + * @private + */ getScrollTop(): number { return this.getNativeElement().scrollTop; } + /** + * @private + */ addCssClass(className: string) { this.getNativeElement().classList.add(className); } + /** + * @private + */ removeCssClass(className: string) { this.getNativeElement().classList.remove(className); } + /** + * @private + */ setScrollElementStyle(prop: string, val: any) { this.scrollElement.style[prop] = val; } From 4113b31fe64b29ffb4496292d7182a34f06cd68a Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 15:33:44 -0600 Subject: [PATCH 26/32] Revert "chore(): update typescript to 1.8" This reverts commit 7a01234e07a5c8b3a105ef06b6972b229fbe2867. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2cfa55ad1f..afc4f9f4bc 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "strip-sourcemap-loader": "0.0.1", "systemjs": "0.19.6", "through2": "^0.6.3", - "typescript": "^1.8.2", + "typescript": "^1.7.3", "vinyl": "^0.4.6", "webpack": "^1.12.2", "yargs": "^3.6.0" @@ -88,4 +88,4 @@ "path": "node_modules/ionic-cz-conventional-changelog" } } -} +} \ No newline at end of file From 79b868b0a891869d381a6c3680bdbdc5fef18001 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 15:34:32 -0600 Subject: [PATCH 27/32] chore(): use typescript 1.7 for now 1.8 breaks a few things, use 1.7 until we figure them out --- gulpfile.js | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 33c5006a30..903a3722e2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,7 +31,8 @@ function getTscOptions(name) { experimentalDecorators: true, target: "es5", module: "commonjs", - isolatedModules: true + isolatedModules: true, + typescript: require('typescript') } if (name === "typecheck") { diff --git a/package.json b/package.json index afc4f9f4bc..d45fc9cb35 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "strip-sourcemap-loader": "0.0.1", "systemjs": "0.19.6", "through2": "^0.6.3", - "typescript": "^1.7.3", + "typescript": "1.7.5", "vinyl": "^0.4.6", "webpack": "^1.12.2", "yargs": "^3.6.0" From dbc5737d82fa52998bb8aa6122e09ba46adbb321 Mon Sep 17 00:00:00 2001 From: Tim Lancina Date: Mon, 29 Feb 2016 15:35:49 -0600 Subject: [PATCH 28/32] chore(): update copy.libs task to work with npm 2 and 3 --- gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 903a3722e2..96bba5bcc7 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -332,7 +332,8 @@ gulp.task('copy.libs', function() { var merge = require('merge2'); var extModules = gulp.src([ 'node_modules/es6-shim/es6-shim.min.js', - 'node_modules/systemjs/node_modules/es6-module-loader/dist/es6-module-loader.src.js', + 'node_modules/systemjs/node_modules/es6-module-loader/dist/es6-module-loader.src.js', //npm2 + 'node_modules/es6-module-loader/dist/es6-module-loader.src.js', //npm3 'node_modules/systemjs/dist/system.src.js', 'node_modules/angular2/bundles/angular2-polyfills.js', 'node_modules/angular2/bundles/angular2.dev.js', From b93bc0c1bdabee41c9a1db562e618312dfe19670 Mon Sep 17 00:00:00 2001 From: perry Date: Mon, 29 Feb 2016 16:45:29 -0600 Subject: [PATCH 29/32] chore(dgeni): filter out local filesystem paths from the docs --- scripts/docs/processors/jekyll.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/docs/processors/jekyll.js b/scripts/docs/processors/jekyll.js index d32357cda0..1b22a03980 100644 --- a/scripts/docs/processors/jekyll.js +++ b/scripts/docs/processors/jekyll.js @@ -18,7 +18,8 @@ module.exports = function jekyll(renderDocsProcessor) { }); docs.forEach(function(doc, i) { docs[i].URL = doc.outputPath.replace('docs/v2//','docs/v2/') - .replace('/index.md',''); + .replace('/index.md','') + .replace('//home/ubuntu/ionic/ionic', '/'); }); docs.push({ From cd723898b743a698138cf5362d203305302f1df5 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 18:43:43 -0500 Subject: [PATCH 30/32] docs(tabs): update tabs demo to include colors and badges references driftyco/ionic-site#453 --- demos/tabs/main.html | 41 +++++++++++---------------------- ionic/components/tabs/tabs.scss | 1 + 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/demos/tabs/main.html b/demos/tabs/main.html index 6a0f7adaa0..4a09a91c59 100644 --- a/demos/tabs/main.html +++ b/demos/tabs/main.html @@ -6,46 +6,46 @@ - + - + - + - + - + - - + + - + - + - - + + @@ -54,30 +54,15 @@ - + - + - - - - - - - - - - - - - - - diff --git a/ionic/components/tabs/tabs.scss b/ionic/components/tabs/tabs.scss index 7d5866ceba..c44040e79e 100644 --- a/ionic/components/tabs/tabs.scss +++ b/ionic/components/tabs/tabs.scss @@ -159,6 +159,7 @@ tab-highlight { right: calc(50% - 30px); } +[tabbarLayout=icon-hide] .tab-badge, [tabbarLayout=icon-bottom] .tab-badge, [tabbarLayout=icon-left] .tab-badge, [tabbarLayout=icon-right] .tab-badge { From ce8eb5ba3b7884d553a031d8d8983ec011819a33 Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 18:44:09 -0500 Subject: [PATCH 31/32] docs(demos): add infinite scroll demo references driftyco/ionic-site#453 --- demos/infinite-scroll/index.ts | 30 +++++++++ demos/infinite-scroll/main.html | 20 ++++++ demos/infinite-scroll/mock-provider.ts | 61 +++++++++++++++++++ .../infinite-scroll/infinite-scroll.ts | 2 + 4 files changed, 113 insertions(+) create mode 100644 demos/infinite-scroll/index.ts create mode 100644 demos/infinite-scroll/main.html create mode 100644 demos/infinite-scroll/mock-provider.ts diff --git a/demos/infinite-scroll/index.ts b/demos/infinite-scroll/index.ts new file mode 100644 index 0000000000..dc45f02012 --- /dev/null +++ b/demos/infinite-scroll/index.ts @@ -0,0 +1,30 @@ +import {App, InfiniteScroll} from 'ionic-angular'; +import {MockProvider} from './mock-provider'; + + +@App({ + templateUrl: 'main.html', + providers: [MockProvider] +}) +class ApiDemoApp { + items: string[]; + + constructor(private mockProvider: MockProvider) { + this.items = mockProvider.getData(); + } + + doInfinite(infiniteScroll: InfiniteScroll) { + this.mockProvider.getAsyncData().then((newData) => { + for (var i = 0; i < newData.length; i++) { + this.items.push( newData[i] ); + } + + infiniteScroll.complete(); + + if (this.items.length > 90) { + infiniteScroll.enable(false); + } + }); + } + +} diff --git a/demos/infinite-scroll/main.html b/demos/infinite-scroll/main.html new file mode 100644 index 0000000000..2e4dea0ff3 --- /dev/null +++ b/demos/infinite-scroll/main.html @@ -0,0 +1,20 @@ + + Infinite Scroll + + + + + + + {{ item }} + + + + + + + + + diff --git a/demos/infinite-scroll/mock-provider.ts b/demos/infinite-scroll/mock-provider.ts new file mode 100644 index 0000000000..515a6d2172 --- /dev/null +++ b/demos/infinite-scroll/mock-provider.ts @@ -0,0 +1,61 @@ +import {Injectable} from 'angular2/core'; + +/** + * Mock Data Access Object + **/ +@Injectable() +export class MockProvider { + + getData() { + // return mock data synchronously + let data = []; + for (var i = 0; i < 3; i++) { + data.push( this._getRandomData() ); + } + return data; + } + + getAsyncData() { + // async receive mock data + return new Promise(resolve => { + + setTimeout(() => { + resolve(this.getData()); + }, 1000); + + }); + } + + private _getRandomData() { + let i = Math.floor( Math.random() * this._data.length ); + return this._data[i]; + } + + private _data = [ + 'Fast Times at Ridgemont High', + 'Peggy Sue Got Married', + 'Raising Arizona', + 'Moonstruck', + 'Fire Birds', + 'Honeymoon in Vegas', + 'Amos & Andrew', + 'It Could Happen to You', + 'Trapped in Paradise', + 'Leaving Las Vegas', + 'The Rock', + 'Con Air', + 'Face/Off', + 'City of Angels', + 'Gone in Sixty Seconds', + 'The Family Man', + 'Windtalkers', + 'Matchstick Men', + 'National Treasure', + 'Ghost Rider', + 'Grindhouse', + 'Next', + 'Kick-Ass', + 'Drive Angry', + ]; + +} diff --git a/ionic/components/infinite-scroll/infinite-scroll.ts b/ionic/components/infinite-scroll/infinite-scroll.ts index 272726f334..058fced460 100644 --- a/ionic/components/infinite-scroll/infinite-scroll.ts +++ b/ionic/components/infinite-scroll/infinite-scroll.ts @@ -88,6 +88,8 @@ import {Content} from '../content/content'; * developers to create their own infinite scroll content components. * You could replace our default content with custom SVG or CSS animations. * + * @demo /docs/v2/demos/infinite-scroll/ + * */ @Directive({ selector: 'ion-infinite-scroll' From 9b8d5bc55a11921395ff8c0cddfa03154e95706e Mon Sep 17 00:00:00 2001 From: Brandy Carney Date: Mon, 29 Feb 2016 18:47:30 -0500 Subject: [PATCH 32/32] docs(demos): fix spacing on modal and nav params demos references driftyco/ionic-site#453 --- demos/modal/modal-content.html | 10 +++++----- demos/nav-params/page-2.html | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/demos/modal/modal-content.html b/demos/modal/modal-content.html index bba486e8df..b165744b1e 100644 --- a/demos/modal/modal-content.html +++ b/demos/modal/modal-content.html @@ -8,11 +8,11 @@
Parameters passed:
-
-    selections: [
-      {{myParam}}
-    ]
-    
+ +
selections: [
+  {{myParam}}
+]
+

No parameters passed.

diff --git a/demos/nav-params/page-2.html b/demos/nav-params/page-2.html index f61fb7a654..b630155c16 100644 --- a/demos/nav-params/page-2.html +++ b/demos/nav-params/page-2.html @@ -5,10 +5,10 @@
Parameters passed:
-
-    selections: [
-      {{myParam}}
-    ]
-    
+ +
selections: [
+  {{myParam}}
+]
+