mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56364a0503 | ||
|
|
7e9bad5092 | ||
|
|
963cdcbe76 | ||
|
|
c79bd5ab81 | ||
|
|
7cc617e84e | ||
|
|
74191c3e92 | ||
|
|
afd99baba0 | ||
|
|
e191321193 | ||
|
|
c236a4bf4b | ||
|
|
19302a9fbf | ||
|
|
cff4c01bf0 | ||
|
|
769e7dc2aa | ||
|
|
eff420f4c7 | ||
|
|
53feb3f699 | ||
|
|
999efaca9a | ||
|
|
1222c56d23 | ||
|
|
05f1e24cb6 | ||
|
|
505ee58407 | ||
|
|
1efc4aea0f | ||
|
|
8f310eba4a | ||
|
|
46fe1ff53c | ||
|
|
aa287ce56f | ||
|
|
e53bad1bc3 | ||
|
|
6918275bd3 | ||
|
|
f9f9a1b441 | ||
|
|
7a9f88fcda | ||
|
|
8c483f2529 | ||
|
|
446e468b59 | ||
|
|
67cbcdea3b | ||
|
|
ba3530657b | ||
|
|
f4c9ba6614 | ||
|
|
62bf2bee28 | ||
|
|
5b1126f7f2 | ||
|
|
84e25a17c2 | ||
|
|
30980b6798 | ||
|
|
e90d692b1f | ||
|
|
bee75f7d00 | ||
|
|
d538245178 | ||
|
|
b541832749 | ||
|
|
05859dba40 | ||
|
|
58beea30f5 | ||
|
|
1d68d1f91f | ||
|
|
0cd87f1078 | ||
|
|
b53219a67c | ||
|
|
28754926b5 | ||
|
|
d666e8b8e5 | ||
|
|
fd53e04a47 | ||
|
|
51c398d614 |
68
CHANGELOG.md
68
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -184,6 +184,7 @@ export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, Co
|
||||
fn(isChecked);
|
||||
this._setChecked(isChecked);
|
||||
this.onTouched();
|
||||
this._cd.detectChanges();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {}
|
||||
40
src/components/content/test/scroll-down-on-load/main.html
Normal file
40
src/components/content/test/scroll-down-on-load/main.html
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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(){};
|
||||
|
||||
44
src/components/datetime/test/issues/app.module.ts
Normal file
44
src/components/datetime/test/issues/app.module.ts
Normal 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 {}
|
||||
108
src/components/datetime/test/issues/main.html
Normal file
108
src/components/datetime/test/issues/main.html
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
}
|
||||
31
src/components/infinite-scroll/test/position-top/main.html
Normal file
31
src/components/infinite-scroll/test/position-top/main.html
Normal 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>
|
||||
@@ -65,7 +65,3 @@ ion-label[floating] {
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.item-select ion-label[floating] {
|
||||
transform: translate3d(0, 0, 0) scale(.8);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
});
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
|
||||
46
src/components/slides/test/rtl/app.module.ts
Normal file
46
src/components/slides/test/rtl/app.module.ts
Normal 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 { }
|
||||
21
src/components/slides/test/rtl/main.html
Normal file
21
src/components/slides/test/rtl/main.html
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -72,3 +72,7 @@ ion-split-pane {
|
||||
display: hidden !important;
|
||||
}
|
||||
}
|
||||
|
||||
.split-pane-visible >.split-pane-side[side=right] {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -494,7 +494,7 @@ export class Tabs extends Ion implements AfterViewInit, RootNode {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getActiveChildNav() {
|
||||
getActiveChildNav(): Tab {
|
||||
return this.getSelected();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {}
|
||||
32
src/components/virtual-scroll/test/infinite-scroll/main.html
Normal file
32
src/components/virtual-scroll/test/infinite-scroll/main.html
Normal 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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user