Compare commits

...

48 Commits

Author SHA1 Message Date
Job
56364a0503 fix(scroll-view): do not set initialized until it is enabled (#10817)
regression caused by 7e9bad5
2017-03-17 23:05:10 +01:00
Manu Mtz.-Almeida
7e9bad5092 perf(content): scrollview magic activated on demand 2017-03-16 20:12:49 +01:00
Manu Mtz.-Almeida
963cdcbe76 feat(split-pane): support for side=right 2017-03-16 19:41:19 +01:00
Manu Mtz.-Almeida
c79bd5ab81 test(date time): datetime without max value 2017-03-16 19:19:31 +01:00
Florian MC [Mint]
7cc617e84e docs(datetime): update docs
Closes #10795
2017-03-16 14:10:48 -04:00
Manu Mtz.-Almeida
74191c3e92 fix(datetime): update selectedIndex according to ngModel value 2017-03-16 16:34:11 +01:00
Manu Mtz.-Almeida
afd99baba0 fix(datetime): respect time limits in hours and minutes
fixes #6850
2017-03-16 00:38:33 +01:00
Mike Hartington
e191321193 fix(slides): simulate touch events for query params (#10579)
* fix(slides): simulate touch events for query params
Slide fail to swipe when platform via query params.
This lets touch events be simulated is ionicPlatform is set.
Closes #10577

* style(remove debug log):
2017-03-15 19:20:03 +01:00
Manu Mtz.-Almeida
c236a4bf4b test(infinite-scroll): nested <ion-infinite-scroll>
references #10507
2017-03-15 19:00:25 +01:00
Manu Mtz.-Almeida
19302a9fbf Merge branch 'ion-infinite-component-fix' of https://github.com/thielCole/ionic 2017-03-15 18:57:51 +01:00
Manu Mtz.-Almeida
cff4c01bf0 test(datetime): adds test for issue #10641 2017-03-15 18:46:09 +01:00
Felix Livni
769e7dc2aa Prefer nested var over nested let 2017-03-15 18:45:22 +01:00
Felix Livni
eff420f4c7 fix(datetime): not always disabling day values when dayValues set 2017-03-15 18:45:22 +01:00
Toru Yoshikawa
53feb3f699 fix(infinite-scroll): event.timeStamp polyfill for firefox (#10752)
* Event.timeStamp polyfill for firefox

* fix(infinite-scroll): event.timeStamp polyfill for firefox
2017-03-15 18:27:45 +01:00
Manu Mtz.-Almeida
999efaca9a fix(virtual-list): works with infinite-scroll
fixes #9350
fixes #9722
fixes #9247
fixes #10778
2017-03-15 17:38:31 +01:00
Manu Mtz.-Almeida
1222c56d23 Merge branch 'feature/fixVirtualScrollDissapearing' of https://github.com/masimplo/ionic into fix-virtual-list2 2017-03-15 17:11:14 +01:00
Quentin
05f1e24cb6 docs(config): update page-transition
Closes #10789
Updated page transition animation sample and description.
2017-03-15 12:05:58 -04:00
Manu Mtz.-Almeida
505ee58407 Merge branch '10590-alert-input-min-max-attributes' of https://github.com/TomDemulierChevret/ionic
# Conflicts:
#	src/components/alert/alert-component.ts
#	src/components/alert/alert-options.ts
2017-03-15 17:03:30 +01:00
Manu Mtz.-Almeida
1efc4aea0f style(range): removes trailing white spaces 2017-03-15 16:56:27 +01:00
Amit Moryossef
8f310eba4a feat(range): add ionFocus and ionBlur events (#10761)
* Added ionLeave output event to be emitted on leave

* Changed `ionLeave` to `ionDragEnd`

* Changed ionDragEnd to ionBlur. Added ionFocus
2017-03-15 16:54:35 +01:00
Manu Mtz.-Almeida
46fe1ff53c fix(alert): inputs have id
fixes #10603
2017-03-15 16:48:15 +01:00
Manu Mtz.-Almeida
aa287ce56f Merge branch 'select-floating-label' of https://github.com/imgx64/ionic 2017-03-15 15:12:28 +01:00
Manu Mtz.-Almeida
e53bad1bc3 docs(infinity-scroll): algorithm used when position is Top 2017-03-15 15:06:42 +01:00
Job
6918275bd3 feat(infinite): add scroll in opposite direction (#8099)
* feat(infinite): add scroll in opposite direction

fixes

* test(infinite-scroll): opposite direction e2e test

* fix(infinite-scroll): keep scroll position

* feat(content): scroll down on load

* fix(infinite-scroll): scroll the content down on load

* Requested changes
2017-03-15 14:58:45 +01:00
Manu Mtz.-Almeida
f9f9a1b441 test(content): fix unit test by using new elementRef api 2017-03-14 22:14:01 +01:00
Manu Mtz.-Almeida
7a9f88fcda Merge branch 'refactor-content2' of https://github.com/manucorporat/ionic 2017-03-14 21:43:22 +01:00
Ibrahim Ghazal
8c483f2529 fix(select): make floating labels work for ion-select
fixes #10751
2017-03-14 12:42:12 +03:00
alex-pl
446e468b59 docs(split-pane): copy editing (#10708)
Fixed typo in word "also".
2017-03-10 10:41:32 -05:00
Adam Bradley
67cbcdea3b chore(toolbar): move button strong to toolbar sass property 2017-03-09 09:35:59 -06:00
Manu Mtz.-Almeida
ba3530657b fix(picker): selectionIndex always initialized 2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
f4c9ba6614 test(datetime): min/max failing cases 2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
62bf2bee28 style(infinite-scroll): add missing type 2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
5b1126f7f2 style(slides): adds missing final linebreak 2017-03-08 23:44:15 +01:00
Sergii Stotskyi
84e25a17c2 feat(infinite-scroll): add waitFor method to InfiniteScroll
This allows to use `$event.waitFor(request())` inside template within `infinite` event. So that, user components do not depend on InifiniteScroll inside javascript.
2017-03-08 23:44:15 +01:00
patricknolan
30980b6798 fix(toggle/checkbox): trigger ui update when using virtalScroll with
Angular Reactive Forms
2017-03-08 23:44:15 +01:00
Ahmad Amri
e90d692b1f fix(slides): fix rtl support.
- fix rtl functionalities in slides when attribute (dir=“rtl”) added to
ion-slides.
- e2e test added: ‘slides/test/rtl’
2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
bee75f7d00 feat(overlay): method chaining pattern to configure overlays 2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
d538245178 fix(range): knob B can only be actived if range is dual 2017-03-08 23:44:15 +01:00
Manu Mtz.-Almeida
b541832749 fix(range): bar width works as expected
fixes  #10150
2017-03-08 23:44:15 +01:00
Mohsen Sarkar
05859dba40 fix(searchbar): add IE support
remove() as a method on HTMLElements is not supported on IE.
2017-03-08 23:44:15 +01:00
Felix Livni
58beea30f5 fix(datetime): picker.refresh() in generate(...) called too early 2017-03-08 23:44:15 +01:00
Brandy Carney
1d68d1f91f chore(npm): update ionic-angular readme to remove 2.x 2017-03-08 15:46:35 -05:00
Brandy Carney
0cd87f1078 chore(ionic): release 2.2.0 2017-03-08 14:38:31 -05:00
Tom Demulier
b53219a67c feat(alert): add attributes min & max to alert inputs of type number 2017-03-06 09:55:03 +01:00
Manu Mtz.-Almeida
28754926b5 refactor(content): implemented the angular way 2017-03-04 19:12:56 +01:00
Tom Demulier
d666e8b8e5 feat(alert): add attributes min & max to alert inputs 2017-03-01 15:54:07 +01:00
Andrew
fd53e04a47 fix(infinite-scroll):infinite-scroll can be placed in child componenets, fixes #6531 2017-02-20 18:30:11 -08:00
Michael Asimakopoulos
51c398d614 fix(virtualscroll): Populate nodes at correct height 2017-01-30 12:10:34 +02:00
67 changed files with 1769 additions and 659 deletions

View File

@@ -1,3 +1,71 @@
<a name="2.2.0"></a>
# [2.2.0](https://github.com/driftyco/ionic/compare/v2.1.0...v2.2.0) (2017-03-08)
### Updating to 2.2.0
1. Update your `package.json` to match the following dependencies, remove the existing `node_modules` directory, and then run `npm install`:
```
"dependencies": {
"@angular/common": "2.4.8",
"@angular/compiler": "2.4.8",
"@angular/compiler-cli": "2.4.8",
"@angular/core": "2.4.8",
"@angular/forms": "2.4.8",
"@angular/http": "2.4.8",
"@angular/platform-browser": "2.4.8",
"@angular/platform-browser-dynamic": "2.4.8",
"@angular/platform-server": "2.4.8",
"@ionic/storage": "2.0.0",
"ionic-angular": "2.2.0",
"ionic-native": "2.4.1",
"ionicons": "3.0.0",
"rxjs": "5.0.1",
"sw-toolbox": "3.4.0",
"zone.js": "0.7.2"
},
"devDependencies": {
"@ionic/app-scripts": "1.1.4",
"typescript": "2.0.9"
}
```
Note: If you are using `ionic-storage`, you need to update it to `2.0.0` or you will run into an error similar to this: `Error: Can't resolve all parameters for Storage: (?, ?).`. For more information, see the [Storage Documentation](https://ionicframework.com/docs/v2/storage/).
### What's new
#### Split Pane
As part of our initiative to improve desktop support we have introduced a new component called [Split Pane](http://ionicframework.com/docs/v2/api/components/split-pane/SplitPane/). Split Pane makes it possible to easily create multi-view layouts. It allows elements, such as a menu or another navigation pane, to be displayed on large viewports. Split Pane can be used to achieve a layout similar to the Gmail (Android) or Mail (Apple) applications.
#### Angular 2.4.8
Ionic has been updated to depend on Angular 2.4.8, which is the latest version that we have tested and confirmed to be compatible with Ionic. This means that updating to the 2.2.0 release of Ionic will automatically work with all of the performance updates, bug fixes and features in Angular 2.4.8!
### Ionic Storage
We recently released the 2.0.0 version of `ionic-storage`. If you are using Ionic Storage in your application, you need to update to this version of `ionic-storage`. Attempting to use an older version of `ionic-storage` with Ionic 2.2.0 will cause errors. You can read about how to update to `ionic-storage` 2.0.0 [here](https://github.com/driftyco/ionic-storage/releases/tag/v2.0.0).
### Bug Fixes
* **components:** clean up event listeners to stop memory leaks ([8d9f374](https://github.com/driftyco/ionic/commit/8d9f374)), closes [#10459](https://github.com/driftyco/ionic/issues/10459) [#10416](https://github.com/driftyco/ionic/issues/10416) [#10286](https://github.com/driftyco/ionic/issues/10286)
* **infinite-scroll:** use icon color from Sass var and add var for text color ([7b97fb7](https://github.com/driftyco/ionic/commit/7b97fb7)), closes [#10574](https://github.com/driftyco/ionic/issues/10574)
* **menu:** disable the menus when they should be ([dc53c8e](https://github.com/driftyco/ionic/commit/dc53c8e))
* **menu:** don't hide menuToggle outside navbar ([e56bad9](https://github.com/driftyco/ionic/commit/e56bad9))
* **radio:** calculate radio-inner width/height with border width ([#10495](https://github.com/driftyco/ionic/issues/10495)) ([176aa23](https://github.com/driftyco/ionic/commit/176aa23))
* **refresher:** don't destroy events manager ([9308694](https://github.com/driftyco/ionic/commit/9308694)), ([1dd8883](https://github.com/driftyco/ionic/commit/1dd8883)), closes [#10652](https://github.com/driftyco/ionic/issues/10652)
* **refresher:** use refresher icon color from Sass var ([116ae38](https://github.com/driftyco/ionic/commit/116ae38)), closes [#10479](https://github.com/driftyco/ionic/issues/10479)
* **tabs:** emit ionChange after the tab is selected ([ac1a886](https://github.com/driftyco/ionic/commit/ac1a886)), closes [#10538](https://github.com/driftyco/ionic/issues/10538)
* **tabs:** catch the rejected promise with popToRoot ([7385158](https://github.com/driftyco/ionic/commit/7385158))
* **view-controller:** set navigation so dimiss() will work synchronously. ([61a5317](https://github.com/driftyco/ionic/commit/61a5317)), closes [#10654](https://github.com/driftyco/ionic/issues/10654)
### Features
* **alert:** add ability to set the mode on alert ([f577e54](https://github.com/driftyco/ionic/commit/f577e54))
* **split-pane:** split pane support for ion-nav and ion-menu ([#10343](https://github.com/driftyco/ionic/issues/10343)) ([9e4c3a6](https://github.com/driftyco/ionic/commit/9e4c3a6))
* **typography:** add text-wrap attribute for all elements ([2c2b87b](https://github.com/driftyco/ionic/commit/2c2b87b)), closes [#7051](https://github.com/driftyco/ionic/issues/7051)
<a name="2.1.0"></a>
# [2.1.0](https://github.com/driftyco/ionic/compare/v2.0.1...v2.1.0) (2017-02-23)

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "ionic2",
"version": "2.1.0",
"version": "2.2.0",
"description": "A powerful framework for building mobile and progressive web apps with JavaScript and Angular 2",
"keywords": [
"ionic",

View File

@@ -1,10 +1,10 @@
## Ionic Framework 2.x
## Ionic Framework
The official npm package for [Ionic 2](http://ionicframework.com/), complete with pre-built ES5 bundles, TypeScript definitions, Sass files, CommonJS ES5 files, and more.
The official npm package for [Ionic](http://ionicframework.com/), complete with pre-built ES5 bundles, TypeScript definitions, Sass files, CommonJS ES5 files, and more.
To get started with Ionic 2, please read the [Installation Guide](http://ionicframework.com/docs/v2/getting-started/installation/).
To get started with Ionic, please read the [Installation Guide](http://ionicframework.com/docs/v2/intro/installation/).
[Ionic 2 Documentation](http://ionicframework.com/docs/v2/)
[Ionic Documentation](http://ionicframework.com/docs/)
### Source files
@@ -22,7 +22,6 @@ Minified and unminified CommonJS and System.register module format bundles, as w
### Installation and More
To use Ionic 2, we recommend installing and utilizing the [Ionic CLI](http://ionicframework.com/docs/v2/getting-started/installation/) which will help you create pre-configured Ionic apps.
For full instructions on using Ionic 2, please visit the [Ionic 2 Documentation](http://ionicframework.com/docs/v2/)
To use Ionic, we recommend installing and utilizing the [Ionic CLI](http://ionicframework.com/docs/v2/intro/installation/) which will help you create pre-configured Ionic apps.
For full instructions on using Ionic, please visit the [Ionic Documentation](http://ionicframework.com/docs/)

View File

@@ -25,7 +25,7 @@ export class ActionSheet extends ViewController {
/**
* @private
*/
getTransitionName(direction: string) {
getTransitionName(direction: string): string {
let key = 'actionSheet' + (direction === 'back' ? 'Leave' : 'Enter');
return this._nav && this._nav.config.get(key);
}
@@ -33,22 +33,25 @@ export class ActionSheet extends ViewController {
/**
* @param {string} title Action sheet title
*/
setTitle(title: string) {
setTitle(title: string): ActionSheet {
this.data.title = title;
return this;
}
/**
* @param {string} subTitle Action sheet subtitle
*/
setSubTitle(subTitle: string) {
setSubTitle(subTitle: string): ActionSheet {
this.data.subTitle = subTitle;
return this;
}
/**
* @param {object} button Action sheet button
*/
addButton(button: any) {
addButton(button: any): ActionSheet {
this.data.buttons.push(button);
return this;
}
/**
@@ -57,7 +60,7 @@ export class ActionSheet extends ViewController {
* @param {NavOptions} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
present(navOptions: NavOptions = {}) {
present(navOptions: NavOptions = {}): Promise<any> {
navOptions.minClickBlockDuration = navOptions.minClickBlockDuration || 400;
return this._app.present(this, navOptions);
}

View File

@@ -12,59 +12,55 @@ export class E2EPage {
presentActionSheet1() {
this.result = '';
let actionSheet = this.actionSheetCtrl.create({
title: 'Albums',
buttons: [
{
text: 'Delete',
role: 'destructive',
icon: 'trash',
handler: () => {
console.log('Delete clicked');
this.result = 'Deleted';
}
},
{
text: 'Share',
icon: 'share',
handler: () => {
console.log('Share clicked');
this.result = 'Shared';
}
},
{
text: 'Play (open modal)',
icon: 'arrow-dropright-circle',
handler: () => {
this.result = 'Play (open modal)';
let modal = this.modalCtrl.create(ModalPage);
modal.present();
// returning false does not allow the actionsheet to be closed
return false;
}
},
{
text: 'Favorite',
icon: !this.plt.is('ios') ? 'heart' : null,
handler: () => {
console.log('Favorite clicked');
this.result = 'Favorited';
}
},
{
text: 'Cancel',
role: 'cancel', // will always sort to be on the bottom
icon: !this.plt.is('ios') ? 'close' : null,
handler: () => {
console.log('Cancel clicked');
this.result = 'Canceled';
}
this.actionSheetCtrl.create()
.setTitle('Albums')
.addButton({
text: 'Delete',
role: 'destructive',
icon: 'trash',
handler: () => {
console.log('Delete clicked');
this.result = 'Deleted';
}
]
});
})
.addButton({
text: 'Share',
icon: 'share',
handler: () => {
console.log('Share clicked');
this.result = 'Shared';
}
})
.addButton({
text: 'Play (open modal)',
icon: 'arrow-dropright-circle',
handler: () => {
this.result = 'Play (open modal)';
let modal = this.modalCtrl.create(ModalPage);
modal.present();
actionSheet.present();
// returning false does not allow the actionsheet to be closed
return false;
}
})
.addButton({
text: 'Favorite',
icon: !this.plt.is('ios') ? 'heart' : null,
handler: () => {
console.log('Favorite clicked');
this.result = 'Favorited';
}
})
.addButton({
text: 'Cancel',
role: 'cancel', // will always sort to be on the bottom
icon: !this.plt.is('ios') ? 'close' : null,
handler: () => {
console.log('Cancel clicked');
this.result = 'Canceled';
}
})
.present();
}
presentActionSheet2() {

View File

@@ -9,6 +9,7 @@ import { NavParams } from '../../navigation/nav-params';
import { NavOptions } from '../../navigation/nav-util';
import { Platform } from '../../platform/platform';
import { ViewController } from '../../navigation/view-controller';
import { AlertInputOptions, AlertOptions, AlertButton } from './alert-options';
/**
@@ -39,7 +40,7 @@ import { ViewController } from '../../navigation/view-controller';
'<template ngSwitchCase="checkbox">' +
'<div class="alert-checkbox-group">' +
'<button ion-button="alert-checkbox-button" *ngFor="let i of d.inputs" (click)="cbClick(i)" [attr.aria-checked]="i.checked" [disabled]="i.disabled" class="alert-tappable alert-checkbox" role="checkbox">' +
'<button ion-button="alert-checkbox-button" *ngFor="let i of d.inputs" (click)="cbClick(i)" [attr.aria-checked]="i.checked" [attr.id]="i.id" [disabled]="i.disabled" class="alert-tappable alert-checkbox" role="checkbox">' +
'<div class="alert-checkbox-icon"><div class="alert-checkbox-inner"></div></div>' +
'<div class="alert-checkbox-label">' +
'{{i.label}}' +
@@ -51,7 +52,7 @@ import { ViewController } from '../../navigation/view-controller';
'<template ngSwitchDefault>' +
'<div class="alert-input-group">' +
'<div *ngFor="let i of d.inputs" class="alert-input-wrapper">' +
'<input [placeholder]="i.placeholder" [(ngModel)]="i.value" [type]="i.type" class="alert-input">' +
'<input [placeholder]="i.placeholder" [(ngModel)]="i.value" [type]="i.type" [min]="i.min" [max]="i.max" [attr.id]="i.id" class="alert-input">' +
'</div>' +
'</div>' +
'</template>' +
@@ -73,16 +74,7 @@ import { ViewController } from '../../navigation/view-controller';
export class AlertCmp {
activeId: string;
descId: string;
d: {
cssClass?: string;
message?: string;
title?: string;
subTitle?: string;
mode?: string;
buttons?: any[];
inputs?: any[];
enableBackdropDismiss?: boolean;
};
d: AlertOptions;
enabled: boolean;
hdrId: string;
id: number;
@@ -147,9 +139,9 @@ export class AlertCmp {
});
data.inputs = data.inputs.map((input, index) => {
return {
let r: AlertInputOptions = {
type: input.type || 'text',
name: isPresent(input.name) ? input.name : index,
name: isPresent(input.name) ? input.name : index + '',
placeholder: isPresent(input.placeholder) ? input.placeholder : '',
value: isPresent(input.value) ? input.value : '',
label: input.label,
@@ -157,7 +149,10 @@ export class AlertCmp {
disabled: !!input.disabled,
id: isPresent(input.id) ? input.id : `alert-input-${this.id}-${index}`,
handler: isPresent(input.handler) ? input.handler : null,
min: isPresent(input.min) ? input.min : null,
max: isPresent(input.max) ? input.max : null
};
return r;
});
@@ -291,7 +286,7 @@ export class AlertCmp {
bdClick() {
if (this.enabled && this.d.enableBackdropDismiss) {
let cancelBtn = this.d.buttons.find(b => b.role === 'cancel');
var cancelBtn = this.d.buttons.find(b => (<AlertButton>b).role === 'cancel');
if (cancelBtn) {
this.btnClick(cancelBtn);

View File

@@ -5,18 +5,27 @@ export interface AlertOptions {
message?: string;
cssClass?: string;
mode?: string;
inputs?: Array<AlertInputOptions>;
buttons?: Array<any>;
inputs?: AlertInputOptions[];
buttons?: (AlertButton|string)[];
enableBackdropDismiss?: boolean;
}
export interface AlertInputOptions {
type?: string;
name?: string;
name?: string | number;
placeholder?: string;
value?: string;
label?: string;
checked?: boolean;
disabled?: boolean;
id?: string;
handler?: Function;
min?: string | number;
max?: string | number;
}
export interface AlertButton {
text?: string;
role?: string;
handler?: Function;
};

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { App } from '../app/app';
import { AlertCmp } from './alert-component';
import { AlertOptions, AlertInputOptions } from './alert-options';
import { AlertOptions, AlertInputOptions, AlertButton } from './alert-options';
import { isPresent } from '../../util/util';
import { NavOptions } from '../../navigation/nav-util';
import { ViewController } from '../../navigation/view-controller';
@@ -27,7 +27,7 @@ export class Alert extends ViewController {
/**
* @private
*/
getTransitionName(direction: string) {
getTransitionName(direction: string): string {
let key = (direction === 'back' ? 'alertLeave' : 'alertEnter');
return this._nav && this._nav.config.get(key);
}
@@ -35,43 +35,49 @@ export class Alert extends ViewController {
/**
* @param {string} title Alert title
*/
setTitle(title: string) {
setTitle(title: string): Alert {
this.data.title = title;
return this;
}
/**
* @param {string} subTitle Alert subtitle
*/
setSubTitle(subTitle: string) {
setSubTitle(subTitle: string): Alert {
this.data.subTitle = subTitle;
return this;
}
/**
* @param {string} message Alert message content
*/
setMessage(message: string) {
setMessage(message: string): Alert {
this.data.message = message;
return this;
}
/**
* @param {object} input Alert input
*/
addInput(input: AlertInputOptions) {
addInput(input: AlertInputOptions): Alert {
this.data.inputs.push(input);
return this;
}
/**
* @param {any} button Alert button
*/
addButton(button: any) {
addButton(button: AlertButton|string): Alert {
this.data.buttons.push(button);
return this;
}
/**
* @param {string} cssClass Set the CSS class names on the alert's outer wrapper.
*/
setCssClass(cssClass: string) {
setCssClass(cssClass: string): Alert {
this.data.cssClass = cssClass;
return this;
}
/**
@@ -87,7 +93,7 @@ export class Alert extends ViewController {
* @param {NavOptions} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
present(navOptions: NavOptions = {}) {
present(navOptions: NavOptions = {}): Promise<any> {
navOptions.minClickBlockDuration = navOptions.minClickBlockDuration || 400;
return this._app.present(this, navOptions);
}
@@ -244,6 +250,12 @@ export class Alert extends ViewController {
* | label | `string` | The input's label (only for radio/checkbox inputs) |
* | checked | `boolean` | Whether or not the input is checked. |
* | id | `string` | The input's id. |
* | min | `string | number` | The input's minimum authorized value (string only for date inputs, number
* only for number inputs)
* |
* | max | `string | number` | The input's maximum authorized value (string only for date inputs, number
* only for number inputs)
* |
*
* Button options
*

View File

@@ -18,14 +18,12 @@ export class E2EPage {
constructor(private alertCtrl: AlertController, private modalCtrl: ModalController) { }
doAlert() {
let alert = this.alertCtrl.create({
title: 'Alert',
subTitle: 'Subtitle',
message: 'This is an alert message.',
buttons: ['OK']
});
alert.present();
this.alertCtrl.create()
.setTitle('Alert')
.setSubTitle('Subtitle')
.setMessage('This is an alert message.')
.addButton('OK')
.present();
}
doConfirm() {
@@ -100,6 +98,7 @@ export class E2EPage {
});
alert.addInput({
name: 'name2',
id: 'name2-id',
value: 'hello',
placeholder: 'Placeholder 2'
});
@@ -109,6 +108,28 @@ export class E2EPage {
type: 'url',
placeholder: 'Favorite site ever'
});
// input date with min & max
alert.addInput({
name: 'name4',
type: 'date',
min: '2017-03-01',
max: '2018-01-12'
});
// input date without min nor max
alert.addInput({
name: 'name5',
type: 'date'
});
alert.addInput({
name: 'name6',
type: 'number',
min: -5,
max: 10
});
alert.addInput({
name: 'name7',
type: 'number'
});
alert.addButton({
text: 'Cancel',
handler: (data: any) => {

View File

@@ -184,6 +184,7 @@ export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, Co
fn(isChecked);
this._setChecked(isChecked);
this.onTouched();
this._cd.detectChanges();
};
}

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, NgZone, OnDestroy, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
@@ -110,10 +110,10 @@ export { ScrollEvent } from '../../util/scroll-view';
@Component({
selector: 'ion-content',
template:
'<div class="fixed-content">' +
'<div class="fixed-content" #fixedContent>' +
'<ng-content select="[ion-fixed],ion-fab"></ng-content>' +
'</div>' +
'<div class="scroll-content">' +
'<div class="scroll-content" #scrollContent>' +
'<ng-content></ng-content>' +
'</div>' +
'<ng-content select="ion-refresher"></ng-content>',
@@ -123,7 +123,7 @@ export { ScrollEvent } from '../../util/scroll-view';
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class Content extends Ion implements OnDestroy, OnInit {
export class Content extends Ion implements OnDestroy {
/** @internal */
_cTop: number;
/** @internal */
@@ -165,15 +165,13 @@ export class Content extends Ion implements OnDestroy, OnInit {
/** @internal */
_dirty: boolean;
/** @internal */
_scrollEle: HTMLElement;
/** @internal */
_fixedEle: HTMLElement;
/** @internal */
_imgs: Img[] = [];
/** @internal */
_viewCtrlReadSub: any;
/** @internal */
_viewCtrlWriteSub: any;
/** @internal */
_scrollDownOnLoad: boolean = false;
private _imgReqBfr: number;
private _imgRndBfr: number;
@@ -182,6 +180,12 @@ export class Content extends Ion implements OnDestroy, OnInit {
/** @private */
statusbarPadding: boolean;
/** @internal */
@ViewChild('fixedContent', { read: ElementRef }) _fixedContent: ElementRef;
/** @internal */
@ViewChild('scrollContent', { read: ElementRef }) _scrollContent: ElementRef;
/**
* Content height of the viewable area. This does not include content
* which is outside the overflow area, or content area which is under
@@ -335,7 +339,12 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._imgReqBfr = config.getNumber('imgRequestBuffer', 1400);
this._imgRndBfr = config.getNumber('imgRenderBuffer', 400);
this._imgVelMax = config.getNumber('imgVelocityMax', 3);
this._scroll = new ScrollView(_plt, _dom);
// use JS scrolling for iOS UIWebView
// goal is to completely remove this when iOS
// fully supports scroll events
// listen to JS scroll events
this._scroll = new ScrollView(_plt, _dom, config.getBoolean('virtualScrollEventAssist'));
if (viewCtrl) {
// content has a view controller
@@ -362,24 +371,21 @@ export class Content extends Ion implements OnDestroy, OnInit {
/**
* @private
*/
ngOnInit() {
if (this._scrollEle) return;
const children = this._elementRef.nativeElement.children;
assert(children && children.length >= 2, 'content needs at least two children');
enableScrollListener() {
assert(this.getFixedElement(), 'fixed element was not found');
assert(this.getScrollElement(), 'scroll element was not found');
const scroll = this._scroll;
scroll.ev.fixedElement = this._fixedEle = children[0];
scroll.ev.scrollElement = this._scrollEle = children[1];
scroll.ev.fixedElement = this.getFixedElement();
scroll.ev.scrollElement = this.getScrollElement();
// subscribe to the scroll start
scroll.scrollStart.subscribe(ev => {
scroll.onScrollStart = (ev) => {
this.ionScrollStart.emit(ev);
});
};
// subscribe to every scroll move
scroll.scroll.subscribe(ev => {
scroll.onScroll = (ev) => {
// remind the app that it's currently scrolling
this._app.setScrolling();
@@ -387,14 +393,16 @@ export class Content extends Ion implements OnDestroy, OnInit {
this.ionScroll.emit(ev);
this.imgsUpdate();
});
};
// subscribe to the scroll end
scroll.scrollEnd.subscribe(ev => {
scroll.onScrollEnd = (ev) => {
this.ionScrollEnd.emit(ev);
this.imgsUpdate();
});
};
scroll.setEnabled();
}
/**
@@ -406,21 +414,28 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._viewCtrlWriteSub && this._viewCtrlWriteSub.unsubscribe();
this._viewCtrlReadSub = this._viewCtrlWriteSub = null;
this._scroll && this._scroll.destroy();
this._scrollEle = this._fixedEle = this._footerEle = this._scLsn = this._scroll = null;
this._footerEle = this._scLsn = this._scroll = null;
}
/**
* @private
*/
getScrollElement(): HTMLElement {
return this._scrollEle;
return this._scrollContent.nativeElement;
}
/**
* @private
*/
getFixedElement(): HTMLElement {
return this._fixedContent.nativeElement;
}
/**
* @private
*/
onScrollElementTransitionEnd(callback: {(ev: TransitionEvent): void}) {
this._plt.transitionEnd(this._scrollEle, callback);
this._plt.transitionEnd(this.getScrollElement(), callback);
}
/**
@@ -458,13 +473,6 @@ export class Content extends Ion implements OnDestroy, OnInit {
return this._scroll.scrollToBottom(duration);
}
/**
* @private
*/
enableJsScroll() {
this._scroll.enableJsScroll(this._cTop, this._cBottom);
}
/**
* @input {boolean} If true, the content will scroll behind the headers
* and footers. This effect can easily be seen by setting the toolbar
@@ -472,13 +480,25 @@ export class Content extends Ion implements OnDestroy, OnInit {
*/
@Input()
get fullscreen(): boolean {
return !!this._fullscreen;
return this._fullscreen;
}
set fullscreen(val: boolean) {
this._fullscreen = isTrueProperty(val);
}
/**
* @input {boolean} If true, the content will scroll down on load.
*/
@Input()
get scrollDownOnLoad(): boolean {
return this._scrollDownOnLoad;
}
set scrollDownOnLoad(val: boolean) {
this._scrollDownOnLoad = isTrueProperty(val);
}
/**
* @private
*/
@@ -498,14 +518,10 @@ export class Content extends Ion implements OnDestroy, OnInit {
* DOM WRITE
*/
setScrollElementStyle(prop: string, val: any) {
if (this._scrollEle) {
const scrollEle = this.getScrollElement();
if (scrollEle) {
this._dom.write(() => {
// double check here as the scroll element
// could have been destroyed in the 16ms it took
// for this dom write to happen
if (this._scrollEle) {
(<any>this._scrollEle.style)[prop] = val;
}
(<any>scrollEle.style)[prop] = val;
});
}
}
@@ -527,7 +543,7 @@ export class Content extends Ion implements OnDestroy, OnInit {
* {number} dimensions.scrollRight scroll scrollLeft + scrollWidth
*/
getContentDimensions(): ContentDimensions {
const scrollEle = this._scrollEle;
const scrollEle = this.getScrollElement();
const parentElement = scrollEle.parentElement;
return {
@@ -558,11 +574,10 @@ export class Content extends Ion implements OnDestroy, OnInit {
console.debug(`content, addScrollPadding, newPadding: ${newPadding}, this._scrollPadding: ${this._scrollPadding}`);
this._scrollPadding = newPadding;
if (this._scrollEle) {
var scrollEle = this.getScrollElement();
if (scrollEle) {
this._dom.write(() => {
if (this._scrollEle) {
this._scrollEle.style.paddingBottom = (newPadding > 0) ? newPadding + 'px' : '';
}
scrollEle.style.paddingBottom = (newPadding > 0) ? newPadding + 'px' : '';
});
}
}
@@ -600,15 +615,15 @@ export class Content extends Ion implements OnDestroy, OnInit {
* DOM READ
*/
private _readDimensions() {
let cachePaddingTop = this._pTop;
let cachePaddingRight = this._pRight;
let cachePaddingBottom = this._pBottom;
let cachePaddingLeft = this._pLeft;
let cacheHeaderHeight = this._hdrHeight;
let cacheFooterHeight = this._ftrHeight;
let cacheTabsPlacement = this._tabsPlacement;
let scrollEvent: ScrollEvent;
const cachePaddingTop = this._pTop;
const cachePaddingRight = this._pRight;
const cachePaddingBottom = this._pBottom;
const cachePaddingLeft = this._pLeft;
const cacheHeaderHeight = this._hdrHeight;
const cacheFooterHeight = this._ftrHeight;
const cacheTabsPlacement = this._tabsPlacement;
let tabsTop = 0;
let scrollEvent: ScrollEvent;
this._pTop = 0;
this._pRight = 0;
this._pBottom = 0;
@@ -622,11 +637,13 @@ export class Content extends Ion implements OnDestroy, OnInit {
// In certain cases this._scroll is undefined
// if that is the case then we should just return
if (!this._scroll) return;
if (!this._scroll) {
return;
}
scrollEvent = this._scroll.ev;
let ele: HTMLElement = this._elementRef.nativeElement;
let ele: HTMLElement = this.getNativeElement();
if (!ele) {
assert(false, 'ele should be valid');
return;
@@ -735,7 +752,7 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._cBottom !== this.contentBottom
);
this._scroll.init(this._scrollEle, this._cTop, this._cBottom);
this._scroll.init(this.getScrollElement(), this._cTop, this._cBottom);
// initial imgs refresh
this.imgsUpdate();
@@ -751,13 +768,13 @@ export class Content extends Ion implements OnDestroy, OnInit {
return;
}
const scrollEle = this._scrollEle;
const scrollEle = this.getScrollElement();
if (!scrollEle) {
assert(false, 'this._scrollEle should be valid');
assert(false, 'this.getScrollElement() should be valid');
return;
}
const fixedEle = this._fixedEle;
const fixedEle = this.getFixedElement();
if (!fixedEle) {
assert(false, 'this._fixedEle should be valid');
return;
@@ -827,6 +844,12 @@ export class Content extends Ion implements OnDestroy, OnInit {
this._tabs.setTabbarPosition(-1, 0);
}
}
// Scroll the page all the way down after setting dimensions
if (this._scrollDownOnLoad) {
this.scrollToBottom(0);
this._scrollDownOnLoad = false;
}
}
/**

View File

@@ -0,0 +1,32 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage {}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage,
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage,
]
})
export class AppModule {}

View File

@@ -0,0 +1,40 @@
<ion-content scrollDownOnLoad="true">
<b>This page should scroll down on load</b>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi scelerisque dolor lacus, ut vehicula arcu dapibus id. Morbi iaculis fermentum blandit. Curabitur tempus, ante et vehicula tempor, urna velit rutrum massa, quis suscipit purus lacus eget est. Sed nisi nulla, tempus id dictum a, cursus ut felis. Aliquam orci magna, rutrum nec tempor ac, fermentum quis eros. Sed ullamcorper felis sit amet tristique sagittis. Nullam sed tempus mi. Morbi sit amet lacinia leo. Nunc facilisis orci id consectetur dignissim. Integer dictum consectetur enim. Vivamus auctor, turpis ut eleifend pharetra, purus magna mattis arcu, vel pharetra tellus orci eget ex. Integer blandit posuere vehicula. Ut ipsum lorem, efficitur vitae eleifend tincidunt, fermentum nec lacus. Ut nec fermentum dui.
</p>
<b>It worked!</b>
</ion-content>

View File

@@ -3,11 +3,11 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Config } from '../../config/config';
import { Picker, PickerController } from '../picker/picker';
import { PickerColumn, PickerColumnOption } from '../picker/picker-options';
import { PickerColumn } from '../picker/picker-options';
import { Form } from '../../util/form';
import { Ion } from '../ion';
import { Item } from '../item/item';
import { deepCopy, isBlank, isPresent, isTrueProperty, isArray, isString } from '../../util/util';
import { deepCopy, isBlank, isPresent, isTrueProperty, isArray, isString, assert } from '../../util/util';
import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
export const DATETIME_VALUE_ACCESSOR: any = {
@@ -196,14 +196,19 @@ export const DATETIME_VALUE_ACCESSOR: any = {
* ### App Config Level
*
* ```ts
* import { ionicBootstrap } from 'ionic-angular';
*
* ionicBootstrap(MyApp, customProviders, {
* //app.module.ts
* @NgModule({
* ...,
* imports: [
* IonicModule.forRoot(MyApp, {
* monthNames: ['janeiro', 'fevereiro', 'mar\u00e7o', ... ],
* monthShortNames: ['jan', 'fev', 'mar', ... ],
* dayNames: ['domingo', 'segunda-feira', 'ter\u00e7a-feira', ... ],
* dayShortNames: ['dom', 'seg', 'ter', ... ],
* });
* })
* ],
* ...
* })
* ```
*
* ### Component Input Level
@@ -277,6 +282,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
_max: DateTimeData;
_value: DateTimeData = {};
_locale: LocaleData = {};
_picker: Picker;
/**
* @private
@@ -408,7 +414,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
*/
@Input() pickerOptions: any = {};
/**
/**
* @input {string} The text to display when there's no date selected yet.
* Using lowercase to match the input attribute
*/
@@ -475,39 +481,36 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
* @private
*/
open() {
assert(!this._isOpen, 'datetime is already open');
if (this._disabled) {
return;
}
console.debug('datetime, open picker');
// the user may have assigned some options specifically for the alert
const pickerOptions = deepCopy(this.pickerOptions);
const picker = this._pickerCtrl.create(pickerOptions);
pickerOptions.buttons = [
{
text: this.cancelText,
role: 'cancel',
handler: () => {
this.ionCancel.emit(null);
}
},
{
text: this.doneText,
handler: (data: any) => {
console.debug('datetime, done', data);
this.onChange(data);
this.ionChange.emit(data);
}
const picker = this._picker = this._pickerCtrl.create(pickerOptions);
picker.addButton({
text: this.cancelText,
role: 'cancel',
handler: () => this.ionCancel.emit(null)
});
picker.addButton({
text: this.doneText,
handler: (data: any) => {
console.debug('datetime, done', data);
this.onChange(data);
this.ionChange.emit(data);
}
];
});
this.generate(picker);
this.validate(picker);
this.generate();
this.validate();
picker.ionChange.subscribe(() => {
this.validate(picker);
this.validate();
picker.refresh();
});
picker.present(pickerOptions);
@@ -516,12 +519,15 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
picker.onDidDismiss(() => {
this._isOpen = false;
});
picker.refresh();
}
/**
* @private
*/
generate(picker: Picker) {
generate() {
const picker = this._picker;
// if a picker format wasn't provided, then fallback
// to use the display format
let template = this.pickerFormat || this.displayFormat || DEFAULT_FORMAT;
@@ -560,6 +566,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
let column: PickerColumn = {
name: key,
selectedIndex: 0,
options: values.map(val => {
return {
value: val,
@@ -568,143 +575,157 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
})
};
if (column.options.length) {
// cool, we've loaded up the columns with options
// preselect the option for this column
var selected = column.options.find(opt => opt.value === getValueFromFormat(this._value, format));
if (selected) {
// set the select index for this column's options
column.selectedIndex = column.options.indexOf(selected);
}
// add our newly created column to the picker
picker.addColumn(column);
// cool, we've loaded up the columns with options
// preselect the option for this column
var optValue = getValueFromFormat(this._value, format);
var selectedIndex = column.options.findIndex(opt => opt.value === optValue);
if (selectedIndex >= 0) {
// set the select index for this column's options
column.selectedIndex = selectedIndex;
}
// add our newly created column to the picker
picker.addColumn(column);
});
this.divyColumns(picker);
const min = <any>this._min;
const max = <any>this._max;
// Normalize min/max
const columns = this._picker.getColumns();
['month', 'day', 'hour', 'minute']
.filter(name => !columns.find(column => column.name === name))
.forEach(name => {
min[name] = 0;
max[name] = 0;
});
this.divyColumns();
}
}
/**
* @private
*/
validate(picker: Picker) {
let i: number;
let today = new Date();
let columns = picker.getColumns();
validateColumn(name: string, index: number, min: number, max: number, lowerBounds: number[], upperBounds: number[]): number {
assert(lowerBounds.length === 5, 'lowerBounds length must be 5');
assert(upperBounds.length === 5, 'upperBounds length must be 5');
// find the columns used
let yearCol = columns.find(col => col.name === 'year');
let monthCol = columns.find(col => col.name === 'month');
let dayCol = columns.find(col => col.name === 'day');
const column = this._picker.getColumn(name);
if (!column) {
return 0;
}
let yearOpt: PickerColumnOption;
let monthOpt: PickerColumnOption;
let dayOpt: PickerColumnOption;
const lb = lowerBounds.slice();
const ub = upperBounds.slice();
const options = column.options;
for (var i = 0; i < options.length; i++) {
var opt = options[i];
var value = opt.value;
lb[index] = opt.value;
ub[index] = opt.value;
opt.disabled = (
value < lowerBounds[index] ||
value > upperBounds[index] ||
dateSortValue(ub[0], ub[1], ub[2], ub[3], ub[4]) < min ||
dateSortValue(lb[0], lb[1], lb[2], lb[3], lb[4]) > max
);
}
opt = column.options[column.selectedIndex];
if (opt) {
return opt.value;
}
return 0;
}
/**
* @private
*/
validate() {
const today = new Date();
const minCompareVal = dateDataSortValue(this._min);
const maxCompareVal = dateDataSortValue(this._max);
const yearCol = this._picker.getColumn('year');
assert(minCompareVal <= maxCompareVal, 'invalid min/max value');
// default to the current year
let selectedYear: number = today.getFullYear();
if (yearCol) {
// default to the first value if the current year doesn't exist in the options
if (!yearCol.options.find(col => col.value === today.getFullYear())) {
selectedYear = yearCol.options[0].value;
}
yearOpt = yearCol.options[yearCol.selectedIndex];
var yearOpt = yearCol.options[yearCol.selectedIndex];
if (yearOpt) {
// they have a selected year value
selectedYear = yearOpt.value;
}
}
// default to assuming this month has 31 days
let numDaysInMonth = 31;
let selectedMonth: number;
if (monthCol) {
monthOpt = monthCol.options[monthCol.selectedIndex];
if (monthOpt) {
// they have a selected month value
selectedMonth = monthOpt.value;
const selectedMonth = this.validateColumn(
'month', 1,
minCompareVal, maxCompareVal,
[selectedYear, 0, 0, 0, 0],
[selectedYear, 12, 31, 23, 59]
);
// calculate how many days are in this month
numDaysInMonth = daysInMonth(selectedMonth, selectedYear);
}
}
const numDaysInMonth = daysInMonth(selectedMonth, selectedYear);
const selectedDay = this.validateColumn(
'day', 2,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, 0, 0, 0],
[selectedYear, selectedMonth, numDaysInMonth, 23, 59]
);
// create sort values for the min/max datetimes
let minCompareVal = dateDataSortValue(this._min);
let maxCompareVal = dateDataSortValue(this._max);
const selectedHour = this.validateColumn(
'hour', 3,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, selectedDay, 0, 0],
[selectedYear, selectedMonth, selectedDay, 23, 59]
);
if (monthCol) {
// enable/disable which months are valid
// to show within the min/max date range
for (i = 0; i < monthCol.options.length; i++) {
monthOpt = monthCol.options[i];
// loop through each month and see if it
// is within the min/max date range
monthOpt.disabled = (dateSortValue(selectedYear, monthOpt.value, 31) < minCompareVal ||
dateSortValue(selectedYear, monthOpt.value, 1) > maxCompareVal);
}
}
if (dayCol) {
if (isPresent(selectedMonth)) {
// enable/disable which days are valid
// to show within the min/max date range
for (i = 0; i < dayCol.options.length; i++) {
dayOpt = dayCol.options[i];
// loop through each day and see if it
// is within the min/max date range
var compareVal = dateSortValue(selectedYear, selectedMonth, dayOpt.value);
dayOpt.disabled = (compareVal < minCompareVal ||
compareVal > maxCompareVal ||
numDaysInMonth <= i);
}
} else {
// enable/disable which numbers of days to show in this month
for (i = 0; i < dayCol.options.length; i++) {
dayCol.options[i].disabled = (numDaysInMonth <= i);
}
}
}
picker.refresh();
this.validateColumn(
'minute', 4,
minCompareVal, maxCompareVal,
[selectedYear, selectedMonth, selectedDay, selectedHour, 0],
[selectedYear, selectedMonth, selectedDay, selectedHour, 59]
);
}
/**
* @private
*/
divyColumns(picker: Picker) {
let pickerColumns = picker.getColumns();
let columns: number[] = [];
divyColumns() {
const pickerColumns = this._picker.getColumns();
let columnsWidth: number[] = [];
let col: PickerColumn;
let width: number;
for (var i = 0; i < pickerColumns.length; i++) {
col = pickerColumns[i];
columnsWidth.push(0);
pickerColumns.forEach((col, i) => {
columns.push(0);
col.options.forEach(opt => {
if (opt.text.length > columns[i]) {
columns[i] = opt.text.length;
for (var j = 0; j < col.options.length; j++) {
width = col.options[j].text.length;
if (width > columnsWidth[i]) {
columnsWidth[i] = width;
}
});
}
}
});
if (columns.length === 2) {
var width = Math.max(columns[0], columns[1]);
if (columnsWidth.length === 2) {
width = Math.max(columnsWidth[0], columnsWidth[1]);
pickerColumns[0].align = 'right';
pickerColumns[1].align = 'left';
pickerColumns[0].optionsWidth = pickerColumns[1].optionsWidth = `${width * 17}px`;
} else if (columns.length === 3) {
var width = Math.max(columns[0], columns[2]);
} else if (columnsWidth.length === 3) {
width = Math.max(columnsWidth[0], columnsWidth[2]);
pickerColumns[0].align = 'right';
pickerColumns[1].columnWidth = `${columns[1] * 17}px`;
pickerColumns[1].columnWidth = `${columnsWidth[1] * 17}px`;
pickerColumns[0].optionsWidth = pickerColumns[2].optionsWidth = `${width * 17}px`;
pickerColumns[2].align = 'left';
}
@@ -747,49 +768,53 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
*/
calcMinMax(now?: Date) {
const todaysYear = (now || new Date()).getFullYear();
if (isBlank(this.min)) {
if (isPresent(this.yearValues)) {
this.min = Math.min.apply(Math, convertToArrayOfNumbers(this.yearValues, 'year'));
} else {
if (isPresent(this.yearValues)) {
var years = convertToArrayOfNumbers(this.yearValues, 'year');
if (isBlank(this.min)) {
this.min = Math.min.apply(Math, years);
}
if (isBlank(this.max)) {
this.max = Math.max.apply(Math, years);
}
} else {
if (isBlank(this.min)) {
this.min = (todaysYear - 100).toString();
}
}
if (isBlank(this.max)) {
if (isPresent(this.yearValues)) {
this.max = Math.max.apply(Math, convertToArrayOfNumbers(this.yearValues, 'year'));
} else {
if (isBlank(this.max)) {
this.max = todaysYear.toString();
}
}
const min = this._min = parseDate(this.min);
const max = this._max = parseDate(this.max);
min.year = min.year || todaysYear;
max.year = max.year || todaysYear;
min.month = min.month || 1;
max.month = max.month || 12;
min.day = min.day || 1;
max.day = max.day || 31;
min.hour = min.hour || 0;
max.hour = max.hour || 23;
min.minute = min.minute || 0;
max.minute = max.minute || 59;
min.second = min.second || 0;
max.second = max.second || 59;
// Ensure min/max constraits
if (min.year > max.year) {
console.error('min.year > max.year');
min.year = max.year - 100;
} else if (min.year === max.year) {
}
if (min.year === max.year) {
if (min.month > max.month) {
console.error('min.month > max.month');
min.month = 1;
} else if (min.month === max.month && min.day > max.day) {
console.error('min.day > max.day');
min.day = 1;
}
}
min.month = min.month || 1;
min.day = min.day || 1;
min.hour = min.hour || 0;
min.minute = min.minute || 0;
min.second = min.second || 0;
max.month = max.month || 12;
max.day = max.day || 31;
max.hour = max.hour || 23;
max.minute = max.minute || 59;
max.second = max.second || 59;
}
/**
@@ -891,25 +916,21 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
* an array of numbers, and clean up any user input
*/
function convertToArrayOfNumbers(input: any, type: string): number[] {
var values: number[] = [];
if (isString(input)) {
// convert the string to an array of strings
// auto remove any whitespace and [] characters
input = input.replace(/\[|\]|\s/g, '').split(',');
}
let values: number[];
if (isArray(input)) {
// ensure each value is an actual number in the returned array
input.forEach((num: any) => {
num = parseInt(num, 10);
if (!isNaN(num)) {
values.push(num);
}
});
values = input
.map((num: any) => parseInt(num, 10))
.filter(isFinite);
}
if (!values.length) {
if (!values || !values.length) {
console.warn(`Invalid "${type}Values". Must be an array of numbers, or a comma separated string of numbers.`);
}
@@ -923,25 +944,19 @@ function convertToArrayOfNumbers(input: any, type: string): number[] {
*/
function convertToArrayOfStrings(input: any, type: string): string[] {
if (isPresent(input)) {
var values: string[] = [];
if (isString(input)) {
// convert the string to an array of strings
// auto remove any [] characters
input = input.replace(/\[|\]/g, '').split(',');
}
var values: string[];
if (isArray(input)) {
// trim up each string value
input.forEach((val: any) => {
val = val.trim();
if (val) {
values.push(val);
}
});
values = input.map((val: string) => val.trim());
}
if (!values.length) {
if (!values || !values.length) {
console.warn(`Invalid "${type}Names". Must be an array of strings, or a comma separated string.`);
}
@@ -949,4 +964,5 @@ function convertToArrayOfStrings(input: any, type: string): string[] {
}
}
const DEFAULT_FORMAT = 'MMM D, YYYY';

View File

@@ -14,15 +14,14 @@ describe('DateTime', () => {
datetime.max = '2001-12-15';
datetime.min = '2000-01-15';
datetime.pickerFormat = 'MM DD YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
columns[0].selectedIndex = 0; // January
columns[1].selectedIndex = 0; // January 1st
columns[2].selectedIndex = 1; // January 1st, 2000
datetime.validate(picker);
datetime.validate();
expect(columns[1].options[0].disabled).toEqual(true);
expect(columns[1].options[13].disabled).toEqual(true);
@@ -31,7 +30,7 @@ describe('DateTime', () => {
columns[0].selectedIndex = 11; // December
columns[2].selectedIndex = 0; // December 1st, 2001
datetime.validate(picker);
datetime.validate();
expect(columns[0].options[11].disabled).toEqual(false);
@@ -44,15 +43,14 @@ describe('DateTime', () => {
datetime.max = '2010-11-15';
datetime.min = '2000-02-15';
datetime.pickerFormat = 'MM DD YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
columns[0].selectedIndex = 1; // February
columns[1].selectedIndex = 0; // February 1st
columns[2].selectedIndex = columns[2].options.length - 1; // February 1st, 2000
datetime.validate(picker);
datetime.validate();
expect(columns[0].options[0].disabled).toEqual(true);
expect(columns[0].options[1].disabled).toEqual(false);
@@ -60,7 +58,7 @@ describe('DateTime', () => {
columns[2].selectedIndex = 0; // December 1st, 2010
datetime.validate(picker);
datetime.validate();
expect(columns[0].options[0].disabled).toEqual(false);
expect(columns[0].options[10].disabled).toEqual(false);
@@ -72,22 +70,21 @@ describe('DateTime', () => {
datetime.min = '2000-01-01';
datetime.pickerFormat = 'MM DD YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
columns[0].selectedIndex = 0; // January
columns[1].selectedIndex = 0; // January 1st
columns[2].selectedIndex = 0; // January 1st, 2010
datetime.validate(picker);
datetime.validate();
for (var i = 0; i < 31; i++) {
expect(columns[1].options[i].disabled).toEqual(false);
}
columns[0].selectedIndex = 1; // February
datetime.validate(picker);
datetime.validate();
for (var i = 0; i < 28; i++) {
expect(columns[1].options[i].disabled).toEqual(false);
@@ -97,7 +94,7 @@ describe('DateTime', () => {
expect(columns[1].options[30].disabled).toEqual(true);
columns[0].selectedIndex = 3; // April
datetime.validate(picker);
datetime.validate();
for (var i = 0; i < 30; i++) {
expect(columns[1].options[i].disabled).toEqual(false);
@@ -107,13 +104,12 @@ describe('DateTime', () => {
it('should enable all of the values given', () => {
datetime.monthValues = '6,7,8';
datetime.dayValues = '01,02,03,04,05,06,08,09,10, 11, 12, 13, 14';
datetime.dayValues = '01,02,03,04,05,06,08,09,10, 11, 12, 13, 31';
datetime.yearValues = '2014,2015';
datetime.pickerFormat = 'MM DD YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
@@ -121,7 +117,8 @@ describe('DateTime', () => {
expect(columns[1].options.length).toEqual(13); // days
expect(columns[2].options.length).toEqual(2); // years
datetime.validate(picker);
columns[0].selectedIndex = 1; // July
datetime.validate();
// Months
for (var i = 0; i < columns[0].options.length; i++) {
@@ -132,6 +129,11 @@ describe('DateTime', () => {
for (var i = 0; i < columns[1].options.length; i++) {
expect(columns[1].options[i].disabled).toEqual(false);
}
columns[0].selectedIndex = 0; // June
datetime.validate();
expect(columns[1].options[12].disabled).toEqual(true);
});
});
@@ -169,8 +171,7 @@ describe('DateTime', () => {
datetime.ngAfterContentInit();
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(3);
@@ -185,8 +186,7 @@ describe('DateTime', () => {
datetime.displayFormat = 'YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(1);
@@ -199,8 +199,7 @@ describe('DateTime', () => {
datetime.pickerFormat = 'YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(1);
@@ -213,8 +212,7 @@ describe('DateTime', () => {
datetime.pickerFormat = 'MMM YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(2);
@@ -229,8 +227,7 @@ describe('DateTime', () => {
datetime.pickerFormat = 'MMMM YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(2);
@@ -243,8 +240,7 @@ describe('DateTime', () => {
datetime.pickerFormat = 'DDDD D M YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(3);
@@ -257,8 +253,7 @@ describe('DateTime', () => {
datetime.pickerFormat = 'DDDD M YYYY';
datetime.setValue('1994-12-15T13:47:20.789Z');
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(3);
@@ -272,8 +267,7 @@ describe('DateTime', () => {
datetime.min = '2000-01-01';
datetime.pickerFormat = 'MM DD YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(3);
@@ -295,8 +289,7 @@ describe('DateTime', () => {
datetime.min = '2000-01-01';
datetime.pickerFormat = 'YYYY';
var picker = new Picker(mockApp());
datetime.generate(picker);
datetime.generate();
var columns = picker.getColumns();
expect(columns.length).toEqual(1);
@@ -633,9 +626,11 @@ describe('DateTime', () => {
});
var datetime: DateTime;
var picker: Picker;
beforeEach(() => {
datetime = new DateTime(new Form(), mockConfig(), mockElementRef(), mockRenderer(), null, <PickerController>{});
datetime._picker = picker = new Picker(mockApp());
});
console.warn = function(){};

View File

@@ -0,0 +1,44 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
tokyoTime = this.calculateTime(1);
calculateTime(offset: number) {
// create Date object for current location
const d = new Date();
// create new Date object for different city
// using supplied offset
const nd = new Date(d.getTime() + (3600000 * offset));
return nd.toISOString();
}
}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EPage
]
})
export class AppModule {}

View File

@@ -0,0 +1,108 @@
<ion-header>
<ion-toolbar>
<ion-title>Datetime</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="outer-content">
<ion-item>
<ion-label>
min="2017-07-01"
max="2017-12-01"
</ion-label>
<ion-datetime
min="2017-07-01"
max="2017-12-01"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
min="2017-01-01"
max="2017-12-01"
</ion-label>
<ion-datetime
min="2017-01-01"
max="2017-12-01"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
min="2017-01-15"
max="2017-01-30"
</ion-label>
<ion-datetime
min="2017-01-15"
max="2017-01-30"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
monthValues="1,2,4,5,6,7,8,9,12"
dayValues="1,2,31"
yearValues="2014,2015,2017"
</ion-label>
<ion-datetime
monthValues="1,2,4,5,6,7,8,9,12"
dayValues="1,2,31"
yearValues="2014,2015,2017"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
displayFormat="HH:mm"
min="01:20"
max="13:45"
</ion-label>
<ion-datetime
displayFormat="HH:mm"
min="01:20"
max="13:45"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
displayFormat="D MMMM YYYY"
pickerFormat="D MMMM YYYY"
min="2017-03-08"
max="2020-12-31"
</ion-label>
<ion-datetime
displayFormat="D MMMM YYYY"
pickerFormat="D MMMM YYYY"
min="2017-03-08"
max="2020-12-31"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>
displayFormat="DD-MM-YYYY"
pickerFormat="DD MM YYYY"
min="2017-03-20"
</ion-label>
<ion-datetime
displayFormat="DD-MM-YYYY"
pickerFormat="DD MM YYYY"
min="2017-03-20"
></ion-datetime>
</ion-item>
<ion-item>
<ion-label>Tokyo</ion-label>
<ion-datetime
displayFormat="hh:mm A"
[(ngModel)]="tokyoTime"
></ion-datetime>
</ion-item>
</ion-content>

View File

@@ -248,7 +248,7 @@ export class Img implements OnDestroy {
const imgEle = this._img;
const renderer = this._renderer;
if (imgEle.src !== srcAttr) {
if (imgEle && imgEle.src !== srcAttr) {
renderer.setElementAttribute(this._img, 'src', srcAttr);
renderer.setElementAttribute(this._img, 'alt', this.alt);
}

View File

@@ -2,7 +2,7 @@ import { ElementRef, Renderer } from '@angular/core';
import { Content } from '../../content/content';
import { DomController } from '../../../platform/dom-controller';
import { Img } from '../img';
import { mockConfig, mockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
import { mockConfig, mockDomController, mockElementRef, mockElementRefEle, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
import { Platform } from '../../../platform/platform';
@@ -60,8 +60,9 @@ describe('Img', () => {
contentElementRef = mockElementRef();
dom = mockDomController();
content = new Content(mockConfig(), mockPlatform(), dom, contentElementRef, mockRenderer(), null, null, mockZone(), null, null);
content._scrollEle = document.createElement('div');
content._scrollEle.className = 'scroll-content';
let ele = document.createElement('div');
ele.className = 'scroll-content';
content._scrollContent = mockElementRefEle(ele);
elementRef = mockElementRef();
renderer = mockRenderer();

View File

@@ -1,14 +1,14 @@
import { Directive, ElementRef, EventEmitter, Host, Input, NgZone, Output } from '@angular/core';
import { Directive, ElementRef, EventEmitter, Input, NgZone, Output } from '@angular/core';
import { Content, ScrollEvent } from '../content/content';
import { DomController } from '../../platform/dom-controller';
import { assert } from '../../util/util';
/**
* @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.
* scrolls a specified distance from the bottom or top of the page.
*
* The expression assigned to the `infinite` event is called when
* the user scrolls to the specified distance. When this expression
@@ -57,6 +57,52 @@ import { DomController } from '../../platform/dom-controller';
* }
* ```
*
* ## `waitFor` method of InfiniteScroll
*
* In case if your async operation returns promise you can utilize
* `waitFor` method inside your template.
*
* ```html
* <ion-content>
*
* <ion-list>
* <ion-item *ngFor="let item of items">{{item}}</ion-item>
* </ion-list>
*
* <ion-infinite-scroll (ionInfinite)="$event.waitFor(doInfinite())">
* <ion-infinite-scroll-content></ion-infinite-scroll-content>
* </ion-infinite-scroll>
*
* </ion-content>
* ```
*
* ```ts
* @Component({...})
* export class NewsFeedPage {
* items = [];
*
* constructor() {
* for (var i = 0; i < 30; i++) {
* this.items.push( this.items.length );
* }
* }
*
* doInfinite(): Promise<any> {
* console.log('Begin async operation');
*
* return new Promise((resolve) => {
* setTimeout(() => {
* for (var i = 0; i < 30; i++) {
* this.items.push( this.items.length );
* }
*
* console.log('Async operation has ended');
* resolve();
* }, 500);
* })
* }
* }
* ```
*
* ## Infinite Scroll Content
*
@@ -102,6 +148,7 @@ export class InfiniteScroll {
_thr: string = '15%';
_thrPx: number = 0;
_thrPc: number = 0.15;
_position: string = POSITION_BOTTOM;
_init: boolean = false;
@@ -146,6 +193,23 @@ export class InfiniteScroll {
this.enable(shouldEnable);
}
/**
* @input {string} The position of the infinite scroll element.
* The value can be either `top` or `bottom`.
* Default is `bottom`.
*/
@Input()
get position(): string {
return this._position;
}
set position(val: string) {
if (val === POSITION_TOP || val === POSITION_BOTTOM) {
this._position = val;
} else {
console.error(`Invalid value for ion-infinite-scroll's position input. Its value should be '${POSITION_BOTTOM}' or '${POSITION_TOP}'.`);
}
}
/**
* @output {event} Emitted when the scroll reaches
* the threshold distance. From within your infinite handler,
@@ -155,7 +219,7 @@ export class InfiniteScroll {
@Output() ionInfinite: EventEmitter<InfiniteScroll> = new EventEmitter<InfiniteScroll>();
constructor(
@Host() private _content: Content,
private _content: Content,
private _zone: NgZone,
private _elementRef: ElementRef,
private _dom: DomController
@@ -183,17 +247,21 @@ export class InfiniteScroll {
// ******** DOM READ ****************
const d = this._content.getContentDimensions();
const height = d.contentHeight;
let reloadY = d.contentHeight;
if (this._thrPc) {
reloadY += (reloadY * this._thrPc);
} else {
reloadY += this._thrPx;
}
const threshold = this._thrPc ? (height * this._thrPc) : this._thrPx;
// ******** DOM READS ABOVE / DOM WRITES BELOW ****************
const distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - reloadY;
let distanceFromInfinite: number;
if (this._position === POSITION_BOTTOM) {
distanceFromInfinite = ((d.scrollHeight - infiniteHeight) - d.scrollTop) - height - threshold;
} else {
assert(this._position === POSITION_TOP, '_position should be top');
distanceFromInfinite = d.scrollTop - infiniteHeight - threshold;
}
if (distanceFromInfinite < 0) {
// ******** DOM WRITE ****************
this._dom.write(() => {
@@ -221,7 +289,60 @@ export class InfiniteScroll {
* to `enabled`.
*/
complete() {
this.state = STATE_ENABLED;
if (this._position === POSITION_BOTTOM) {
this.state = STATE_ENABLED;
return;
}
assert(this._position === POSITION_TOP, 'position should be top');
/* New content is being added at the top, but the scrollTop position stays the same,
which causes a scroll jump visually. This algorithm makes sure to prevent this.
(Frame 1)
complete() is called, but the UI hasn't had time to update yet.
Save the current content dimensions.
Wait for the next frame using _dom.read, so the UI will be updated.
(Frame 2)
Read the new content dimensions.
Calculate the height difference and the new scroll position.
Delay the scroll position change until other possible dom reads are done using _dom.write to be performant.
(Still frame 2, if I'm correct)
Change the scroll position (= visually maintain the scroll position).
Change the state to re-enable the InfiniteScroll. This should be after changing the scroll position, or it could cause the InfiniteScroll to be triggered again immediately.
(Frame 3)
Done.
*/
// ******** DOM READ ****************
// Save the current content dimensions before the UI updates
const prevDim = this._content.getContentDimensions();
// ******** DOM READ ****************
this._dom.read(() => {
// UI has updated, save the new content dimensions
const newDim = this._content.getContentDimensions();
// New content was added on top, so the scroll position should be changed immediately to prevent it from jumping around
const newScrollTop = newDim.scrollHeight - (prevDim.scrollHeight - prevDim.scrollTop);
// ******** DOM WRITE ****************
this._dom.write(() => {
this._content.scrollTop = newScrollTop;
this.state = STATE_ENABLED;
});
});
}
/**
* Pass a promise inside `waitFor()` within the `infinite` output event handler in order to
* change state of infiniteScroll to "complete"
*/
waitFor(action: Promise<any>) {
const enable = this.complete.bind(this);
action.then(enable, enable);
}
/**
@@ -245,9 +366,8 @@ export class InfiniteScroll {
if (this._init) {
if (shouldListen) {
if (!this._scLsn) {
this._scLsn = this._content.ionScroll.subscribe((ev: ScrollEvent) => {
this._onScroll(ev);
});
this._scLsn = this._content.ionScroll.subscribe(this._onScroll.bind(this));
this._content.enableScrollListener();
}
} else {
this._scLsn && this._scLsn.unsubscribe();
@@ -262,6 +382,10 @@ export class InfiniteScroll {
ngAfterContentInit() {
this._init = true;
this._setListeners(this.state !== STATE_DISABLED);
if (this._position === POSITION_TOP) {
this._content.scrollDownOnLoad = true;
}
}
/**
@@ -276,3 +400,6 @@ export class InfiniteScroll {
const STATE_ENABLED = 'enabled';
const STATE_DISABLED = 'disabled';
const STATE_LOADING = 'loading';
const POSITION_TOP = 'top';
const POSITION_BOTTOM = 'bottom';

View File

@@ -3,9 +3,10 @@ import { IonicApp, IonicModule, InfiniteScroll, NavController } from '../../../.
@Component({
selector: 'my-content',
templateUrl: 'main.html'
})
export class E2EPage1 {
export class MyContent {
@ViewChild(InfiniteScroll) infiniteScroll: InfiniteScroll;
items: number[] = [];
enabled: boolean = true;
@@ -16,16 +17,15 @@ export class E2EPage1 {
}
}
doInfinite(infiniteScroll: InfiniteScroll) {
doInfinite(): Promise<any> {
console.log('Begin async operation');
getAsyncData().then(newData => {
return 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) {
this.enabled = false;
@@ -43,6 +43,22 @@ export class E2EPage1 {
}
@Component({
template: `
<ion-header>
<ion-toolbar>
<ion-title>Infinite Scroll</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<my-content></my-content>
</ion-content>
`
})
export class E2EPage1 {}
@Component({
template: '<ion-content><button ion-button (click)="navCtrl.pop()">Pop</button></ion-content>'
})
@@ -62,7 +78,8 @@ export class E2EApp {
declarations: [
E2EApp,
E2EPage1,
E2EPage2
E2EPage2,
MyContent
],
imports: [
IonicModule.forRoot(E2EApp)
@@ -71,7 +88,8 @@ export class E2EApp {
entryComponents: [
E2EApp,
E2EPage1,
E2EPage2
E2EPage2,
MyContent
]
})
export class AppModule {}

View File

@@ -1,33 +1,21 @@
<ion-header>
<ion-toolbar>
<ion-title>Infinite Scroll</ion-title>
</ion-toolbar>
<p>
InfiniteScroll is enabled: {{enabled}}
</p>
</ion-header>
<button ion-button (click)="toggleInfiniteScroll()" block>
Toggle InfiniteScroll Enabled
</button>
<ion-content>
<p>
InfiniteScroll is enabled: {{enabled}}
</p>
<button ion-button (click)="toggleInfiniteScroll()" block>
Toggle InfiniteScroll Enabled
<ion-list>
<button ion-item (click)="goToPage2()" *ngFor="let item of items">
{{ item }}
</button>
</ion-list>
<ion-list>
<button ion-item (click)="goToPage2()" *ngFor="let item of items">
{{ item }}
</button>
</ion-list>
<ion-infinite-scroll (ionInfinite)="doInfinite($event)" [enabled]="enabled" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>
<ion-infinite-scroll (ionInfinite)="$event.waitFor(doInfinite())" [enabled]="enabled" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>

View File

@@ -1,7 +1,7 @@
import { Content, ScrollEvent } from '../../content/content';
import { DomController } from '../../../platform/dom-controller';
import { InfiniteScroll } from '../infinite-scroll';
import { mockConfig, mockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
import { mockConfig, mockDomController, mockElementRef, mockElementRefEle, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
describe('Infinite Scroll', () => {
@@ -91,6 +91,29 @@ describe('Infinite Scroll', () => {
});
describe('position', () => {
it('should default to bottom', () => {
expect(inf._position).toEqual('bottom');
});
it('should set to top', () => {
inf.position = 'top';
expect(inf._position).toEqual('top');
});
it('should set to bottom', () => {
inf.position = 'bottom';
expect(inf._position).toEqual('bottom');
});
it('should not set to anything else', () => {
inf.position = 'derp';
expect(inf._position).toEqual('bottom');
});
});
let config = mockConfig();
let inf: InfiniteScroll;
@@ -104,8 +127,9 @@ describe('Infinite Scroll', () => {
contentElementRef = mockElementRef();
dom = mockDomController();
content = new Content(config, mockPlatform(), dom, contentElementRef, mockRenderer(), null, null, mockZone(), null, null);
content._scrollEle = document.createElement('div');
content._scrollEle.className = 'scroll-content';
let ele = document.createElement('div');
ele.className = 'scroll-content';
content._scrollContent = mockElementRefEle(ele);
infiniteElementRef = mockElementRef();

View File

@@ -0,0 +1,95 @@
import { Component, ViewChild, NgModule } from '@angular/core';
import { Content, IonicApp, IonicModule, InfiniteScroll, NavController } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage1 {
@ViewChild(InfiniteScroll) infiniteScroll: InfiniteScroll;
@ViewChild(Content) content: Content;
items: number[] = [];
enabled: boolean = true;
constructor(public navCtrl: NavController) {
for (var i = 0; i < 30; i++) {
this.items.unshift( this.items.length );
}
}
doInfinite(infiniteScroll: InfiniteScroll) {
console.log('Begin async operation');
getAsyncData().then(newData => {
for (var i = 0; i < newData.length; i++) {
this.items.unshift( this.items.length );
}
console.log('Finished receiving data, async operation complete');
infiniteScroll.complete();
if (this.items.length > 90) {
this.enabled = false;
}
});
}
goToPage2() {
this.navCtrl.push(E2EPage2);
}
toggleInfiniteScroll() {
this.enabled = !this.enabled;
}
}
@Component({
template: '<ion-content><button ion-button (click)="navCtrl.pop()">Pop</button></ion-content>'
})
export class E2EPage2 {
constructor(public navCtrl: NavController) {}
}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage1;
}
@NgModule({
declarations: [
E2EApp,
E2EPage1,
E2EPage2
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage1,
E2EPage2
]
})
export class AppModule {}
function getAsyncData(): Promise<any[]> {
// async return mock data
return new Promise(resolve => {
setTimeout(() => {
let data: number[] = [];
for (var i = 0; i < 30; i++) {
data.unshift(i);
}
resolve(data);
}, 2000);
});
}

View File

@@ -0,0 +1,31 @@
<ion-header>
<ion-toolbar>
<ion-title>Infinite Scroll</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-infinite-scroll (ionInfinite)="doInfinite($event)" position="top" [enabled]="enabled">
<ion-infinite-scroll-content>
</ion-infinite-scroll-content>
</ion-infinite-scroll>
<ion-list>
<button ion-item (click)="goToPage2()" *ngFor="let item of items">
{{ item }}
</button>
</ion-list>
<p>
InfiniteScroll is enabled: {{enabled}}
</p>
<button ion-button (click)="toggleInfiniteScroll()" block>
Toggle InfiniteScroll Enabled
</button>
</ion-content>

View File

@@ -65,7 +65,3 @@ ion-label[floating] {
max-width: 100%;
}
.item-select ion-label[floating] {
transform: translate3d(0, 0, 0) scale(.8);
}

View File

@@ -26,16 +26,49 @@ export class Loading extends ViewController {
/**
* @private
*/
getTransitionName(direction: string) {
getTransitionName(direction: string): string {
let key = (direction === 'back' ? 'loadingLeave' : 'loadingEnter');
return this._nav && this._nav.config.get(key);
}
/**
* @param {string} content loading message content
* @param {string} sets the html content for the loading indicator.
*/
setContent(content: string) {
setContent(content: string): Loading {
this.data.content = content;
return this;
}
/**
* @param {string} sets the name of the SVG spinner for the loading indicator.
*/
setSpinner(spinner: string): Loading {
this.data.spinner = spinner;
return this;
}
/**
* @param {string} sets additional classes for custom styles, separated by spaces.
*/
setCssClass(cssClass: string): Loading {
this.data.cssClass = cssClass;
return this;
}
/**
* @param {string} sets whether to show the backdrop.
*/
setShowBackdrop(showBackdrop: boolean): Loading {
this.data.showBackdrop = showBackdrop;
return this;
}
/**
* @param {string} how many milliseconds to wait before hiding the indicator.
*/
setDuration(dur: number): Loading {
this.data.duration = dur;
return this;
}
/**
@@ -44,7 +77,7 @@ export class Loading extends ViewController {
* @param {NavOptions} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
present(navOptions: NavOptions = {}) {
present(navOptions: NavOptions = {}): Promise<any> {
return this._app.present(this, navOptions, AppPortal.LOADING);
}
@@ -175,7 +208,7 @@ export class LoadingController {
* @param {LoadingOptions} opts Loading options
* @returns {Loading} Returns a Loading Instance
*/
create(opts: LoadingOptions = {}) {
create(opts: LoadingOptions = {}): Loading {
return new Loading(this._app, opts);
}

View File

@@ -127,13 +127,11 @@ export class E2EPage {
}
presentLoadingCrescent() {
let loading = this.loadingCtrl.create({
spinner: 'crescent',
content: 'Please wait...',
duration: 1000
});
loading.present();
this.loadingCtrl.create()
.setSpinner('crescent')
.setContent('Please wait...')
.setDuration(1000)
.present();
}
// Pass the fixed-spinner class so we can turn off
@@ -207,10 +205,9 @@ export class E2EPage {
loading2.present();
}, 1000);
let loading3 = this.loadingCtrl.create({
spinner: 'hide',
content: 'Loading 3 Please Wait...'
});
let loading3 = this.loadingCtrl.create()
.setSpinner('hide')
.setContent('Loading 3 Please Wait...');
setTimeout(() => {
loading3.present();
@@ -256,7 +253,6 @@ export class E2EPage {
}
presentLoadingOpenDismiss() {
// debugger;
const loading = this.loadingCtrl.create({
content: 'Loading 1'
});

View File

@@ -229,11 +229,11 @@ export class ModalPassData {
this.called.ionViewCanLeave++;
return new Promise((resolve: any, reject: any) => {
let alert = this.alertCtrl.create();
alert.setTitle('Do you want to submit?');
alert.addButton({ text: 'Submit', handler: resolve });
alert.addButton({ text: 'Cancel', role: 'cancel', handler: reject });
alert.present();
this.alertCtrl.create()
.setTitle('Do you want to submit?')
.addButton({ text: 'Submit', handler: resolve })
.addButton({ text: 'Cancel', role: 'cancel', handler: reject })
.present();
});
}

View File

@@ -422,7 +422,7 @@ export class PickerColumnCmp {
}
}
var selectedIndex = clamp(min, this.col.selectedIndex, max);
const selectedIndex = clamp(min, this.col.selectedIndex, max);
if (selectedIndex !== this.col.selectedIndex) {
var y = (selectedIndex * this.optHeight) * -1;
@@ -512,7 +512,7 @@ export class PickerCmp {
if (!isPresent(column.options)) {
column.options = [];
}
column.selectedIndex = column.selectedIndex || 0;
column.options = column.options.map(inputOpt => {
let opt: PickerColumnOption = {
text: '',

View File

@@ -53,6 +53,10 @@ export class Picker extends ViewController {
return this.data.columns;
}
getColumn(name: string): PickerColumn {
return this.getColumns().find(column => column.name === name);
}
refresh() {
this._cmp && this._cmp.instance.refresh && this._cmp.instance.refresh();
}

View File

@@ -302,11 +302,20 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
return null;
}
/**
* @output {Range} Emitted when the range selector drag starts.
*/
@Output() ionFocus: EventEmitter<Range> = new EventEmitter<Range>();
/**
* @output {Range} Emitted when the range value changes.
*/
@Output() ionChange: EventEmitter<Range> = new EventEmitter<Range>();
/**
* @output {Range} Emitted when the range selector drag ends.
*/
@Output() ionBlur: EventEmitter<Range> = new EventEmitter<Range>();
constructor(
private _form: Form,
@@ -356,6 +365,9 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
return false;
}
// trigger ionFocus event
this.ionFocus.emit(this);
// prevent default so scrolling does not happen
ev.preventDefault();
ev.stopPropagation();
@@ -368,7 +380,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
// figure out which knob they started closer to
const ratio = clamp(0, (current.x - rect.left) / (rect.width), 1);
this._activeB = (Math.abs(ratio - this._ratioA) > Math.abs(ratio - this._ratioB));
this._activeB = this._dual && (Math.abs(ratio - this._ratioA) > Math.abs(ratio - this._ratioB));
// update the active knob's position
this._update(current, rect, true);
@@ -411,6 +423,9 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
// trigger a haptic end
this._haptic.gestureSelectionEnd();
// trigger ionBlur event
this.ionBlur.emit(this);
}
}
@@ -428,18 +443,14 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
// update which knob is pressed
this._pressed = isPressed;
let valChanged = false;
if (this._activeB) {
// when the pointer down started it was determined
// that knob B was the one they were interacting with
this._pressedB = isPressed;
this._pressedA = false;
this._ratioB = ratio;
if (val === this._valB) {
// hasn't changed
return false;
}
valChanged = val === this._valB;
this._valB = val;
} else {
@@ -447,13 +458,13 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this._pressedA = isPressed;
this._pressedB = false;
this._ratioA = ratio;
if (val === this._valA) {
// hasn't changed
return false;
}
valChanged = val === this._valA;
this._valA = val;
}
this._updateBar();
if (valChanged) {
return false;
}
// value has been updated
if (this._dual) {
@@ -478,8 +489,6 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
this.ionChange.emit(this);
});
this._updateBar();
return true;
}

View File

@@ -1,7 +1,7 @@
import { Refresher } from '../refresher';
import { Content } from '../../content/content';
import { GestureController } from '../../../gestures/gesture-controller';
import { mockConfig, mockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
import { mockConfig, mockDomController, mockElementRef, mockElementRefEle, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
describe('Refresher', () => {
@@ -234,8 +234,9 @@ describe('Refresher', () => {
contentElementRef = mockElementRef();
dom = mockDomController();
content = new Content(mockConfig(), mockPlatform(), dom, contentElementRef, mockRenderer(), null, null, mockZone(), null, null);
content._scrollEle = document.createElement('div');
content._scrollEle.className = 'scroll-content';
let ele = document.createElement('div');
ele.className = 'scroll-content';
content._scrollContent = mockElementRefEle(ele);
let gestureController = new GestureController(null);
@@ -270,8 +271,4 @@ describe('Refresher', () => {
};
}
// function getScrollElementStyles() {
// return content._scrollEle.style;
// }
});

View File

@@ -289,7 +289,7 @@ export class Searchbar extends Ion {
// Get the width of the span then remove it
var textWidth = tempSpan.offsetWidth;
tempSpan.remove();
doc.body.removeChild(tempSpan);
// Set the input padding left
var inputLeft = 'calc(50% - ' + (textWidth / 2) + 'px)';

View File

@@ -387,6 +387,21 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
return (this._multi ? this._texts : this._texts.join());
}
/**
* @private
*/
checkHasValue(inputValue: any) {
if (this._item) {
let hasValue: boolean;
if (Array.isArray(inputValue)) {
hasValue = inputValue.length > 0;
} else {
hasValue = !isBlank(inputValue);
}
this._item.setElementClass('input-has-value', hasValue);
}
}
/**
* @private
*/
@@ -445,6 +460,7 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
console.debug('select, writeValue', val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
}
/**
@@ -464,6 +480,7 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
fn(val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
this.onTouched();
};
}
@@ -481,6 +498,7 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
console.debug('select, onChange w/out formControlName', val);
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
this._updOpts();
this.checkHasValue(val);
this.onTouched();
}

View File

@@ -83,4 +83,20 @@
</ion-list>
</form>
<ion-item>
<ion-label floating>Floating label</ion-label>
<ion-select multiple="true">
<ion-option value="bacon">Bacon</ion-option>
<ion-option value="olives">Black Olives</ion-option>
<ion-option value="xcheese">Extra Cheese</ion-option>
<ion-option value="peppers">Green Peppers</ion-option>
<ion-option value="mushrooms">Mushrooms</ion-option>
<ion-option value="onions">Onions</ion-option>
<ion-option value="pepperoni">Pepperoni</ion-option>
<ion-option value="pineapple">Pineapple</ion-option>
<ion-option value="sausage">Sausage</ion-option>
<ion-option value="Spinach">Spinach</ion-option>
</ion-select>
</ion-item>
</ion-content>

View File

@@ -137,4 +137,12 @@
</ion-item>
</form>
<ion-item>
<ion-label floating>Floating label</ion-label>
<ion-select>
<ion-option value="f">Female</ion-option>
<ion-option value="m">Male</ion-option>
</ion-select>
</ion-item>
</ion-content>

View File

@@ -139,7 +139,7 @@ import { ViewController } from '../../navigation/view-controller';
@Component({
selector: 'ion-slides',
template:
'<div class="swiper-container">' +
'<div class="swiper-container" [attr.dir]="_rtl? \'rtl\' : null">' +
'<div class="swiper-wrapper">' +
'<ng-content></ng-content>' +
'</div>' +
@@ -249,6 +249,14 @@ export class Slides extends Ion {
}
private _pager = false;
/**
* @input {string} If dir attribute is equal to rtl, set interal _rtl to true;
*/
@Input()
set dir(val: string) {
this._rtl = (val.toLowerCase() === 'rtl');
}
/**
* @input {string} Type of pagination. Possible values are:
* `bullets`, `fraction`, `progress`. Default: `bullets`.

View File

@@ -66,7 +66,7 @@ export function initEvents(s: Slides, plt: Platform): Function {
}, { passive: true, zone: false }, unregs);
}
if ((s.simulateTouch && !plt.is('ios') && !plt.is('android')) || (s.simulateTouch && !s._supportTouch && plt.is('ios'))) {
if ((s.simulateTouch && !plt.is('ios') && !plt.is('android')) || (s.simulateTouch && !s._supportTouch && plt.is('ios')) || plt.getQueryParam('ionicPlatform')) {
// mousedown
plt.registerListener(touchEventsTarget, 'mousedown', (ev: SlideUIEvent) => {
onTouchStart(s, plt, ev);

View File

@@ -0,0 +1,46 @@
import { Component, ViewChild, NgModule } from '@angular/core';
import { IonicApp, IonicModule, Slides } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
@ViewChild(Slides) slider: Slides;
onSlideWillChange(s: Slides) {
console.log(`onSlideWillChange: ${s}`);
}
onSlideDidChange(s: Slides) {
console.log(`onSlideDidChange: ${s}`);
}
onSlideDrag(s: Slides) {
console.log(`onSlideDrag: ${s}`);
}
}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage
]
})
export class AppModule { }

View File

@@ -0,0 +1,21 @@
<ion-slides style="background: black"
(ionSlideWillChange)="onSlideWillChange($event)"
(ionSlideDidChange)="onSlideDidChange($event)"
(ionSlideDrag)="onSlideDrag($event)"
pager="true"
dir="rtl"
effect="slide">
<ion-slide style="background: red; color: white;">
<h1>شريحة ١</h1>
</ion-slide>
<ion-slide style="background: white; color: blue;">
<h1>شريحة ٢</h1>
</ion-slide>
<ion-slide style="background: blue; color: white;">
<h1>شريحة ٣</h1>
</ion-slide>
</ion-slides>

View File

@@ -4,9 +4,17 @@
// Split Pane
// --------------------------------------------------
$split-pane-ios-border: $hairlines-width solid $list-ios-border-color;
.split-pane-ios.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: $hairlines-width solid $list-ios-border-color;
border-right: $split-pane-ios-border;
border-left: 0;
}
.split-pane-ios.split-pane-visible > .split-pane-side[side=right] {
border-right: 0;
border-left: $split-pane-ios-border;
}

View File

@@ -4,10 +4,17 @@
// Split Pane
// --------------------------------------------------
$split-pane-md-border: 1px solid $list-md-border-color;
.split-pane-md.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: 1px solid $list-md-border-color;
border-right: $split-pane-md-border;
border-left: 0;
}
.split-pane-md.split-pane-visible > .split-pane-side[side=right] {
border-right: 0;
border-left: $split-pane-md-border;
}

View File

@@ -72,3 +72,7 @@ ion-split-pane {
display: hidden !important;
}
}
.split-pane-visible >.split-pane-side[side=right] {
order: 1;
}

View File

@@ -172,7 +172,7 @@ export class SplitPane extends Ion implements RootNode {
/**
* @input {string | boolean} When the split-pane should be shown.
* Can be a CSS media query expression, or a shortcut expression.
* Can aslo be a boolean expression.
* Can also be a boolean expression.
*/
@Input()
set when(query: string | boolean) {

View File

@@ -4,9 +4,17 @@
// Split Pane
// --------------------------------------------------
$split-pane-wp-border: 1px solid $list-wp-border-color;
.split-pane-wp.split-pane-visible >.split-pane-side {
min-width: 270px;
max-width: 28%;
border-right: 1px solid $list-wp-border-color;
border-right: $split-pane-wp-border;
border-left: 0;
}
.split-pane-wp.split-pane-visible > .split-pane-side[side=right] {
border-right: 0;
border-left: $split-pane-wp-border;
}

View File

@@ -35,6 +35,8 @@ export class E2EPage2 {}
<button ion-button (click)="menu1Active()">Enable menu 1</button>
<button ion-button (click)="menu2Active()">Enable menu 2</button>
<button ion-button (click)="menu3Active()">Enable menu 3</button>
<button ion-button (click)="menu4Active()">Enable menu 4 (right)</button>
<button ion-button (click)="disableAll()">Disable all</button>
<div f></div>
@@ -49,20 +51,33 @@ export class E2EPage {
constructor(
public navCtrl: NavController,
public menuCtrl: MenuController,
) { }
) {
this.menuCtrl.enable(false, 'menu4');
}
push() {
this.navCtrl.push(E2EPage2);
}
menu1Active() {
this.menuCtrl.enable(false, 'menu4');
this.menuCtrl.enable(true, 'menu1');
}
menu2Active() {
this.menuCtrl.enable(false, 'menu4');
this.menuCtrl.enable(true, 'menu2');
}
menu3Active() {
this.menuCtrl.enable(false, 'menu4');
this.menuCtrl.enable(true, 'menu3');
}
menu4Active() {
this.menuCtrl.enable(false, 'menu1');
this.menuCtrl.enable(false, 'menu2');
this.menuCtrl.enable(false, 'menu3');
this.menuCtrl.enable(true, 'menu4');
}
disableAll() {
this.menuCtrl.enable(false);
}

View File

@@ -48,5 +48,20 @@
</ion-content>
</ion-menu>
<ion-menu [content]="content" id="menu4" side="right">
<ion-header>
<ion-toolbar color="dark">
<ion-title>Menu 4</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-item>Example</ion-item>
</ion-list>
</ion-content>
</ion-menu>
<ion-nav [root]="root" main #content></ion-nav>
</ion-split-pane>

View File

@@ -8,6 +8,7 @@ import { IonicApp, IonicModule, NavController, MenuController } from '../../../.
<ion-navbar><ion-title>Navigation</ion-title></ion-navbar>
</ion-header>
<ion-content>
<ion-slides style="background: black"
pager="true"
effect="flip">
@@ -47,6 +48,7 @@ export class SidePage {
</ion-navbar>
</ion-header>
<ion-content padding>
<button ion-button (click)="push()">Push</button>
<div f></div>
<div f></div>
@@ -57,6 +59,7 @@ export class SidePage {
`
})
export class E2EPage {
constructor(
public navCtrl: NavController,
public menuCtrl: MenuController,
@@ -74,6 +77,7 @@ export class E2EPage {
export class E2EApp {
root = E2EPage;
root2 = SidePage;
side = 'right';
}
@NgModule({

View File

@@ -1,7 +1,7 @@
<ion-split-pane>
<ion-nav [root]="root2" #content main></ion-nav>
<ion-tabs>
<ion-tabs [attr.side]="side">
<ion-tab [root]="root" tabTitle="Page1"></ion-tab>
<ion-tab [root]="root" tabTitle="Page2"></ion-tab>
</ion-tabs>

View File

@@ -494,7 +494,7 @@ export class Tabs extends Ion implements AfterViewInit, RootNode {
/**
* @internal
*/
getActiveChildNav() {
getActiveChildNav(): Tab {
return this.getSelected();
}

View File

@@ -45,11 +45,10 @@ export class E2EPage {
}
showLongToast() {
const toast = this.toastCtrl.create({
message: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea voluptatibus quibusdam eum nihil optio, ullam accusamus magni, nobis suscipit reprehenderit, sequi quam amet impedit. Accusamus dolorem voluptates laborum dolor obcaecati.',
duration: 5000,
cssClass: 'custom-class my-toast'
});
const toast = this.toastCtrl.create()
.setMessage('Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ea voluptatibus quibusdam eum nihil optio, ullam accusamus magni, nobis suscipit reprehenderit, sequi quam amet impedit. Accusamus dolorem voluptates laborum dolor obcaecati.')
.setDuration(5000)
.setCssClass('custom-class my-toast');
toast.onDidDismiss(this.dismissHandler);
toast.present();

View File

@@ -30,7 +30,7 @@ export class Toast extends ViewController {
/**
* @private
*/
getTransitionName(direction: string) {
getTransitionName(direction: string): string {
let key = 'toast' + (direction === 'back' ? 'Leave' : 'Enter');
return this._nav && this._nav.config.get(key);
}
@@ -38,15 +38,48 @@ export class Toast extends ViewController {
/**
* @private
*/
isValidPosition(position: string) {
isValidPosition(position: string): boolean {
return position === TOAST_POSITION_TOP || position === TOAST_POSITION_MIDDLE || position === TOAST_POSITION_BOTTOM;
}
/**
* @param {string} message Toast message content
*/
setMessage(message: string) {
setMessage(message: string): Toast {
this.data.message = message;
return this;
}
/**
* @param {string} message Toast message content
*/
setDuration(dur: number): Toast {
this.data.duration = dur;
return this;
}
/**
* @param {string} message Toast message content
*/
setPosition(pos: 'top' | 'middle' | 'bottom'): Toast {
this.data.position = pos;
return this;
}
/**
* @param {string} message Toast message content
*/
setCssClass(cssClass: string): Toast {
this.data.cssClass = cssClass;
return this;
}
/**
* @param {string} message Toast message content
*/
setShowCloseButton(closeButton: boolean): Toast {
this.data.showCloseButton = closeButton;
return this;
}
/**
@@ -55,7 +88,7 @@ export class Toast extends ViewController {
* @param {NavOptions} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
present(navOptions: NavOptions = {}) {
present(navOptions: NavOptions = {}): Promise<any> {
navOptions.disableApp = false;
return this._app.present(this, navOptions, AppPortal.TOAST);
}
@@ -142,7 +175,7 @@ export class ToastController {
* Create a new toast component. See options below
* @param {ToastOptions} opts Toast options. See the below table for available options.
*/
create(opts: ToastOptions = {}) {
create(opts: ToastOptions = {}): Toast {
return new Toast(this._app, opts);
}

View File

@@ -36,6 +36,9 @@ $toolbar-ios-button-color: color-contrast($colors-ios, $toolbar
/// @prop - Border radius of the toolbar button
$toolbar-ios-button-border-radius: 4px !default;
/// @prop - Font weight of the strong toolbar button
$toolbar-ios-button-strong-font-weight: 600 !default;
/// @prop - Height of the navigation bar
$navbar-ios-height: $toolbar-ios-height !default;
@@ -378,5 +381,5 @@ $navbar-ios-height: $toolbar-ios-height !default;
// --------------------------------------------------
.bar-button-strong-ios {
font-weight: $button-ios-strong-font-weight;
font-weight: $toolbar-ios-button-strong-font-weight;
}

View File

@@ -30,6 +30,9 @@ $toolbar-md-button-color: $toolbar-md-title-text-color !default;
/// @prop - Border radius of the toolbar button
$toolbar-md-button-border-radius: 2px !default;
/// @prop - Font weight of the strong toolbar button
$toolbar-md-button-strong-font-weight: bold !default;
/// @prop - Height of the navigation bar
$navbar-md-height: $toolbar-md-height !default;
@@ -396,5 +399,5 @@ $navbar-md-height: $toolbar-md-height !default;
// --------------------------------------------------
.bar-button-strong-md {
font-weight: $button-md-strong-font-weight;
font-weight: $toolbar-md-button-strong-font-weight;
}

View File

@@ -39,6 +39,9 @@ $toolbar-wp-button-color: color-contrast($colors-wp, $toolbar-wp-
/// @prop - Border radius of the toolbar button
$toolbar-wp-button-border-radius: 2px !default;
/// @prop - Font weight of the strong toolbar button
$toolbar-wp-button-strong-font-weight: bold !default;
/// @prop - Height of the navigation bar
$navbar-wp-height: $toolbar-wp-height !default;
@@ -350,5 +353,5 @@ $navbar-wp-height: $toolbar-wp-height !default;
// --------------------------------------------------
.bar-button-strong-wp {
font-weight: $button-wp-strong-font-weight;
font-weight: $toolbar-wp-button-strong-font-weight;
}

View File

@@ -1,22 +1,20 @@
import { Component, NgModule } from '@angular/core';
import { Component, NgModule, enableProdMode } from '@angular/core';
import { IonicApp, IonicModule, NavController, Platform } from '../../../../../ionic-angular';
enableProdMode();
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
items: any[] = [];
webview: string = '';
counter: number = 0;
constructor(plt: Platform, public navCtrl: NavController) {
for (var i = 0; i < 200; i++) {
this.items.push({
value: i,
someMethod: function() {
return '!!';
}
});
this.addItem();
}
if (plt.is('ios')) {
@@ -43,6 +41,16 @@ export class E2EPage {
this.navCtrl.push(E2EPage);
}
addItem() {
this.items.push({
value: this.counter,
someMethod: function() {
return '!!';
}
});
this.counter++;
}
reload() {
window.location.reload(true);
}

View File

@@ -5,6 +5,9 @@
<button ion-button (click)="reload()">
Reload
</button>
<button ion-button icon-only (click)="addItem()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>

View File

@@ -0,0 +1,82 @@
import { Component, NgModule } from '@angular/core';
import { IonicApp, IonicModule } from '../../../../../ionic-angular';
@Component({
templateUrl: 'main.html'
})
export class E2EPage {
counter = 1;
items: any[] = [];
enabled = true;
constructor() {
for (let i = 0; i < 100; i++) {
this.addItem();
}
}
addItem() {
this.items.push(this.counter);
this.counter++;
}
doInfinite(): Promise<any> {
console.log('Begin async operation');
return 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');
if (this.items.length > 900) {
this.enabled = false;
}
});
}
}
function getAsyncData(): Promise<any[]> {
// async return mock data
return new Promise(resolve => {
setTimeout(() => {
let data: number[] = [];
for (var i = 0; i < 30; i++) {
data.push(i);
}
resolve(data);
}, 500);
});
}
@Component({
template: '<ion-nav [root]="root"></ion-nav>'
})
export class E2EApp {
root = E2EPage;
}
@NgModule({
declarations: [
E2EApp,
E2EPage
],
imports: [
IonicModule.forRoot(E2EApp)
],
bootstrap: [IonicApp],
entryComponents: [
E2EApp,
E2EPage
]
})
export class AppModule {}

View File

@@ -0,0 +1,32 @@
<ion-header>
<ion-navbar>
<ion-title>Virtual Scroll</ion-title>
<ion-buttons end>
<button ion-button icon-only (click)="addItem()">
<ion-icon name="add"></ion-icon>
</button>
</ion-buttons>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-list [virtualScroll]="items">
<ion-item *virtualItem="let item">
Item: {{item}}
</ion-item>
</ion-list>
<ion-infinite-scroll (ionInfinite)="$event.waitFor(doInfinite())" [enabled]="enabled" threshold="100px">
<ion-infinite-scroll-content
loadingSpinner="bubbles"
loadingText="Loading more data...">
</ion-infinite-scroll-content>
</ion-infinite-scroll>
</ion-content>

View File

@@ -216,7 +216,8 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
_differ: any;
_scrollSub: any;
_scrollEndSub: any;
_init: boolean;
_resizeSub: any;
_init: boolean = false;
_lastEle: boolean;
_hdrFn: Function;
_ftrFn: Function;
@@ -229,6 +230,7 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
scrollTop: 0,
};
_queue: number;
_recordSize: number = 0;
@ContentChild(VirtualItem) _itmTmp: VirtualItem;
@ContentChild(VirtualHeader) _hdrTmp: VirtualHeader;
@@ -376,24 +378,23 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
private _plt: Platform,
private _ctrl: ViewController,
private _config: Config,
private _dom: DomController) {
private _dom: DomController
) {
// hide the virtual scroll element with opacity so we don't
// see jank as it loads up, but we're still able to read
// dimensions because it's still rendered and only opacity hidden
this._renderer.setElementClass(_elementRef.nativeElement, 'virtual-loading', true);
this.setElementClass('virtual-loading', true);
// wait for the content to be rendered and has readable dimensions
_ctrl.readReady.subscribe(() => {
this._init = true;
if (this._hasChanges()) {
this.readUpdate();
if (isPresent(this._changes())) {
this.readUpdate(true);
// wait for the content to be writable
var subscription = _ctrl.writeReady.subscribe(() => {
subscription.unsubscribe();
this.writeUpdate();
this.writeUpdate(true);
});
}
@@ -405,34 +406,55 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
* @private
*/
ngDoCheck() {
if (this._init && this._hasChanges()) {
// only continue if we've already initialized
// and if there actually are changes
this.readUpdate();
this.writeUpdate();
// only continue if we've already initialized
if (!this._init) {
return;
}
// and if there actually are changes
const changes = this._changes();
if (!isPresent(changes)) {
return;
}
let needClean = false;
if (changes) {
changes.forEachOperation((item: any, _: number, cindex: number) => {
if (item.previousIndex != null || (cindex < this._recordSize)) {
needClean = true;
}
});
} else {
needClean = true;
}
this._recordSize = this._records.length;
this.readUpdate(needClean);
this.writeUpdate(needClean);
}
readUpdate(needClean: boolean) {
if (needClean) {
// reset everything
console.debug(`virtual-scroll, readUpdate: slow path`);
this._cells.length = 0;
this._nodes.length = 0;
this._itmTmp.viewContainer.clear();
// ******** DOM READ ****************
this.calcDimensions();
} else {
console.debug(`virtual-scroll, readUpdate: fast path`);
}
}
readUpdate() {
console.debug(`virtual-scroll, readUpdate`);
// reset everything
this._cells.length = 0;
this._nodes.length = 0;
this._itmTmp.viewContainer.clear();
// ******** DOM READ ****************
calcDimensions(this._data, this._elementRef.nativeElement,
this.approxItemWidth, this.approxItemHeight,
this.approxHeaderWidth, this.approxHeaderHeight,
this.approxFooterWidth, this.approxFooterHeight,
this.bufferRatio);
}
writeUpdate() {
writeUpdate(needClean: boolean) {
console.debug(`virtual-scroll, writeUpdate`);
const data = this._data;
const stopAtHeight = (data.scrollTop + data.renderHeight);
data.scrollDiff = SCROLL_DIFFERENCE_MINIMUM + 1;
processRecords(this._data.renderHeight,
processRecords(stopAtHeight,
this._records,
this._cells,
this._hdrFn,
@@ -440,85 +462,116 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
this._data);
// ******** DOM WRITE ****************
this.renderVirtual();
this.renderVirtual(needClean);
}
private _hasChanges() {
return (isPresent(this._records) && isPresent(this._differ) && isPresent(this._differ.diff(this._records)));
private calcDimensions() {
calcDimensions(this._data, this._elementRef.nativeElement,
this.approxItemWidth, this.approxItemHeight,
this.approxHeaderWidth, this.approxHeaderHeight,
this.approxFooterWidth, this.approxFooterHeight,
this.bufferRatio);
}
private _changes() {
if (isPresent(this._records) && isPresent(this._differ)) {
return this._differ.diff(this._records);
}
return null;
}
/**
* @private
* DOM WRITE
*/
renderVirtual() {
renderVirtual(needClean: boolean) {
const nodes = this._nodes;
const cells = this._cells;
const data = this._data;
const records = this._records;
// initialize nodes with the correct cell data
data.topCell = 0;
data.bottomCell = (cells.length - 1);
populateNodeData(0, data.bottomCell,
data.viewWidth, true,
cells, records, nodes,
this._itmTmp.viewContainer,
this._itmTmp.templateRef,
this._hdrTmp && this._hdrTmp.templateRef,
this._ftrTmp && this._ftrTmp.templateRef, true);
// ******** DOM WRITE ****************
this._cd.detectChanges();
// at this point, this fn was called from within another
// requestAnimationFrame, so the next dom reads/writes within the next frame
// wait a frame before trying to read and calculate the dimensions
this._dom.read(() => {
// ******** DOM READ ****************
initReadNodes(this._plt, nodes, cells, data);
});
this._dom.write(() => {
const ele = this._elementRef.nativeElement;
const recordsLength = records.length;
const renderer = this._renderer;
// update the bound context for each node
updateNodeContext(nodes, cells, data);
if (needClean) {
// ******** DOM WRITE ****************
for (var i = 0; i < nodes.length; i++) {
(<any>nodes[i].view).detectChanges();
}
updateDimensions(this._plt, nodes, cells, data, true);
data.topCell = 0;
data.bottomCell = (cells.length - 1);
}
adjustRendered(cells, data);
populateNodeData(data.topCell, data.bottomCell,
data.viewWidth, true,
cells, records, nodes,
this._itmTmp.viewContainer,
this._itmTmp.templateRef,
this._hdrTmp && this._hdrTmp.templateRef,
this._ftrTmp && this._ftrTmp.templateRef, needClean);
if (needClean) {
this._cd.detectChanges();
}
this._plt.raf(() => {
// at this point, this fn was called from within another
// requestAnimationFrame, so the next dom reads/writes within the next frame
// wait a frame before trying to read and calculate the dimensions
this._dom.read(() => {
// ******** DOM READ ****************
initReadNodes(this._plt, nodes, cells, data);
});
this._dom.write(() => {
const ele = this._elementRef.nativeElement;
const recordsLength = records.length;
const renderer = this._renderer;
// update the bound context for each node
updateNodeContext(nodes, cells, data);
if (!this._lastEle) {
// add an element at the end so :last-child css doesn't get messed up
// ******** DOM WRITE ****************
var lastEle: HTMLElement = renderer.createElement(ele, 'div');
lastEle.className = 'virtual-last';
this._lastEle = true;
}
for (var i = 0; i < nodes.length; i++) {
(<any>nodes[i].view).detectChanges();
}
// ******** DOM WRITE ****************
renderer.setElementClass(ele, 'virtual-scroll', true);
if (!this._lastEle) {
// add an element at the end so :last-child css doesn't get messed up
// ******** DOM WRITE ****************
var lastEle: HTMLElement = renderer.createElement(ele, 'div');
lastEle.className = 'virtual-last';
this._lastEle = true;
}
// ******** DOM WRITE ****************
renderer.setElementClass(ele, 'virtual-loading', false);
// ******** DOM WRITE ****************
this.setElementClass('virtual-scroll', true);
// ******** DOM WRITE ****************
writeToNodes(this._plt, nodes, cells, recordsLength);
// ******** DOM WRITE ****************
this.setElementClass('virtual-loading', false);
// ******** DOM WRITE ****************
this._setHeight(
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
);
// ******** DOM WRITE ****************
writeToNodes(this._plt, nodes, cells, recordsLength);
this._content.imgsUpdate();
// ******** DOM WRITE ****************
this._setHeight(
estimateHeight(recordsLength, cells[cells.length - 1], this._vHeight, 0.25)
);
this._content.imgsUpdate();
});
});
}
/**
* @private
*/
resize() {
// only continue if we've already initialized
if (!this._init) {
return;
}
console.debug('virtual-list: resized window');
this.calcDimensions();
this.writeUpdate(false);
}
/**
@@ -655,21 +708,10 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
*/
private _listeners() {
if (!this._scrollSub) {
if (this._config.getBoolean('virtualScrollEventAssist')) {
// use JS scrolling for iOS UIWebView
// goal is to completely remove this when iOS
// fully supports scroll events
// listen to JS scroll events
this._content.enableJsScroll();
}
this._scrollSub = this._content.ionScroll.subscribe((ev: ScrollEvent) => {
this.scrollUpdate(ev);
});
this._scrollEndSub = this._content.ionScrollEnd.subscribe((ev: ScrollEvent) => {
this.scrollEnd(ev);
});
this._resizeSub = this._plt.resize.subscribe(this.resize.bind(this));
this._scrollSub = this._content.ionScroll.subscribe(this.scrollUpdate.bind(this));
this._scrollEndSub = this._content.ionScrollEnd.subscribe(this.scrollEnd.bind(this));
this._content.enableScrollListener();
}
}
@@ -700,14 +742,20 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
}
}
setElementClass(className: string, add: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
}
/**
* @private
*/
ngOnDestroy() {
this._resizeSub && this._resizeSub.unsubscribe();
this._scrollSub && this._scrollSub.unsubscribe();
this._scrollEndSub && this._scrollEndSub.unsubscribe();
this._scrollEndSub = this._scrollSub = null;
this._hdrFn = this._ftrFn = this._records = this._cells = this._nodes = this._data = null;
}
}
const SCROLL_DIFFERENCE_MINIMUM = 40;

View File

@@ -29,7 +29,7 @@ import { isObject, isDefined, isFunction, isArray } from '../util/util';
* modalEnter: 'modal-slide-in',
* modalLeave: 'modal-slide-out',
* tabsPlacement: 'bottom',
* pageTransition: 'ios'
* pageTransition: 'ios-transition'
* }, {}
* )],
* bootstrap: [IonicApp],
@@ -110,7 +110,7 @@ import { isObject, isDefined, isFunction, isArray } from '../util/util';
* | `modalEnter` | `string` | The name of the transition to use while a modal is presented. |
* | `modalLeave` | `string` | The name of the transition to use while a modal is dismiss. |
* | `mode` | `string` | The mode to use throughout the application. |
* | `pageTransition` | `string` | The name of the transition to use while changing pages. |
* | `pageTransition` | `string` | The name of the transition to use while changing pages. Available options: `"ios-transition"`, `"md-transition"`, `"wp-transition"`. |
* | `pickerEnter` | `string` | The name of the transition to use while a picker is presented. |
* | `pickerLeave` | `string` | The name of the transition to use while a picker is dismissed. |
* | `popoverEnter` | `string` | The name of the transition to use while a popover is presented. |

View File

@@ -150,13 +150,13 @@ export function dateValueRange(format: string, min: DateTimeData, max: DateTimeD
return opts;
}
export function dateSortValue(year: number, month: number, day: number): number {
return parseInt(`1${fourDigit(year)}${twoDigit(month)}${twoDigit(day)}`, 10);
export function dateSortValue(year: number, month: number, day: number, hour: number = 0, minute: number = 0): number {
return parseInt(`1${fourDigit(year)}${twoDigit(month)}${twoDigit(day)}${twoDigit(hour)}${twoDigit(minute)}`, 10);
}
export function dateDataSortValue(data: DateTimeData): number {
if (data) {
return dateSortValue(data.year, data.month, data.day);
return dateSortValue(data.year, data.month, data.day, data.hour, data.minute);
}
return -1;
}
@@ -229,7 +229,7 @@ export function parseDate(val: any): DateTimeData {
}
export function updateDate(existingData: DateTimeData, newData: any) {
export function updateDate(existingData: DateTimeData, newData: any): boolean {
if (isPresent(newData) && newData !== '') {
if (isString(newData)) {
@@ -239,7 +239,7 @@ export function updateDate(existingData: DateTimeData, newData: any) {
if (newData) {
// successfully parsed the ISO string to our DateTimeData
Object.assign(existingData, newData);
return;
return true;
}
} else if ((isPresent(newData.year) || isPresent(newData.hour) || isPresent(newData.month) || isPresent(newData.day) || isPresent(newData.minute) || isPresent(newData.second))) {
@@ -262,7 +262,7 @@ export function updateDate(existingData: DateTimeData, newData: any) {
(<any>existingData)[k] = newData[k].value;
}
return;
return true;
}
// eww, invalid data
@@ -274,6 +274,7 @@ export function updateDate(existingData: DateTimeData, newData: any) {
delete (<any>existingData)[k];
}
}
return false;
}

View File

@@ -134,22 +134,23 @@ export function setupEvents(plt: Platform, dom: DomController): Events {
let el = <HTMLElement>doc.elementFromPoint(plt.width() / 2, plt.height() / 2);
if (!el) { return; }
let contentEle = <HTMLElement>el.closest('.scroll-content');
let contentEle = <any>el.closest('.scroll-content');
if (contentEle) {
var scroll = new ScrollView(plt, dom);
var style = contentEle.style;
var scroll = new ScrollView(plt, dom, false);
scroll.init(contentEle, 0, 0);
// We need to stop scrolling if it's happening and scroll up
(<any>contentEle.style)['WebkitBackfaceVisibility'] = 'hidden';
(<any>contentEle.style)['WebkitTransform'] = 'translate3d(0,0,0)';
style['WebkitBackfaceVisibility'] = 'hidden';
style['WebkitTransform'] = 'translate3d(0,0,0)';
dom.write(function() {
contentEle.style.overflow = 'hidden';
style.overflow = 'hidden';
function finish() {
contentEle.style.overflow = '';
(<any>contentEle.style)['WebkitBackfaceVisibility'] = '';
(<any>contentEle.style)['WebkitTransform'] = '';
style.overflow = '';
style['WebkitBackfaceVisibility'] = '';
style['WebkitTransform'] = '';
}
let didScrollTimeout = plt.timeout(() => {

View File

@@ -229,7 +229,10 @@ export function mockChangeDetectorRef(): ChangeDetectorRef {
}
export class MockElementRef implements ElementRef {
nativeElement: any = new MockElement();
nativeElement: any;
constructor(ele: any) {
this.nativeElement = ele;
}
}
export class MockElement {
@@ -299,7 +302,11 @@ export class ClassList {
}
export function mockElementRef(): ElementRef {
return new MockElementRef();
return new MockElementRef(new MockElement());
}
export function mockElementRefEle(ele: any): ElementRef {
return new MockElementRef(ele);
}
export class MockRenderer {

View File

@@ -1,4 +1,3 @@
import { Subject } from 'rxjs/Subject';
import { assert } from './util';
import { DomController, DomCallback } from '../platform/dom-controller';
@@ -9,10 +8,13 @@ import { pointerCoord } from './dom';
export class ScrollView {
ev: ScrollEvent;
isScrolling = false;
scrollStart = new Subject<ScrollEvent>();
scroll = new Subject<ScrollEvent>();
scrollEnd = new Subject<ScrollEvent>();
initialized: boolean;
onScrollStart: (ev: ScrollEvent) => void;
onScroll: (ev: ScrollEvent) => void;
onScrollEnd: (ev: ScrollEvent) => void;
initialized: boolean = false;
enabled: boolean = false;
contentTop: number;
contentBottom: number;
private _el: HTMLElement;
private _js: boolean;
@@ -22,7 +24,12 @@ export class ScrollView {
private _endTmr: Function;
constructor(private _plt: Platform, private _dom: DomController) {
constructor(
private _plt: Platform,
private _dom: DomController,
virtualScrollEventAssist: boolean
) {
this._js = virtualScrollEventAssist;
this.ev = {
timeStamp: 0,
scrollTop: 0,
@@ -41,27 +48,46 @@ export class ScrollView {
velocityX: 0,
directionY: 'down',
directionX: null,
domWrite: function(fn: DomCallback, ctx?: any): void {
_dom.write(fn, ctx);
}
domWrite: _dom.write.bind(_dom)
};
}
init(ele: HTMLElement, contentTop: number, contentBottom: number) {
assert(ele, 'scroll-view, element can not be null');
this._el = ele;
this.contentTop = contentTop;
this.contentBottom = contentBottom;
if (!this.initialized) {
this.initialized = true;
assert(ele, 'scroll-view, element can not be null');
this._el = ele;
if (this._js) {
this.enableJsScroll(contentTop, contentBottom);
} else {
this.enableNativeScrolling();
if (this.enabled) {
this.enable();
}
}
}
setEnabled() {
if (!this.enabled) {
this.enabled = true;
if (this.initialized) {
this.enable();
}
}
}
enable() {
assert(this.initialized, 'scroll must be initialized');
assert(this.enabled, 'scroll-view must be enabled');
assert(this._el, 'scroll-view, element can not be null');
if (this._js) {
this.enableJsScroll();
} else {
this.enableNativeScrolling();
}
}
private enableNativeScrolling() {
this._js = false;
if (!this._el) {
@@ -76,6 +102,10 @@ export class ScrollView {
function scrollCallback(scrollEvent: UIEvent) {
ev.timeStamp = scrollEvent.timeStamp;
// Event.timeStamp is 0 in firefox
if (!ev.timeStamp) {
ev.timeStamp = Date.now();
}
// get the current scrollTop
// ******** DOM READ ****************
@@ -99,7 +129,7 @@ export class ScrollView {
positions.length = 0;
// emit only on the first scroll event
self.scrollStart.next(ev);
self.onScrollStart(ev);
}
// actively scrolling
@@ -143,13 +173,13 @@ export class ScrollView {
ev.velocityY = ev.velocityX = 0;
// emit that the scroll has ended
self.scrollEnd && self.scrollEnd.next(ev);
self.onScrollEnd(ev);
self._endTmr = null;
}
// emit on each scroll event
self.scroll.next(ev);
self.onScroll(ev);
// debounce for a moment after the last scroll event
self._dom.cancel(self._endTmr);
@@ -177,7 +207,7 @@ export class ScrollView {
* inertia then this can be burned to the ground. iOS's more modern
* WKWebView does not have this issue, only UIWebView does.
*/
enableJsScroll(contentTop: number, contentBottom: number) {
enableJsScroll() {
const self = this;
self._js = true;
const ele = self._el;
@@ -196,7 +226,7 @@ export class ScrollView {
function setMax() {
if (!max) {
// ******** DOM READ ****************
max = ele.scrollHeight - ele.parentElement.offsetHeight + contentTop + contentBottom;
max = ele.scrollHeight - ele.parentElement.offsetHeight + self.contentTop + self.contentBottom;
}
};
@@ -217,7 +247,7 @@ export class ScrollView {
ev.scrollTop = self._t;
// emit on each scroll event
self.scroll.next(ev);
self.onScroll(ev);
self._dom.write(() => {
// ******** DOM WRITE ****************
@@ -236,7 +266,7 @@ export class ScrollView {
ev.velocityY = ev.velocityX = 0;
// emit that the scroll has ended
self.scrollEnd && self.scrollEnd.next(ev);
self.onScrollEnd(ev);
}
});
}
@@ -275,7 +305,7 @@ export class ScrollView {
self.isScrolling = true;
// emit only on the first scroll event
self.scrollStart.next(ev);
self.onScrollStart(ev);
}
self._dom.write(() => {
@@ -291,7 +321,7 @@ export class ScrollView {
if (!positions.length && self.isScrolling) {
self.isScrolling = false;
ev.velocityY = ev.velocityX = 0;
self.scrollEnd && self.scrollEnd.next(ev);
self.onScrollEnd(ev);
return;
}
@@ -329,7 +359,7 @@ export class ScrollView {
} else {
self.isScrolling = false;
ev.velocityY = 0;
self.scrollEnd && self.scrollEnd.next(ev);
self.onScrollEnd(ev);
}
positions.length = 0;
@@ -516,9 +546,7 @@ export class ScrollView {
this._endTmr && this._dom.cancel(this._endTmr);
this._lsn && this._lsn();
this.scrollStart && this.scrollStart.unsubscribe();
this.scroll && this.scroll.unsubscribe();
this.scrollEnd && this.scrollEnd.unsubscribe();
this.onScrollStart = this.onScroll = this.onScrollEnd = null;
let ev = this.ev;
ev.domWrite = ev.contentElement = ev.fixedElement = ev.scrollElement = ev.headerElement = null;

View File

@@ -63,25 +63,25 @@ export function defaults(dest: any, ...args: any[]) {
/** @private */
export function isBoolean(val: any) { return typeof val === 'boolean'; }
export function isBoolean(val: any): val is boolean { return typeof val === 'boolean'; }
/** @private */
export function isString(val: any) { return typeof val === 'string'; }
export function isString(val: any): val is string { return typeof val === 'string'; }
/** @private */
export function isNumber(val: any) { return typeof val === 'number'; }
export function isNumber(val: any): val is number { return typeof val === 'number'; }
/** @private */
export function isFunction(val: any) { return typeof val === 'function'; }
export function isFunction(val: any): val is Function { return typeof val === 'function'; }
/** @private */
export function isDefined(val: any) { return typeof val !== 'undefined'; }
export function isDefined(val: any): boolean { return typeof val !== 'undefined'; }
/** @private */
export function isUndefined(val: any) { return typeof val === 'undefined'; }
export function isUndefined(val: any): val is undefined { return typeof val === 'undefined'; }
/** @private */
export function isPresent(val: any) { return val !== undefined && val !== null; }
export function isPresent(val: any): val is any { return val !== undefined && val !== null; }
/** @private */
export function isBlank(val: any) { return val === undefined || val === null; }
export function isBlank(val: any): val is null { return val === undefined || val === null; }
/** @private */
export function isObject(val: any) { return typeof val === 'object'; }
export function isObject(val: any): val is Object { return typeof val === 'object'; }
/** @private */
export function isArray(val: any) { return Array.isArray(val); };
export function isArray(val: any): val is any[] { return Array.isArray(val); };
/** @private */