mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b92a5a8c2 | ||
|
|
59f9737d9b | ||
|
|
0967b63a51 | ||
|
|
db37072c40 | ||
|
|
9316f73b81 | ||
|
|
55dfd254e5 | ||
|
|
0a97cf2d5f | ||
|
|
94a33a74d2 | ||
|
|
b4c6cea760 | ||
|
|
0b440edc51 | ||
|
|
156b982510 | ||
|
|
84e84d3280 | ||
|
|
8eef99d82f | ||
|
|
404d977c81 | ||
|
|
515de26a8b | ||
|
|
c9851442c6 | ||
|
|
47e1cdce9d | ||
|
|
6360d41f6a | ||
|
|
c19615ed11 | ||
|
|
b5c7ab2e98 | ||
|
|
3d569eb88a | ||
|
|
9be5751eeb | ||
|
|
9a4d81b329 |
140
CHANGELOG.md
140
CHANGELOG.md
@@ -1,3 +1,143 @@
|
||||
<a name="3.0.0"></a>
|
||||
# [3.0.0](https://github.com/driftyco/ionic/compare/v2.3.0...v3.0.0) (2017-04-05)
|
||||
|
||||
|
||||
### Steps to Upgrade
|
||||
|
||||
With this release comes a major update to Angular (Angular 4.0!), the latest version of TypeScript, and some optional structural changes to your application.
|
||||
|
||||
1. Update your package.json to match the following dependencies, remove the existing `node_modules` directory, and then run `npm install`:
|
||||
|
||||
```
|
||||
"dependencies": {
|
||||
"@angular/common": "4.0.0",
|
||||
"@angular/compiler": "4.0.0",
|
||||
"@angular/compiler-cli": "4.0.0",
|
||||
"@angular/core": "4.0.0",
|
||||
"@angular/forms": "4.0.0",
|
||||
"@angular/http": "4.0.0",
|
||||
"@angular/platform-browser": "4.0.0",
|
||||
"@angular/platform-browser-dynamic": "4.0.0",
|
||||
"@ionic-native/core": "3.4.2",
|
||||
"@ionic-native/splash-screen": "3.4.2",
|
||||
"@ionic-native/status-bar": "3.4.2",
|
||||
"@ionic/storage": "2.0.1",
|
||||
"ionic-angular": "3.0.0",
|
||||
"ionicons": "3.0.0",
|
||||
"rxjs": "5.1.1",
|
||||
"sw-toolbox": "3.4.0",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ionic/app-scripts": "1.3.0",
|
||||
"typescript": "~2.2.1"
|
||||
}
|
||||
```
|
||||
|
||||
2. Import the `BrowserModule` in your `app/app.module.ts` file:
|
||||
|
||||
```
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
```
|
||||
|
||||
and then add it to the imports in the same file:
|
||||
|
||||
```
|
||||
imports: [
|
||||
BrowserModule,
|
||||
IonicModule.forRoot(MyApp)
|
||||
],
|
||||
```
|
||||
|
||||
3. If you are using Http, Import the `HttpModule` in your `app/app.module.ts` file:
|
||||
|
||||
```
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpModule,
|
||||
IonicModule.forRoot(MyApp)
|
||||
],
|
||||
```
|
||||
|
||||
4. Upgrading to Ionic Native 3.x will result in a smaller bundle size. With Ionic Native 3, native functionality was moved from static methods to using Angular injectables.
|
||||
- Blog Post: http://blog.ionic.io/ionic-native-3-x/
|
||||
- Example Upgrade Commit: https://github.com/driftyco/ionic-conference-app/commit/62088
|
||||
|
||||
And that’s it! Your app should still function the same without any issues.
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **alert:** add missing cssClass property for buttons ([4fbcda7](https://github.com/driftyco/ionic/commit/4fbcda7))
|
||||
* **content:** remove scroll bouncing from desktop ([7cee7b0](https://github.com/driftyco/ionic/commit/7cee7b0))
|
||||
* **content:** enable scroll listener automatically ([d9a7652](https://github.com/driftyco/ionic/commit/d9a7652)), closes [#10938](https://github.com/driftyco/ionic/issues/10938)
|
||||
* **datetime:** emit ionCancel event on backdrop click ([#10532](https://github.com/driftyco/ionic/issues/10532)) ([4ac8ffb](https://github.com/driftyco/ionic/commit/4ac8ffb))
|
||||
* **datetime:** remove unnecessary calls and performance improvements ([99142f8](https://github.com/driftyco/ionic/commit/99142f8))
|
||||
* **infinite-scroll:** don't call `complete` if the state isn't loading ([58b57c0](https://github.com/driftyco/ionic/commit/58b57c0))
|
||||
* **item-sliding:** add forwardRef for item options ([0ccd96e](https://github.com/driftyco/ionic/commit/0ccd96e))
|
||||
* **navigation:** update the URL if there are no children navs ([8586cc1](https://github.com/driftyco/ionic/commit/8586cc1))
|
||||
* **slides:** negative number indicates position starting at end ([#10997](https://github.com/driftyco/ionic/issues/10997)) ([33be36d](https://github.com/driftyco/ionic/commit/33be36d))
|
||||
* **split-pane:** add RTL support ([57f3f97](https://github.com/driftyco/ionic/commit/57f3f97)), closes [#10903](https://github.com/driftyco/ionic/issues/10903)
|
||||
* **virtual-list:** fix the rendering of the first item ([ccb49f3](https://github.com/driftyco/ionic/commit/ccb49f3))
|
||||
* **virtual-list:** update compatibility with angular ([682ce49](https://github.com/driftyco/ionic/commit/682ce49))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **grid:** remove the old grid system ([19c36de](https://github.com/driftyco/ionic/commit/19c36de))
|
||||
* **typography:** remove the native element selectors ([2827275](https://github.com/driftyco/ionic/commit/2827275))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **module-loader:** add caching for ngModuleLoader on load ([17359b7](https://github.com/driftyco/ionic/commit/17359b7))
|
||||
* **module-loader:** add preloadModules config option, set to false ([bdbd521](https://github.com/driftyco/ionic/commit/bdbd521))
|
||||
* **module-loader:** preload modules based on priority ([5a4f8b9](https://github.com/driftyco/ionic/commit/5a4f8b9))
|
||||
* **module-loader:** run outside of angular and in requestIdleCallback ([c7ad3ce](https://github.com/driftyco/ionic/commit/c7ad3ce))
|
||||
* **navigation:** [@DeepLink](https://github.com/DeepLink) decorator ([3e70856](https://github.com/driftyco/ionic/commit/3e70856))
|
||||
* **navigation:** add optional priority to deep link config ([b47848c](https://github.com/driftyco/ionic/commit/b47848c))
|
||||
* **split-pane:** adds enabled input ([fa7ea0c](https://github.com/driftyco/ionic/commit/fa7ea0c)), closes [#10949](https://github.com/driftyco/ionic/issues/10949)
|
||||
* **split-pane:** sizing via scss variables ([979ca63](https://github.com/driftyco/ionic/commit/979ca63)), closes [#10893](https://github.com/driftyco/ionic/issues/10893)
|
||||
* **util:** system.js ng-module loader ([beabe32](https://github.com/driftyco/ionic/commit/beabe32))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
#### Grid
|
||||
|
||||
Removed the deprecated (old) grid. See the blog post for more information including steps to migrate:
|
||||
http://blog.ionic.io/build-awesome-desktop-apps-with-ionics-new-responsive-grid/
|
||||
|
||||
#### Typography
|
||||
|
||||
The following selectors to style the text color of the native text elements have been removed:
|
||||
|
||||
```
|
||||
h1[color], h2[color], h3[color], h4[color], h5[color], h6[color], a[color]:not([ion-button]):not([ion-item]):not([ion-fab]), p[color], span[color], b[color], i[color], strong[color], em[color], small[color], sub[color], sup[color]
|
||||
```
|
||||
|
||||
These have been throwing a deprecation warning since rc.3 but still working. They are officially gone and therefore these elements will not get the color unless the `ion-text` attribute is added. Please see the documentation for more information: http://ionicframework.com/docs/api/components/typography/Typography/
|
||||
|
||||
#### Slides
|
||||
|
||||
The following properties and functions have been printing console warnings and are officially removed:
|
||||
|
||||
Slides input `options` has been removed. Please use the input
|
||||
properties instead.
|
||||
|
||||
Slide event `ionWillChange` has been removed, please use
|
||||
`ionSlideWillChange` instead.
|
||||
|
||||
Slide event `ionDidChange` has been removed, please use
|
||||
`ionSlideDidChange` instead.
|
||||
|
||||
Slide event `ionDrag` has been removed, please use `ionSlideDrag`
|
||||
instead.
|
||||
|
||||
Slides `getSlider()` method has been removed, please use the instance
|
||||
of ion-slides.
|
||||
|
||||
|
||||
<a name="2.3.0"></a>
|
||||
# [2.3.0](https://github.com/driftyco/ionic/compare/v2.2.0...v2.3.0) (2017-03-22)
|
||||
|
||||
|
||||
@@ -58,13 +58,13 @@ export class PageOne {
|
||||
buttons: [
|
||||
{
|
||||
text: 'Cancel',
|
||||
handler: (data: any) => {
|
||||
handler: (data) => {
|
||||
console.log('Cancel clicked');
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Save',
|
||||
handler: (data: any) => {
|
||||
handler: (data) => {
|
||||
console.log('Saved clicked');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<ion-item>
|
||||
<ion-label stacked color="primary">Mobile</ion-label>
|
||||
<ion-input type="tel" value="+1 (555) 123-1234"></ion-input>
|
||||
<ion-icon color="primary" item-right ios="ios-chatbubbles-outline" md="md-chatbubbles" class="mobile1"></ion-icon>
|
||||
<ion-icon color="primary" item-right ios="ios-call-outline" md="md-call" class="mobile2"></ion-icon>
|
||||
<ion-icon color="primary" item-right ios="ios-chatbubbles-outline" md="md-chatbubbles"></ion-icon>
|
||||
<ion-icon color="primary" item-right ios="ios-call-outline" md="md-call"></ion-icon>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
@@ -61,15 +61,3 @@ United States">
|
||||
</ion-list>
|
||||
|
||||
</ion-content>
|
||||
|
||||
<style>
|
||||
.label-demo .mobile1, .label-demo .mobile2 {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.label-demo .mobile1 {
|
||||
right: 35px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@ export class PageOne {
|
||||
|
||||
filterItems(ev: any) {
|
||||
this.setItems();
|
||||
let val = ev.target.value;
|
||||
let val = ev.value;
|
||||
|
||||
if (val && val.trim() !== '') {
|
||||
this.items = this.items.filter(function(item) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "ionic2",
|
||||
"version": "2.3.0",
|
||||
"version": "3.0.0",
|
||||
"description": "A powerful framework for building mobile and progressive web apps with JavaScript and Angular 2",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -144,4 +144,4 @@
|
||||
"pre-push#master": [
|
||||
"test"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ function copyDemoPolyfills(outputDir: string) {
|
||||
function copyDemoContent(outputDir: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = src([
|
||||
`${DIST_DEMOS_ROOT}/**/*`
|
||||
`${DIST_DEMOS_ROOT}/src/**/*`
|
||||
]).pipe(dest(outputDir));
|
||||
stream.on('end', () => {
|
||||
resolve();
|
||||
|
||||
@@ -28,5 +28,5 @@ export interface AlertButton {
|
||||
text?: string;
|
||||
role?: string;
|
||||
cssClass?: string;
|
||||
handler?: Function;
|
||||
handler?: (value: any) => boolean|void;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AfterContentInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { Config } from '../../config/config';
|
||||
import { Form, IonicTapInput } from '../../util/form';
|
||||
import { Ion } from '../ion';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
import { Form, IonicTapInput } from '../../util/form';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { Item } from '../item/item';
|
||||
|
||||
export const CHECKBOX_VALUE_ACCESSOR: any = {
|
||||
@@ -54,14 +54,14 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
|
||||
@Component({
|
||||
selector: 'ion-checkbox',
|
||||
template:
|
||||
'<div class="checkbox-icon" [class.checkbox-checked]="_checked">' +
|
||||
'<div class="checkbox-icon" [class.checkbox-checked]="_value">' +
|
||||
'<div class="checkbox-inner"></div>' +
|
||||
'</div>' +
|
||||
'<button role="checkbox" ' +
|
||||
'type="button" ' +
|
||||
'ion-button="item-cover" ' +
|
||||
'[id]="id" ' +
|
||||
'[attr.aria-checked]="_checked" ' +
|
||||
'[attr.aria-checked]="_value" ' +
|
||||
'[attr.aria-labelledby]="_labelId" ' +
|
||||
'[attr.aria-disabled]="_disabled" ' +
|
||||
'class="item-cover"> ' +
|
||||
@@ -72,129 +72,29 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
|
||||
providers: [CHECKBOX_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
/** @hidden */
|
||||
_checked: boolean = false;
|
||||
/** @hidden */
|
||||
_init: boolean;
|
||||
/** @hidden */
|
||||
_disabled: boolean = false;
|
||||
/** @hidden */
|
||||
_labelId: string;
|
||||
/** @hidden */
|
||||
_fn: Function;
|
||||
/** @hidden */
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @output {Checkbox} Emitted when the checkbox value changes.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<Checkbox> = new EventEmitter<Checkbox>();
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
private _form: Form,
|
||||
@Optional() private _item: Item,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super(config, elementRef, renderer, 'checkbox');
|
||||
|
||||
_form.register(this);
|
||||
|
||||
if (_item) {
|
||||
this.id = 'chk-' + _item.registerInput('checkbox');
|
||||
this._labelId = 'lbl-' + _item.id;
|
||||
this._item.setElementClass('item-checkbox', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
_click(ev: UIEvent) {
|
||||
console.debug('checkbox, checked');
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.onChange(!this._checked);
|
||||
}
|
||||
export class Checkbox extends BaseInput<boolean> implements IonicTapInput, AfterViewInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the element is selected.
|
||||
*/
|
||||
@Input()
|
||||
get checked(): boolean {
|
||||
return this._checked;
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set checked(val: boolean) {
|
||||
this._setChecked(isTrueProperty(val));
|
||||
this.onChange(this._checked);
|
||||
this.value = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_setChecked(isChecked: boolean) {
|
||||
if (isChecked !== this._checked) {
|
||||
this._checked = isChecked;
|
||||
if (this._init) {
|
||||
this.ionChange.emit(this);
|
||||
}
|
||||
this._item && this._item.setElementClass('item-checkbox-checked', isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
this._setChecked(isTrueProperty(val));
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function): void {
|
||||
this._fn = fn;
|
||||
this.onChange = (isChecked: boolean) => {
|
||||
console.debug('checkbox, onChange', isChecked);
|
||||
fn(isChecked);
|
||||
this._setChecked(isChecked);
|
||||
this.onTouched();
|
||||
this._cd.detectChanges();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
this._item && this._item.setElementClass('item-checkbox-disabled', this._disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange(isChecked: boolean) {
|
||||
// used when this input does not have an ngModel or formControlName
|
||||
console.debug('checkbox, onChange (no ngModel)', isChecked);
|
||||
this._setChecked(isChecked);
|
||||
this.onTouched();
|
||||
this._cd.detectChanges();
|
||||
constructor(
|
||||
config: Config,
|
||||
form: Form,
|
||||
@Optional() item: Item,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super(config, elementRef, renderer, 'checkbox', false, form, item, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,26 +107,25 @@ export class Checkbox extends Ion implements IonicTapInput, AfterContentInit, Co
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched() { }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
@HostListener('click', ['$event'])
|
||||
_click(ev: UIEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.value = !this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
_inputNormalize(val: any): boolean {
|
||||
return isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
_inputCheckHasValue(val: boolean) {
|
||||
this._item && this._item.setElementClass('item-checkbox-checked', val);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
24
src/components/checkbox/test/checkbox.spec.ts
Normal file
24
src/components/checkbox/test/checkbox.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
import { Checkbox } from '../checkbox';
|
||||
import { mockConfig, mockElementRef, mockRenderer, mockItem, mockChangeDetectorRef } from '../../../util/mock-providers';
|
||||
import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester';
|
||||
|
||||
describe('Checkbox', () => {
|
||||
|
||||
it('should pass common test', () => {
|
||||
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
const item: any = mockItem();
|
||||
const cd = mockChangeDetectorRef();
|
||||
const checkbox = new Checkbox(config, null, item, elementRef, renderer, cd);
|
||||
|
||||
commonInputTest(checkbox, {
|
||||
defaultValue: false,
|
||||
corpus: BOOLEAN_CORPUS
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -349,7 +349,7 @@ export class Content extends Ion implements OnDestroy, AfterViewInit {
|
||||
) {
|
||||
super(config, elementRef, renderer, 'content');
|
||||
|
||||
let enableScrollListener = this.enableScrollListener.bind(this);
|
||||
const enableScrollListener = () => this._scroll.enableEvents();
|
||||
this.ionScroll.onSubscribe = enableScrollListener;
|
||||
this.ionScrollStart.onSubscribe = enableScrollListener;
|
||||
this.ionScrollEnd.onSubscribe = enableScrollListener;
|
||||
@@ -359,12 +359,7 @@ export class Content extends Ion implements OnDestroy, AfterViewInit {
|
||||
this._imgRndBfr = config.getNumber('imgRenderBuffer', 400);
|
||||
this._imgVelMax = config.getNumber('imgVelocityMax', 3);
|
||||
|
||||
// use JS scrolling for iOS UIWebView
|
||||
// goal is to completely remove this when iOS
|
||||
// fully supports scroll events
|
||||
// listen to JS scroll events
|
||||
const jsScroll = config.getBoolean('virtualScrollEventAssist');
|
||||
this._scroll = new ScrollView(_app, _plt, _dom, jsScroll);
|
||||
this._scroll = new ScrollView(_app, _plt, _dom);
|
||||
|
||||
while (navCtrl) {
|
||||
if (isTabs(<any>navCtrl)) {
|
||||
@@ -431,8 +426,8 @@ export class Content extends Ion implements OnDestroy, AfterViewInit {
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
enableScrollListener() {
|
||||
this._scroll.eventsEnabled = true;
|
||||
enableJsScroll() {
|
||||
this._scroll.enableJsScroll(this._cTop, this._cBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,10 @@ import { Picker } from '../picker/picker';
|
||||
import { PickerController } from '../picker/picker-controller';
|
||||
import { PickerColumn } from '../picker/picker-options';
|
||||
import { Form } from '../../util/form';
|
||||
import { Ion } from '../ion';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { Item } from '../item/item';
|
||||
import { deepCopy, isBlank, isPresent, isTrueProperty, isArray, isString, assert, clamp } from '../../util/util';
|
||||
import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, convertDataToISO, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
|
||||
import { deepCopy, isBlank, isPresent, isArray, isString, assert, clamp } from '../../util/util';
|
||||
import { dateValueRange, renderDateTime, renderTextFormat, convertFormatToKey, getValueFromFormat, parseTemplate, parseDate, updateDate, DateTimeData, daysInMonth, dateSortValue, dateDataSortValue, LocaleData } from '../../util/datetime-util';
|
||||
|
||||
export const DATETIME_VALUE_ACCESSOR: any = {
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
@@ -273,23 +273,14 @@ export const DATETIME_VALUE_ACCESSOR: any = {
|
||||
providers: [DATETIME_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class DateTime extends Ion implements AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
_disabled: any = false;
|
||||
_labelId: string;
|
||||
export class DateTime extends BaseInput<DateTimeData> implements AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
|
||||
_text: string = '';
|
||||
_fn: Function;
|
||||
_isOpen: boolean = false;
|
||||
_min: DateTimeData;
|
||||
_max: DateTimeData;
|
||||
_value: DateTimeData = {};
|
||||
_locale: LocaleData = {};
|
||||
_picker: Picker;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @input {string} The minimum datetime allowed. Value must be a date string
|
||||
* following the
|
||||
@@ -421,39 +412,63 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
*/
|
||||
@Input() placeholder: string = '';
|
||||
|
||||
/**
|
||||
* @output {any} Emitted when the datetime selection has changed.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @output {any} Emitted when the datetime selection was cancelled.
|
||||
*/
|
||||
@Output() ionCancel: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
private _form: Form,
|
||||
form: Form,
|
||||
config: Config,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
@Optional() private _item: Item,
|
||||
@Optional() item: Item,
|
||||
@Optional() private _pickerCtrl: PickerController
|
||||
) {
|
||||
super(config, elementRef, renderer, 'datetime');
|
||||
super(config, elementRef, renderer, 'datetime', {}, form, item, null);
|
||||
}
|
||||
|
||||
_form.register(this);
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
// first see if locale names were provided in the inputs
|
||||
// then check to see if they're in the config
|
||||
// if neither were provided then it will use default English names
|
||||
['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => {
|
||||
(<any>this)._locale[type] = convertToArrayOfStrings(isPresent((<any>this)[type]) ? (<any>this)[type] : this._config.get(type), type);
|
||||
});
|
||||
|
||||
if (_item) {
|
||||
this.id = 'dt-' + _item.registerInput('datetime');
|
||||
this._labelId = 'lbl-' + _item.id;
|
||||
this._item.setElementClass('item-datetime', true);
|
||||
}
|
||||
// update how the datetime value is displayed as formatted text
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputUpdated() {
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputNormalize(val: any): DateTimeData {
|
||||
updateDate(this._value, val);
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputShouldChange(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
_click(ev: UIEvent) {
|
||||
// do not continue if the click event came from a form submit
|
||||
if (ev.detail === 0) {
|
||||
// do not continue if the click event came from a form submit
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
@@ -463,17 +478,14 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
|
||||
@HostListener('keyup.space')
|
||||
_keyup() {
|
||||
if (!this._isOpen) {
|
||||
this.open();
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
open() {
|
||||
assert(!this._isOpen, 'datetime is already open');
|
||||
if (this._disabled) {
|
||||
if (this.isFocus() || this._disabled) {
|
||||
return;
|
||||
}
|
||||
console.debug('datetime, open picker');
|
||||
@@ -481,35 +493,33 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
// the user may have assigned some options specifically for the alert
|
||||
const pickerOptions = deepCopy(this.pickerOptions);
|
||||
|
||||
// Configure picker under the hood
|
||||
const picker = this._picker = this._pickerCtrl.create(pickerOptions);
|
||||
picker.addButton({
|
||||
text: this.cancelText,
|
||||
role: 'cancel',
|
||||
handler: () => this.ionCancel.emit(null)
|
||||
handler: () => this.ionCancel.emit(this)
|
||||
});
|
||||
picker.addButton({
|
||||
text: this.doneText,
|
||||
handler: (data: any) => {
|
||||
console.debug('datetime, done', data);
|
||||
this.onChange(data);
|
||||
this.ionChange.emit(data);
|
||||
}
|
||||
handler: (data: any) => this.value = data,
|
||||
});
|
||||
|
||||
this.generate();
|
||||
this.validate();
|
||||
|
||||
picker.ionChange.subscribe(() => {
|
||||
this.validate();
|
||||
picker.refresh();
|
||||
});
|
||||
|
||||
this._isOpen = true;
|
||||
picker.onDidDismiss(() => {
|
||||
this._isOpen = false;
|
||||
});
|
||||
// Update picker status before presenting
|
||||
this.generate();
|
||||
this.validate();
|
||||
|
||||
// Present picker
|
||||
this._fireFocus();
|
||||
picker.present(pickerOptions);
|
||||
picker.onDidDismiss(() => {
|
||||
this._fireBlur();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -566,7 +576,7 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
|
||||
// cool, we've loaded up the columns with options
|
||||
// preselect the option for this column
|
||||
const optValue = getValueFromFormat(this._value, format);
|
||||
const optValue = getValueFromFormat(this.getValue(), format);
|
||||
const selectedIndex = column.options.findIndex(opt => opt.value === optValue);
|
||||
if (selectedIndex >= 0) {
|
||||
// set the select index for this column's options
|
||||
@@ -729,8 +739,10 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setValue(newData: any) {
|
||||
updateDate(this._value, newData);
|
||||
updateText() {
|
||||
// create the text of the formatted data
|
||||
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
|
||||
this._text = renderDateTime(template, this.getValue(), this._locale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -740,24 +752,6 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
return this._value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
checkHasValue(inputValue: any) {
|
||||
if (this._item) {
|
||||
this._item.setElementClass('input-has-value', !!(inputValue && inputValue !== ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
updateText() {
|
||||
// create the text of the formatted data
|
||||
const template = this.displayFormat || this.pickerFormat || DEFAULT_FORMAT;
|
||||
this._text = renderDateTime(template, this._value, this._locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@@ -812,97 +806,6 @@ export class DateTime extends Ion implements AfterContentInit, ControlValueAcces
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
this._item && this._item.setElementClass('item-datetime-disabled', this._disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
console.debug('datetime, writeValue', val);
|
||||
this.setValue(val);
|
||||
this.updateText();
|
||||
this.checkHasValue(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
// first see if locale names were provided in the inputs
|
||||
// then check to see if they're in the config
|
||||
// if neither were provided then it will use default English names
|
||||
['monthNames', 'monthShortNames', 'dayNames', 'dayShortNames'].forEach(type => {
|
||||
(<any>this)._locale[type] = convertToArrayOfStrings(isPresent((<any>this)[type]) ? (<any>this)[type] : this._config.get(type), type);
|
||||
});
|
||||
|
||||
// update how the datetime value is displayed as formatted text
|
||||
this.updateText();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function): void {
|
||||
this._fn = fn;
|
||||
this.onChange = (val: any) => {
|
||||
console.debug('datetime, onChange', val);
|
||||
this.setValue(val);
|
||||
this.updateText();
|
||||
this.checkHasValue(val);
|
||||
|
||||
// convert DateTimeData value to iso datetime format
|
||||
fn(convertDataToISO(this._value));
|
||||
|
||||
this.onTouched();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange(val: any) {
|
||||
// onChange used when there is not an formControlName
|
||||
console.debug('datetime, onChange w/out formControlName', val);
|
||||
this.setValue(val);
|
||||
this.updateText();
|
||||
this.checkHasValue(val);
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched() { }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,8 @@ import { mockApp, mockConfig, mockElementRef, mockRenderer } from '../../../util
|
||||
|
||||
|
||||
describe('DateTime', () => {
|
||||
// TODO
|
||||
// pass commonInputTest()
|
||||
|
||||
describe('validate', () => {
|
||||
|
||||
@@ -604,10 +606,6 @@ describe('DateTime', () => {
|
||||
datetime.setValue(null);
|
||||
expect(datetime.getValue()).toEqual({});
|
||||
|
||||
datetime.setValue('1994-12-15T13:47:20.789Z');
|
||||
datetime.setValue(undefined);
|
||||
expect(datetime.getValue()).toEqual({});
|
||||
|
||||
datetime.setValue('1994-12-15T13:47:20.789Z');
|
||||
datetime.setValue('');
|
||||
expect(datetime.getValue()).toEqual({});
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
<ion-input (input)="input6($event.target.value)"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label floating>Date of Birth</ion-label>
|
||||
<ion-datetime></ion-datetime>
|
||||
</ion-item>
|
||||
|
||||
</ion-list>
|
||||
|
||||
</ion-content>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Config } from '../../config/config';
|
||||
import { DeepLinker } from '../../navigation/deep-linker';
|
||||
import { DomController } from '../../platform/dom-controller';
|
||||
import { GestureController } from '../../gestures/gesture-controller';
|
||||
import { isTrueProperty } from '../../util/util';
|
||||
import { Keyboard } from '../../platform/keyboard';
|
||||
import { NavController } from '../../navigation/nav-controller';
|
||||
import { NavControllerBase } from '../../navigation/nav-controller-base';
|
||||
@@ -56,6 +55,7 @@ import { RootNode } from '../split-pane/split-pane';
|
||||
providers: [{provide: RootNode, useExisting: forwardRef(() => Nav) }]
|
||||
})
|
||||
export class Nav extends NavControllerBase implements AfterViewInit, RootNode {
|
||||
|
||||
private _root: any;
|
||||
private _hasInit: boolean = false;
|
||||
|
||||
@@ -149,18 +149,6 @@ export class Nav extends NavControllerBase implements AfterViewInit, RootNode {
|
||||
*/
|
||||
@Input() rootParams: any;
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, swipe to go back is enabled.
|
||||
*/
|
||||
@Input()
|
||||
get swipeBackEnabled(): boolean {
|
||||
return this._sbEnabled;
|
||||
}
|
||||
set swipeBackEnabled(val: boolean) {
|
||||
this._sbEnabled = isTrueProperty(val);
|
||||
this._swipeBackCheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
|
||||
@@ -13,26 +13,30 @@ import { isPresent, isTrueProperty } from '../../util/util';
|
||||
selector: 'ion-option'
|
||||
})
|
||||
export class Option {
|
||||
_selected: any = false;
|
||||
_disabled: any = false;
|
||||
|
||||
_selected: boolean = false;
|
||||
_disabled: boolean = false;
|
||||
_value: any;
|
||||
|
||||
/**
|
||||
* @output {any} Event to evaluate when option is selected.
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Output() ionSelect: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
constructor(private _elementRef: ElementRef) {}
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the element is selected.
|
||||
*/
|
||||
@Input()
|
||||
get selected() {
|
||||
get selected(): boolean {
|
||||
return this._selected;
|
||||
}
|
||||
|
||||
set selected(val) {
|
||||
set selected(val: boolean) {
|
||||
this._selected = isTrueProperty(val);
|
||||
}
|
||||
|
||||
@@ -46,22 +50,16 @@ export class Option {
|
||||
}
|
||||
return this.text;
|
||||
}
|
||||
|
||||
set value(val: any) {
|
||||
this._value = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
* @output {any} Event to evaluate when option is selected.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
@Output() ionSelect: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
}
|
||||
constructor(private _elementRef: ElementRef) {}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, Optional, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { clamp, isPresent, isTrueProperty } from '../../util/util';
|
||||
import { clamp, isTrueProperty } from '../../util/util';
|
||||
import { Config } from '../../config/config';
|
||||
import { DomController } from '../../platform/dom-controller';
|
||||
import { Form } from '../../util/form';
|
||||
import { Haptic } from '../../tap-click/haptic';
|
||||
import { Ion } from '../ion';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { Item } from '../item/item';
|
||||
import { Platform } from '../../platform/platform';
|
||||
import { PointerCoordinates, pointerCoord } from '../../util/dom';
|
||||
import { TimeoutDebouncer } from '../../util/debouncer';
|
||||
import { UIEventManager } from '../../gestures/ui-event-manager';
|
||||
|
||||
|
||||
@@ -112,13 +111,11 @@ export const RANGE_VALUE_ACCESSOR: any = {
|
||||
providers: [RANGE_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class Range extends Ion implements AfterViewInit, ControlValueAccessor, OnDestroy {
|
||||
export class Range extends BaseInput<any> implements AfterViewInit, ControlValueAccessor, OnDestroy {
|
||||
|
||||
_dual: boolean;
|
||||
_pin: boolean;
|
||||
_disabled: boolean = false;
|
||||
_pressed: boolean;
|
||||
_lblId: string;
|
||||
_fn: Function;
|
||||
|
||||
_activeB: boolean;
|
||||
_rect: ClientRect;
|
||||
@@ -141,21 +138,10 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
_barL: string;
|
||||
_barR: string;
|
||||
|
||||
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(0);
|
||||
_events: UIEventManager;
|
||||
|
||||
@ViewChild('slider') public _slider: ElementRef;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
value: any;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @input {number} Minimum integer value of the range. Defaults to `0`.
|
||||
*/
|
||||
@@ -245,19 +231,6 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
this._dual = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = val = isTrueProperty(val);
|
||||
const item = this._item;
|
||||
item && item.setElementClass('item-range-disabled', val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ratio of the knob's is current location, which is a number
|
||||
* between `0` and `1`. If two knobs are used, this property represents
|
||||
@@ -282,25 +255,10 @@ 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,
|
||||
form: Form,
|
||||
private _haptic: Haptic,
|
||||
@Optional() private _item: Item,
|
||||
@Optional() item: Item,
|
||||
config: Config,
|
||||
private _plt: Platform,
|
||||
elementRef: ElementRef,
|
||||
@@ -308,21 +266,16 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
private _dom: DomController,
|
||||
private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super(config, elementRef, renderer, 'range');
|
||||
super(config, elementRef, renderer, 'range', 0, form, item, null);
|
||||
this._events = new UIEventManager(_plt);
|
||||
_form.register(this);
|
||||
|
||||
if (_item) {
|
||||
this.id = 'rng-' + _item.registerInput('range');
|
||||
this._lblId = 'lbl-' + _item.id;
|
||||
_item.setElementClass('item-range', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this._initialize();
|
||||
|
||||
// add touchstart/mousedown listeners
|
||||
this._events.pointerEvents({
|
||||
element: this._slider.nativeElement,
|
||||
@@ -346,7 +299,7 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
}
|
||||
|
||||
// trigger ionFocus event
|
||||
this.ionFocus.emit(this);
|
||||
this._fireFocus();
|
||||
|
||||
// prevent default so scrolling does not happen
|
||||
ev.preventDefault();
|
||||
@@ -375,38 +328,40 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
|
||||
/** @internal */
|
||||
_pointerMove(ev: UIEvent) {
|
||||
if (!this._disabled) {
|
||||
// prevent default so scrolling does not happen
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (this._disabled) {
|
||||
return;
|
||||
}
|
||||
// prevent default so scrolling does not happen
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// update the active knob's position
|
||||
const hasChanged = this._update(pointerCoord(ev), this._rect, true);
|
||||
// update the active knob's position
|
||||
const hasChanged = this._update(pointerCoord(ev), this._rect, true);
|
||||
|
||||
if (hasChanged && this._snaps) {
|
||||
// trigger a haptic selection changed event
|
||||
// if this is a snap range
|
||||
this._haptic.gestureSelectionChanged();
|
||||
}
|
||||
if (hasChanged && this._snaps) {
|
||||
// trigger a haptic selection changed event
|
||||
// if this is a snap range
|
||||
this._haptic.gestureSelectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_pointerUp(ev: UIEvent) {
|
||||
if (!this._disabled) {
|
||||
// prevent default so scrolling does not happen
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// update the active knob's position
|
||||
this._update(pointerCoord(ev), this._rect, false);
|
||||
|
||||
// trigger a haptic end
|
||||
this._haptic.gestureSelectionEnd();
|
||||
|
||||
// trigger ionBlur event
|
||||
this.ionBlur.emit(this);
|
||||
if (this._disabled) {
|
||||
return;
|
||||
}
|
||||
// prevent default so scrolling does not happen
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// update the active knob's position
|
||||
this._update(pointerCoord(ev), this._rect, false);
|
||||
|
||||
// trigger a haptic end
|
||||
this._haptic.gestureSelectionEnd();
|
||||
|
||||
// trigger ionBlur event
|
||||
this._fireBlur();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -447,27 +402,24 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
}
|
||||
|
||||
// value has been updated
|
||||
let value;
|
||||
if (this._dual) {
|
||||
// dual knobs have an lower and upper value
|
||||
if (!this.value) {
|
||||
// ensure we're always updating the same object
|
||||
this.value = {};
|
||||
}
|
||||
this.value.lower = Math.min(this._valA, this._valB);
|
||||
this.value.upper = Math.max(this._valA, this._valB);
|
||||
value = {
|
||||
lower: Math.min(this._valA, this._valB),
|
||||
upper: Math.max(this._valA, this._valB)
|
||||
};
|
||||
|
||||
console.debug(`range, updateKnob: ${ratio}, lower: ${this.value.lower}, upper: ${this.value.upper}`);
|
||||
|
||||
} else {
|
||||
// single knob only has one value
|
||||
this.value = this._valA;
|
||||
value = this._valA;
|
||||
console.debug(`range, updateKnob: ${ratio}, value: ${this.value}`);
|
||||
}
|
||||
|
||||
this._debouncer.debounce(() => {
|
||||
this.onChange(this.value);
|
||||
this.ionChange.emit(this);
|
||||
});
|
||||
// Update input value
|
||||
this.value = value;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -566,70 +518,40 @@ export class Range extends Ion implements AfterViewInit, ControlValueAccessor, O
|
||||
return clamp(0, value, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
if (isPresent(val)) {
|
||||
this.value = val;
|
||||
|
||||
if (this._dual) {
|
||||
this._valA = val.lower;
|
||||
this._valB = val.upper;
|
||||
this._ratioA = this._valueToRatio(val.lower);
|
||||
this._ratioB = this._valueToRatio(val.upper);
|
||||
|
||||
} else {
|
||||
this._valA = val;
|
||||
this._ratioA = this._valueToRatio(val);
|
||||
}
|
||||
|
||||
this._updateBar();
|
||||
_inputNormalize(val: any): any {
|
||||
if (this._dual) {
|
||||
return val;
|
||||
} else {
|
||||
val = parseFloat(val);
|
||||
return isNaN(val) ? undefined : val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function): void {
|
||||
this._fn = fn;
|
||||
this.onChange = (val: any) => {
|
||||
fn(val);
|
||||
this.onTouched();
|
||||
};
|
||||
}
|
||||
_inputUpdated() {
|
||||
const val = this.value;
|
||||
if (this._dual) {
|
||||
this._valA = val.lower;
|
||||
this._valB = val.upper;
|
||||
this._ratioA = this._valueToRatio(val.lower);
|
||||
this._ratioB = this._valueToRatio(val.upper);
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
} else {
|
||||
this._valA = val;
|
||||
this._ratioA = this._valueToRatio(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange(val: any) {
|
||||
// used when this input does not have an ngModel or formControlName
|
||||
this.onTouched();
|
||||
this._updateBar();
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched() { }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
super.ngOnDestroy();
|
||||
this._events.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { Range } from '../range';
|
||||
import { mockChangeDetectorRef, mockConfig, mockDomController, mockElementRef, mockHaptic, mockPlatform, mockRenderer } from '../../../util/mock-providers';
|
||||
import { mockChangeDetectorRef, mockConfig, mockDomController, mockItem, mockElementRef, mockHaptic, mockPlatform, mockRenderer } from '../../../util/mock-providers';
|
||||
import { Form } from '../../../util/form';
|
||||
import { commonInputTest, NUMBER_CORPUS } from '../../../util/input-tester';
|
||||
|
||||
|
||||
describe('Range', () => {
|
||||
|
||||
it('should pass common test', () => {
|
||||
// TODO, validate range inside bounds
|
||||
const range = createRange();
|
||||
range._slider = mockElementRef();
|
||||
commonInputTest(range, {
|
||||
defaultValue: 0,
|
||||
corpus: NUMBER_CORPUS
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('valueToRatio', () => {
|
||||
it('step=1', () => {
|
||||
let range = createRange();
|
||||
@@ -68,5 +80,5 @@ describe('Range', () => {
|
||||
|
||||
function createRange(): Range {
|
||||
let form = new Form();
|
||||
return new Range(form, mockHaptic(), null, mockConfig(), mockPlatform(), mockElementRef(), mockRenderer(), mockDomController(), mockChangeDetectorRef());
|
||||
return new Range(form, mockHaptic(), mockItem(), mockConfig(), mockPlatform(), mockElementRef(), mockRenderer(), mockDomController(), mockChangeDetectorRef());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Component, ElementRef, EventEmitter, HostBinding, Input, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
import { Component, ElementRef, EventEmitter, Input, Optional, Output, Renderer, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
|
||||
import { Config } from '../../config/config';
|
||||
import { Ion } from '../ion';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { isPresent, isTrueProperty } from '../../util/util';
|
||||
import { Platform } from '../../platform/platform';
|
||||
import { TimeoutDebouncer } from '../../util/debouncer';
|
||||
|
||||
|
||||
/**
|
||||
* @name Searchbar
|
||||
@@ -49,13 +47,13 @@ import { TimeoutDebouncer } from '../../util/debouncer';
|
||||
'[class.searchbar-has-value]': '_value',
|
||||
'[class.searchbar-active]': '_isActive',
|
||||
'[class.searchbar-show-cancel]': '_showCancelButton',
|
||||
'[class.searchbar-left-aligned]': '_shouldAlignLeft'
|
||||
'[class.searchbar-left-aligned]': '_shouldAlignLeft',
|
||||
'[class.searchbar-has-focus]': '_isFocus'
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class Searchbar extends Ion {
|
||||
export class Searchbar extends BaseInput<string> {
|
||||
|
||||
_value: string|number = '';
|
||||
_shouldBlur: boolean = true;
|
||||
_shouldAlignLeft: boolean = true;
|
||||
_isCancelVisible: boolean = false;
|
||||
@@ -63,7 +61,6 @@ export class Searchbar extends Ion {
|
||||
_autocomplete: string = 'off';
|
||||
_autocorrect: string = 'off';
|
||||
_isActive: boolean = false;
|
||||
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(250);
|
||||
_showCancelButton: boolean = false;
|
||||
_animated: boolean = false;
|
||||
|
||||
@@ -144,16 +141,6 @@ export class Searchbar extends Ion {
|
||||
*/
|
||||
@Output() ionInput: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
|
||||
|
||||
/**
|
||||
* @output {event} Emitted when the Searchbar input has blurred.
|
||||
*/
|
||||
@Output() ionBlur: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
|
||||
|
||||
/**
|
||||
* @output {event} Emitted when the Searchbar input has focused.
|
||||
*/
|
||||
@Output() ionFocus: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
|
||||
|
||||
/**
|
||||
* @output {event} Emitted when the cancel button is clicked.
|
||||
*/
|
||||
@@ -164,10 +151,6 @@ export class Searchbar extends Ion {
|
||||
*/
|
||||
@Output() ionClear: EventEmitter<UIEvent> = new EventEmitter<UIEvent>();
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@HostBinding('class.searchbar-has-focus') _sbHasFocus: boolean;
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
@@ -176,12 +159,8 @@ export class Searchbar extends Ion {
|
||||
renderer: Renderer,
|
||||
@Optional() ngControl: NgControl
|
||||
) {
|
||||
super(config, elementRef, renderer, 'searchbar');
|
||||
|
||||
// If the user passed a ngControl we need to set the valueAccessor
|
||||
if (ngControl) {
|
||||
ngControl.valueAccessor = this;
|
||||
}
|
||||
super(config, elementRef, renderer, 'searchbar', '', null, null, ngControl);
|
||||
this.debounce = 250;
|
||||
}
|
||||
|
||||
@ViewChild('searchbarInput') _searchbarInput: ElementRef;
|
||||
@@ -191,21 +170,12 @@ export class Searchbar extends Ion {
|
||||
@ViewChild('cancelButton', {read: ElementRef}) _cancelButton: ElementRef;
|
||||
|
||||
/**
|
||||
* @input {string} Set the input value.
|
||||
* @hidden
|
||||
* After View Checked position the elements
|
||||
*/
|
||||
@Input()
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
this._value = val;
|
||||
if (this._searchbarInput) {
|
||||
let ele = this._searchbarInput.nativeElement;
|
||||
if (ele) {
|
||||
ele.value = val;
|
||||
}
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
this._initialize();
|
||||
this.positionElements();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,7 +183,7 @@ export class Searchbar extends Ion {
|
||||
* On Initialization check for attributes
|
||||
*/
|
||||
ngOnInit() {
|
||||
let showCancelButton = this.showCancelButton;
|
||||
const showCancelButton = this.showCancelButton;
|
||||
if (typeof showCancelButton === 'string') {
|
||||
this.showCancelButton = (showCancelButton === '' || showCancelButton === 'true');
|
||||
}
|
||||
@@ -221,9 +191,14 @@ export class Searchbar extends Ion {
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* After View Checked position the elements
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
_inputUpdated() {
|
||||
if (this._searchbarInput) {
|
||||
var ele = this._searchbarInput.nativeElement;
|
||||
if (ele) {
|
||||
ele.value = this.value;
|
||||
}
|
||||
}
|
||||
this.positionElements();
|
||||
}
|
||||
|
||||
@@ -233,9 +208,9 @@ export class Searchbar extends Ion {
|
||||
* based on the input value and if it is focused. (ios only)
|
||||
*/
|
||||
positionElements() {
|
||||
let isAnimated = this._animated;
|
||||
let prevAlignLeft = this._shouldAlignLeft;
|
||||
let shouldAlignLeft = (!isAnimated || (this._value && this._value.toString().trim() !== '') || this._sbHasFocus === true);
|
||||
const isAnimated = this._animated;
|
||||
const prevAlignLeft = this._shouldAlignLeft;
|
||||
const shouldAlignLeft = (!isAnimated || (this._value && this._value.toString().trim() !== '') || this._isFocus === true);
|
||||
this._shouldAlignLeft = shouldAlignLeft;
|
||||
|
||||
if (this._mode !== 'ios') {
|
||||
@@ -254,8 +229,8 @@ export class Searchbar extends Ion {
|
||||
if (!this._searchbarInput || !this._searchbarIcon) {
|
||||
return;
|
||||
}
|
||||
let inputEle = this._searchbarInput.nativeElement;
|
||||
let iconEle = this._searchbarIcon.nativeElement;
|
||||
const inputEle = this._searchbarInput.nativeElement;
|
||||
const iconEle = this._searchbarIcon.nativeElement;
|
||||
|
||||
if (this._shouldAlignLeft) {
|
||||
inputEle.removeAttribute('style');
|
||||
@@ -290,15 +265,15 @@ export class Searchbar extends Ion {
|
||||
if (!this._cancelButton || !this._cancelButton.nativeElement) {
|
||||
return;
|
||||
}
|
||||
let showShowCancel = this._sbHasFocus;
|
||||
const showShowCancel = this._isFocus;
|
||||
if (showShowCancel !== this._isCancelVisible) {
|
||||
let cancelStyleEle = this._cancelButton.nativeElement;
|
||||
let cancelStyle = cancelStyleEle.style;
|
||||
var cancelStyleEle = this._cancelButton.nativeElement;
|
||||
var cancelStyle = cancelStyleEle.style;
|
||||
this._isCancelVisible = showShowCancel;
|
||||
if (showShowCancel) {
|
||||
cancelStyle.marginRight = '0';
|
||||
} else {
|
||||
let offset = cancelStyleEle.offsetWidth;
|
||||
var offset = cancelStyleEle.offsetWidth;
|
||||
if (offset > 0) {
|
||||
cancelStyle.marginRight = -offset + 'px';
|
||||
}
|
||||
@@ -312,11 +287,8 @@ export class Searchbar extends Ion {
|
||||
* Update the Searchbar input value when the input changes
|
||||
*/
|
||||
inputChanged(ev: any) {
|
||||
this._value = ev.target.value;
|
||||
this._debouncer.debounce(() => {
|
||||
this.onChange(this._value);
|
||||
this.ionInput.emit(ev);
|
||||
});
|
||||
this.value = ev.target.value;
|
||||
this.ionInput.emit(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,10 +296,8 @@ export class Searchbar extends Ion {
|
||||
* Sets the Searchbar to focused and active on input focus.
|
||||
*/
|
||||
inputFocused(ev: UIEvent) {
|
||||
this.ionFocus.emit(ev);
|
||||
|
||||
this._sbHasFocus = true;
|
||||
this._isActive = true;
|
||||
this._fireFocus();
|
||||
this.positionElements();
|
||||
}
|
||||
|
||||
@@ -344,9 +314,7 @@ export class Searchbar extends Ion {
|
||||
this._shouldBlur = true;
|
||||
return;
|
||||
}
|
||||
this.ionBlur.emit(ev);
|
||||
|
||||
this._sbHasFocus = false;
|
||||
this._fireBlur();
|
||||
this.positionElements();
|
||||
}
|
||||
|
||||
@@ -363,7 +331,6 @@ export class Searchbar extends Ion {
|
||||
let value = this._value;
|
||||
if (isPresent(value) && value !== '') {
|
||||
this.value = ''; // DOM WRITE
|
||||
this.onChange(this._value);
|
||||
this.ionInput.emit(ev);
|
||||
}
|
||||
}, 16 * 4);
|
||||
@@ -384,42 +351,8 @@ export class Searchbar extends Ion {
|
||||
this._isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Write a new value to the element.
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
this.value = val;
|
||||
this.positionElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange = (_: any) => {};
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched = () => {};
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Set the function to be called when the control receives a change event.
|
||||
*/
|
||||
registerOnChange(fn: (_: any) => {}): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Set the function to be called when the control receives a touch event.
|
||||
*/
|
||||
registerOnTouched(fn: () => {}): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
setFocus() {
|
||||
_fireFocus() {
|
||||
this._renderer.invokeElementMethod(this._searchbarInput.nativeElement, 'focus');
|
||||
super._fireFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<ion-content>
|
||||
<h5 padding-left> Search - Default </h5>
|
||||
<ion-searchbar [(ngModel)]="defaultSearch" type="tel" showCancelButton debounce="500" (ionInput)="triggerInput($event)" (ionBlur)="inputBlurred($event)" (ionFocus)="inputFocused($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
<ion-searchbar [(ngModel)]="defaultSearch" type="tel" showCancelButton debounce="500" (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionBlur)="inputBlurred($event)" (ionFocus)="inputFocused($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<h5 padding-left> Search - Animated </h5>
|
||||
<ion-searchbar animated="true" showCancelButton debounce="500" (ionInput)="triggerInput($event)" (ionBlur)="inputBlurred($event)" (ionFocus)="inputFocused($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
<ion-searchbar [(ngModel)]="defaultSearch" animated="true" showCancelButton debounce="500" (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionBlur)="inputBlurred($event)" (ionFocus)="inputFocused($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<p padding-left>
|
||||
defaultSearch: <b>{{ defaultSearch }}</b>
|
||||
</p>
|
||||
|
||||
<h5 padding-left> Search - Custom Placeholder </h5>
|
||||
<ion-searchbar [autocorrect]="isAutocorrect" showCancelButton="true" [autocomplete]="isAutocomplete" [spellcheck]="isSpellcheck" type="number" [(ngModel)]="customPlaceholder" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)" placeholder="Filter Schedules"></ion-searchbar>
|
||||
<ion-searchbar [autocorrect]="isAutocorrect" showCancelButton="true" [autocomplete]="isAutocomplete" [spellcheck]="isSpellcheck" type="number" placeholder="Filter Schedules" [(ngModel)]="customPlaceholder" (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<p padding-left>
|
||||
customPlaceholder: <b>{{ customPlaceholder }}</b>
|
||||
</p>
|
||||
|
||||
<h5 padding-left> Search - No Cancel Button </h5>
|
||||
<ion-searchbar autocorrect="off" autocomplete="off" spellcheck="true" type="text" [(ngModel)]="defaultCancel" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)" showCancelButton="false"></ion-searchbar>
|
||||
<ion-searchbar autocorrect="off" autocomplete="off" spellcheck="true" type="text" [(ngModel)]="defaultCancel" showCancelButton="false" (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<p padding-left>
|
||||
defaultCancel: <b>{{ defaultCancel }}</b>
|
||||
</p>
|
||||
|
||||
<h5 padding-left> Search - Custom Cancel Button Danger </h5>
|
||||
<ion-searchbar (ionInput)="triggerInput($event)" showCancelButton (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)" cancelButtonText="Really Long Cancel" color="danger"></ion-searchbar>
|
||||
<ion-searchbar showCancelButton cancelButtonText="Really Long Cancel" color="danger" (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<h5 padding-left> Search - Value passed </h5>
|
||||
<ion-searchbar value="mysearch" showCancelButton (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)" cancelButtonText="Really Long Cancel" color="dark"></ion-searchbar>
|
||||
<ion-searchbar value="mysearch" cancelButtonText="Really Long Cancel" color="dark" showCancelButton (ionChange)="changedInput($event)" (ionInput)="triggerInput($event)" (ionCancel)="onCancelSearchbar($event)" (ionClear)="onClearSearchbar($event)"></ion-searchbar>
|
||||
|
||||
<h5 padding-left> Search - Mode iOS</h5>
|
||||
<ion-searchbar mode="ios" animated="true" showCancelButton placeholder="Search"></ion-searchbar>
|
||||
|
||||
@@ -19,23 +19,27 @@ export class RootPage {
|
||||
}
|
||||
|
||||
onClearSearchbar(ev: any) {
|
||||
console.log('ionClear', ev.target.value);
|
||||
console.log('ionClear', ev);
|
||||
}
|
||||
|
||||
onCancelSearchbar(ev: any) {
|
||||
console.log('ionCancel', ev.target.value);
|
||||
console.log('ionCancel', ev);
|
||||
}
|
||||
|
||||
triggerInput(ev: any) {
|
||||
console.log('ionInput', ev.target.value);
|
||||
console.log('ionInput', ev);
|
||||
}
|
||||
|
||||
changedInput(ev: any) {
|
||||
console.log('ionChange', ev);
|
||||
}
|
||||
|
||||
inputBlurred(ev: any) {
|
||||
console.log('ionBlur', ev.target.value);
|
||||
console.log('ionBlur', ev);
|
||||
}
|
||||
|
||||
inputFocused(ev: any) {
|
||||
console.log('ionFocus', ev.target.value);
|
||||
console.log('ionFocus', ev);
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ion-header>
|
||||
|
||||
<ion-navbar>
|
||||
<ion-searchbar color="primary" autofocus (ionInput)="getItems($event)" placeholder="Filter Schedules">
|
||||
<ion-searchbar color="primary" autofocus (ionChange)="getItems($event.value)" placeholder="Filter Schedules">
|
||||
</ion-searchbar>
|
||||
</ion-navbar>
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
</ion-header>
|
||||
|
||||
<ion-content>
|
||||
|
||||
<ion-input [(ngModel)]="value"></ion-input>
|
||||
<form>
|
||||
<ion-searchbar (ionInput)="getItems($event)"></ion-searchbar>
|
||||
<ion-searchbar [(ngModel)]="value" (ionChange)="getItems($event.value)" name="search"></ion-searchbar>
|
||||
</form>
|
||||
<ion-list>
|
||||
<button ion-item *ngFor="let item of items" (click)="showDetail(item)" class="e2eSearchbarNavItem">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IonicPage, NavController, ModalController } from '../../../../../..';
|
||||
})
|
||||
export class SearchPage {
|
||||
items: string[];
|
||||
value = '';
|
||||
|
||||
constructor(public navCtrl: NavController, public modalCtrl: ModalController) {
|
||||
this.initializeItems();
|
||||
@@ -58,13 +59,10 @@ export class SearchPage {
|
||||
];
|
||||
}
|
||||
|
||||
getItems(ev: any) {
|
||||
getItems(q: string) {
|
||||
// Reset items back to all of the items
|
||||
this.initializeItems();
|
||||
|
||||
// set q to the value of the searchbar
|
||||
var q = ev.target.value;
|
||||
|
||||
// if the value is an empty string don't filter the items
|
||||
if (!q || q.trim() === '') {
|
||||
return;
|
||||
|
||||
@@ -46,11 +46,16 @@ import { isPresent, isTrueProperty } from '../../util/util';
|
||||
host: {
|
||||
'tappable': '',
|
||||
'class': 'segment-button',
|
||||
'role': 'button'
|
||||
'role': 'button',
|
||||
'[class.segment-button-disabled]': '_disabled',
|
||||
'[class.segment-activated]': 'isActive',
|
||||
'[attr.aria-pressed]': 'isActive'
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class SegmentButton {
|
||||
|
||||
isActive: boolean = false;
|
||||
_disabled: boolean = false;
|
||||
|
||||
/**
|
||||
@@ -63,8 +68,6 @@ export class SegmentButton {
|
||||
*/
|
||||
@Output() ionSelect: EventEmitter<SegmentButton> = new EventEmitter<SegmentButton>();
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@@ -75,15 +78,9 @@ export class SegmentButton {
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
this._setElementClass('segment-button-disabled', this._disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_setElementClass(cssClass: string, shouldAdd: boolean) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
|
||||
}
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
@@ -104,12 +101,4 @@ export class SegmentButton {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
set isActive(isActive: any) {
|
||||
this._renderer.setElementClass(this._elementRef.nativeElement, 'segment-activated', isActive);
|
||||
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-pressed', isActive);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -130,6 +130,12 @@ $segment-button-ios-toolbar-icon-line-height: 2.4rem !default;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-ios.segment-disabled {
|
||||
opacity: .4;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.segment-ios .segment-button-disabled {
|
||||
color: rgba($segment-button-ios-background-color-activated, $segment-button-ios-opacity-disabled);
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ $segment-button-md-icon-line-height: $segment-button-md-line-height !d
|
||||
}
|
||||
}
|
||||
|
||||
.segment-md.segment-disabled,
|
||||
.segment-md .segment-button-disabled {
|
||||
opacity: $segment-button-md-opacity-disabled;
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ContentChildren, Directive, ElementRef, EventEmitter, Input, Output, Optional, QueryList, Renderer } from '@angular/core';
|
||||
import { ContentChildren, Directive, ElementRef, Optional, QueryList, Renderer } from '@angular/core';
|
||||
import { NgControl } from '@angular/forms';
|
||||
|
||||
import { Config } from '../../config/config';
|
||||
import { Ion } from '../ion';
|
||||
import { isPresent, isTrueProperty } from '../../util/util';
|
||||
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { SegmentButton } from './segment-button';
|
||||
|
||||
/**
|
||||
@@ -64,21 +62,12 @@ import { SegmentButton } from './segment-button';
|
||||
* @see [Angular 2 Forms](http://learnangular2.com/forms/)
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ion-segment'
|
||||
selector: 'ion-segment',
|
||||
host: {
|
||||
'[class.segment-disabled]': '_disabled'
|
||||
}
|
||||
})
|
||||
export class Segment extends Ion {
|
||||
_disabled: boolean = false;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
value: string;
|
||||
|
||||
/**
|
||||
* @output {Any} Emitted when a segment button has been changed.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<SegmentButton> = new EventEmitter<SegmentButton>();
|
||||
|
||||
export class Segment extends BaseInput<string> {
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
@@ -91,85 +80,32 @@ export class Segment extends Ion {
|
||||
renderer: Renderer,
|
||||
@Optional() ngControl: NgControl
|
||||
) {
|
||||
super(config, elementRef, renderer, 'segment');
|
||||
|
||||
if (ngControl) {
|
||||
ngControl.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with any of the buttons in the segment.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
|
||||
if (this._buttons) {
|
||||
this._buttons.forEach(button => {
|
||||
button._setElementClass('segment-button-disabled', this._disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Write a new value to the element.
|
||||
*/
|
||||
writeValue(value: any) {
|
||||
this.value = isPresent(value) ? value : '';
|
||||
if (this._buttons) {
|
||||
let buttons = this._buttons.toArray();
|
||||
for (let button of buttons) {
|
||||
button.isActive = (button.value === this.value);
|
||||
}
|
||||
}
|
||||
super(config, elementRef, renderer, 'segment', null, null, null, ngControl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this._buttons.forEach(button => {
|
||||
button.ionSelect.subscribe((selectedButton: any) => {
|
||||
this.writeValue(selectedButton.value);
|
||||
this.onChange(selectedButton.value);
|
||||
this.ionChange.emit(selectedButton);
|
||||
});
|
||||
|
||||
if (isPresent(this.value)) {
|
||||
button.isActive = (button.value === this.value);
|
||||
}
|
||||
|
||||
if (isTrueProperty(this._disabled)) {
|
||||
button._setElementClass('segment-button-disabled', this._disabled);
|
||||
}
|
||||
});
|
||||
this._initialize();
|
||||
this._buttons.forEach(button => {
|
||||
button.ionSelect.subscribe((selectedButton: any) => this.value = selectedButton.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Write a new value to the element.
|
||||
*/
|
||||
onChange = (_: any) => {};
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched = (_: any) => {};
|
||||
_inputUpdated() {
|
||||
if (this._buttons) {
|
||||
var buttons = this._buttons.toArray();
|
||||
var value = this.value;
|
||||
for (var button of buttons) {
|
||||
button.isActive = (button.value === value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Set the function to be called when the control receives a change event.
|
||||
*/
|
||||
registerOnChange(fn: any) { this.onChange = fn; }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
* Set the function to be called when the control receives a touch event.
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ $segment-button-wp-buttons-justify-content: flex-start !default;
|
||||
}
|
||||
}
|
||||
|
||||
.segment-wp.segment-disabled,
|
||||
.segment-wp .segment-button-disabled {
|
||||
opacity: $segment-button-wp-opacity-disabled;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<ion-segment-button value="active" class="e2eSegmentStandard">
|
||||
Active
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="disabled" [disabled]="isDisabled">
|
||||
<ion-segment-button value="disabled" [disabled]="isDisabledB">
|
||||
Disabled
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="inactive" disabled="false">
|
||||
@@ -44,20 +44,20 @@
|
||||
<p>
|
||||
Map mode: <b>{{myForm.controls.mapStyle.value}}</b> -
|
||||
<span [ngSwitch]="myForm.controls.mapStyle.value">
|
||||
<span *ngSwitchCase="'standard'">
|
||||
<span *ngSwitchCase="'active'">
|
||||
<b>Standard</b>
|
||||
</span>
|
||||
<span *ngSwitchCase="'hybrid'">
|
||||
<span *ngSwitchCase="'disabled'">
|
||||
<b>Hybrid</b>
|
||||
</span>
|
||||
<span *ngSwitchCase="'sat'">
|
||||
<span *ngSwitchCase="'inactive'">
|
||||
<b>Satellite</b>
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<hr>
|
||||
<h4>Model style: NgModel</h4>
|
||||
<ion-segment [(ngModel)]="modelStyle" color="dark">
|
||||
<ion-segment [(ngModel)]="modelStyle" color="dark" [disabled]="isDisabledS">
|
||||
<ion-segment-button value="A">
|
||||
Model A
|
||||
</ion-segment-button>
|
||||
@@ -67,12 +67,12 @@
|
||||
<ion-segment-button value="C" class="e2eSegmentModelC">
|
||||
Model C
|
||||
</ion-segment-button>
|
||||
<ion-segment-button value="D" [disabled]="isDisabled">
|
||||
<ion-segment-button value="D" [disabled]="isDisabledB">
|
||||
Model D
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<p>Model Style: <b>Model {{ modelStyle }}</b></p>
|
||||
<ion-segment [(ngModel)]="icons">
|
||||
<ion-segment [(ngModel)]="icons" >
|
||||
<ion-segment-button value="camera">
|
||||
<ion-icon name="camera"></ion-icon>
|
||||
</ion-segment-button>
|
||||
@@ -80,7 +80,9 @@
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
</ion-segment-button>
|
||||
</ion-segment>
|
||||
<button ion-button block color="dark" (click)="toggleDisabled()">Toggle Disabled</button>
|
||||
<button ion-button color="dark" (click)="toggleBDisabled()">Toggle Button Disabled</button>
|
||||
<button ion-button color="dark" (click)="toggleSDisabled()">Toggle Segment Disabled</button>
|
||||
|
||||
</ion-content>
|
||||
|
||||
|
||||
@@ -112,7 +114,7 @@
|
||||
</ion-segment>
|
||||
</ion-toolbar>
|
||||
<ion-toolbar>
|
||||
<ion-segment [(ngModel)]="appType" color="dark" [disabled]="isDisabled">
|
||||
<ion-segment [(ngModel)]="appType" color="dark" [disabled]="isDisabledS">
|
||||
<ion-segment-button value="paid">
|
||||
Default
|
||||
</ion-segment-button>
|
||||
|
||||
@@ -11,7 +11,9 @@ export class HomePage {
|
||||
modelStyle: string = 'B';
|
||||
appType: string = 'free';
|
||||
icons: string = 'camera';
|
||||
isDisabled: boolean = true;
|
||||
isDisabledB: boolean = true;
|
||||
isDisabledS: boolean = false;
|
||||
|
||||
myForm: any;
|
||||
|
||||
constructor(fb: FormBuilder) {
|
||||
@@ -20,8 +22,12 @@ export class HomePage {
|
||||
});
|
||||
}
|
||||
|
||||
toggleDisabled() {
|
||||
this.isDisabled = !this.isDisabled;
|
||||
toggleBDisabled() {
|
||||
this.isDisabledB = !this.isDisabledB;
|
||||
}
|
||||
|
||||
toggleSDisabled() {
|
||||
this.isDisabledS = !this.isDisabledS;
|
||||
}
|
||||
|
||||
onSegmentChanged(segmentButton: SegmentButton) {
|
||||
|
||||
31
src/components/segment/test/segment.spec.ts
Normal file
31
src/components/segment/test/segment.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import { QueryList } from '@angular/core';
|
||||
import { Segment } from '../segment';
|
||||
import { SegmentButton } from '../segment-button';
|
||||
import { mockConfig, mockElementRef, mockRenderer } from '../../../util/mock-providers';
|
||||
import { commonInputTest } from '../../../util/input-tester';
|
||||
|
||||
describe('Segment', () => {
|
||||
|
||||
it('should pass common test', () => {
|
||||
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
const segment = new Segment(config, elementRef, renderer, null);
|
||||
segment._buttons = new QueryList<SegmentButton>();
|
||||
|
||||
commonInputTest(segment, {
|
||||
defaultValue: null,
|
||||
corpus: [
|
||||
['option1', 'option1'],
|
||||
['option2', 'option2'],
|
||||
['option3', 'option3'],
|
||||
['option4', 'option4'],
|
||||
['', ''],
|
||||
]
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { AfterContentInit, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { AfterViewInit, Component, ContentChildren, ElementRef, EventEmitter, forwardRef, Input, HostListener, OnDestroy, Optional, Output, Renderer, QueryList, ViewEncapsulation } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ActionSheet } from '../action-sheet/action-sheet';
|
||||
import { Alert } from '../alert/alert';
|
||||
import { App } from '../app/app';
|
||||
import { Config } from '../../config/config';
|
||||
import { Form } from '../../util/form';
|
||||
import { Ion } from '../ion';
|
||||
import { isBlank, isCheckedProperty, isTrueProperty, deepCopy } from '../../util/util';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { isCheckedProperty, isTrueProperty, deepCopy, deepEqual } from '../../util/util';
|
||||
import { Item } from '../item/item';
|
||||
import { NavController } from '../../navigation/nav-controller';
|
||||
import { Option } from '../option/option';
|
||||
@@ -141,21 +141,12 @@ export const SELECT_VALUE_ACCESSOR: any = {
|
||||
providers: [SELECT_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class Select extends Ion implements AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
_disabled: any = false;
|
||||
_labelId: string;
|
||||
export class Select extends BaseInput<string[]> implements AfterViewInit, OnDestroy {
|
||||
|
||||
_multi: boolean = false;
|
||||
_options: QueryList<Option>;
|
||||
_values: string[] = [];
|
||||
_texts: string[] = [];
|
||||
_text: string = '';
|
||||
_fn: Function;
|
||||
_isOpen: boolean = false;
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @input {string} The text to display on the cancel button. Default: `Cancel`.
|
||||
@@ -190,34 +181,26 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
*/
|
||||
@Input() selectedText: string = '';
|
||||
|
||||
/**
|
||||
* @output {any} Emitted when the selection has changed.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* @output {any} Emitted when the selection was cancelled.
|
||||
*/
|
||||
@Output() ionCancel: EventEmitter<any> = new EventEmitter();
|
||||
@Output() ionCancel: EventEmitter<Select> = new EventEmitter();
|
||||
|
||||
constructor(
|
||||
private _app: App,
|
||||
private _form: Form,
|
||||
form: Form,
|
||||
public config: Config,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
@Optional() public _item: Item,
|
||||
@Optional() item: Item,
|
||||
@Optional() private _nav: NavController
|
||||
) {
|
||||
super(config, elementRef, renderer, 'select');
|
||||
super(config, elementRef, renderer, 'select', [], form, item, null);
|
||||
}
|
||||
|
||||
_form.register(this);
|
||||
|
||||
if (_item) {
|
||||
this.id = 'sel-' + _item.registerInput('select');
|
||||
this._labelId = 'lbl-' + _item.id;
|
||||
this._item.setElementClass('item-select', true);
|
||||
}
|
||||
ngAfterContentInit() {
|
||||
this._inputUpdated();
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event'])
|
||||
@@ -233,16 +216,14 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
|
||||
@HostListener('keyup.space')
|
||||
_keyup() {
|
||||
if (!this._isOpen) {
|
||||
this.open();
|
||||
}
|
||||
this.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the select interface.
|
||||
*/
|
||||
open() {
|
||||
if (this._disabled) {
|
||||
if (this.isFocus() || this._disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -257,7 +238,7 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
text: this.cancelText,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
this.ionCancel.emit(null);
|
||||
this.ionCancel.emit(this);
|
||||
}
|
||||
}];
|
||||
|
||||
@@ -277,15 +258,14 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
this.interface = 'alert';
|
||||
}
|
||||
|
||||
let overlay: any;
|
||||
let overlay: ActionSheet | Alert;
|
||||
if (this.interface === 'action-sheet') {
|
||||
selectOptions.buttons = selectOptions.buttons.concat(options.map(input => {
|
||||
return {
|
||||
role: (input.selected ? 'selected' : ''),
|
||||
text: input.text,
|
||||
handler: () => {
|
||||
this.onChange(input.value);
|
||||
this.ionChange.emit(input.value);
|
||||
this.value = input.value;
|
||||
input.ionSelect.emit(input.value);
|
||||
}
|
||||
};
|
||||
@@ -340,19 +320,15 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
|
||||
overlay.addButton({
|
||||
text: this.okText,
|
||||
handler: (selectedValues: any) => {
|
||||
this.onChange(selectedValues);
|
||||
this.ionChange.emit(selectedValues);
|
||||
}
|
||||
handler: (selectedValues) => this.value = selectedValues
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
overlay.present(selectOptions);
|
||||
|
||||
this._isOpen = true;
|
||||
this._fireFocus();
|
||||
overlay.onDidDismiss(() => {
|
||||
this._isOpen = false;
|
||||
this._fireBlur();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -377,20 +353,6 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
return (this._multi ? this._texts : this._texts.join());
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
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
|
||||
@@ -399,25 +361,37 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
set options(val: QueryList<Option>) {
|
||||
this._options = val;
|
||||
|
||||
if (!this._values.length) {
|
||||
if (this._value.length === 0) {
|
||||
// there are no values set at this point
|
||||
// so check to see who should be selected
|
||||
this._values = val.filter(o => o.selected).map(o => o.value);
|
||||
// we use writeValue() because we don't want to update ngModel
|
||||
this.writeValue(val.filter(o => o.selected).map(o => o.value));
|
||||
}
|
||||
|
||||
this._updOpts();
|
||||
this._inputUpdated();
|
||||
}
|
||||
|
||||
_inputNormalize(val: any): string[] {
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
return [val + ''];
|
||||
}
|
||||
|
||||
_inputShouldChange(val: string[]): boolean {
|
||||
return !deepEqual(this._value, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_updOpts() {
|
||||
this._texts = [];
|
||||
_inputUpdated() {
|
||||
this._texts.length = 0;
|
||||
|
||||
if (this._options) {
|
||||
this._options.forEach(option => {
|
||||
// check this option if the option's value is in the values array
|
||||
option.selected = this._values.some(selectValue => {
|
||||
option.selected = this._value.some(selectValue => {
|
||||
return isCheckedProperty(selectValue, option.value);
|
||||
});
|
||||
|
||||
@@ -430,84 +404,4 @@ export class Select extends Ion implements AfterContentInit, ControlValueAccesso
|
||||
this._text = this._texts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
this._item && this._item.setElementClass('item-select-disabled', this._disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
console.debug('select, writeValue', val);
|
||||
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
|
||||
this._updOpts();
|
||||
this.checkHasValue(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
this._updOpts();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function): void {
|
||||
this._fn = fn;
|
||||
this.onChange = (val: any) => {
|
||||
console.debug('select, onChange', val);
|
||||
fn(val);
|
||||
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
|
||||
this._updOpts();
|
||||
this.checkHasValue(val);
|
||||
this.onTouched();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) { this.onTouched = fn; }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange(val: any) {
|
||||
// onChange used when there is not an formControlName
|
||||
console.debug('select, onChange w/out formControlName', val);
|
||||
this._values = (Array.isArray(val) ? val : isBlank(val) ? [] : [val]);
|
||||
this._updOpts();
|
||||
this.checkHasValue(val);
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched() { }
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form.deregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
30
src/components/select/test/select.spec.ts
Normal file
30
src/components/select/test/select.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
import { Select } from '../select';
|
||||
import { mockApp, mockConfig, mockElementRef, mockRenderer, mockItem, mockForm } from '../../../util/mock-providers';
|
||||
import { commonInputTest } from '../../../util/input-tester';
|
||||
|
||||
describe('Select', () => {
|
||||
|
||||
it('should pass common test', () => {
|
||||
|
||||
const app = mockApp();
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
const item: any = mockItem();
|
||||
const form = mockForm();
|
||||
const select = new Select(app, form, config, elementRef, renderer, item, null);
|
||||
|
||||
commonInputTest(select, {
|
||||
defaultValue: [],
|
||||
corpus: [
|
||||
[['hola'], ['hola']],
|
||||
[null, []],
|
||||
['hola', ['hola']],
|
||||
[['hola', 'adios'], ['hola', 'adios']]
|
||||
]
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -119,7 +119,7 @@
|
||||
<br>
|
||||
<code>date: {{month}}/{{year}}</code>
|
||||
<br>
|
||||
<code>status: {{status}}</code>
|
||||
<code>status: {{status | json}}</code>
|
||||
<br>
|
||||
<code>currency: {{currency | json}}</code>
|
||||
<br>
|
||||
|
||||
@@ -82,8 +82,8 @@ export class PageOne {
|
||||
console.log('Notification select', selectedValue);
|
||||
}
|
||||
|
||||
statusChange(ev: string) {
|
||||
this.status = ev;
|
||||
statusChange(ev: any) {
|
||||
this.status = ev.value;
|
||||
}
|
||||
|
||||
resetGender() {
|
||||
|
||||
@@ -233,17 +233,6 @@ export class Tab extends NavControllerBase {
|
||||
this._isShown = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, swipe to go back is enabled.
|
||||
*/
|
||||
@Input()
|
||||
get swipeBackEnabled(): boolean {
|
||||
return this._sbEnabled;
|
||||
}
|
||||
set swipeBackEnabled(val: boolean) {
|
||||
this._sbEnabled = isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, hide the tabs on child pages.
|
||||
*/
|
||||
|
||||
28
src/components/toggle/test/toggle.spec.ts
Normal file
28
src/components/toggle/test/toggle.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
import { Toggle } from '../toggle';
|
||||
import { mockConfig, mockPlatform, mockHaptic, mockElementRef, mockGestureController, mockRenderer, mockItem, mockForm, mockChangeDetectorRef } from '../../../util/mock-providers';
|
||||
import { commonInputTest, BOOLEAN_CORPUS } from '../../../util/input-tester';
|
||||
|
||||
describe('Toggle', () => {
|
||||
|
||||
it('should pass common test', () => {
|
||||
|
||||
const platform = mockPlatform();
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
const item: any = mockItem();
|
||||
const form = mockForm();
|
||||
const haptic = mockHaptic();
|
||||
const cd = mockChangeDetectorRef();
|
||||
const gesture = mockGestureController();
|
||||
const toggle = new Toggle(form, config, platform, elementRef, renderer, haptic, item, gesture, null, cd);
|
||||
|
||||
commonInputTest(toggle, {
|
||||
defaultValue: false,
|
||||
corpus: BOOLEAN_CORPUS,
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { AfterContentInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, HostListener, Input, OnDestroy, Optional, Output, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, HostListener, Input, OnDestroy, Optional, Renderer, ViewEncapsulation } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { Config } from '../../config/config';
|
||||
import { DomController } from '../../platform/dom-controller';
|
||||
import { Form, IonicTapInput } from '../../util/form';
|
||||
import { GestureController } from '../../gestures/gesture-controller';
|
||||
import { Haptic } from '../../tap-click/haptic';
|
||||
import { Ion } from '../ion';
|
||||
import { isTrueProperty, assert } from '../../util/util';
|
||||
import { assert, isTrueProperty } from '../../util/util';
|
||||
import { BaseInput } from '../../util/base-input';
|
||||
import { Item } from '../item/item';
|
||||
import { KEY_ENTER, KEY_SPACE } from '../../platform/key';
|
||||
import { Platform } from '../../platform/platform';
|
||||
@@ -60,14 +60,14 @@ export const TOGGLE_VALUE_ACCESSOR: any = {
|
||||
@Component({
|
||||
selector: 'ion-toggle',
|
||||
template:
|
||||
'<div class="toggle-icon" [class.toggle-checked]="_checked" [class.toggle-activated]="_activated">' +
|
||||
'<div class="toggle-icon" [class.toggle-checked]="_value" [class.toggle-activated]="_activated">' +
|
||||
'<div class="toggle-inner"></div>' +
|
||||
'</div>' +
|
||||
'<button role="checkbox" ' +
|
||||
'type="button" ' +
|
||||
'ion-button="item-cover" ' +
|
||||
'[id]="id" ' +
|
||||
'[attr.aria-checked]="_checked" ' +
|
||||
'[attr.aria-checked]="_value" ' +
|
||||
'[attr.aria-labelledby]="_labelId" ' +
|
||||
'[attr.aria-disabled]="_disabled" ' +
|
||||
'class="item-cover">' +
|
||||
@@ -78,57 +78,64 @@ export const TOGGLE_VALUE_ACCESSOR: any = {
|
||||
providers: [TOGGLE_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class Toggle extends Ion implements IonicTapInput, AfterContentInit, ControlValueAccessor, OnDestroy {
|
||||
export class Toggle extends BaseInput<boolean> implements IonicTapInput, AfterViewInit, OnDestroy {
|
||||
|
||||
_checked: boolean = false;
|
||||
_init: boolean = false;
|
||||
_disabled: boolean = false;
|
||||
_labelId: string;
|
||||
_activated: boolean = false;
|
||||
_startX: number;
|
||||
_msPrv: number = 0;
|
||||
_fn: Function = null;
|
||||
_gesture: ToggleGesture;
|
||||
|
||||
/** @hidden */
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @output {Toggle} Emitted when the toggle value changes.
|
||||
* @input {boolean} If true, the element is selected.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<Toggle> = new EventEmitter<Toggle>();
|
||||
@Input()
|
||||
get checked(): boolean {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set checked(val: boolean) {
|
||||
this.value = val;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public _form: Form,
|
||||
form: Form,
|
||||
config: Config,
|
||||
private _plt: Platform,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
private _haptic: Haptic,
|
||||
@Optional() public _item: Item,
|
||||
@Optional() item: Item,
|
||||
private _gestureCtrl: GestureController,
|
||||
private _domCtrl: DomController,
|
||||
private _cd: ChangeDetectorRef
|
||||
) {
|
||||
super(config, elementRef, renderer, 'toggle');
|
||||
_form.register(this);
|
||||
|
||||
if (_item) {
|
||||
this.id = 'tgl-' + _item.registerInput('toggle');
|
||||
this._labelId = 'lbl-' + _item.id;
|
||||
this._item.setElementClass('item-toggle', true);
|
||||
}
|
||||
super(config, elementRef, renderer, 'toggle', false, form, item, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterContentInit() {
|
||||
this._init = true;
|
||||
ngAfterViewInit() {
|
||||
this._initialize();
|
||||
this._gesture = new ToggleGesture(this._plt, this, this._gestureCtrl, this._domCtrl);
|
||||
this._gesture.listen();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputNormalize(val: any): boolean {
|
||||
return isTrueProperty(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputUpdated() {
|
||||
this._item && this._item.setElementClass('item-toggle-checked', this.value);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@@ -137,6 +144,7 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
|
||||
console.debug('toggle, _onDragStart', startX);
|
||||
|
||||
this._startX = startX;
|
||||
this._fireFocus();
|
||||
this._activated = true;
|
||||
}
|
||||
|
||||
@@ -151,16 +159,16 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
|
||||
|
||||
console.debug('toggle, _onDragMove', currentX);
|
||||
|
||||
if (this._checked) {
|
||||
if (this._value) {
|
||||
if (currentX + 15 < this._startX) {
|
||||
this.onChange(false);
|
||||
this.value = false;
|
||||
this._haptic.selection();
|
||||
this._startX = currentX;
|
||||
this._activated = true;
|
||||
}
|
||||
|
||||
} else if (currentX - 15 > this._startX) {
|
||||
this.onChange(true);
|
||||
this.value = true;
|
||||
this._haptic.selection();
|
||||
this._startX = currentX;
|
||||
this._activated = (currentX < this._startX + 5);
|
||||
@@ -177,98 +185,22 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
|
||||
}
|
||||
console.debug('toggle, _onDragEnd', endX);
|
||||
|
||||
if (this.checked) {
|
||||
if (this._value) {
|
||||
if (this._startX + 4 > endX) {
|
||||
this.onChange(false);
|
||||
this.value = false;
|
||||
this._haptic.selection();
|
||||
}
|
||||
|
||||
} else if (this._startX - 4 < endX) {
|
||||
this.onChange(true);
|
||||
this.value = true;
|
||||
this._haptic.selection();
|
||||
}
|
||||
|
||||
this._activated = false;
|
||||
this._fireBlur();
|
||||
this._startX = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the element is selected.
|
||||
*/
|
||||
@Input()
|
||||
get checked(): boolean {
|
||||
return this._checked;
|
||||
}
|
||||
|
||||
set checked(val: boolean) {
|
||||
this._setChecked(isTrueProperty(val));
|
||||
this.onChange(this._checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_setChecked(isChecked: boolean) {
|
||||
if (isChecked !== this._checked) {
|
||||
this._checked = isChecked;
|
||||
if (this._init) {
|
||||
this.ionChange.emit(this);
|
||||
}
|
||||
this._item && this._item.setElementClass('item-toggle-checked', isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
this._setChecked( isTrueProperty(val) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function): void {
|
||||
this._fn = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(val: boolean) {
|
||||
this._disabled = isTrueProperty(val);
|
||||
this._item && this._item.setElementClass('item-toggle-disabled', this._disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onChange(isChecked: boolean) {
|
||||
// used when this input does not have an ngModel or formControlName
|
||||
console.debug('toggle, onChange', isChecked);
|
||||
this._fn && this._fn(isChecked);
|
||||
this._setChecked(isChecked);
|
||||
this.onTouched();
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
onTouched() {}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
@@ -277,7 +209,7 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
|
||||
console.debug(`toggle, keyup: ${ev.keyCode}`);
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.onChange(!this._checked);
|
||||
this.value = !this.value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,20 +220,12 @@ export class Toggle extends Ion implements IonicTapInput, AfterContentInit, Cont
|
||||
this._elementRef.nativeElement.querySelector('button').focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form && this._form.deregister(this);
|
||||
super.ngOnDestroy();
|
||||
this._gesture && this._gesture.destroy();
|
||||
this._fn = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ export class E2EPage {
|
||||
counter: number = 0;
|
||||
|
||||
constructor(plt: Platform, public navCtrl: NavController) {
|
||||
for (var i = 0; i < 200; i++) {
|
||||
this.addItem();
|
||||
}
|
||||
|
||||
if (plt.is('ios')) {
|
||||
if (plt.testUserAgent('Safari')) {
|
||||
this.webview = ': iOS Safari';
|
||||
@@ -29,6 +25,14 @@ export class E2EPage {
|
||||
}
|
||||
}
|
||||
|
||||
addItems() {
|
||||
if (this.items.length === 0) {
|
||||
for (var i = 0; i < 200; i++) {
|
||||
this.addItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headerFn(record: any, index: number, records: any[]) {
|
||||
if (index % 4 === 0) {
|
||||
return index + ' is divisible by 4';
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
|
||||
</ion-list>
|
||||
|
||||
<div padding>
|
||||
<button ion-button (click)="addItems()">Add items</button>
|
||||
</div>
|
||||
|
||||
<div padding>
|
||||
<button ion-button (click)="pushPage()">Push Virtual Scroll Page</button>
|
||||
</div>
|
||||
|
||||
@@ -706,6 +706,14 @@ export class VirtualScroll implements DoCheck, AfterContentInit, OnDestroy {
|
||||
private _listeners() {
|
||||
assert(!this._scrollSub, '_listeners was already called');
|
||||
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._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));
|
||||
|
||||
@@ -317,40 +317,43 @@ export function updateDimensions(plt: Platform, nodes: VirtualNode[], cells: Vir
|
||||
data.topViewCell = totalCells;
|
||||
data.bottomViewCell = 0;
|
||||
|
||||
// completely realign position to ensure they're all accurately placed
|
||||
cell = cells[0];
|
||||
previousCell = {
|
||||
row: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: cell.top,
|
||||
left: 0,
|
||||
tmpl: -1
|
||||
};
|
||||
for (var i = 0; i < totalCells; i++) {
|
||||
cell = cells[i];
|
||||
if (totalCells > 0) {
|
||||
// completely realign position to ensure they're all accurately placed
|
||||
cell = cells[0];
|
||||
previousCell = {
|
||||
row: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: cell.top,
|
||||
left: 0,
|
||||
tmpl: -1
|
||||
};
|
||||
|
||||
if (previousCell.left + previousCell.width + cell.width > data.viewWidth) {
|
||||
// new row
|
||||
cell.row++;
|
||||
cell.top = (previousCell.top + previousCell.height);
|
||||
cell.left = 0;
|
||||
for (var i = 0; i < totalCells; i++) {
|
||||
cell = cells[i];
|
||||
|
||||
} else {
|
||||
// same row
|
||||
cell.row = previousCell.row;
|
||||
cell.top = previousCell.top;
|
||||
cell.left = (previousCell.left + previousCell.width);
|
||||
if (previousCell.left + previousCell.width + cell.width > data.viewWidth) {
|
||||
// new row
|
||||
cell.row++;
|
||||
cell.top = (previousCell.top + previousCell.height);
|
||||
cell.left = 0;
|
||||
|
||||
} else {
|
||||
// same row
|
||||
cell.row = previousCell.row;
|
||||
cell.top = previousCell.top;
|
||||
cell.left = (previousCell.left + previousCell.width);
|
||||
}
|
||||
|
||||
// figure out which cells are viewable within the viewport
|
||||
if (cell.top + cell.height > data.scrollTop && i < data.topViewCell) {
|
||||
data.topViewCell = i;
|
||||
|
||||
} else if (cell.top < viewableBottom && i > data.bottomViewCell) {
|
||||
data.bottomViewCell = i;
|
||||
}
|
||||
previousCell = cell;
|
||||
}
|
||||
|
||||
// figure out which cells are viewable within the viewport
|
||||
if (cell.top + cell.height > data.scrollTop && i < data.topViewCell) {
|
||||
data.topViewCell = i;
|
||||
|
||||
} else if (cell.top < viewableBottom && i > data.bottomViewCell) {
|
||||
data.bottomViewCell = i;
|
||||
}
|
||||
previousCell = cell;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentRef, ComponentFactoryResolver, ElementRef, EventEmitter, NgZone, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core';
|
||||
import { ComponentRef, Input, ComponentFactoryResolver, ElementRef, EventEmitter, NgZone, ReflectiveInjector, Renderer, ViewContainerRef } from '@angular/core';
|
||||
|
||||
import { AnimationOptions } from '../animations/animation';
|
||||
import { App } from '../components/app/app';
|
||||
@@ -9,7 +9,7 @@ import { setZIndex } from './nav-util';
|
||||
import { DeepLinker } from './deep-linker';
|
||||
import { DomController } from '../platform/dom-controller';
|
||||
import { GestureController } from '../gestures/gesture-controller';
|
||||
import { isBlank, isNumber, isPresent, assert, removeArrayItem } from '../util/util';
|
||||
import { isBlank, isNumber, isPresent, isTrueProperty, assert, removeArrayItem } from '../util/util';
|
||||
import { isViewController, ViewController } from './view-controller';
|
||||
import { Ion } from '../components/ion';
|
||||
import { Keyboard } from '../platform/keyboard';
|
||||
@@ -49,6 +49,15 @@ export class NavControllerBase extends Ion implements NavController {
|
||||
|
||||
id: string;
|
||||
|
||||
@Input()
|
||||
get swipeBackEnabled(): boolean {
|
||||
return this._sbEnabled;
|
||||
}
|
||||
set swipeBackEnabled(val: boolean) {
|
||||
this._sbEnabled = isTrueProperty(val);
|
||||
this._swipeBackCheck();
|
||||
}
|
||||
|
||||
constructor(
|
||||
public parent: any,
|
||||
public _app: App,
|
||||
|
||||
@@ -403,6 +403,11 @@ export abstract class NavController {
|
||||
*/
|
||||
config: Config;
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, swipe to go back is enabled.
|
||||
*/
|
||||
swipeBackEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Push a new component onto the current navigation stack. Pass any aditional information
|
||||
* along as an object. This additional information is accessible through NavParams
|
||||
|
||||
@@ -427,7 +427,7 @@ export class Platform {
|
||||
* if this registered action has the highest priority.
|
||||
* @param {number} priority Set the priority for this action. Only the highest priority will execute. Defaults to `0`.
|
||||
* @returns {Function} A function that, when called, will unregister
|
||||
* the its back button action.
|
||||
* the back button action.
|
||||
*/
|
||||
registerBackButtonAction(fn: Function, priority: number = 0): Function {
|
||||
const action: BackButtonAction = {fn, priority};
|
||||
|
||||
@@ -9,7 +9,7 @@ import { DomController } from '../platform/dom-controller';
|
||||
import { GestureController } from '../gestures/gesture-controller';
|
||||
import { Platform } from '../platform/platform';
|
||||
import { pointerCoord, hasPointerMoved } from '../util/dom';
|
||||
import { PointerEvents, POINTER_EVENT_TYPE_MOUSE } from '../gestures/pointer-events';
|
||||
import { PointerEvents, POINTER_EVENT_TYPE_TOUCH } from '../gestures/pointer-events';
|
||||
import { RippleActivator } from './ripple';
|
||||
import { UIEventManager } from '../gestures/ui-event-manager';
|
||||
|
||||
@@ -108,7 +108,7 @@ export class TapClick {
|
||||
this.activator.upAction(ev, activatableEle, this.startCoord);
|
||||
}
|
||||
}
|
||||
if (this.usePolyfill && pointerEventType === POINTER_EVENT_TYPE_MOUSE && this.app.isEnabled()) {
|
||||
if (this.usePolyfill && pointerEventType === POINTER_EVENT_TYPE_TOUCH && this.app.isEnabled()) {
|
||||
this.handleTapPolyfill(ev);
|
||||
}
|
||||
this.startCoord = null;
|
||||
|
||||
280
src/util/base-input.ts
Normal file
280
src/util/base-input.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { ElementRef, EventEmitter, Input, Output, Renderer } from '@angular/core';
|
||||
import { ControlValueAccessor } from '@angular/forms';
|
||||
import { NgControl } from '@angular/forms';
|
||||
|
||||
import { isPresent, isUndefined, isArray, isTrueProperty, deepCopy, assert } from './util';
|
||||
import { Ion } from '../components/ion';
|
||||
import { Config } from '../config/config';
|
||||
import { Item } from '../components/item/item';
|
||||
import { Form } from './form';
|
||||
import { TimeoutDebouncer } from './debouncer';
|
||||
|
||||
|
||||
export interface CommonInput<T> extends ControlValueAccessor {
|
||||
|
||||
id: string;
|
||||
disabled: boolean;
|
||||
value: T;
|
||||
|
||||
ionFocus: EventEmitter<CommonInput<T>>;
|
||||
ionChange: EventEmitter<BaseInput<T>>;
|
||||
ionBlur: EventEmitter<BaseInput<T>>;
|
||||
|
||||
initFocus(): void;
|
||||
isFocus(): boolean;
|
||||
|
||||
_inputNormalize(val: any): T;
|
||||
_inputShouldChange(val: T): boolean;
|
||||
_inputUpdated(): void;
|
||||
}
|
||||
|
||||
export class BaseInput<T> extends Ion implements CommonInput<T> {
|
||||
|
||||
_value: T;
|
||||
_onChanged: Function;
|
||||
_onTouched: Function;
|
||||
_isFocus: boolean = false;
|
||||
_labelId: string;
|
||||
_disabled: boolean = false;
|
||||
_debouncer: TimeoutDebouncer = new TimeoutDebouncer(0);
|
||||
_init: boolean = false;
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* @output {Range} Emitted when the range selector drag starts.
|
||||
*/
|
||||
@Output() ionFocus: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
|
||||
|
||||
/**
|
||||
* @output {Range} Emitted when the range value changes.
|
||||
*/
|
||||
@Output() ionChange: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
|
||||
|
||||
/**
|
||||
* @output {Range} Emitted when the range selector drag ends.
|
||||
*/
|
||||
@Output() ionBlur: EventEmitter<BaseInput<T>> = new EventEmitter<BaseInput<T>>();
|
||||
|
||||
/**
|
||||
* @input {boolean} If true, the user cannot interact with this element.
|
||||
*/
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
set disabled(val: boolean) {
|
||||
this.setDisabledState(val);
|
||||
}
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
elementRef: ElementRef,
|
||||
renderer: Renderer,
|
||||
name: string,
|
||||
private _defaultValue: T,
|
||||
public _form: Form,
|
||||
public _item: Item,
|
||||
ngControl: NgControl
|
||||
) {
|
||||
super(config, elementRef, renderer, name);
|
||||
_form && _form.register(this);
|
||||
this._value = deepCopy(this._defaultValue);
|
||||
|
||||
if (_item) {
|
||||
this.id = name + '-' + _item.registerInput(name);
|
||||
this._labelId = 'lbl-' + _item.id;
|
||||
this._item.setElementClass('item-' + name, true);
|
||||
}
|
||||
|
||||
// If the user passed a ngControl we need to set the valueAccessor
|
||||
if (ngControl) {
|
||||
ngControl.valueAccessor = this;
|
||||
}
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
set value(val: T) {
|
||||
if (this._writeValue(val)) {
|
||||
this.onChange();
|
||||
this._fireIonChange();
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Updates the value
|
||||
// 2. Calls _inputUpdated()
|
||||
// 3. Dispatch onChange events
|
||||
setValue(val: any) {
|
||||
this.value = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
setDisabledState(isDisabled: boolean) {
|
||||
this._disabled = isTrueProperty(isDisabled);
|
||||
this._item && this._item.setElementClass(`item-${this._componentName}-disabled`, isDisabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
writeValue(val: any) {
|
||||
if (this._writeValue(val)) {
|
||||
this._fireIonChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_writeValue(val: any): boolean {
|
||||
if (isUndefined(val)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = (val === null)
|
||||
? deepCopy(this._defaultValue)
|
||||
: this._inputNormalize(val);
|
||||
|
||||
const notUpdate = isUndefined(normalized) || !this._inputShouldChange(normalized);
|
||||
if (notUpdate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.debug('BaseInput: value changed:', normalized, this);
|
||||
this._value = normalized;
|
||||
this._inputCheckHasValue(normalized);
|
||||
this._inputUpdated();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_fireIonChange() {
|
||||
if (this._init) {
|
||||
this._debouncer.debounce(() => this.ionChange.emit(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnChange(fn: Function) {
|
||||
this._onChanged = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
registerOnTouched(fn: any) {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_initialize() {
|
||||
if (this._init) {
|
||||
assert(false, 'input was already initilized');
|
||||
return;
|
||||
}
|
||||
this._init = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_fireFocus() {
|
||||
if (this._isFocus) {
|
||||
return;
|
||||
}
|
||||
// assert(NgZone.isInAngularZone(), 'callback should be zoned');
|
||||
|
||||
this._isFocus = true;
|
||||
this.ionFocus.emit(this);
|
||||
this._inputUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_fireBlur() {
|
||||
if (!this._isFocus) {
|
||||
return;
|
||||
}
|
||||
this._isFocus = false;
|
||||
this.ionBlur.emit(this);
|
||||
this._inputUpdated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
private onChange() {
|
||||
this._onChanged && this._onChanged(this._value);
|
||||
this._onTouched && this._onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
isFocus(): boolean {
|
||||
return this._isFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._form && this._form.deregister(this);
|
||||
this._init = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
ngAfterViewInit() {
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputCheckHasValue(val: T) {
|
||||
if (!this._item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasValue = isArray(val)
|
||||
? val.length > 0
|
||||
: isPresent(val);
|
||||
|
||||
this._item.setElementClass('input-has-value', hasValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
initFocus() {}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputNormalize(val: any): T {
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputShouldChange(val: T): boolean {
|
||||
return this._value !== val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
_inputUpdated() {}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export function setupEvents(plt: Platform, dom: DomController): Events {
|
||||
let contentEle = <any>el.closest('.scroll-content');
|
||||
if (contentEle) {
|
||||
var style = contentEle.style;
|
||||
var scroll = new ScrollView(null, plt, dom, false);
|
||||
var scroll = new ScrollView(null, plt, dom);
|
||||
scroll._el = contentEle;
|
||||
// We need to stop scrolling if it's happening and scroll up
|
||||
|
||||
|
||||
252
src/util/input-tester.ts
Normal file
252
src/util/input-tester.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
|
||||
import { BaseInput } from './base-input';
|
||||
import { assert } from './util';
|
||||
|
||||
const lorem_ipsum = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi maximus nisl lobortis interdum condimentum. Cras volutpat, massa quis vehicula eleifend, turpis mauris sodales erat, ut varius ligula ipsum et turpis. Aliquam erat volutpat. Maecenas sodales pellentesque auctor. Suspendisse faucibus a erat sit amet pretium. Vestibulum nec tempus tellus. Mauris fringilla faucibus dui sed vestibulum. Curabitur porttitor consectetur nisl. Nulla porta, neque sed congue tempus, erat nunc rutrum diam, eu elementum sapien leo quis eros. Donec non convallis felis. Nam eu pharetra sapien.';
|
||||
|
||||
export const TEXT_CORPUS: any = [
|
||||
['hola', 'hola'],
|
||||
['', ''],
|
||||
[' ', ' '],
|
||||
['adiós', 'adiós'],
|
||||
['hola y adiós', 'hola y adiós'],
|
||||
[lorem_ipsum, lorem_ipsum]
|
||||
];
|
||||
|
||||
export const NUMBER_CORPUS: any[] = [
|
||||
[-1, -1],
|
||||
[0, 0],
|
||||
[-123456789, -123456789],
|
||||
[1.1234, 1.1234],
|
||||
[123456789, 123456789],
|
||||
['1.1234', 1.1234],
|
||||
['123456789', 123456789],
|
||||
['-123456789', -123456789]
|
||||
];
|
||||
|
||||
export const BOOLEAN_CORPUS: any[] = [
|
||||
[true, true],
|
||||
[false, false],
|
||||
['', true],
|
||||
['false', false],
|
||||
['true', true],
|
||||
];
|
||||
|
||||
export const ANY_CORPUS: any[] = [
|
||||
[true, true],
|
||||
[false, false],
|
||||
[0, 0],
|
||||
['', ''],
|
||||
[' ', ' '],
|
||||
['hola', 'hola']
|
||||
];
|
||||
|
||||
export interface TestConfig {
|
||||
defaultValue: any;
|
||||
corpus: any;
|
||||
}
|
||||
|
||||
|
||||
export function commonInputTest<T>(input: BaseInput<T>, config: TestConfig) {
|
||||
// TODO test form register/deregister
|
||||
// TODO test item classes
|
||||
// TODO test disable
|
||||
|
||||
testInput(input, config, false);
|
||||
input.ngAfterViewInit();
|
||||
testInput(input, config, true);
|
||||
input.ngOnDestroy();
|
||||
assert(!input._init, 'input was not destroyed correctly');
|
||||
}
|
||||
|
||||
function testInput<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
|
||||
testState(input, config, isInit);
|
||||
testWriteValue(input, config, isInit);
|
||||
testNgModelChange(input, config, isInit);
|
||||
}
|
||||
|
||||
function testState<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
|
||||
assertEqual(input._init, isInit, 'input must be init');
|
||||
assertEqual(input._isFocus, false, 'should not be focus');
|
||||
assertEqual(input.isFocus(), false, 'should not be focus');
|
||||
assertEqual(input.value, config.defaultValue, 'default value is wrong');
|
||||
|
||||
let blurCount = 0;
|
||||
let focusCount = 0;
|
||||
const subBlur = input.ionBlur.subscribe((ev: any) => {
|
||||
assertEqual(ev, input, 'ionBlur argument is wrong');
|
||||
blurCount++;
|
||||
});
|
||||
const subFocus = input.ionFocus.subscribe((ev: any) => {
|
||||
assertEqual(ev, input, 'ionFocus argument is wrong');
|
||||
focusCount++;
|
||||
});
|
||||
input._fireFocus();
|
||||
assertEqual(input._isFocus, true, 'should be focus');
|
||||
assertEqual(input.isFocus(), true, 'should be focus');
|
||||
input._fireFocus();
|
||||
|
||||
input._fireBlur();
|
||||
assertEqual(input._isFocus, false, 'should be not focus');
|
||||
assertEqual(input.isFocus(), false, 'should be not focus');
|
||||
input._fireBlur(); // it should not crash
|
||||
|
||||
assertEqual(focusCount, 1, 'ionFocus was not called correctly');
|
||||
assertEqual(blurCount, 1, 'ionBlur was not called correctly');
|
||||
|
||||
subBlur.unsubscribe();
|
||||
subFocus.unsubscribe();
|
||||
}
|
||||
|
||||
function testWriteValue<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
|
||||
let test: any;
|
||||
let i: number;
|
||||
let ionChangeCalled = 0;
|
||||
let OnChangeCalled = 0;
|
||||
let OnTouchedCalled = 0;
|
||||
let ngModelValue: any;
|
||||
|
||||
// Test ionChange
|
||||
let sub = input.ionChange.subscribe((ev: any) => {
|
||||
assertEqual(ionChangeCalled, 0, 'ionChange: internal error');
|
||||
assertEqual(ev, input, 'ionChange: ev is not the input');
|
||||
assertEqual(ev.value, test[1], 'ionChange: value does not match');
|
||||
assertEqual(ngModelValue, test[1], 'ionChange: ngmodel was not updated');
|
||||
|
||||
ionChangeCalled++;
|
||||
});
|
||||
|
||||
// Test registerOnChange
|
||||
input.registerOnChange((ev: any) => {
|
||||
assertEqual(OnChangeCalled, 0, 'registerOnChange: internal error');
|
||||
assertEqual(input.value, ev, 'registerOnChange: ev output does not match');
|
||||
assertEqual(input.value, test[1], 'registerOnChange: value does not match');
|
||||
ngModelValue = ev;
|
||||
OnChangeCalled++;
|
||||
});
|
||||
|
||||
// Test registerOnChange
|
||||
input.registerOnTouched(() => {
|
||||
assertEqual(OnTouchedCalled, 0, 'registerOnTouched: internal error');
|
||||
|
||||
OnTouchedCalled++;
|
||||
});
|
||||
|
||||
// Run corpus
|
||||
for (i = 0; i < config.corpus.length; i++) {
|
||||
test = config.corpus[i];
|
||||
input.value = test[0];
|
||||
assertEqual(input.value, test[1], 'loop: input/output does not match');
|
||||
if (isInit) {
|
||||
assertEqual(ionChangeCalled, 1, 'loop: ionChange error');
|
||||
} else {
|
||||
assertEqual(ionChangeCalled, 0, 'loop: ionChange error');
|
||||
}
|
||||
assertEqual(OnChangeCalled, 1, 'loop: OnChangeCalled was not called');
|
||||
assertEqual(OnTouchedCalled, 1, 'loop: OnTouchedCalled was not called');
|
||||
|
||||
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
|
||||
|
||||
console.log(test[0], input.value);
|
||||
// Set same value (it should not redispatch)
|
||||
input.value = test[0];
|
||||
assertEqual(ionChangeCalled, 0, 'loop: ionChange should not be called');
|
||||
assertEqual(OnChangeCalled, 0, 'loop: OnChangeCalled should not be called');
|
||||
// TODO OnTouchedCalled?
|
||||
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
|
||||
}
|
||||
|
||||
// Test undefined
|
||||
input.value = undefined;
|
||||
assertEqual(input.value, test[1], 'undefined should not change the value');
|
||||
assertEqual(ionChangeCalled, 0, 'undefined: ionChange should not be called');
|
||||
assertEqual(OnChangeCalled, 0, 'undefined: OnChangeCalled should not be called');
|
||||
assertEqual(OnTouchedCalled, 0, 'undefined: OnTouchedCalled should not be called');
|
||||
|
||||
|
||||
// Test null (reset)
|
||||
test = [null, config.defaultValue];
|
||||
input.value = null;
|
||||
assertEqual(input.value, config.defaultValue, 'null: wrong default value');
|
||||
assertEqual(OnChangeCalled, 1, 'null: OnChangeCalled was not called');
|
||||
assertEqual(OnTouchedCalled, 1, 'null: OnTouchedCalled was not called');
|
||||
|
||||
|
||||
input.registerOnChange(null);
|
||||
input.registerOnTouched(null);
|
||||
sub.unsubscribe();
|
||||
}
|
||||
|
||||
function testNgModelChange<T>(input: BaseInput<T>, config: TestConfig, isInit: boolean) {
|
||||
let test: any;
|
||||
let i: number;
|
||||
let ionChangeCalled = 0;
|
||||
let OnChangeCalled = 0;
|
||||
let OnTouchedCalled = 0;
|
||||
|
||||
// Test ionChange
|
||||
let sub = input.ionChange.subscribe((ev: any) => {
|
||||
assertEqual(ionChangeCalled, 0, 'internal error');
|
||||
assertEqual(ev, input, 'ev output does not match');
|
||||
assertEqual(test[1], ev.value, 'value does not match');
|
||||
|
||||
ionChangeCalled++;
|
||||
});
|
||||
|
||||
// Test registerOnChange
|
||||
input.registerOnChange((ev: any) => {
|
||||
OnChangeCalled++;
|
||||
});
|
||||
|
||||
// Test registerOnChange
|
||||
input.registerOnTouched(() => {
|
||||
OnTouchedCalled++;
|
||||
});
|
||||
|
||||
// Run corpus
|
||||
for (i = 0; i < config.corpus.length; i++) {
|
||||
test = config.corpus[i];
|
||||
input.writeValue(test[0]);
|
||||
|
||||
assertEqual(input.value, test[1], 'input/output does not match');
|
||||
if (isInit) {
|
||||
assertEqual(ionChangeCalled, 1, 'ionChange error');
|
||||
} else {
|
||||
assertEqual(ionChangeCalled, 0, 'ionChange error');
|
||||
}
|
||||
assertEqual(OnChangeCalled, 0, 'OnChangeCalled should not be called');
|
||||
assertEqual(OnTouchedCalled, 0, 'OnTouchedCalled should not be called');
|
||||
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
|
||||
|
||||
// Set same value (it should not redispatch)
|
||||
input.writeValue(test[0]);
|
||||
input.value = test[0];
|
||||
assertEqual(ionChangeCalled, 0, 'ionChange should not be called');
|
||||
assertEqual(OnChangeCalled, 0, 'OnChangeCalled should not be called');
|
||||
|
||||
// TODO OnTouchedCalled?
|
||||
OnTouchedCalled = OnChangeCalled = ionChangeCalled = 0;
|
||||
}
|
||||
|
||||
input.registerOnChange(null);
|
||||
input.registerOnTouched(null);
|
||||
sub.unsubscribe();
|
||||
input.value = config.defaultValue;
|
||||
}
|
||||
|
||||
function assertEqual(a: any, b: any, message: string) {
|
||||
if (!equal(a, b)) {
|
||||
assert(false, a + ' != ' + b + ' ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function equal(a: any, b: any): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
// return false;
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ import { ViewController } from '../navigation/view-controller';
|
||||
import { ModuleLoader } from './module-loader';
|
||||
import { NgModuleLoader } from './ng-module-loader';
|
||||
import { DeepLinkConfig, STATE_INITIALIZED } from '../navigation/nav-util';
|
||||
import { Ion } from '../components/ion';
|
||||
import { Item } from '../components/item/item';
|
||||
import { Form } from './form';
|
||||
|
||||
|
||||
export function mockConfig(config?: any, url: string = '/', platform?: Platform) {
|
||||
@@ -231,6 +234,14 @@ export function mockChangeDetectorRef(): ChangeDetectorRef {
|
||||
return cd;
|
||||
}
|
||||
|
||||
export function mockGestureController(app?: App): GestureController {
|
||||
if (!app) {
|
||||
app = mockApp();
|
||||
}
|
||||
return new GestureController(app);
|
||||
|
||||
}
|
||||
|
||||
export class MockElementRef implements ElementRef {
|
||||
nativeElement: any;
|
||||
constructor(ele: any) {
|
||||
@@ -242,7 +253,8 @@ export class MockElement {
|
||||
children: any[] = [];
|
||||
classList = new ClassList();
|
||||
attributes: { [name: string]: any } = {};
|
||||
style: {[property: string]: any} = {};
|
||||
style: { [property: string]: any } = {};
|
||||
nodeName: string = 'ION-MOCK';
|
||||
|
||||
clientWidth = 0;
|
||||
clientHeight = 0;
|
||||
@@ -258,6 +270,7 @@ export class MockElement {
|
||||
get className() {
|
||||
return this.classList.classes.join(' ');
|
||||
}
|
||||
|
||||
set className(val: string) {
|
||||
this.classList.classes = val.split(' ');
|
||||
}
|
||||
@@ -274,6 +287,10 @@ export class MockElement {
|
||||
this.attributes[name] = val;
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: Function, options?: any) { }
|
||||
|
||||
removeEventListener(type: string, listener: Function, options?: any) { }
|
||||
|
||||
removeAttribute(name: string) {
|
||||
delete this.attributes[name];
|
||||
}
|
||||
@@ -493,6 +510,25 @@ export function mockTab(parentTabs: Tabs): Tab {
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function mockForm(): Form {
|
||||
return new Form();
|
||||
}
|
||||
|
||||
export function mockIon(): Ion {
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
return new Ion(config, elementRef, renderer, 'ion');
|
||||
}
|
||||
|
||||
export function mockItem(): Item {
|
||||
const form = mockForm();
|
||||
const config = mockConfig();
|
||||
const elementRef = mockElementRef();
|
||||
const renderer = mockRenderer();
|
||||
return new Item(form, config, elementRef, renderer, null);
|
||||
}
|
||||
|
||||
export function mockTabs(app?: App): Tabs {
|
||||
let platform = mockPlatform();
|
||||
let config = mockConfig(null, '/', platform);
|
||||
|
||||
@@ -7,31 +7,28 @@ import { pointerCoord } from './dom';
|
||||
|
||||
|
||||
export class ScrollView {
|
||||
|
||||
ev: ScrollEvent;
|
||||
isScrolling = false;
|
||||
onScrollStart: (ev: ScrollEvent) => void;
|
||||
onScroll: (ev: ScrollEvent) => void;
|
||||
onScrollEnd: (ev: ScrollEvent) => void;
|
||||
initialized: boolean = false;
|
||||
eventsEnabled: boolean = false;
|
||||
contentTop: number;
|
||||
contentBottom: number;
|
||||
|
||||
_el: HTMLElement;
|
||||
|
||||
|
||||
private _eventsEnabled = false;
|
||||
private _js: boolean;
|
||||
private _t: number = 0;
|
||||
private _l: number = 0;
|
||||
private _lsn: Function;
|
||||
private _endTmr: Function;
|
||||
|
||||
|
||||
constructor(
|
||||
private _app: App,
|
||||
private _plt: Platform,
|
||||
private _dom: DomController,
|
||||
virtualScrollEventAssist: boolean
|
||||
private _dom: DomController
|
||||
) {
|
||||
this._js = virtualScrollEventAssist;
|
||||
this.ev = {
|
||||
timeStamp: 0,
|
||||
scrollTop: 0,
|
||||
@@ -57,19 +54,20 @@ export class ScrollView {
|
||||
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;
|
||||
if (this._js) {
|
||||
this.enableJsScroll();
|
||||
this.enableJsScroll(contentTop, contentBottom);
|
||||
} else {
|
||||
this.enableNativeScrolling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableEvents() {
|
||||
this._eventsEnabled = true;
|
||||
}
|
||||
|
||||
private enableNativeScrolling() {
|
||||
assert(this.onScrollStart, 'onScrollStart is not defined');
|
||||
assert(this.onScroll, 'onScroll is not defined');
|
||||
@@ -91,7 +89,7 @@ export class ScrollView {
|
||||
self._app.setScrolling();
|
||||
|
||||
// if events are disabled, we do nothing
|
||||
if (!self.eventsEnabled) {
|
||||
if (!self._eventsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -200,7 +198,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() {
|
||||
enableJsScroll(contentTop: number, contentBottom: number) {
|
||||
const self = this;
|
||||
self._js = true;
|
||||
const ele = self._el;
|
||||
@@ -219,7 +217,7 @@ export class ScrollView {
|
||||
function setMax() {
|
||||
if (!max) {
|
||||
// ******** DOM READ ****************
|
||||
max = ele.scrollHeight - ele.parentElement.offsetHeight + self.contentTop + self.contentBottom;
|
||||
max = ele.scrollHeight - ele.parentElement.offsetHeight + contentTop + contentBottom;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
55
src/util/test/base-input.spec.ts
Normal file
55
src/util/test/base-input.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
import { BaseInput } from '../base-input';
|
||||
import { Form } from '../form';
|
||||
import { Item } from '../../components/item/item';
|
||||
import { commonInputTest, ANY_CORPUS } from '../input-tester';
|
||||
import { mockConfig, mockPlatform, mockElementRef, mockRenderer, mockForm } from '../mock-providers';
|
||||
|
||||
let platform: any;
|
||||
let config: any;
|
||||
let elementRef: any;
|
||||
let renderer: any;
|
||||
|
||||
describe('BaseInput', () => {
|
||||
|
||||
it('should initialize', () => {
|
||||
const input = mockInput(null, null, null);
|
||||
|
||||
expect(input._init).toBeFalsy();
|
||||
expect(input._isFocus).toBeFalsy();
|
||||
expect(input._config).toEqual(config);
|
||||
expect(input._elementRef).toEqual(elementRef);
|
||||
|
||||
expect(input._renderer).toEqual(renderer);
|
||||
expect(input._componentName).toEqual('input');
|
||||
|
||||
expect(input.id).toBeUndefined();
|
||||
expect(input._labelId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should configure with item', () => {
|
||||
const form = new Form();
|
||||
const item = new Item(form, config, elementRef, renderer, null);
|
||||
const input = mockInput(form, item, null);
|
||||
|
||||
expect(input.id).toEqual('input-0-0');
|
||||
expect(input._labelId).toEqual('lbl-0');
|
||||
});
|
||||
|
||||
it('should pass base test', () => {
|
||||
const input = mockInput(mockForm(), null, null);
|
||||
commonInputTest(input, {
|
||||
defaultValue: null,
|
||||
corpus: ANY_CORPUS
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function mockInput(form: any, item: any, ngControl: any): BaseInput<any> {
|
||||
platform = mockPlatform();
|
||||
config = mockConfig(null, '/', platform);
|
||||
elementRef = mockElementRef();
|
||||
renderer = mockRenderer();
|
||||
return new BaseInput(config, elementRef, renderer, 'input', null, form, item, ngControl);
|
||||
}
|
||||
@@ -16,6 +16,14 @@ export function deepCopy(obj: any) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function deepEqual(a: any, b: any) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
return JSON.stringify(a) === JSON.stringify(b);
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
export function debounce(fn: Function, wait: number, immediate: boolean = false): any {
|
||||
var timeout: number, args: any, context: any, timestamp: number, result: any;
|
||||
@@ -63,25 +71,26 @@ export function defaults(dest: any, ...args: any[]) {
|
||||
|
||||
|
||||
/** @hidden */
|
||||
export function isBoolean(val: any) { return typeof val === 'boolean'; }
|
||||
export function isBoolean(val: any): val is boolean { return typeof val === 'boolean'; }
|
||||
/** @hidden */
|
||||
export function isString(val: any) { return typeof val === 'string'; }
|
||||
export function isString(val: any): val is string { return typeof val === 'string'; }
|
||||
/** @hidden */
|
||||
export function isNumber(val: any) { return typeof val === 'number'; }
|
||||
export function isNumber(val: any): val is number { return typeof val === 'number'; }
|
||||
/** @hidden */
|
||||
export function isFunction(val: any) { return typeof val === 'function'; }
|
||||
export function isFunction(val: any): val is Function { return typeof val === 'function'; }
|
||||
/** @hidden */
|
||||
export function isDefined(val: any) { return typeof val !== 'undefined'; }
|
||||
export function isDefined(val: any): boolean { return typeof val !== 'undefined'; }
|
||||
/** @hidden */
|
||||
export function isUndefined(val: any) { return typeof val === 'undefined'; }
|
||||
export function isUndefined(val: any): val is undefined { return typeof val === 'undefined'; }
|
||||
/** @hidden */
|
||||
export function isPresent(val: any) { return val !== undefined && val !== null; }
|
||||
export function isPresent(val: any): val is any { return val !== undefined && val !== null; }
|
||||
/** @hidden */
|
||||
export function isBlank(val: any) { return val === undefined || val === null; }
|
||||
export function isBlank(val: any): val is null { return val === undefined || val === null; }
|
||||
/** @hidden */
|
||||
export function isObject(val: any) { return typeof val === 'object'; }
|
||||
export function isObject(val: any): val is Object { return typeof val === 'object'; }
|
||||
/** @hidden */
|
||||
export function isArray(val: any) { return Array.isArray(val); };
|
||||
export function isArray(val: any): val is any[] { return Array.isArray(val); };
|
||||
|
||||
|
||||
|
||||
/** @hidden */
|
||||
|
||||
Reference in New Issue
Block a user