Merge branch '2.0' into layout-refactor

# Conflicts:
#	src/components/tabs/test/advanced/index.ts
This commit is contained in:
Adam Bradley
2016-06-21 11:37:47 -05:00
32 changed files with 876 additions and 297 deletions

View File

@ -1,3 +1,61 @@
<a name="2.0.0-beta.9"></a>
# [2.0.0-beta.9](https://github.com/driftyco/ionic/compare/v2.0.0-beta.8...v2.0.0-beta.9) (2016-06-16)
### Features
* **backButton:** register back button actions ([84f37cf](https://github.com/driftyco/ionic/commit/84f37cf))
* **item:** add the ability to show a forward arrow on md and wp modes ([c41f24d](https://github.com/driftyco/ionic/commit/c41f24d))
* **item:** two-way sliding of items ([c28aa53](https://github.com/driftyco/ionic/commit/c28aa53)), closes [#5073](https://github.com/driftyco/ionic/issues/5073)
* **item-sliding:** two-way item sliding gestures ([5d873ff](https://github.com/driftyco/ionic/commit/5d873ff))
* **modal:** background click and escape key dismiss (#6831) ([e5473b6](https://github.com/driftyco/ionic/commit/e5473b6)), closes [#6738](https://github.com/driftyco/ionic/issues/6738)
* **navPop:** add nav pop method on the app instance ([9f293e8](https://github.com/driftyco/ionic/commit/9f293e8))
* **popover:** background dismiss, escape dismiss ([1d78f78](https://github.com/driftyco/ionic/commit/1d78f78)), closes [#6817](https://github.com/driftyco/ionic/issues/6817)
* **range:** range can be disabled ([ccd926b](https://github.com/driftyco/ionic/commit/ccd926b))
* **select:** add placeholder as an input for select ([461ba11](https://github.com/driftyco/ionic/commit/461ba11)), closes [#6862](https://github.com/driftyco/ionic/issues/6862)
* **tabs:** track tab selecting history, create previousTab() method ([d98f3c9](https://github.com/driftyco/ionic/commit/d98f3c9))
### Bug Fixes
* **button:** check for icon and add css after content checked ([f7b2ea2](https://github.com/driftyco/ionic/commit/f7b2ea2)), closes [#6662](https://github.com/driftyco/ionic/issues/6662)
* **click-block:** click block is now showing on all screns. ([761a1f6](https://github.com/driftyco/ionic/commit/761a1f6))
* **click-block:** fix for the click block logic ([9b78aeb](https://github.com/driftyco/ionic/commit/9b78aeb))
* **datetime:** add styling for datetime with different labels ([adcd2fc](https://github.com/driftyco/ionic/commit/adcd2fc)), closes [#6764](https://github.com/driftyco/ionic/issues/6764)
* **decorators:** change to match angular style guide ([9315c68](https://github.com/driftyco/ionic/commit/9315c68))
* **item:** change ion-item-swiping to use .item-wrapper css instead ([31f62e7](https://github.com/driftyco/ionic/commit/31f62e7))
* **item:** encode hex value in the detail arrow so it works on firefox ([03986d4](https://github.com/driftyco/ionic/commit/03986d4)), closes [#6830](https://github.com/driftyco/ionic/issues/6830)
* **item:** improve open/close logic, update demos ([db9fa7e](https://github.com/driftyco/ionic/commit/db9fa7e))
* **item:** item-options width calculated correctly ([64af0c8](https://github.com/driftyco/ionic/commit/64af0c8))
* **item:** sliding item supports dynamic options + tests ([14d29e6](https://github.com/driftyco/ionic/commit/14d29e6)), closes [#5192](https://github.com/driftyco/ionic/issues/5192)
* **item:** sliding item's width must be 100% ([efcdd20](https://github.com/driftyco/ionic/commit/efcdd20))
* **menu:** push/overlay working correctly in landscape ([0c88589](https://github.com/driftyco/ionic/commit/0c88589))
* **menu:** swiping menu distinguishes between opening and closing direction ([29791f8](https://github.com/driftyco/ionic/commit/29791f8)), closes [#5511](https://github.com/driftyco/ionic/issues/5511)
* **Menu:** fix right overlay menu when rotating device ([07d55c5](https://github.com/driftyco/ionic/commit/07d55c5))
* **modal:** add status bar padding to modal ([181129b](https://github.com/driftyco/ionic/commit/181129b))
* **modal:** change modal display so you can scroll the entire height ([01bbc94](https://github.com/driftyco/ionic/commit/01bbc94)), closes [#6839](https://github.com/driftyco/ionic/issues/6839)
* **navigation:** keep the click block up longer if the keyboard is open (#6884) ([d6b7d5d](https://github.com/driftyco/ionic/commit/d6b7d5d))
* **popover:** allow target element to be positioned at left:0 ([ea450d4](https://github.com/driftyco/ionic/commit/ea450d4)), closes [#6896](https://github.com/driftyco/ionic/issues/6896)
* **popover:** hide arrow if no event was passed ([8350df0](https://github.com/driftyco/ionic/commit/8350df0)), closes [#6796](https://github.com/driftyco/ionic/issues/6796)
* **range:** bar height for ios should be 1px, add disabled for wp ([f2a9f2d](https://github.com/driftyco/ionic/commit/f2a9f2d))
* **range:** stop sliding after releasing mouse outside the window ([9b2e934](https://github.com/driftyco/ionic/commit/9b2e934)), closes [#6802](https://github.com/driftyco/ionic/issues/6802)
* **scrollView:** ensure scroll element exists for event listeners ([1188730](https://github.com/driftyco/ionic/commit/1188730))
* **searchbar:** add opacity so the searchbar doesn't show when it's moved over ([b5f93f9](https://github.com/driftyco/ionic/commit/b5f93f9))
* **searchbar:** only trigger the input event on clear if there is a value ([99fdcc0](https://github.com/driftyco/ionic/commit/99fdcc0)), closes [#6382](https://github.com/driftyco/ionic/issues/6382)
* **searchbar:** position elements when the value changes not after content checked ([31c7e59](https://github.com/driftyco/ionic/commit/31c7e59))
* **searchbar:** set a negative tabindex for the cancel button ([614ace4](https://github.com/driftyco/ionic/commit/614ace4))
* **searchbar:** use the contrast color for the background in a toolbar ([b4028c6](https://github.com/driftyco/ionic/commit/b4028c6)), closes [#6379](https://github.com/driftyco/ionic/issues/6379)
* **tabs:** reduce padding on tabs for ios ([fd9cdc7](https://github.com/driftyco/ionic/commit/fd9cdc7)), closes [#6679](https://github.com/driftyco/ionic/issues/6679)
* **tap:** export isActivatable as a const so its transpiled correctly ([ce3da97](https://github.com/driftyco/ionic/commit/ce3da97))
* **toast:** close toasts when two or more are open (#6814) ([8ff2476](https://github.com/driftyco/ionic/commit/8ff2476)), closes [(#6814](https://github.com/(/issues/6814)
* **toast:** toast will now be enabled (#6904) ([c068828](https://github.com/driftyco/ionic/commit/c068828))
* **virtualScroll:** detect changes in individual nodes ([f049521](https://github.com/driftyco/ionic/commit/f049521)), closes [#6137](https://github.com/driftyco/ionic/issues/6137)
### Performance Improvements
* **virtualScroll:** improve UIWebView virtual scroll ([ff1daa6](https://github.com/driftyco/ionic/commit/ff1daa6))
<a name="2.0.0-beta.8"></a> <a name="2.0.0-beta.8"></a>
# [2.0.0-beta.8](https://github.com/driftyco/ionic/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2016-06-06) # [2.0.0-beta.8](https://github.com/driftyco/ionic/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2016-06-06)

View File

@ -1,7 +1,7 @@
{ {
"private": "true", "private": "true",
"name": "ionic2", "name": "ionic2",
"version": "2.0.0-beta.8", "version": "2.0.0-beta.9",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -11,7 +11,7 @@ In the root of the package are ES5 sources in the CommonJS module format, their
Usually, the only import required by the user is `ionic-angular`, as everything from Ionic is exported by the package: Usually, the only import required by the user is `ionic-angular`, as everything from Ionic is exported by the package:
``` ```
import {App, Page} from 'ionic-angular'; import {App} from 'ionic-angular';
``` ```
### Bundles ### Bundles

View File

@ -186,6 +186,13 @@ export class Content extends Ion {
}; };
} }
/**
* @private
*/
getScrollElement(): HTMLElement {
return this._scrollEle;
}
/** /**
* @private * @private
* Call a method when scrolling has stopped * Call a method when scrolling has stopped

View File

@ -143,7 +143,7 @@ export class Icon {
css += this._name; css += this._name;
} }
if (this.mode === 'ios' && !this.isActive) { if (this.mode === 'ios' && !this.isActive && css.indexOf('logo') < 0) {
css += '-outline'; css += '-outline';
} }

View File

@ -31,7 +31,7 @@ export class NativeInput {
} }
@HostListener('input', ['$event']) @HostListener('input', ['$event'])
private _change(ev) { private _change(ev: any) {
this.valueChange.emit(ev.target.value); this.valueChange.emit(ev.target.value);
} }
@ -41,8 +41,8 @@ export class NativeInput {
self.focusChange.emit(true); self.focusChange.emit(true);
function docTouchEnd(ev) { function docTouchEnd(ev: TouchEvent) {
var tapped: HTMLElement = ev.target; var tapped = <HTMLElement>ev.target;
if (tapped && self.element()) { if (tapped && self.element()) {
if (tapped.tagName !== 'INPUT' && tapped.tagName !== 'TEXTAREA' && !tapped.classList.contains('input-cover')) { if (tapped.tagName !== 'INPUT' && tapped.tagName !== 'TEXTAREA' && !tapped.classList.contains('input-cover')) {
self.element().blur(); self.element().blur();
@ -178,7 +178,7 @@ export class NativeInput {
} }
function cloneInput(focusedInputEle, addCssClass) { function cloneInput(focusedInputEle: any, addCssClass: string) {
let clonedInputEle = focusedInputEle.cloneNode(true); let clonedInputEle = focusedInputEle.cloneNode(true);
clonedInputEle.classList.add('cloned-input'); clonedInputEle.classList.add('cloned-input');
clonedInputEle.classList.add(addCssClass); clonedInputEle.classList.add(addCssClass);
@ -191,7 +191,7 @@ function cloneInput(focusedInputEle, addCssClass) {
return clonedInputEle; return clonedInputEle;
} }
function removeClone(focusedInputEle, queryCssClass) { function removeClone(focusedInputEle: any, queryCssClass: string) {
let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass); let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass);
if (clonedInputEle) { if (clonedInputEle) {
clonedInputEle.parentNode.removeChild(clonedInputEle); clonedInputEle.parentNode.removeChild(clonedInputEle);

View File

@ -0,0 +1,148 @@
import {Item} from './item';
import {List} from '../list/list';
import {UIEventManager} from '../../util/ui-event-manager';
import {closest, Coordinates, pointerCoord, CSS, nativeRaf} from '../../util/dom';
const AUTO_SCROLL_MARGIN = 60;
const SCROLL_JUMP = 10;
const ITEM_REORDER_ACTIVE = 'reorder-active';
/**
* @private
*/
export class ItemReorderGesture {
private selectedItem: Item = null;
private offset: Coordinates;
private lastToIndex: number;
private lastYcoord: number;
private emptyZone: boolean;
private itemHeight: number;
private windowHeight: number;
private events: UIEventManager = new UIEventManager(false);
constructor(public list: List) {
let element = this.list.getNativeElement();
this.events.pointerEvents(element,
(ev: any) => this.onDragStart(ev),
(ev: any) => this.onDragMove(ev),
(ev: any) => this.onDragEnd(ev));
}
private onDragStart(ev: any): boolean {
let itemEle = ev.target;
if (itemEle.nodeName !== 'ION-REORDER') {
return false;
}
let item = itemEle['$ionComponent'];
if (!item) {
console.error('item does not contain ion component');
return false;
}
ev.preventDefault();
// Preparing state
this.offset = pointerCoord(ev);
this.offset.y += this.list.scrollContent(0);
this.selectedItem = item;
this.itemHeight = item.height();
this.lastToIndex = item.index;
this.windowHeight = window.innerHeight - AUTO_SCROLL_MARGIN;
item.setCssClass(ITEM_REORDER_ACTIVE, true);
return true;
}
private onDragMove(ev: any) {
if (!this.selectedItem) {
return;
}
ev.preventDefault();
// Get coordinate
var coord = pointerCoord(ev);
// Scroll if we reach the scroll margins
let scrollPosition = this.scroll(coord);
// Update selected item position
let ydiff = Math.round(coord.y - this.offset.y + scrollPosition);
this.selectedItem.setCssStyle(CSS.transform, `translateY(${ydiff}px)`);
// Only perform hit test if we moved at least 30px from previous position
if (Math.abs(coord.y - this.lastYcoord) < 30) {
return;
}
// Hit test
let overItem = this.itemForCoord(coord);
if (!overItem) {
this.emptyZone = true;
return;
}
// Move surrounding items if needed
let toIndex = overItem.index;
if (toIndex !== this.lastToIndex || this.emptyZone) {
let fromIndex = this.selectedItem.index;
this.lastToIndex = overItem.index;
this.lastYcoord = coord.y;
this.emptyZone = false;
nativeRaf(() => {
this.list.reorderMove(fromIndex, toIndex, this.itemHeight);
});
}
}
private onDragEnd(ev: any) {
if (!this.selectedItem) {
return;
}
nativeRaf(() => {
let toIndex = this.lastToIndex;
let fromIndex = this.selectedItem.index;
this.selectedItem.setCssClass(ITEM_REORDER_ACTIVE, false);
this.selectedItem = null;
this.list.reorderEmit(fromIndex, toIndex);
});
}
private itemForCoord(coord: Coordinates): Item {
let element = <any>document.elementFromPoint(this.offset.x - 100, coord.y);
if (!element) {
return null;
}
element = closest(element, 'ion-item', true);
if (!element) {
return null;
}
let item = <Item>(<any>element)['$ionComponent'];
if (!item) {
console.error('item does not have $ionComponent');
return null;
}
return item;
}
private scroll(coord: Coordinates): number {
let scrollDiff = 0;
if (coord.y < AUTO_SCROLL_MARGIN) {
scrollDiff = -SCROLL_JUMP;
} else if (coord.y > this.windowHeight) {
scrollDiff = SCROLL_JUMP;
}
return this.list.scrollContent(scrollDiff);
}
/**
* @private
*/
destroy() {
this.events.unlistenAll();
this.events = null;
this.list = null;
}
}

View File

@ -0,0 +1,48 @@
// Item reorder
// --------------------------------------------------
ion-reorder {
display: none;
flex: 1;
align-items: center;
justify-content: center;
max-width: 40px;
height: 100%;
font-size: 1.6em;
pointer-events: all;
touch-action: manipulation;
ion-icon {
pointer-events: none;
}
}
.reorder-enabled {
ion-item {
will-change: transform;
}
ion-reorder {
display: flex;
}
}
ion-item.reorder-active {
z-index: 4;
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
opacity: .8;
transition: none;
pointer-events: none;
ion-reorder {
pointer-events: none;
}
}

View File

@ -0,0 +1,17 @@
import {Component, ElementRef, Inject, forwardRef} from '@angular/core';
import {Item} from './item';
/**
* @private
*/
@Component({
selector: 'ion-reorder',
template: `<ion-icon name="menu"></ion-icon>`
})
export class ItemReorder {
constructor(
@Inject(forwardRef(() => Item)) item: Item,
elementRef: ElementRef) {
elementRef.nativeElement['$ionComponent'] = item;
}
}

View File

@ -12,8 +12,8 @@ export class ItemSlidingGesture extends DragGesture {
selectedContainer: ItemSliding = null; selectedContainer: ItemSliding = null;
openContainer: ItemSliding = null; openContainer: ItemSliding = null;
constructor(public list: List, public listEle: HTMLElement) { constructor(public list: List) {
super(listEle, { super(list.getNativeElement(), {
direction: 'x', direction: 'x',
threshold: DRAG_THRESHOLD threshold: DRAG_THRESHOLD
}); });

View File

@ -73,7 +73,7 @@ ion-item-sliding.active-slide {
opacity: 1; opacity: 1;
transition: all 300ms cubic-bezier(.36, .66, .04, 1); transition: all 300ms cubic-bezier(.36, .66, .04, 1);
pointer-events: all; pointer-events: none;
} }
ion-item-options { ion-item-options {

View File

@ -16,13 +16,40 @@ export const enum SideFlags {
} }
/** /**
* @private * @name ItemOptions
* @description
* The option buttons for an `ion-item-sliding`. These buttons can be placed either on the left or right side.
* You can combind the `(ionSiwpe)` event plus the `expandable` directive to create a full swipe action for the item.
*
* @usage
*
* ```html
* <ion-item-sliding>
* <ion-item>
* Item 1
* </ion-item>
* <ion-item-options side="right" (ionSwipe)="saveItem(item)">
* <button expandable (click)="saveItem(item)">
* <ion-icon name="star"></ion-icon>
* </button>
* </ion-item-options>
* </ion-item-sliding>
*```
*/ */
@Directive({ @Directive({
selector: 'ion-item-options', selector: 'ion-item-options',
}) })
export class ItemOptions { export class ItemOptions {
/**
* @input {string} the side the option button should be on. Defaults to right
* If you have multiple `ion-item-options`, a side must be provided for each.
*/
@Input() side: string; @Input() side: string;
/**
* @output {event} Expression to evaluate when the item has been fully swiped.
*/
@Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter(); @Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter();
constructor(private _elementRef: ElementRef, private _renderer: Renderer) { constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
@ -46,6 +73,9 @@ export class ItemOptions {
} }
} }
/**
* @private
*/
width() { width() {
return this._elementRef.nativeElement.offsetWidth; return this._elementRef.nativeElement.offsetWidth;
} }
@ -62,12 +92,30 @@ const enum SlidingState {
/** /**
* @name ItemSliding * @name ItemSliding
*
* @description * @description
* A sliding item is a list item that can be swiped to reveal buttons. It requires * A sliding item is a list item that can be swiped to reveal buttons. It requires
* an [Item](../Item) component as a child and a [List](../../list/List) component as * an [Item](../Item) component as a child and a [List](../../list/List) component as
* a parent. All buttons to reveal can be placed in the `<ion-item-options>` element. * a parent. All buttons to reveal can be placed in the `<ion-item-options>` element.
* *
* @usage
* ```html
* <ion-list>
* <ion-item-sliding #item>
* <ion-item>
* Item
* </ion-item>
* <ion-item-options side="left">
* <button (click)="favorite(item)">Favorite</button>
* <button danger (click)="share(item)">Share</button>
* </ion-item-options>
* <ion-item-options side="right">
* <button (click)="unread(item)">Unread</button>
* </ion-item-options>
* </ion-item-sliding>
* </ion-list>
* ```
*
* ### Swipe Direction * ### Swipe Direction
* By default, the buttons are revealed when the sliding item is swiped from right to left, * By default, the buttons are revealed when the sliding item is swiped from right to left,
* so the buttons are placed in the right side. But it's also possible to reveal them * so the buttons are placed in the right side. But it's also possible to reveal them
@ -83,7 +131,7 @@ const enum SlidingState {
* </button> * </button>
* </ion-item-options> * </ion-item-options>
* <ion-item-options> * <ion-item-options side="left">
* <button (click)="archive(item)"> * <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon> * <ion-icon name="archive"></ion-icon>
* Archive * Archive
@ -96,19 +144,12 @@ const enum SlidingState {
* to the (ionDrag)` event. * to the (ionDrag)` event.
* *
* ```html * ```html
* <ion-item-options side="right"> * <ion-item-sliding (ionDrag)="logDrag($event)">
* <button (click)="archive(item)"> * <ion-item>Item</ion-item>
* <ion-icon name="archive"></ion-icon> * <ion-item-options>
* Archive * <button>Favorite</button>
* </button> * </ion-item-options>
* </ion-item-options> * </ion-item-sliding>
* <ion-item-options>
* <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon>
* Archive
* </button>
* </ion-item-options>
* ``` * ```
* *
* ### Button Layout * ### Button Layout
@ -118,32 +159,15 @@ const enum SlidingState {
* `<ion-item-options>` element. * `<ion-item-options>` element.
* *
* ```html * ```html
* <ion-item-sliding (ionDrag)="ondrag($event)"> * <ion-item-options icon-left>
* <ion-item>Item</ion-item> * <button (click)="archive(item)">
* <ion-item-options> * <ion-icon name="archive"></ion-icon>
* <button>Favorite</button> * Archive
* </ion-item-options> * </button>
* </ion-item-sliding> * </ion-item-options>
*
* ``` * ```
* *
* @usage
* ```html
* <ion-list>
* <ion-item-sliding #item>
* <ion-item>
* Item
* </ion-item>
* <ion-item-options>
* <button (click)="favorite(item)">Favorite</button>
* <button danger (click)="share(item)">Share</button>
* </ion-item-options>
* <ion-item-options side="right">
* <button (click)="unread(item)">Unread</button>
* </ion-item-options>
* </ion-item-sliding>
* </ion-list>
* ```
* *
* @demo /docs/v2/demos/item-sliding/ * @demo /docs/v2/demos/item-sliding/
* @see {@link /docs/v2/components#lists List Component Docs} * @see {@link /docs/v2/components#lists List Component Docs}
@ -169,8 +193,15 @@ export class ItemSliding {
private _rightOptions: ItemOptions; private _rightOptions: ItemOptions;
private _optsDirty: boolean = true; private _optsDirty: boolean = true;
private _state: SlidingState = SlidingState.Disabled; private _state: SlidingState = SlidingState.Disabled;
/**
* @private
* */
slidingPercent: number = 0; slidingPercent: number = 0;
/**
* @private
* */
@ContentChild(Item) private item: Item; @ContentChild(Item) private item: Item;
@ -290,6 +321,9 @@ export class ItemSliding {
return restingPoint; return restingPoint;
} }
/**
* @private
* */
fireSwipeEvent() { fireSwipeEvent() {
if (this.slidingPercent > SWIPE_FACTOR) { if (this.slidingPercent > SWIPE_FACTOR) {
this._rightOptions.ionSwipe.emit(this); this._rightOptions.ionSwipe.emit(this);
@ -298,6 +332,9 @@ export class ItemSliding {
} }
} }
/**
* @private
* */
calculateOptsWidth() { calculateOptsWidth() {
nativeRaf(() => { nativeRaf(() => {
if (this._optsDirty) { if (this._optsDirty) {
@ -382,7 +419,7 @@ export class ItemSliding {
/** /**
* Close the sliding item. Items can also be closed from the [List](../../list/List). * Close the sliding item. Items can also be closed from the [List](../../list/List).
* *
* The sliding item can be closed by garbbing a reference to `ItemSliding`. In the * The sliding item can be closed by grabbing a reference to `ItemSliding`. In the
* below example, the template reference variable `slidingItem` is placed on the element * below example, the template reference variable `slidingItem` is placed on the element
* and passed to the `share` method. * and passed to the `share` method.
* *

View File

@ -85,3 +85,4 @@ ion-input.item {
@import "item-media"; @import "item-media";
@import "item-sliding"; @import "item-sliding";
@import "item-reorder";

View File

@ -1,9 +1,10 @@
import {Component, ContentChildren, forwardRef, ViewChild, ContentChild, Renderer, ElementRef, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; import {Component, ContentChildren, forwardRef, Input, ViewChild, ContentChild, Renderer, ElementRef, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import {Button} from '../button/button'; import {Button} from '../button/button';
import {Form} from '../../util/form'; import {Form} from '../../util/form';
import {Icon} from '../icon/icon'; import {Icon} from '../icon/icon';
import {Label} from '../label/label'; import {Label} from '../label/label';
import {ItemReorder} from './item-reorder';
/** /**
@ -235,11 +236,13 @@ import {Label} from '../label/label';
'<ng-content select="ion-select,ion-input,ion-textarea,ion-datetime,ion-range,[item-content]"></ng-content>' + '<ng-content select="ion-select,ion-input,ion-textarea,ion-datetime,ion-range,[item-content]"></ng-content>' +
'</div>' + '</div>' +
'<ng-content select="[item-right],ion-radio,ion-toggle"></ng-content>' + '<ng-content select="[item-right],ion-radio,ion-toggle"></ng-content>' +
'<ion-reorder></ion-reorder>' +
'</div>' + '</div>' +
'<ion-button-effect></ion-button-effect>', '<ion-button-effect></ion-button-effect>',
host: { host: {
'class': 'item' 'class': 'item'
}, },
directives: [forwardRef(() => ItemReorder)],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
@ -249,6 +252,11 @@ export class Item {
private _label: Label; private _label: Label;
private _viewLabel: boolean = true; private _viewLabel: boolean = true;
/**
* @private
*/
@Input() index: number;
/** /**
* @private * @private
*/ */
@ -261,6 +269,7 @@ export class Item {
constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) { constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
this.id = form.nextId().toString(); this.id = form.nextId().toString();
_elementRef.nativeElement['$ionComponent'] = this;
} }
/** /**
@ -354,4 +363,11 @@ export class Item {
icon.addClass('item-icon'); icon.addClass('item-icon');
}); });
} }
/**
* @private
*/
height(): number {
return this._elementRef.nativeElement.offsetHeight;
}
} }

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,30 @@
import {Component, ChangeDetectorRef} from '@angular/core';
import {ionicBootstrap} from '../../../../../src';
@Component({
templateUrl: 'main.html'
})
class E2EPage {
items: any[] = [];
isReordering: boolean = false;
constructor(private d: ChangeDetectorRef) {
let nu = 30;
for (let i = 0; i < nu; i++) {
this.items.push(i);
}
}
toggle() {
this.isReordering = !this.isReordering;
}
reorder(indexes: any) {
let element = this.items[indexes.from];
this.items.splice(indexes.from, 1);
this.items.splice(indexes.to, 0, element);
}
}
ionicBootstrap(E2EPage);

View File

@ -0,0 +1,22 @@
<ion-toolbar primary>
<ion-title>Reorder items</ion-title>
<ion-buttons end>
<button (click)="toggle()">
Edit
</button>
</ion-buttons>
</ion-toolbar>
<ion-content>
<ion-list [reorder]="isReordering" (ionItemReorder)="reorder($event)">
<ion-item *ngFor="let item of items; let index=index"
[index]="index"
[style.background]="'rgb('+(255-item*4)+','+(255-item*4)+','+(255-item*4)+')'"
[style.height]="item*2+35+'px'">
{{item}}
</ion-item>
</ion-list>
</ion-content>

View File

@ -1,7 +1,11 @@
import {Directive, ElementRef, Renderer, Attribute, NgZone} from '@angular/core'; import {Directive, ElementRef, EventEmitter, Renderer, Input, Optional, Output, Attribute, NgZone} from '@angular/core';
import {Content} from '../content/content';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {ItemSlidingGesture} from '../item/item-sliding-gesture'; import {ItemSlidingGesture} from '../item/item-sliding-gesture';
import {ItemReorderGesture} from '../item/item-reorder-gesture';
import {isTrueProperty} from '../../util/util';
import {nativeTimeout} from '../../util/dom';
/** /**
* The List is a widely used interface element in almost any mobile app, * The List is a widely used interface element in almost any mobile app,
@ -20,32 +24,30 @@ import {ItemSlidingGesture} from '../item/item-sliding-gesture';
* *
*/ */
@Directive({ @Directive({
selector: 'ion-list' selector: 'ion-list',
host: {
'[class.reorder-enabled]': '_enableReorder',
}
}) })
export class List extends Ion { export class List extends Ion {
private _enableReorder: boolean = false;
private _enableSliding: boolean = false; private _enableSliding: boolean = false;
private _slidingGesture: ItemSlidingGesture;
private _reorderGesture: ItemReorderGesture;
private _lastToIndex: number = -1;
/** @Output() ionItemReorder: EventEmitter<{ from: number, to: number }> = new EventEmitter();
* @private
*/
ele: HTMLElement;
/** constructor(elementRef: ElementRef, private _zone: NgZone, @Optional() private _content: Content) {
* @private
*/
slidingGesture: ItemSlidingGesture;
constructor(elementRef: ElementRef, private _zone: NgZone) {
super(elementRef); super(elementRef);
this.ele = elementRef.nativeElement;
} }
/** /**
* @private * @private
*/ */
ngOnDestroy() { ngOnDestroy() {
this.slidingGesture && this.slidingGesture.destroy(); this._slidingGesture && this._slidingGesture.destroy();
this.ele = this.slidingGesture = null; this._reorderGesture && this._reorderGesture.destroy();
} }
/** /**
@ -76,12 +78,10 @@ export class List extends Ion {
this._enableSliding = shouldEnable; this._enableSliding = shouldEnable;
if (shouldEnable) { if (shouldEnable) {
console.debug('enableSlidingItems'); console.debug('enableSlidingItems');
this._zone.runOutsideAngular(() => { nativeTimeout(() => this._slidingGesture = new ItemSlidingGesture(this));
setTimeout(() => this.slidingGesture = new ItemSlidingGesture(this, this.ele));
});
} else { } else {
this.slidingGesture && this.slidingGesture.unlisten(); this._slidingGesture && this._slidingGesture.unlisten();
} }
} }
@ -105,7 +105,96 @@ export class List extends Ion {
* ``` * ```
*/ */
closeSlidingItems() { closeSlidingItems() {
this.slidingGesture && this.slidingGesture.closeOpened(); this._slidingGesture && this._slidingGesture.closeOpened();
}
/**
* @private
*/
reorderEmit(fromIndex: number, toIndex: number) {
this.reorderReset();
if (fromIndex !== toIndex) {
this._zone.run(() => {
this.ionItemReorder.emit({
from: fromIndex,
to: toIndex,
});
});
}
}
/**
* @private
*/
scrollContent(scroll: number) {
let scrollTop = this._content.getScrollTop() + scroll;
if (scroll !== 0) {
this._content.scrollTo(0, scrollTop, 0);
}
return scrollTop;
}
/**
* @private
*/
reorderReset() {
let children = this.elementRef.nativeElement.children;
let len = children.length;
for (let i = 0; i < len; i++) {
children[i].style.transform = '';
}
this._lastToIndex = -1;
}
/**
* @private
*/
reorderMove(fromIndex: number, toIndex: number, itemHeight: number) {
if (this._lastToIndex === -1) {
this._lastToIndex = fromIndex;
}
let lastToIndex = this._lastToIndex;
this._lastToIndex = toIndex;
let children = this.elementRef.nativeElement.children;
if (toIndex >= lastToIndex) {
for (var i = lastToIndex; i <= toIndex; i++) {
if (i !== fromIndex) {
children[i].style.transform = (i > fromIndex)
? `translateY(${-itemHeight}px)` : '';
}
}
}
if (toIndex <= lastToIndex) {
for (var i = toIndex; i <= lastToIndex; i++) {
if (i !== fromIndex) {
children[i].style.transform = (i < fromIndex)
? `translateY(${itemHeight}px)` : '';
}
}
}
}
@Input()
get reorder(): boolean {
return this._enableReorder;
}
set reorder(val: boolean) {
let enabled = isTrueProperty(val);
if (this._enableReorder === enabled) {
return;
}
this._enableReorder = enabled;
if (enabled) {
console.debug('enableReorderItems');
nativeTimeout(() => this._reorderGesture = new ItemReorderGesture(this));
} else {
this._reorderGesture && this._reorderGesture.destroy();
}
} }
} }

View File

@ -1752,6 +1752,13 @@ export class NavController extends Ion {
return this._views.length; return this._views.length;
} }
/**
* @private
*/
isSwipeBackEnabled(): boolean {
return this._sbEnabled;
}
/** /**
* Returns the root `NavController`. * Returns the root `NavController`.
* @returns {NavController} * @returns {NavController}

View File

@ -191,7 +191,6 @@ export class Nav extends NavController implements AfterViewInit {
get swipeBackEnabled(): boolean { get swipeBackEnabled(): boolean {
return this._sbEnabled; return this._sbEnabled;
} }
set swipeBackEnabled(val: boolean) { set swipeBackEnabled(val: boolean) {
this._sbEnabled = isTrueProperty(val); this._sbEnabled = isTrueProperty(val);
} }

View File

@ -9,6 +9,7 @@ import {Key} from '../../util/key';
import {NavParams} from '../nav/nav-params'; import {NavParams} from '../nav/nav-params';
import {ViewController} from '../nav/view-controller'; import {ViewController} from '../nav/view-controller';
import {raf, cancelRaf, CSS, pointerCoord} from '../../util/dom'; import {raf, cancelRaf, CSS, pointerCoord} from '../../util/dom';
import {UIEventManager} from '../../util/ui-event-manager';
/** /**
@ -98,12 +99,6 @@ export class Picker extends ViewController {
'[style.min-width]': 'col.columnWidth', '[style.min-width]': 'col.columnWidth',
'[class.picker-opts-left]': 'col.align=="left"', '[class.picker-opts-left]': 'col.align=="left"',
'[class.picker-opts-right]': 'col.align=="right"', '[class.picker-opts-right]': 'col.align=="right"',
'(touchstart)': 'pointerStart($event)',
'(touchmove)': 'pointerMove($event)',
'(touchend)': 'pointerEnd($event)',
'(mousedown)': 'pointerStart($event)',
'(mousemove)': 'pointerMove($event)',
'(body:mouseup)': 'pointerEnd($event)'
} }
}) })
class PickerColumnCmp { class PickerColumnCmp {
@ -114,7 +109,6 @@ class PickerColumnCmp {
optHeight: number; optHeight: number;
velocity: number; velocity: number;
pos: number[] = []; pos: number[] = [];
msPrv: number = 0;
startY: number = null; startY: number = null;
rafId: number; rafId: number;
bounceFrom: number; bounceFrom: number;
@ -123,10 +117,11 @@ class PickerColumnCmp {
rotateFactor: number; rotateFactor: number;
lastIndex: number; lastIndex: number;
receivingEvents: boolean = false; receivingEvents: boolean = false;
events: UIEventManager = new UIEventManager();
@Output() ionChange: EventEmitter<any> = new EventEmitter(); @Output() ionChange: EventEmitter<any> = new EventEmitter();
constructor(config: Config, private _sanitizer: DomSanitizationService) { constructor(config: Config, private elementRef: ElementRef, private _sanitizer: DomSanitizationService) {
this.rotateFactor = config.getNumber('pickerRotateFactor', 0); this.rotateFactor = config.getNumber('pickerRotateFactor', 0);
} }
@ -141,15 +136,21 @@ class PickerColumnCmp {
// set the scroll position for the selected option // set the scroll position for the selected option
this.setSelected(this.col.selectedIndex, 0); this.setSelected(this.col.selectedIndex, 0);
// Listening for pointer events
this.events.pointerEventsRef(this.elementRef,
(ev: any) => this.pointerStart(ev),
(ev: any) => this.pointerMove(ev),
(ev: any) => this.pointerEnd(ev)
);
} }
pointerStart(ev: UIEvent) { ngOnDestroy() {
console.debug('picker, pointerStart', ev.type, this.startY); this.events.unlistenAll();
}
if (this.isPrevented(ev)) { pointerStart(ev: UIEvent): boolean {
// do not both with mouse events if a touch event already fired console.debug('picker, pointerStart', ev.type, this.startY);
return;
}
// cancel any previous raf's that haven't fired yet // cancel any previous raf's that haven't fired yet
cancelRaf(this.rafId); cancelRaf(this.rafId);
@ -175,6 +176,7 @@ class PickerColumnCmp {
this.minY = (minY * this.optHeight * -1); this.minY = (minY * this.optHeight * -1);
this.maxY = (maxY * this.optHeight * -1); this.maxY = (maxY * this.optHeight * -1);
return true;
} }
pointerMove(ev: UIEvent) { pointerMove(ev: UIEvent) {
@ -185,10 +187,6 @@ class PickerColumnCmp {
return; return;
} }
if (this.isPrevented(ev)) {
return;
}
var currentY = pointerCoord(ev).y; var currentY = pointerCoord(ev).y;
this.pos.push(currentY, Date.now()); this.pos.push(currentY, Date.now());
@ -213,10 +211,6 @@ class PickerColumnCmp {
} }
pointerEnd(ev: UIEvent) { pointerEnd(ev: UIEvent) {
if (this.isPrevented(ev)) {
return;
}
if (!this.receivingEvents) { if (!this.receivingEvents) {
return; return;
} }
@ -410,22 +404,6 @@ class PickerColumnCmp {
} }
} }
isPrevented(ev: UIEvent): boolean {
let now = Date.now();
if (ev.type.indexOf('touch') > -1) {
// this is a touch event, so prevent mouse events for a while
this.msPrv = now + 2000;
} else if (this.msPrv > now && ev.type.indexOf('mouse') > -1) {
// this is a mouse event, and a touch event already happend recently
// prevent the calling method from continuing
ev.preventDefault();
ev.stopPropagation();
return true;
}
return false;
}
} }

View File

@ -4,7 +4,9 @@ import {NG_VALUE_ACCESSOR} from '@angular/common';
import {Form} from '../../util/form'; import {Form} from '../../util/form';
import {isTrueProperty, isNumber, isString, isPresent, clamp} from '../../util/util'; import {isTrueProperty, isNumber, isString, isPresent, clamp} from '../../util/util';
import {Item} from '../item/item'; import {Item} from '../item/item';
import {UIEventManager} from '../../util/ui-event-manager';
import {pointerCoord, Coordinates, raf} from '../../util/dom'; import {pointerCoord, Coordinates, raf} from '../../util/dom';
import {Debouncer} from '../../util/debouncer';
const RANGE_VALUE_ACCESSOR = new Provider( const RANGE_VALUE_ACCESSOR = new Provider(
@ -212,9 +214,9 @@ export class Range {
private _max: number = 100; private _max: number = 100;
private _step: number = 1; private _step: number = 1;
private _snaps: boolean = false; private _snaps: boolean = false;
private _removes: Function[] = [];
private _mouseRemove: Function;
private _debouncer: Debouncer = new Debouncer(0);
private _events: UIEventManager = new UIEventManager();
/** /**
* @private * @private
*/ */
@ -293,6 +295,17 @@ export class Range {
this._pin = isTrueProperty(val); this._pin = isTrueProperty(val);
} }
/**
* @input {number} If true, a pin with integer value is shown when the knob is pressed. Defaults to `false`.
*/
@Input()
get debounce(): number {
return this._debouncer.wait;
}
set debounce(val: number) {
this._debouncer.wait = val;
}
/** /**
* @input {boolean} Show two knobs. Defaults to `false`. * @input {boolean} Show two knobs. Defaults to `false`.
*/ */
@ -346,8 +359,10 @@ export class Range {
this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR); this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR);
// add touchstart/mousedown listeners // add touchstart/mousedown listeners
this._renderer.listen(this._slider.nativeElement, 'touchstart', this.pointerDown.bind(this)); this._events.pointerEventsRef(this._slider,
this._mouseRemove = this._renderer.listen(this._slider.nativeElement, 'mousedown', this.pointerDown.bind(this)); this.pointerDown.bind(this),
this.pointerMove.bind(this),
this.pointerUp.bind(this));
this.createTicks(); this.createTicks();
} }
@ -355,12 +370,12 @@ export class Range {
/** /**
* @private * @private
*/ */
pointerDown(ev: UIEvent) { pointerDown(ev: UIEvent): boolean {
// TODO: we could stop listening for events instead of checking this._disabled. // TODO: we could stop listening for events instead of checking this._disabled.
// since there are a lot of events involved, this solution is // since there are a lot of events involved, this solution is
// enough for the moment // enough for the moment
if (this._disabled) { if (this._disabled) {
return; return false;
} }
console.debug(`range, ${ev.type}`); console.debug(`range, ${ev.type}`);
@ -368,11 +383,6 @@ export class Range {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
if (ev.type === 'touchstart') {
// if this was a touchstart, then let's remove the mousedown
this._mouseRemove && this._mouseRemove();
}
// get the start coordinates // get the start coordinates
this._start = pointerCoord(ev); this._start = pointerCoord(ev);
@ -398,25 +408,11 @@ export class Range {
// update the ratio for the active knob // update the ratio for the active knob
this.updateKnob(this._start, rect); this.updateKnob(this._start, rect);
// ensure past listeners have been removed
this.clearListeners();
// update the active knob's position // update the active knob's position
this._active.position(); this._active.position();
this._pressed = this._active.pressed = true; this._pressed = this._active.pressed = true;
// add a move listener depending on touch/mouse return true;
let renderer = this._renderer;
let removes = this._removes;
if (ev.type === 'touchstart') {
removes.push(renderer.listen(this._slider.nativeElement, 'touchmove', this.pointerMove.bind(this)));
removes.push(renderer.listen(this._slider.nativeElement, 'touchend', this.pointerUp.bind(this)));
} else {
removes.push(renderer.listenGlobal('body', 'mousemove', this.pointerMove.bind(this)));
removes.push(renderer.listenGlobal('window', 'mouseup', this.pointerUp.bind(this)));
}
} }
/** /**
@ -440,9 +436,6 @@ export class Range {
this._active.position(); this._active.position();
this._pressed = this._active.pressed = true; this._pressed = this._active.pressed = true;
} else {
// ensure listeners have been removed
this.clearListeners();
} }
} }
@ -464,21 +457,7 @@ export class Range {
// clear the start coordinates and active knob // clear the start coordinates and active knob
this._start = this._active = null; this._start = this._active = null;
// ensure listeners have been removed
this.clearListeners();
}
/**
* @private
*/
clearListeners() {
this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false; this._pressed = this._knobs.first.pressed = this._knobs.last.pressed = false;
for (var i = 0; i < this._removes.length; i++) {
this._removes[i]();
}
this._removes.length = 0;
} }
/** /**
@ -519,9 +498,10 @@ export class Range {
this.value = newVal; this.value = newVal;
} }
this.onChange(this.value); this._debouncer.debounce(() => {
this.onChange(this.value);
this.ionChange.emit(this); this.ionChange.emit(this);
});
} }
this.updateBar(); this.updateBar();
@ -695,7 +675,7 @@ export class Range {
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form.deregister(this); this._form.deregister(this);
this.clearListeners(); this._events.unlistenAll();
} }
} }

View File

@ -19,7 +19,7 @@
<ion-list> <ion-list>
<ion-item> <ion-item>
<ion-range [(ngModel)]="singleValue" danger pin="true" (ionChange)="rangeChange($event)"></ion-range> <ion-range [(ngModel)]="singleValue" danger pin="true" (ionChange)="rangeChange($event)" [debounce]="100"></ion-range>
</ion-item> </ion-item>
<ion-item> <ion-item>

View File

@ -4,6 +4,7 @@ import {Content} from '../content/content';
import {Icon} from '../icon/icon'; import {Icon} from '../icon/icon';
import {isTrueProperty} from '../../util/util'; import {isTrueProperty} from '../../util/util';
import {CSS, pointerCoord, transitionEnd} from '../../util/dom'; import {CSS, pointerCoord, transitionEnd} from '../../util/dom';
import {PointerEvents, UIEventManager} from '../../util/ui-event-manager';
/** /**
@ -95,15 +96,10 @@ import {CSS, pointerCoord, transitionEnd} from '../../util/dom';
export class Refresher { export class Refresher {
private _appliedStyles: boolean = false; private _appliedStyles: boolean = false;
private _didStart: boolean; private _didStart: boolean;
private _lastStart: number = 0;
private _lastCheck: number = 0; private _lastCheck: number = 0;
private _isEnabled: boolean = true; private _isEnabled: boolean = true;
private _mDown: Function; private _events: UIEventManager = new UIEventManager(false);
private _mMove: Function; private _pointerEvents: PointerEvents;
private _mUp: Function;
private _tStart: Function;
private _tMove: Function;
private _tEnd: Function;
/** /**
* The current state which the refresher is in. The refresher's states include: * The current state which the refresher is in. The refresher's states include:
@ -155,7 +151,7 @@ export class Refresher {
* will automatically go into the `refreshing` state. By default, the pull * will automatically go into the `refreshing` state. By default, the pull
* maximum will be the result of `pullMin + 60`. * maximum will be the result of `pullMin + 60`.
*/ */
@Input() pullMax: number = null; @Input() pullMax: number = this.pullMin + 60;
/** /**
* @input {number} How many milliseconds it takes to close the refresher. Default is `280`. * @input {number} How many milliseconds it takes to close the refresher. Default is `280`.
@ -202,8 +198,7 @@ export class Refresher {
constructor( constructor(
@Host() private _content: Content, @Host() private _content: Content,
private _zone: NgZone, private _zone: NgZone,
elementRef: ElementRef elementRef: ElementRef) {
) {
_content.addCssClass('has-refresher'); _content.addCssClass('has-refresher');
// deprecated warning // deprecated warning
@ -222,31 +217,29 @@ export class Refresher {
private _onStart(ev: TouchEvent): any { private _onStart(ev: TouchEvent): any {
// if multitouch then get out immediately // if multitouch then get out immediately
if (ev.touches && ev.touches.length > 1) { if (ev.touches && ev.touches.length > 1) {
return 1; return false;
}
if (this.state !== STATE_INACTIVE) {
return false;
}
let scrollHostScrollTop = this._content.getContentDimensions().scrollTop;
// if the scrollTop is greater than zero then it's
// not possible to pull the content down yet
if (scrollHostScrollTop > 0) {
return false;
} }
let coord = pointerCoord(ev); let coord = pointerCoord(ev);
console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y); console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y);
let now = Date.now();
if (this._lastStart + 100 > now) {
return 2;
}
this._lastStart = now;
if ( ev.type === 'mousedown' && !this._mMove) {
this._mMove = this._content.addMouseMoveListener( this._onMove.bind(this) );
}
this.startY = this.currentY = coord.y; this.startY = this.currentY = coord.y;
this.progress = 0; this.progress = 0;
this.state = STATE_PULLING;
if (!this.pullMax) { return true;
this.pullMax = (this.pullMin + 60);
}
} }
private _onMove(ev: TouchEvent): any { private _onMove(ev: TouchEvent) {
// this method can get called like a bazillion times per second, // this method can get called like a bazillion times per second,
// so it's built to be as efficient as possible, and does its // so it's built to be as efficient as possible, and does its
// best to do any DOM read/writes only when absolutely necessary // best to do any DOM read/writes only when absolutely necessary
@ -396,12 +389,6 @@ export class Refresher {
// reset on any touchend/mouseup // reset on any touchend/mouseup
this.startY = null; this.startY = null;
if (this._mMove) {
// we don't want to always listen to mousemoves
// remove it if we're still listening
this._mMove();
this._mMove = null;
}
} }
private _beginRefresh() { private _beginRefresh() {
@ -463,10 +450,8 @@ export class Refresher {
this.state = state; this.state = state;
this._setCss(0, '', true, delay); this._setCss(0, '', true, delay);
if (this._mMove) { if (this._pointerEvents) {
// always remove the mousemove event this._pointerEvents.stop();
this._mMove();
this._mMove = null;
} }
} }
@ -481,43 +466,13 @@ export class Refresher {
} }
private _setListeners(shouldListen: boolean) { private _setListeners(shouldListen: boolean) {
const self = this; this._events.unlistenAll();
const content = self._content; this._pointerEvents = null;
if (shouldListen) { if (shouldListen) {
// add listener outside of zone this._pointerEvents = this._events.pointerEvents(this._content.getScrollElement(),
// touch handlers this._onStart.bind(this),
self._zone.runOutsideAngular(function() { this._onMove.bind(this),
if (!self._tStart) { this._onEnd.bind(this));
self._tStart = content.addTouchStartListener( self._onStart.bind(self) );
}
if (!self._tMove) {
self._tMove = content.addTouchMoveListener( self._onMove.bind(self) );
}
if (!self._tEnd) {
self._tEnd = content.addTouchEndListener( self._onEnd.bind(self) );
}
// mouse handlers
// mousemove does not get added until mousedown fires
if (!self._mDown) {
self._mDown = content.addMouseDownListener( self._onStart.bind(self) );
}
if (!self._mUp) {
self._mUp = content.addMouseUpListener( self._onEnd.bind(self) );
}
});
} else {
// unregister event listeners from content element
self._mDown && self._mDown();
self._mMove && self._mMove();
self._mUp && self._mUp();
self._tStart && self._tStart();
self._tMove && self._tMove();
self._tEnd && self._tEnd();
self._mDown = self._mMove = self._mUp = self._tStart = self._tMove = self._tEnd = null;
} }
} }

View File

@ -3,6 +3,7 @@ import {NgControl} from '@angular/common';
import {Config} from '../../config/config'; import {Config} from '../../config/config';
import {isPresent} from '../../util/util'; import {isPresent} from '../../util/util';
import {Debouncer} from '../../util/debouncer';
/** /**
@ -46,10 +47,10 @@ import {isPresent} from '../../util/util';
}) })
export class Searchbar { export class Searchbar {
private _value: string|number = ''; private _value: string|number = '';
private _tmr: any;
private _shouldBlur: boolean = true; private _shouldBlur: boolean = true;
private _isActive: boolean = false; private _isActive: boolean = false;
private _searchbarInput: ElementRef; private _searchbarInput: ElementRef;
private _debouncer: Debouncer = new Debouncer(250);
/** /**
* @input {string} Set the the cancel button text. Default: `"Cancel"`. * @input {string} Set the the cancel button text. Default: `"Cancel"`.
@ -64,7 +65,13 @@ export class Searchbar {
/** /**
* @input {number} How long, in milliseconds, to wait to trigger the `input` event after each keystroke. Default `250`. * @input {number} How long, in milliseconds, to wait to trigger the `input` event after each keystroke. Default `250`.
*/ */
@Input() debounce: number = 250; @Input()
get debounce(): number {
return this._debouncer.wait;
}
set debounce(val: number) {
this._debouncer.wait = val;
}
/** /**
* @input {string} Set the input's placeholder. Default `"Search"`. * @input {string} Set the input's placeholder. Default `"Search"`.
@ -268,13 +275,11 @@ export class Searchbar {
*/ */
inputChanged(ev: any) { inputChanged(ev: any) {
let value = ev.target.value; let value = ev.target.value;
this._debouncer.debounce(() => {
clearTimeout(this._tmr);
this._tmr = setTimeout(() => {
this._value = value; this._value = value;
this.onChange(this._value); this.onChange(this._value);
this.ionInput.emit(ev); this.ionInput.emit(ev);
}, Math.round(this.debounce)); });
} }
/** /**

View File

@ -201,6 +201,17 @@ export class Tab extends NavController {
this._isShown = isTrueProperty(val); this._isShown = isTrueProperty(val);
} }
/**
* @input {boolean} Whether it's possible to swipe-to-go-back on this tab or not.
*/
@Input()
get swipeBackEnabled(): boolean {
return this._sbEnabled;
}
set swipeBackEnabled(val: boolean) {
this._sbEnabled = isTrueProperty(val);
}
/** /**
* @output {Tab} Method to call when the current tab is selected * @output {Tab} Method to call when the current tab is selected
*/ */
@ -222,6 +233,10 @@ export class Tab extends NavController {
parent.add(this); parent.add(this);
if (parentTabs.rootNav) {
this._sbEnabled = parentTabs.rootNav.isSwipeBackEnabled();
}
this._panelId = 'tabpanel-' + this.id; this._panelId = 'tabpanel-' + this.id;
this._btnId = 'tab-' + this.id; this._btnId = 'tab-' + this.id;
} }

View File

@ -324,7 +324,7 @@ class Tab3Page1 {
@Component({ @Component({
template: `<ion-nav [root]="root"></ion-nav>` template: '<ion-nav [root]="root" swipeBackEnabled="false"></ion-nav>'
}) })
class E2EApp { class E2EApp {
root = SignIn; root = SignIn;

View File

@ -1,6 +1,6 @@
<ion-tabs preloadTabs="false" (ionChange)="onTabChange()"> <ion-tabs preloadTabs="false" (ionChange)="onTabChange()">
<ion-tab tabTitle="Recents" tabIcon="call" [root]="tab1Root" [rootParams]="params"></ion-tab> <ion-tab tabTitle="Recents" tabIcon="call" [root]="tab1Root" [rootParams]="params" swipeBackEnabled="true"></ion-tab>
<ion-tab tabTitle="Favorites" tabIcon="star" [root]="tab2Root"></ion-tab> <ion-tab tabTitle="Favorites" tabIcon="star" [root]="tab2Root"></ion-tab>
<ion-tab tabTitle="Settings" tabIcon="settings" [root]="tab3Root"></ion-tab> <ion-tab tabTitle="Settings" tabIcon="settings" [root]="tab3Root"></ion-tab>
<ion-tab tabTitle="Chat" tabIcon="chatbubbles" (ionSelect)="chat()"></ion-tab> <ion-tab tabTitle="Chat" tabIcon="chatbubbles" (ionSelect)="chat()"></ion-tab>

View File

@ -235,7 +235,7 @@ export class Tab3 {
<ion-tabs #content (ionChange)="onChange($event)"> <ion-tabs #content (ionChange)="onChange($event)">
<ion-tab tabTitle="Plain List" tabIcon="star" [root]="root1" (ionSelect)="onSelect($event)"></ion-tab> <ion-tab tabTitle="Plain List" tabIcon="star" [root]="root1" (ionSelect)="onSelect($event)"></ion-tab>
<ion-tab tabTitle="Schedule" tabIcon="globe" [root]="root2"></ion-tab> <ion-tab tabTitle="Schedule" tabIcon="globe" [root]="root2"></ion-tab>
<ion-tab tabTitle="Stopwatch" tabIcon="stopwatch" [root]="root3"></ion-tab> <ion-tab tabTitle="Stopwatch" tabIcon="logo-facebook" [root]="root3"></ion-tab>
<ion-tab tabTitle="Messages" tabIcon="chatboxes" [root]="root1"></ion-tab> <ion-tab tabTitle="Messages" tabIcon="chatboxes" [root]="root1"></ion-tab>
<ion-tab tabTitle="My Profile" tabIcon="person" [root]="root2"></ion-tab> <ion-tab tabTitle="My Profile" tabIcon="person" [root]="root2"></ion-tab>
</ion-tabs> </ion-tabs>

View File

@ -5,6 +5,7 @@ import {Form} from '../../util/form';
import {isTrueProperty} from '../../util/util'; import {isTrueProperty} from '../../util/util';
import {Item} from '../item/item'; import {Item} from '../item/item';
import {pointerCoord} from '../../util/dom'; import {pointerCoord} from '../../util/dom';
import {UIEventManager} from '../../util/ui-event-manager';
const TOGGLE_VALUE_ACCESSOR = new Provider( const TOGGLE_VALUE_ACCESSOR = new Provider(
@ -87,6 +88,7 @@ export class Toggle implements ControlValueAccessor {
private _startX: number; private _startX: number;
private _msPrv: number = 0; private _msPrv: number = 0;
private _fn: Function; private _fn: Function;
private _events: UIEventManager = new UIEventManager();
/** /**
* @private * @private
@ -113,27 +115,14 @@ export class Toggle implements ControlValueAccessor {
} }
} }
/** private pointerDown(ev: UIEvent): boolean {
* @private
*/
private pointerDown(ev: UIEvent) {
if (this._isPrevented(ev)) {
return;
}
this._startX = pointerCoord(ev).x; this._startX = pointerCoord(ev).x;
this._activated = true; this._activated = true;
return true;
} }
/**
* @private
*/
private pointerMove(ev: UIEvent) { private pointerMove(ev: UIEvent) {
if (this._startX) { if (this._startX) {
if (this._isPrevented(ev)) {
return;
}
let currentX = pointerCoord(ev).x; let currentX = pointerCoord(ev).x;
console.debug('toggle, pointerMove', ev.type, currentX); console.debug('toggle, pointerMove', ev.type, currentX);
@ -152,16 +141,8 @@ export class Toggle implements ControlValueAccessor {
} }
} }
/**
* @private
*/
private pointerUp(ev: UIEvent) { private pointerUp(ev: UIEvent) {
if (this._startX) { if (this._startX) {
if (this._isPrevented(ev)) {
return;
}
let endX = pointerCoord(ev).x; let endX = pointerCoord(ev).x;
if (this.checked) { if (this.checked) {
@ -188,9 +169,7 @@ export class Toggle implements ControlValueAccessor {
this.onChange(this._checked); this.onChange(this._checked);
} }
/**
* @private
*/
private _setChecked(isChecked: boolean) { private _setChecked(isChecked: boolean) {
if (isChecked !== this._checked) { if (isChecked !== this._checked) {
this._checked = isChecked; this._checked = isChecked;
@ -256,6 +235,11 @@ export class Toggle implements ControlValueAccessor {
*/ */
ngAfterContentInit() { ngAfterContentInit() {
this._init = true; this._init = true;
this._events.pointerEventsRef(this._elementRef,
(ev: any) => this.pointerDown(ev),
(ev: any) => this.pointerMove(ev),
(ev: any) => this.pointerUp(ev)
);
} }
/** /**
@ -263,20 +247,7 @@ export class Toggle implements ControlValueAccessor {
*/ */
ngOnDestroy() { ngOnDestroy() {
this._form.deregister(this); this._form.deregister(this);
} this._events.unlistenAll();
/**
* @private
*/
private _isPrevented(ev: UIEvent) {
if (ev.type.indexOf('touch') > -1) {
this._msPrv = Date.now() + 2000;
} else if (this._msPrv > Date.now() && ev.type.indexOf('mouse') > -1) {
ev.preventDefault();
ev.stopPropagation();
return true;
}
} }
} }

25
src/util/debouncer.ts Normal file
View File

@ -0,0 +1,25 @@
export class Debouncer {
private timer: number = null;
callback: Function;
constructor(public wait: number) { }
debounce(callback: Function) {
this.callback = callback;
this.schedule();
}
schedule() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (this.wait <= 0) {
this.callback();
} else {
this.timer = setTimeout(this.callback, this.wait);
}
}
}

View File

@ -0,0 +1,170 @@
import {ElementRef} from '@angular/core';
/**
* @private
*/
export class PointerEvents {
private rmTouchStart: Function = null;
private rmTouchMove: Function = null;
private rmTouchEnd: Function = null;
private rmMouseStart: Function = null;
private rmMouseMove: Function = null;
private rmMouseUp: Function = null;
private lastTouchEvent: number = 0;
mouseWait: number = 2 * 1000;
constructor(private ele: any,
private pointerDown: any,
private pointerMove: any,
private pointerUp: any,
private zone: boolean,
private option: any) {
this.rmTouchStart = listenEvent(ele, 'touchstart', zone, option, (ev: any) => this.handleTouchStart(ev));
this.rmMouseStart = listenEvent(ele, 'mousedown', zone, option, (ev: any) => this.handleMouseDown(ev));
}
private handleTouchStart(ev: any) {
this.lastTouchEvent = Date.now() + this.mouseWait;
if (!this.pointerDown(ev)) {
return;
}
if (!this.rmTouchMove) {
this.rmTouchMove = listenEvent(this.ele, 'touchmove', this.zone, this.option, this.pointerMove);
}
if (!this.rmTouchEnd) {
this.rmTouchEnd = listenEvent(this.ele, 'touchend', this.zone, this.option, (ev: any) => this.handleTouchEnd(ev));
}
}
private handleMouseDown(ev: any) {
if (this.lastTouchEvent > Date.now()) {
console.debug('mousedown event dropped because of previous touch');
return;
}
if (!this.pointerDown(ev)) {
return;
}
if (!this.rmMouseMove) {
this.rmMouseMove = listenEvent(window, 'mousemove', this.zone, this.option, this.pointerMove);
}
if (!this.rmMouseUp) {
this.rmMouseUp = listenEvent(window, 'mouseup', this.zone, this.option, (ev: any) => this.handleMouseUp(ev));
}
}
private handleTouchEnd(ev: any) {
this.rmTouchMove && this.rmTouchMove();
this.rmTouchMove = null;
this.rmTouchEnd && this.rmTouchEnd();
this.rmTouchEnd = null;
this.pointerUp(ev);
}
private handleMouseUp(ev: any) {
this.rmMouseMove && this.rmMouseMove();
this.rmMouseMove = null;
this.rmMouseUp && this.rmMouseUp();
this.rmMouseUp = null;
this.pointerUp(ev);
}
stop() {
this.rmTouchMove && this.rmTouchMove();
this.rmTouchEnd && this.rmTouchEnd();
this.rmTouchMove = null;
this.rmTouchEnd = null;
this.rmMouseMove && this.rmMouseMove();
this.rmMouseUp && this.rmMouseUp();
this.rmMouseMove = null;
this.rmMouseUp = null;
}
destroy() {
this.rmTouchStart && this.rmTouchStart();
this.rmTouchStart = null;
this.rmMouseStart && this.rmMouseStart();
this.rmMouseStart = null;
this.stop();
this.pointerDown = null;
this.pointerMove = null;
this.pointerUp = null;
this.ele = null;
}
}
/**
* @private
*/
export class UIEventManager {
private events: Function[] = [];
constructor(public zoneWrapped: boolean = true) {}
listenRef(ref: ElementRef, eventName: string, callback: any, option?: any): Function {
return this.listen(ref.nativeElement, eventName, callback, option);
}
pointerEventsRef(ref: ElementRef, pointerStart: any, pointerMove: any, pointerEnd: any, option?: any): Function {
return this.pointerEvents(ref.nativeElement, pointerStart, pointerMove, pointerEnd, option);
}
pointerEvents(element: any, pointerDown: any, pointerMove: any, pointerUp: any, option: any = false): PointerEvents {
if (!element) {
return;
}
let submanager = new PointerEvents(
element,
pointerDown,
pointerMove,
pointerUp,
this.zoneWrapped,
option);
let removeFunc = () => submanager.destroy();
this.events.push(removeFunc);
return submanager;
}
listen(element: any, eventName: string, callback: any, option: any = false): Function {
if (!element) {
return;
}
let removeFunc = listenEvent(element, eventName, this.zoneWrapped, option, callback);
this.events.push(removeFunc);
return removeFunc;
}
unlistenAll() {
for (let event of this.events) {
event();
}
this.events.length = 0;
}
}
function listenEvent(ele: any, eventName: string, zoneWrapped: boolean, option: any, callback: any): Function {
let rawEvent = ('__zone_symbol__addEventListener' in ele && !zoneWrapped);
if (rawEvent) {
ele.__zone_symbol__addEventListener(eventName, callback, option);
return () => ele.__zone_symbol__removeEventListener(eventName, callback);
} else {
ele.addEventListener(eventName, callback, option);
return () => ele.removeEventListener(eventName, callback);
}
}