Merge branch '2.0' into windows-mode

# Conflicts:
#	ionic/components/searchbar/searchbar.ts
This commit is contained in:
Brandy Carney
2016-02-29 19:02:55 -05:00
69 changed files with 2677 additions and 1197 deletions

View File

@ -2,7 +2,6 @@
<ion-title>Action Sheet</ion-title>
</ion-navbar>
<ion-content>
<ion-content padding>
<button block (click)="present()">Basic Action Sheet</button>
<button block secondary (click)="present()">Custom Animation Action Sheet</button>
</ion-content>

View File

@ -213,8 +213,4 @@ export class InitialPage {
this.nav.present(alert);
}
doCustomAnimation() {
}
}

View File

@ -2,11 +2,10 @@
<ion-title>Alert</ion-title>
</ion-navbar>
<ion-content>
<ion-content padding>
<button block (click)="doAlert()">Basic Alert</button>
<button light block (click)="doConfirm()">Confirm Alert</button>
<button secondary block (click)="doPrompt()">Prompt Alert</button>
<button danger block (click)="doRadio()">Radio Alert</button>
<button dark block (click)="doCheckbox()">Checkbox Alert</button>
<button block (click)="doCustomAnimation()">Custom Animation Alert</button>
</ion-content>

View File

@ -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);
}
});
}
}

View File

@ -0,0 +1,20 @@
<ion-toolbar>
<ion-title>Infinite Scroll</ion-title>
</ion-toolbar>
<ion-content>
<ion-list>
<ion-item *ngFor="#item of items">
{{ item }}
</ion-item>
</ion-list>
<ion-infinite-scroll (infinite)="doInfinite($event)" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>

View File

@ -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',
];
}

View File

@ -8,11 +8,11 @@
<ion-content padding>
<div [hidden]="!myParam">
<h5>Parameters passed:</h5>
<pre margin style="background-color: #f8f8f8">
selections: [
{{myParam}}
]
</pre>
<pre style="background-color: #f8f8f8">selections: [
{{myParam}}
]</pre>
</div>
<div [hidden]="myParam">
<p>No parameters passed.</p>

View File

@ -5,10 +5,10 @@
<ion-content padding>
<div>
<h5>Parameters passed:</h5>
<pre margin style="background-color: #f8f8f8">
selections: [
{{myParam}}
]
</pre>
<pre style="background-color: #f8f8f8">selections: [
{{myParam}}
]</pre>
</div>
</ion-content>

View File

@ -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[];
constructor(private mockProvider: MockProvider) {
this.items = mockProvider.getData();
}
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] );
}
setTimeout(() => {
refresher.complete();
})
});
}
doStarting() {
console.log('DOSTARTING');
}
doPulling(amt) {
console.log('DOPULLING', amt);
doPulling(refresher: Refresher) {
console.log('DOPULLING', refresher.progress);
}
}

View File

@ -1,33 +1,20 @@
<ion-toolbar>
<ion-title>Refresher</ion-title>
<ion-title>Pull To Refresh</ion-title>
</ion-toolbar>
<ion-content>
<ion-refresher (starting)="doStarting()" (refresh)="doRefresh($event, refresher)" (pulling)="doPulling($event, amt)">
<ion-refresher (refresh)="doRefresh($event)" (pulling)="doPulling($event)">
<ion-refresher-content
pullingText="Pull to refresh..."
refreshingText="Refreshing...">
</ion-refresher-content>
</ion-refresher>
<ion-list>
<ion-item>Item 1</ion-item>
<ion-item>Item 2</ion-item>
<ion-item>Item 3</ion-item>
<ion-item>Item 4</ion-item>
<ion-item>Item 5</ion-item>
<ion-item>Item 6</ion-item>
<ion-item>Item 7</ion-item>
<ion-item>Item 8</ion-item>
<ion-item>Item 9</ion-item>
<ion-item>Item 10</ion-item>
<ion-item>Item 11</ion-item>
<ion-item>Item 12</ion-item>
<ion-item>Item 13</ion-item>
<ion-item>Item 14</ion-item>
<ion-item>Item 15</ion-item>
<ion-item>Item 16</ion-item>
<ion-item>Item 17</ion-item>
<ion-item>Item 18</ion-item>
<ion-item>Item 19</ion-item>
<ion-item>Item 20</ion-item>
<ion-item *ngFor="#item of items">
{{ item }}
</ion-item>
</ion-list>
</ion-content>

View File

@ -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',
];
}

View File

@ -134,13 +134,3 @@
</ion-segment-button>
</ion-segment>
</ion-toolbar>
<style>
.icon-segment-demo .segment-button ion-icon {
font-size: 2.2rem;
}
.icon-segment-demo .segment-button {
max-height: 2.4rem;
}
</style>

View File

@ -6,78 +6,63 @@
<!-- Text -->
<ion-tabs no-navbar>
<ion-tab tabTitle="Recents" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" [root]="root" tabBadge="23"></ion-tab>
<ion-tab tabTitle="Settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- Icons -->
<ion-tabs no-navbar selectedIndex="1">
<ion-tabs no-navbar selectedIndex="1" primary>
<ion-tab tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabIcon="settings" [root]="root"></ion-tab>
<ion-tab tabIcon="settings" [root]="root" tabBadge="2" tabBadgeStyle="danger"></ion-tab>
</ion-tabs>
<!-- Icons on top of text -->
<ion-tabs no-navbar selectedIndex="2">
<ion-tabs no-navbar selectedIndex="2" secondary>
<ion-tab tabTitle="Location" tabIcon="navigate" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="star" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="star" [root]="root" tabBadge="12" tabBadgeStyle="dark"></ion-tab>
<ion-tab tabTitle="Radio" tabIcon="musical-notes" [root]="root"></ion-tab>
</ion-tabs>
<!-- Icons below text -->
<ion-tabs tabbarIcons="bottom" no-navbar selectedIndex="1">
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tabs tabbarLayout="icon-bottom" no-navbar selectedIndex="1" danger>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root" tabBadge="47" tabBadgeStyle="light"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- Icons right of text -->
<ion-tabs tabbarIcons="right" no-navbar selectedIndex="0">
<ion-tabs tabbarLayout="icon-right" no-navbar selectedIndex="0" light>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root" tabBadge="4" tabBadgeStyle="secondary"></ion-tab>
</ion-tabs>
<!-- Icons left of text -->
<ion-tabs tabbarIcons="left" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tabs tabbarLayout="icon-left" no-navbar dark>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root" tabBadge="1" tabBadgeStyle="danger"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- No icons -->
<ion-tabs tabbarIcons="hide" no-navbar>
<ion-tabs tabbarLayout="icon-hide" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root" tabBadge="61" tabBadgeStyle="dark"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- No overflow text -->
<ion-tabs no-navbar>
<ion-tabs no-navbar primary>
<ion-tab tabTitle="Indiana Jones and the Raiders of the Lost Ark" [root]="root"></ion-tab>
<ion-tab tabTitle="Indiana Jones and the Temple of Doom" [root]="root"></ion-tab>
<ion-tab tabTitle="Indiana Jones and the Last Crusade" [root]="root"></ion-tab>
</ion-tabs>
<!-- primary color tabbar -->
<ion-tabs no-navbar primary>
<ion-tab tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- primary color tabbar with a badge -->
<ion-tabs no-navbar primary>
<ion-tab tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabIcon="heart" [root]="root" tabBadge="23" tabBadgeStyle="danger"></ion-tab>
<ion-tab tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
</ion-content>

View File

@ -31,7 +31,8 @@ function getTscOptions(name) {
experimentalDecorators: true,
target: "es5",
module: "commonjs",
isolatedModules: true
isolatedModules: true,
typescript: require('typescript')
}
if (name === "typecheck") {
@ -251,7 +252,6 @@ function tsCompile(options, cacheName){
.pipe(tsc(options, undefined, tscReporter))
.on('error', function(error) {
console.log(error.message);
this.emit('end');
});
}
@ -334,7 +334,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',
@ -409,7 +410,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()))
@ -593,7 +593,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

View File

@ -16,11 +16,13 @@
@import
"components/grid/grid",
"components/icon/icon",
"components/infinite-scroll/infinite-scroll",
"components/menu/menu",
"components/modal/modal",
"components/refresher/refresher",
"components/scroll/scroll",
"components/scroll/pull-to-refresh",
"components/slides/slides";
"components/slides/slides",
"components/spinner/spinner";
// Ionicons (to be replaced with SVGs)

View File

@ -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'
@ -31,8 +33,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'

View File

@ -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: `
<ion-toolbar>
<ion-buttons>
<button (click)="dismiss()">Close</button>
</ion-buttons>
<ion-title>Modal</ion-title>
</ion-toolbar>
<ion-content padding>
Hi, I'm Bob, and I'm a modal.
</ion-content>
`
})
class ModalPage {
constructor(private viewCtrl: ViewController) {}
dismiss() {
this.viewCtrl.dismiss();
}
}
@App({
template: '<ion-nav [root]="root"></ion-nav>'

View File

@ -1,6 +1,5 @@
@import "../../globals.ios";
@import "./alert";
@import "../checkbox/checkbox.ios";
// iOS Alerts
// --------------------------------------------------
@ -38,17 +37,17 @@ $alert-ios-button-border-radius: 0 !default;
$alert-ios-button-border-color: #dbdbdf !default;
$alert-ios-button-activated-background-color: #e9e9e9 !default;
$alert-checkbox-ios-icon-size: $checkbox-ios-icon-size !default;
$alert-checkbox-ios-icon-border-radius: $checkbox-ios-icon-border-radius !default;
$alert-checkbox-ios-icon-border-width: $checkbox-ios-icon-border-width !default;
$alert-checkbox-ios-icon-border-style: $checkbox-ios-icon-border-style !default;
$alert-checkbox-ios-icon-border-color-off: $checkbox-ios-icon-border-color-off !default;
$alert-checkbox-ios-icon-border-color-on: $checkbox-ios-icon-border-color-on !default;
$alert-checkbox-ios-background-color-off: $checkbox-ios-background-color-off !default;
$alert-checkbox-ios-background-color-on: $checkbox-ios-background-color-on !default;
$alert-checkbox-ios-icon-checkmark-width: $checkbox-ios-icon-checkmark-width !default;
$alert-checkbox-ios-icon-checkmark-style: $checkbox-ios-icon-checkmark-style !default;
$alert-checkbox-ios-icon-checkmark-color: $checkbox-ios-icon-checkmark-color !default;
$alert-checkbox-ios-icon-size: 21px !default;
$alert-checkbox-ios-icon-border-radius: 50% !default;
$alert-checkbox-ios-icon-border-width: 1px !default;
$alert-checkbox-ios-icon-border-style: solid !default;
$alert-checkbox-ios-icon-border-color-off: $list-ios-border-color !default;
$alert-checkbox-ios-icon-border-color-on: map-get($colors-ios, primary) !default;
$alert-checkbox-ios-background-color-off: $list-ios-background-color !default;
$alert-checkbox-ios-background-color-on: map-get($colors-ios, primary) !default;
$alert-checkbox-ios-icon-checkmark-width: $alert-checkbox-ios-icon-border-width !default;
$alert-checkbox-ios-icon-checkmark-style: $alert-checkbox-ios-icon-border-style !default;
$alert-checkbox-ios-icon-checkmark-color: $background-ios-color !default;
ion-alert {
@ -96,7 +95,7 @@ ion-alert {
.alert-message {
max-height: 240px;
&:empty {
padding: 0 0 12px 0;
}

View File

@ -1,36 +1,36 @@
@import "../../globals.md";
@import "./alert";
@import "../button/button.md";
// Material Design Alerts
// --------------------------------------------------
$alert-md-max-width: 280px !default;
$alert-md-border-radius: 2px !default;
$alert-md-background-color: #fafafa !default;
$alert-md-box-shadow: 0px 16px 20px rgba(0, 0, 0, 0.4) !default;
$alert-md-max-width: 280px !default;
$alert-md-border-radius: 2px !default;
$alert-md-background-color: #fafafa !default;
$alert-md-box-shadow: 0px 16px 20px rgba(0, 0, 0, 0.4) !default;
$alert-md-head-text-align: left !default;
$alert-md-head-padding: 24px 24px 20px 24px !default;
$alert-md-head-text-align: left !default;
$alert-md-head-padding: 24px 24px 20px 24px !default;
$alert-md-title-font-size: 22px !default;
$alert-md-sub-title-font-size: 16px !default;
$alert-md-title-font-size: 22px !default;
$alert-md-sub-title-font-size: 16px !default;
$alert-md-message-font-size: 15px !default;
$alert-md-message-padding: 0 24px 24px 24px !default;
$alert-md-message-text-color: rgba(0,0,0,.5) !default;
$alert-md-message-font-size: 15px !default;
$alert-md-message-padding: 0 24px 24px 24px !default;
$alert-md-message-text-color: rgba(0,0,0,.5) !default;
$alert-md-input-border-color: #dedede !default;
$alert-md-input-text-color: #000000 !default;
$alert-md-input-highlight-color: map-get($colors-md, primary) !default;
$alert-md-input-margin-top: 5px !default;
$alert-md-input-margin-bottom: 5px !default;
$alert-md-input-border-color: #dedede !default;
$alert-md-input-text-color: #000000 !default;
$alert-md-input-highlight-color: map-get($colors-md, primary) !default;
$alert-md-input-margin-top: 5px !default;
$alert-md-input-margin-bottom: 5px !default;
$alert-md-button-text-color: map-get($colors-md, primary) !default;
$alert-md-button-background-color: transparent !default;
$alert-md-button-border-radius: 2px !default;
$alert-md-buttons-padding: 8px 8px 8px 24px !default;
$alert-md-buttons-justify-content: flex-end !default;
$alert-md-button-text-color: map-get($colors-md, primary) !default;
$alert-md-button-background-color: transparent !default;
$alert-md-button-background-color-activated: rgba(158, 158, 158, 0.2) !default;
$alert-md-button-border-radius: 2px !default;
$alert-md-buttons-padding: 8px 8px 8px 24px !default;
$alert-md-buttons-justify-content: flex-end !default;
.alert-wrapper {
@ -69,7 +69,7 @@ $alert-md-buttons-justify-content: flex-end !default;
.alert-message {
font-size: $alert-md-message-font-size;
max-height: 240px;
max-height: 240px;
&:empty {
padding: 0;
@ -232,7 +232,7 @@ $alert-md-buttons-justify-content: flex-end !default;
text-align: right;
&.activated {
background-color: $button-md-clear-active-background-color;
background-color: $alert-md-button-background-color-activated;
opacity: 1;
}
}

View File

@ -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;

View File

@ -11,27 +11,20 @@
Card List
</ion-list-header>
<ion-item>
<ion-icon name="wifi" item-left></ion-icon>
<ion-label>Wifi</ion-label>
<ion-toggle></ion-toggle>
</ion-item>
<button ion-item>
<ion-icon name="cart" item-left></ion-icon>
Shopping
</button>
<ion-item>
<ion-icon name="heart" item-left></ion-icon>
Affection
<ion-note item-right>
Very Little
</ion-note>
</ion-item>
<button ion-item>
<ion-icon name="medical" item-left></ion-icon>
Hospital
</button>
<ion-item>
<ion-icon name="home" item-left></ion-icon>
Home
<ion-note item-right>
Where the heart is
</ion-note>
</ion-item>
<button ion-item>
<ion-icon name="paw" item-left></ion-icon>
Dog Park
</button>
</ion-list>

View File

@ -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
* <ion-content id="myContent">
* <ion-content>
* Add your content here!
* </ion-content>
* ```
@ -30,22 +30,19 @@ import {ScrollTo} from '../../animations/scroll-to';
template:
'<scroll-content>' +
'<ng-content></ng-content>' +
'</scroll-content>'
'</scroll-content>' +
'<ng-content select="ion-refresher"></ng-content>'
})
export class Content extends Ion {
private _padding: number = 0;
private _onScroll: any;
private _scrollTo: ScrollTo;
private _scLsn: Function;
/**
* @private
*/
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,
@ -68,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();
});
});
}
}
@ -83,7 +78,8 @@ export class Content extends Ion {
* @private
*/
ngOnDestroy() {
this.scrollElement.removeEventListener('scroll', this._onScroll);
this._scLsn && this._scLsn();
this.scrollElement = this._scLsn = null;
}
/**
@ -112,28 +108,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 +201,8 @@ export class Content extends Ion {
setTimeout(next, 100);
}
/**
* @private
* Adds the specified touchmove handler to the content's scroll element.
*
* ```ts
* @Page({
* template: `<ion-content id="my-content"></ion-content>`
* )}
* 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);
}
/**
@ -278,6 +281,33 @@ export class Content extends Ion {
/**
* @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;
}
/**
* Returns the content and scroll elements' dimensions.
* @returns {object} dimensions The content and scroll elements' dimensions
* {number} dimensions.contentHeight content offsetHeight
@ -313,7 +343,7 @@ export class Content extends Ion {
scrollWidth: _scrollEle.scrollWidth,
scrollLeft: _scrollEle.scrollLeft,
scrollRight: _scrollEle.scrollLeft + _scrollEle.scrollWidth,
}
};
}
/**

View File

@ -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-scroll-content',
template:
'<div class="infinite-loading">' +
'<div class="infinite-loading-spinner" *ngIf="loadingSpinner">' +
'<ion-spinner [name]="loadingSpinner"></ion-spinner>' +
'</div>' +
'<div class="infinite-loading-text" [innerHTML]="loadingText" *ngIf="loadingText"></div>' +
'</div>',
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'));
}
}
}

View File

@ -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-scroll {
display: block;
width: 100%;
}
// Infinite Scroll Content
// --------------------------------------------------
ion-infinite-scroll-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-scroll-content[state=disabled] .infinite-loading {
display: none;
}

View File

@ -0,0 +1,257 @@
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 perform an action when the user
* scrolls a specified distance from the bottom of the page.
*
* 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
* <ion-content>
*
* <ion-list>
* <ion-item *ngFor="#i of items">{{i}}</ion-item>
* </ion-list>
*
* <ion-infinite-scroll (infinite)="doInfinite($event)">
* <ion-infinite-scroll-content></ion-infinite-scroll-content>
* </ion-infinite-scroll>
*
* </ion-content>
* ```
*
* ```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.complete();
* }, 500);
* }
*
* }
* ```
*
*
* ## Infinite Scroll Content
*
* 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 or add text by adding properties to the
* `ion-infinite-scroll-content` component.
*
* ```html
* <ion-content>
*
* <ion-infinite-scroll (infinite)="doInfinite($event)">
* <ion-infinite-scroll-content
* loadingSpinner="bubbles"
* loadingText="Loading more data...">
* </ion-infinite-scroll-content>
* </ion-infinite-scroll>
*
* </ion-content>
* ```
*
*
* ## Further Customizing Infinite Scroll Content
*
* 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.
* You could replace our default content with custom SVG or CSS animations.
*
* @demo /docs/v2/demos/infinite-scroll/
*
*/
@Directive({
selector: 'ion-infinite-scroll'
})
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 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 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()
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 distance. From within your infinite handler,
* you must call the infinite scroll's `complete()` method when
* your async operation has completed.
*/
@Output() infinite: EventEmitter<InfiniteScroll> = 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 `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
* 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`.
*/
complete() {
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';

View File

@ -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.complete();
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);
});
}

View File

@ -0,0 +1,18 @@
<ion-toolbar><ion-title>Infinite Scroll</ion-title></ion-toolbar>
<ion-content>
<ion-list>
<ion-item *ngFor="#item of items">
{{ item }}
</ion-item>
</ion-list>
<ion-infinite-scroll (infinite)="doInfinite($event)" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>

View File

@ -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;
}
});
}

View File

@ -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);
}
}

View File

@ -41,7 +41,7 @@ import {Platform} from '../../platform/platform';
* </ion-item>
*
* <ion-item>
* <ion-labe fixed>Website</ion-label>
* <ion-label fixed>Website</ion-label>
* <ion-input type="url"></ion-input>
* </ion-item>
*

View File

@ -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);
}
}

View File

@ -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);

View File

@ -60,18 +60,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;

View File

@ -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;
}

View File

@ -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
// --------------------------------------------------

View File

@ -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
// --------------------------------------------------

View File

@ -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;
}
/**

View File

@ -1,7 +0,0 @@
<ion-toolbar><ion-title>Infinite List</ion-title></ion-toolbar>
<ion-content padding>
TODO
</ion-content>

View File

@ -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) {}
}

View File

@ -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 {

View File

@ -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:
'<div class="refresher-pulling">' +
'<div class="refresher-pulling-icon" *ngIf="pullingIcon">' +
'<ion-icon [name]="pullingIcon"></ion-icon>' +
'</div>' +
'<div class="refresher-pulling-text" [innerHTML]="pullingText" *ngIf="pullingText"></div>' +
'</div>' +
'<div class="refresher-refreshing">' +
'<div class="refresher-refreshing-icon">' +
'<ion-spinner [name]="refreshingSpinner"></ion-spinner>' +
'</div>' +
'<div class="refresher-refreshing-text" [innerHTML]="refreshingText" *ngIf="refreshingText"></div>' +
'</div>',
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('refresherPullingIcon', 'arrow-down');
}
if (!this.refreshingSpinner) {
this.refreshingSpinner = this._config.get('refresherRefreshingSpinner', this._config.get('spinner', 'ios'));
}
}
}

View File

@ -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=completing] {
.refresher-refreshing {
display: block;
}
.refresher-refreshing-icon {
transform: scale(0);
}
}

View File

@ -0,0 +1,544 @@
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 `complete()`.
*
* @usage
* ```html
* <ion-content>
*
* <ion-refresher (refresh)="doRefresh($event)">
* <ion-refresher-content></ion-refresher-content>
* </ion-refresher>
*
* </ion-content>
* ```
*
* ```ts
* @Page({...})
* export class NewsFeedPage {
*
* doRefresh(refresher) {
* console.log('Begin async operation', refresher);
*
* setTimeout(() => {
* console.log('Async operation has ended');
* refresher.complete();
* }, 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
* <ion-content>
*
* <ion-refresher (refresh)="doRefresh($event)">
* <ion-refresher-content
* pullingIcon="arrow-dropdown"
* pullingText="Pull to refresh"
* refreshingSpinner="circles"
* refreshingText="Refreshing...">
* </ion-refresher-content>
* </ion-refresher>
*
* </ion-content>
* ```
*
*
* ## 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 `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;
/**
* 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
* `complete()` method when your async operation has completed.
*/
@Output() refresh: EventEmitter<Refresher> = new EventEmitter();
/**
* @output {event} While the user is pulling down the content and exposing the refresher.
*/
@Output() pulling: EventEmitter<Refresher> = new EventEmitter();
/**
* @output {event} When the user begins to start pulling down.
*/
@Output() start: EventEmitter<Refresher> = 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('<ion-refresher> property "' + attrName + '" should now be placed on the inner <ion-refresher-content> component instead of <ion-refresher>. Please review the Refresher docs for API updates.');
}
});
if (!ele.children.length) {
console.warn('<ion-refresher> should now have an inner <ion-refresher-content> component. 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_COMPLETING) {
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 already actively pulling down
// get the content's 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;
}
// 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.cancel();
});
}
// 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 `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 `completing`.
*/
complete() {
this._close(STATE_COMPLETING, '120ms');
}
/**
* Changes the refresher's state from `refreshing` to `cancelling`.
*/
cancel() {
this._close(STATE_CANCELLING, '');
}
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_COMPLETING = 'completing';

View File

@ -0,0 +1,85 @@
import {App} from 'ionic-angular';
@App({
templateUrl: 'main.html'
})
class E2EApp {
items = [];
constructor() {
for (var i = 0; i < 15; i++) {
this.items.push( getRandomData() );
}
}
doRefresh(refresher) {
console.info('Begin async operation');
getAsyncData().then(newData => {
for (var i = 0; i < newData.length; i++) {
this.items.unshift( newData[i] );
}
console.info('Finished receiving data, async operation complete');
refresher.complete();
});
}
doStart(refresher) {
console.info('Refresher, start');
}
doPulling(refresher) {
console.info('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'
];

View File

@ -0,0 +1,21 @@
<ion-toolbar><ion-title>Pull To Refresh</ion-title></ion-toolbar>
<ion-content>
<ion-refresher (start)="doStart($event)" (pulling)="doPulling($event)" (refresh)="doRefresh($event)">
<ion-refresher-content
pullingText="Pull to refresh..."
refreshingSpinner="bubbles"
refreshingText="Refreshing...">
</ion-refresher-content>
</ion-refresher>
<ion-list>
<ion-item *ngFor="#item of items">
{{ item }}
</ion-item>
</ion-list>
</ion-content>

View File

@ -0,0 +1,267 @@
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(refresher.progress).toEqual(0);
expect(refresher.startY).toEqual(null);
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', () => {
setContentScrollTop(1);
refresher.startY = 100;
refresher._onMove( touchEv(133) );
expect(refresher.deltaY).toEqual(33);
refresher._lastCheck = 0; // force allow next check
refresher.startY = 100;
var results = refresher._onMove( touchEv(50) );
expect(results).toEqual(6);
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=completing', () => {
refresher.startY = 100;
refresher.state = 'completing';
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(){},
children: {length: 1 }
}
}
}
function setContentScrollTop(scrollTop) {
content.getContentDimensions = function() {
return {
scrollTop: scrollTop
};
};
}
function getScrollElementStyles() {
return content.scrollElement.style;
}
});
}

View File

@ -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); }
}

View File

@ -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
* <ion-content>
* <ion-refresher (start)="doStart($event)"
* (refresh)="doRefresh($event)"
* (pulling)="doPulling($event)">
* </ion-refresher>
*
* </ion-content>
* ```
*
* ```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:
'<div class="refresher-content" [class.refresher-with-text]="pullingText || refreshingText">' +
'<div class="icon-pulling">' +
'<ion-icon [name]="pullingIcon"></ion-icon>' +
'</div>' +
'<div class="text-pulling" [innerHTML]="pullingText" *ngIf="pullingText"></div>' +
'<div class="icon-refreshing">' +
'<ion-icon [name]="refreshingIcon"></ion-icon>' +
'</div>' +
'<div class="text-refreshing" [innerHTML]="refreshingText" *ngIf="refreshingText"></div>' +
'</div>',
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<Refresher> = new EventEmitter();
/**
* @output {event} When you are refreshing
*/
@Output() refresh: EventEmitter<Refresher> = new EventEmitter();
/**
* @output {event} When you start pulling down
*/
@Output() start: EventEmitter<Refresher> = 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);
}
}

View File

@ -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);
}
}

View File

@ -1,18 +0,0 @@
<ion-toolbar><ion-title>Pull To Refresh</ion-title></ion-toolbar>
<ion-content>
<ion-refresher
(start)="doStart($event)"
(refresh)="doRefresh($event)"
(pulling)="doPulling($event)"
pullingIcon="heart"
pullingText="release to refresh..."
refreshingIcon="star"
refreshingText="refreshing...">
</ion-refresher>
<ion-list>
<ion-item *ngFor="#item of items">
Item {{ item.index }}
</ion-item>
</ion-list>
</ion-content>

View File

@ -119,7 +119,7 @@ ion-searchbar {
// Searchbar Focused
// -----------------------------------------
.searchbar-focused {
.searchbar-focused:not(.searchbar-hide-cancel) {
.searchbar-search-icon {
display: none;
}

View File

@ -39,7 +39,12 @@ export class SearchbarInput {
*
* @usage
* ```html
* <ion-searchbar [(ngModel)]="defaultSearch" (input)="triggerInput($event)" (cancel)="onCancelSearchbar($event)" (clear)="onClearSearchbar($event)"></ion-searchbar>
* <ion-searchbar
* [(ngModel)]="myInput"
* [hideCancelButton]="shouldHideCancel"
* (input)="onInput($event)"
* (cancel)="onCancel($event)">
* </ion-searchbar>
* ```
*
* @demo /docs/v2/demos/searchbar/
@ -49,10 +54,11 @@ export class SearchbarInput {
selector: 'ion-searchbar',
host: {
'[class.searchbar-has-value]': 'value'
'[class.searchbar-hide-cancel]': 'hideCancelButton'
},
template:
'<div class="searchbar-input-container">' +
'<button (click)="cancelSearchbar()" (mousedown)="cancelSearchbar()" clear dark class="searchbar-md-cancel">' +
'<button (click)="cancelSearchbar()" (mousedown)="cancelSearchbar()" [hidden]="hideCancelButton" clear dark class="searchbar-md-cancel">' +
'<ion-icon name="arrow-back"></ion-icon>' +
'</button>' +
'<div class="searchbar-search-icon"></div>' +

View File

@ -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); }
}

View File

@ -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 youd 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 its running.
*
* <table class="table spinner-table">
* <tr>
* <th>
* <code>ios</code>
* </th>
* <td>
* <ion-spinner name="ios"></ion-spinner>
* </td>
* </tr>
* <tr>
* <th>
* <code>ios-small</code>
* </th>
* <td>
* <ion-spinner name="ios-small"></ion-spinner>
* </td>
* </tr>
* <tr>
* <th>
* <code>bubbles</code>
* </th>
* <td>
* <ion-spinner name="bubbles"></ion-spinner>
* </td>
* </tr>
* <tr>
* <th>
* <code>circles</code>
* </th>
* <td>
* <ion-spinner name="circles"></ion-spinner>
* </td>
* </tr>
* <tr>
* <th>
* <code>crescent</code>
* </th>
* <td>
* <ion-spinner name="crescent"></ion-spinner>
* </td>
* </tr>
* <tr>
* <th>
* <code>dots</code>
* </th>
* <td>
* <ion-spinner name="dots"></ion-spinner>
* </td>
* </tr>
* </table>
*
* @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
* <ion-spinner></ion-spinner>
* ```
*
* By setting the `name` property, you can specify which predefined spinner to
* use, no matter what the platform is.
*
* ```html
* <ion-spinner name="bubbles"></ion-spinner>
* ```
*
* ## 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:
'<svg viewBox="0 0 64 64" *ngFor="#i of _c" [ngStyle]="i.style">' +
'<circle [attr.r]="i.r" transform="translate(32,32)"></circle>' +
'</svg>' +
'<svg viewBox="0 0 64 64" *ngFor="#i of _l" [ngStyle]="i.style">' +
'<line [attr.y1]="i.y1" [attr.y2]="i.y2" transform="translate(32,32)"></line>' +
'</svg>',
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'
}
}
}
}
};

View File

@ -5,5 +5,9 @@ import {App} from 'ionic-angular';
templateUrl: 'main.html'
})
class E2EApp {
// TODO
paused: boolean = false;
toggleState() {
this.paused = !this.paused;
}
}

View File

@ -0,0 +1,55 @@
<ion-toolbar>
<ion-title>Spinners</ion-title>
</ion-toolbar>
<ion-content padding>
<table style="border-collapse:initial; border-spacing: 20px">
<tr>
<td>Platform Default</td>
<td>
<ion-spinner [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>ios</td>
<td>
<ion-spinner name="ios" duration="1000" [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>ios-small</td>
<td>
<ion-spinner name="ios-small" duration="1000" [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>bubbles</td>
<td>
<ion-spinner name="bubbles" [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>circles</td>
<td>
<ion-spinner name="circles" [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>crescent</td>
<td>
<ion-spinner name="crescent" [paused]="paused"></ion-spinner>
</td>
</tr>
<tr>
<td>dots</td>
<td>
<ion-spinner name="dots" [paused]="paused"></ion-spinner>
</td>
</tr>
</table>
<button (click)="toggleState()">Toggle Paused</button>
</ion-content>

View File

@ -59,8 +59,8 @@ ion-tabs[tabbarPlacement=top] tabbar {
}
}
[tabbarIcons=right] .tab-button,
[tabbarIcons=left] .tab-button {
[tabbarLayout=icon-right] .tab-button,
[tabbarLayout=icon-left] .tab-button {
.tab-button-text {
font-size: 1.4rem;
@ -76,11 +76,7 @@ ion-tabs[tabbarPlacement=top] tabbar {
}
}
[tabbarIcons=hide] .tab-button-text {
font-size: 1.4rem;
line-height: 1.1;
}
[tabbarLayout=icon-hide] .tab-button,
.tab-button.has-title-only {
min-height: $tabbar-ios-height - 8;
@ -90,6 +86,7 @@ ion-tabs[tabbarPlacement=top] tabbar {
}
}
[tabbarLayout=title-hide] .tab-button,
.tab-button.icon-only {
min-height: $tabbar-ios-height - 8;
}

View File

@ -49,13 +49,13 @@ tabbar {
min-width: $tabbar-md-item-icon-size + 5;
}
[tabbarIcons=bottom] .tab-button {
[tabbarLayout=icon-bottom] .tab-button {
padding-top: 8px;
padding-bottom: 8px;
}
[tabbarIcons=right] .tab-button,
[tabbarIcons=left] .tab-button {
[tabbarLayout=icon-right] .tab-button,
[tabbarLayout=icon-left] .tab-button {
padding-bottom: 10px;
ion-icon {
@ -63,6 +63,8 @@ tabbar {
}
}
[tabbarLayout=icon-hide] .tab-button,
[tabbarLayout=title-hide] .tab-button,
.tab-button.icon-only,
.tab-button.has-title-only {
padding: 6px 10px 6px 10px;

View File

@ -108,13 +108,13 @@ tab-highlight {
display: none;
}
[tabbarIcons=bottom] .tab-button {
[tabbarLayout=icon-bottom] .tab-button {
.tab-button-icon {
order: 10;
}
}
[tabbarIcons=left] .tab-button {
[tabbarLayout=icon-left] .tab-button {
flex-direction: row;
.tab-button-icon {
@ -123,7 +123,7 @@ tab-highlight {
}
}
[tabbarIcons=right] .tab-button {
[tabbarLayout=icon-right] .tab-button {
flex-direction: row;
.tab-button-icon {
@ -133,7 +133,11 @@ tab-highlight {
}
}
[tabbarIcons=hide] .tab-button-icon {
[tabbarLayout=icon-hide] .tab-button-icon {
display: none;
}
[tabbarLayout=title-hide] .tab-button-text {
display: none;
}
@ -155,8 +159,9 @@ tab-highlight {
right: calc(50% - 30px);
}
[tabbarIcons=bottom] .tab-badge,
[tabbarIcons=left] .tab-badge,
[tabbarIcons=right] .tab-badge {
[tabbarLayout=icon-hide] .tab-badge,
[tabbarLayout=icon-bottom] .tab-badge,
[tabbarLayout=icon-left] .tab-badge,
[tabbarLayout=icon-right] .tab-badge {
right: calc(50% - 50px);
}

View File

@ -90,27 +90,32 @@ export class Tabs extends Ion {
subPages: boolean;
/**
* @input {number} The default selected tab index when first loaded. If a selected index wasn't provided then it'll use `0`, the first tab.
* @input {number} The default selected tab index when first loaded. If a selected index isn't provided then it will use `0`, the first tab.
*/
@Input() selectedIndex: any;
/**
* @input {boolean} Sets whether to preload all the tabs, true or false
* @input {boolean} Set whether to preload all the tabs: `true`, `false`.
*/
@Input() preloadTabs: any;
/**
* @input {string} set the position of the tabbar's icons: top, bottom, left, right, hide
* @input {string} Deprecated, use `tabbarLayout` instead. Set the position of the tabbar's icons: `top`, `bottom`, `left`, `right`, `hide`.
*/
@Input() tabbarIcons: string;
/**
* @input {string} Set position of the tabbar, top or bottom
* @input {string} Set the tabbar layout: `icon-top`, `icon-left`, `icon-right`, `icon-bottom`, `icon-hide`, `title-hide`.
*/
@Input() tabbarLayout: string;
/**
* @input {string} Set position of the tabbar: `top`, `bottom`.
*/
@Input() tabbarPlacement: string;
/**
* @input {any} expression you want to evaluate when the tabs change
* @input {any} Expression to evaluate when the tab changes.
*/
@Output() change: EventEmitter<Tab> = new EventEmitter();
@ -162,8 +167,13 @@ export class Tabs extends Ion {
*/
ngAfterViewInit() {
this._setConfig('tabbarPlacement', 'bottom');
this._setConfig('tabbarLayout', 'icon-top');
this._setConfig('tabbarIcons', 'top');
if (this.tabbarIcons) {
console.warn("DEPRECATION WARNING: 'tabbarIcons' is no longer supported and will be removed in next major release. Use 'tabbarLayout' instead. Available values: 'icon-top', 'icon-left', 'icon-right', 'icon-bottom', 'icon-hide', 'title-hide'.");
}
if (this._useHighlight) {
this._platform.onResize(() => {
this._highlight.select(this.getSelected());
@ -201,7 +211,7 @@ export class Tabs extends Ion {
private _setConfig(attrKey, fallback) {
var val = this[attrKey];
if (isUndefined(val)) {
val = this._config.get(attrKey);
val = this._config.get(attrKey, fallback);
}
this._renderer.setElementAttribute(this._elementRef.nativeElement, attrKey, val);
}

View File

@ -24,7 +24,7 @@
<!-- Icons below text -->
<ion-tabs tabbarIcons="bottom" no-navbar>
<ion-tabs tabbarLayout="icon-bottom" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" tabBadge="577" tabBadgeStyle="dark"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings"></ion-tab>
@ -32,21 +32,21 @@
<!-- Icons right of text -->
<ion-tabs tabbarIcons="right" no-navbar>
<ion-tabs tabbarLayout="icon-right" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" tabBadge="1030" tabBadgeStyle="light"></ion-tab>
</ion-tabs>
<!-- Icons left of text -->
<ion-tabs tabbarIcons="left" no-navbar>
<ion-tabs tabbarLayout="icon-left" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call" tabBadge="32" tabBadgeStyle="danger"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings"></ion-tab>
</ion-tabs>
<!-- Dynamic Badge -->
<ion-tabs tabbarIcons="left" no-navbar>
<ion-tabs tabbarLayout="icon-left" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [tabBadge]="myBadge" tabBadgeStyle="primary"></ion-tab>

View File

@ -24,7 +24,7 @@
<!-- Icons below text -->
<ion-tabs tabbarIcons="bottom" no-navbar selectedIndex="1">
<ion-tabs tabbarLayout="icon-bottom" no-navbar selectedIndex="1">
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
@ -32,7 +32,7 @@
<!-- Icons right of text -->
<ion-tabs tabbarIcons="right" no-navbar selectedIndex="0">
<ion-tabs tabbarLayout="icon-right" no-navbar selectedIndex="0">
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
@ -40,7 +40,7 @@
<!-- Icons left of text -->
<ion-tabs tabbarIcons="left" no-navbar>
<ion-tabs tabbarLayout="icon-left" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
@ -48,7 +48,15 @@
<!-- No icons -->
<ion-tabs tabbarIcons="hide" no-navbar>
<ion-tabs tabbarLayout="icon-hide" no-navbar>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>
<!-- No title -->
<ion-tabs tabbarLayout="title-hide" no-navbar secondary>
<ion-tab tabTitle="Recents" tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="root"></ion-tab>
@ -61,11 +69,3 @@
<ion-tab tabTitle="Indiana Jones and the Temple of Doom" [root]="root"></ion-tab>
<ion-tab tabTitle="Indiana Jones and the Last Crusade" [root]="root"></ion-tab>
</ion-tabs>
<!-- primary color tabbar -->
<ion-tabs no-navbar primary>
<ion-tab tabIcon="call" [root]="root"></ion-tab>
<ion-tab tabIcon="heart" [root]="root"></ion-tab>
<ion-tab tabIcon="settings" [root]="root"></ion-tab>
</ion-tabs>

View File

@ -101,6 +101,7 @@ import {isObject, isDefined, isFunction, isArray} from '../util/util';
* | pageTransitionDelay | 16 | 120 |
* | tabbarPlacement | bottom | top |
* | tabbarHighlight | | top |
* | tabbarLayout | | |
* | tabSubPages | | true |
*
**/

View File

@ -10,7 +10,10 @@ 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 {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';
import {Tabs} from '../components/tabs/tabs';
import {Tab} from '../components/tabs/tab';
@ -19,6 +22,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';
@ -56,7 +60,10 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when';
* - Blur
* - Content
* - Scroll
* - InfiniteScroll
* - InfiniteScrollContent
* - Refresher
* - RefresherContent
*
* **Lists**
* - List
@ -80,6 +87,7 @@ import {ShowWhen, HideWhen} from '../components/show-hide-when/show-hide-when';
*
* **Media**
* - Icon
* - Spinner
*
* **Forms**
* - Searchbar
@ -122,7 +130,10 @@ export const IONIC_DIRECTIVES = [
Blur,
Content,
Scroll,
InfiniteScroll,
InfiniteScrollContent,
Refresher,
RefresherContent,
// Lists
List,
@ -146,6 +157,7 @@ export const IONIC_DIRECTIVES = [
// Media
Icon,
Spinner,
// Forms
Searchbar,

View File

@ -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',

View File

@ -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

View File

@ -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"

View File

@ -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({

View File

@ -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);
}