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>
# [2.0.0-beta.8](https://github.com/driftyco/ionic/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2016-06-06)
@ -36,7 +94,7 @@ All Ionic component events have been renamed to start with `ion`. This is to pre
- `change` -> `ionChange`
- **DateTime**
- `change` -> `ionChange`
- `cancel` -> `ionCancel`
- `cancel` -> `ionCancel`
- **InfiniteScroll**
- `infinite` -> `ionInfinite`
- **Menu**
@ -88,13 +146,13 @@ All Ionic component events have been renamed to start with `ion`. This is to pre
```
npm install --save ionic-angular@2.0.0-beta.8
```
_or_ modify the following line to use `beta.8` in your `package.json` and then run `npm install`:
```
"ionic-angular": "^2.0.0-beta.8",
```
**This is the way to update Ionic to any version, more information can be found in the [docs](http://ionicframework.com/docs/v2/resources/using-npm/).**
2. Replace all instances of `@Page` with `@Component`:
@ -179,29 +237,29 @@ All Ionic component events have been renamed to start with `ion`. This is to pre
5. Rename any uses of the lifecycle events, for example:
```
onPageDidEnter() {
onPageDidEnter() {
console.log("Entered page!");
}
```
becomes
```
ionViewDidEnter() {
ionViewDidEnter() {
console.log("Entered page!");
}
```
The full list of lifecycle name changes is in the [section above](https://github.com/driftyco/ionic/blob/2.0/CHANGELOG.md#ionic-lifecycle-events-renamed).
6. Rename any Ionic events, for example:
```
<ion-slides (slideChangeStart)="onSlideChangeStart($event)">
```
becomes
```
<ion-slides (ionWillChange)="onSlideChangeStart($event)">
```
@ -1144,6 +1202,6 @@ Enjoy!
### Breaking Changes
The Web Animations polyfill is no longer shipped with the framework and may cause build errors.
The Web Animations polyfill is no longer shipped with the framework and may cause build errors.
Projects will need to be [updated accordingly](https://github.com/driftyco/ionic-conference-app/commit/2ed59e6fd275c4616792c7b2e5aa9da4a20fb188).

View File

@ -1,7 +1,7 @@
{
"private": "true",
"name": "ionic2",
"version": "2.0.0-beta.8",
"version": "2.0.0-beta.9",
"license": "MIT",
"repository": {
"type": "git",
@ -102,4 +102,4 @@
"path": "node_modules/ionic-cz-conventional-changelog"
}
}
}
}

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:
```
import {App, Page} from 'ionic-angular';
import {App} from 'ionic-angular';
```
### Bundles

View File

@ -185,6 +185,13 @@ export class Content extends Ion {
}
};
}
/**
* @private
*/
getScrollElement(): HTMLElement {
return this._scrollEle;
}
/**
* @private

View File

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

View File

@ -31,7 +31,7 @@ export class NativeInput {
}
@HostListener('input', ['$event'])
private _change(ev) {
private _change(ev: any) {
this.valueChange.emit(ev.target.value);
}
@ -40,9 +40,9 @@ export class NativeInput {
var self = this;
self.focusChange.emit(true);
function docTouchEnd(ev) {
var tapped: HTMLElement = ev.target;
function docTouchEnd(ev: TouchEvent) {
var tapped = <HTMLElement>ev.target;
if (tapped && self.element()) {
if (tapped.tagName !== 'INPUT' && tapped.tagName !== 'TEXTAREA' && !tapped.classList.contains('input-cover')) {
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);
clonedInputEle.classList.add('cloned-input');
clonedInputEle.classList.add(addCssClass);
@ -191,7 +191,7 @@ function cloneInput(focusedInputEle, addCssClass) {
return clonedInputEle;
}
function removeClone(focusedInputEle, queryCssClass) {
function removeClone(focusedInputEle: any, queryCssClass: string) {
let clonedInputEle = focusedInputEle.parentElement.querySelector('.' + queryCssClass);
if (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;
openContainer: ItemSliding = null;
constructor(public list: List, public listEle: HTMLElement) {
super(listEle, {
constructor(public list: List) {
super(list.getNativeElement(), {
direction: 'x',
threshold: DRAG_THRESHOLD
});

View File

@ -73,7 +73,7 @@ ion-item-sliding.active-slide {
opacity: 1;
transition: all 300ms cubic-bezier(.36, .66, .04, 1);
pointer-events: all;
pointer-events: none;
}
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({
selector: 'ion-item-options',
})
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;
/**
* @output {event} Expression to evaluate when the item has been fully swiped.
*/
@Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter();
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
@ -46,6 +73,9 @@ export class ItemOptions {
}
}
/**
* @private
*/
width() {
return this._elementRef.nativeElement.offsetWidth;
}
@ -62,12 +92,30 @@ const enum SlidingState {
/**
* @name ItemSliding
*
* @description
* 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
* 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
* 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
@ -83,7 +131,7 @@ const enum SlidingState {
* </button>
* </ion-item-options>
* <ion-item-options>
* <ion-item-options side="left">
* <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon>
* Archive
@ -96,19 +144,12 @@ const enum SlidingState {
* to the (ionDrag)` event.
*
* ```html
* <ion-item-options side="right">
* <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon>
* Archive
* </button>
* </ion-item-options>
* <ion-item-options>
* <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon>
* Archive
* </button>
* </ion-item-options>
* <ion-item-sliding (ionDrag)="logDrag($event)">
* <ion-item>Item</ion-item>
* <ion-item-options>
* <button>Favorite</button>
* </ion-item-options>
* </ion-item-sliding>
* ```
*
* ### Button Layout
@ -118,32 +159,15 @@ const enum SlidingState {
* `<ion-item-options>` element.
*
* ```html
* <ion-item-sliding (ionDrag)="ondrag($event)">
* <ion-item>Item</ion-item>
* <ion-item-options>
* <button>Favorite</button>
* </ion-item-options>
* </ion-item-sliding>
* <ion-item-options icon-left>
* <button (click)="archive(item)">
* <ion-icon name="archive"></ion-icon>
* Archive
* </button>
* </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/
* @see {@link /docs/v2/components#lists List Component Docs}
@ -169,8 +193,15 @@ export class ItemSliding {
private _rightOptions: ItemOptions;
private _optsDirty: boolean = true;
private _state: SlidingState = SlidingState.Disabled;
/**
* @private
* */
slidingPercent: number = 0;
/**
* @private
* */
@ContentChild(Item) private item: Item;
@ -290,6 +321,9 @@ export class ItemSliding {
return restingPoint;
}
/**
* @private
* */
fireSwipeEvent() {
if (this.slidingPercent > SWIPE_FACTOR) {
this._rightOptions.ionSwipe.emit(this);
@ -298,6 +332,9 @@ export class ItemSliding {
}
}
/**
* @private
* */
calculateOptsWidth() {
nativeRaf(() => {
if (this._optsDirty) {
@ -382,7 +419,7 @@ export class ItemSliding {
/**
* 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
* and passed to the `share` method.
*
@ -437,4 +474,4 @@ function shouldClose(isCloseDirection: boolean, isMovingFast: boolean, isOnClose
let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast);
return shouldClose;
}

View File

@ -85,3 +85,4 @@ ion-input.item {
@import "item-media";
@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 {Form} from '../../util/form';
import {Icon} from '../icon/icon';
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>' +
'</div>' +
'<ng-content select="[item-right],ion-radio,ion-toggle"></ng-content>' +
'<ion-reorder></ion-reorder>' +
'</div>' +
'<ion-button-effect></ion-button-effect>',
host: {
'class': 'item'
},
directives: [forwardRef(() => ItemReorder)],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
@ -249,6 +252,11 @@ export class Item {
private _label: Label;
private _viewLabel: boolean = true;
/**
* @private
*/
@Input() index: number;
/**
* @private
*/
@ -261,6 +269,7 @@ export class Item {
constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
this.id = form.nextId().toString();
_elementRef.nativeElement['$ionComponent'] = this;
}
/**
@ -354,4 +363,11 @@ export class Item {
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 {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,
@ -20,32 +24,30 @@ import {ItemSlidingGesture} from '../item/item-sliding-gesture';
*
*/
@Directive({
selector: 'ion-list'
selector: 'ion-list',
host: {
'[class.reorder-enabled]': '_enableReorder',
}
})
export class List extends Ion {
private _enableReorder: boolean = false;
private _enableSliding: boolean = false;
private _slidingGesture: ItemSlidingGesture;
private _reorderGesture: ItemReorderGesture;
private _lastToIndex: number = -1;
/**
* @private
*/
ele: HTMLElement;
@Output() ionItemReorder: EventEmitter<{ from: number, to: number }> = new EventEmitter();
/**
* @private
*/
slidingGesture: ItemSlidingGesture;
constructor(elementRef: ElementRef, private _zone: NgZone) {
constructor(elementRef: ElementRef, private _zone: NgZone, @Optional() private _content: Content) {
super(elementRef);
this.ele = elementRef.nativeElement;
}
/**
* @private
*/
ngOnDestroy() {
this.slidingGesture && this.slidingGesture.destroy();
this.ele = this.slidingGesture = null;
this._slidingGesture && this._slidingGesture.destroy();
this._reorderGesture && this._reorderGesture.destroy();
}
/**
@ -76,12 +78,10 @@ export class List extends Ion {
this._enableSliding = shouldEnable;
if (shouldEnable) {
console.debug('enableSlidingItems');
this._zone.runOutsideAngular(() => {
setTimeout(() => this.slidingGesture = new ItemSlidingGesture(this, this.ele));
});
nativeTimeout(() => this._slidingGesture = new ItemSlidingGesture(this));
} else {
this.slidingGesture && this.slidingGesture.unlisten();
this._slidingGesture && this._slidingGesture.unlisten();
}
}
@ -105,7 +105,96 @@ export class List extends Ion {
* ```
*/
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;
}
/**
* @private
*/
isSwipeBackEnabled(): boolean {
return this._sbEnabled;
}
/**
* Returns the root `NavController`.
* @returns {NavController}

View File

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

View File

@ -9,6 +9,7 @@ import {Key} from '../../util/key';
import {NavParams} from '../nav/nav-params';
import {ViewController} from '../nav/view-controller';
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',
'[class.picker-opts-left]': 'col.align=="left"',
'[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 {
@ -114,7 +109,6 @@ class PickerColumnCmp {
optHeight: number;
velocity: number;
pos: number[] = [];
msPrv: number = 0;
startY: number = null;
rafId: number;
bounceFrom: number;
@ -123,10 +117,11 @@ class PickerColumnCmp {
rotateFactor: number;
lastIndex: number;
receivingEvents: boolean = false;
events: UIEventManager = new UIEventManager();
@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);
}
@ -141,16 +136,22 @@ class PickerColumnCmp {
// set the scroll position for the selected option
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)
);
}
ngOnDestroy() {
this.events.unlistenAll();
}
pointerStart(ev: UIEvent) {
pointerStart(ev: UIEvent): boolean {
console.debug('picker, pointerStart', ev.type, this.startY);
if (this.isPrevented(ev)) {
// do not both with mouse events if a touch event already fired
return;
}
// cancel any previous raf's that haven't fired yet
cancelRaf(this.rafId);
@ -175,6 +176,7 @@ class PickerColumnCmp {
this.minY = (minY * this.optHeight * -1);
this.maxY = (maxY * this.optHeight * -1);
return true;
}
pointerMove(ev: UIEvent) {
@ -185,10 +187,6 @@ class PickerColumnCmp {
return;
}
if (this.isPrevented(ev)) {
return;
}
var currentY = pointerCoord(ev).y;
this.pos.push(currentY, Date.now());
@ -213,10 +211,6 @@ class PickerColumnCmp {
}
pointerEnd(ev: UIEvent) {
if (this.isPrevented(ev)) {
return;
}
if (!this.receivingEvents) {
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 {isTrueProperty, isNumber, isString, isPresent, clamp} from '../../util/util';
import {Item} from '../item/item';
import {UIEventManager} from '../../util/ui-event-manager';
import {pointerCoord, Coordinates, raf} from '../../util/dom';
import {Debouncer} from '../../util/debouncer';
const RANGE_VALUE_ACCESSOR = new Provider(
@ -212,9 +214,9 @@ export class Range {
private _max: number = 100;
private _step: number = 1;
private _snaps: boolean = false;
private _removes: Function[] = [];
private _mouseRemove: Function;
private _debouncer: Debouncer = new Debouncer(0);
private _events: UIEventManager = new UIEventManager();
/**
* @private
*/
@ -293,6 +295,17 @@ export class Range {
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`.
*/
@ -346,8 +359,10 @@ export class Range {
this._renderer.setElementStyle(this._bar.nativeElement, 'right', barR);
// add touchstart/mousedown listeners
this._renderer.listen(this._slider.nativeElement, 'touchstart', this.pointerDown.bind(this));
this._mouseRemove = this._renderer.listen(this._slider.nativeElement, 'mousedown', this.pointerDown.bind(this));
this._events.pointerEventsRef(this._slider,
this.pointerDown.bind(this),
this.pointerMove.bind(this),
this.pointerUp.bind(this));
this.createTicks();
}
@ -355,12 +370,12 @@ export class Range {
/**
* @private
*/
pointerDown(ev: UIEvent) {
pointerDown(ev: UIEvent): boolean {
// TODO: we could stop listening for events instead of checking this._disabled.
// since there are a lot of events involved, this solution is
// enough for the moment
if (this._disabled) {
return;
return false;
}
console.debug(`range, ${ev.type}`);
@ -368,11 +383,6 @@ export class Range {
ev.preventDefault();
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
this._start = pointerCoord(ev);
@ -398,25 +408,11 @@ export class Range {
// update the ratio for the active knob
this.updateKnob(this._start, rect);
// ensure past listeners have been removed
this.clearListeners();
// update the active knob's position
this._active.position();
this._pressed = this._active.pressed = true;
// add a move listener depending on touch/mouse
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)));
}
return true;
}
/**
@ -440,9 +436,6 @@ export class Range {
this._active.position();
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
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;
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.onChange(this.value);
this.ionChange.emit(this);
this._debouncer.debounce(() => {
this.onChange(this.value);
this.ionChange.emit(this);
});
}
this.updateBar();
@ -695,7 +675,7 @@ export class Range {
*/
ngOnDestroy() {
this._form.deregister(this);
this.clearListeners();
this._events.unlistenAll();
}
}

View File

@ -19,7 +19,7 @@
<ion-list>
<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>

View File

@ -4,6 +4,7 @@ import {Content} from '../content/content';
import {Icon} from '../icon/icon';
import {isTrueProperty} from '../../util/util';
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 {
private _appliedStyles: boolean = false;
private _didStart: boolean;
private _lastStart: number = 0;
private _lastCheck: number = 0;
private _isEnabled: boolean = true;
private _mDown: Function;
private _mMove: Function;
private _mUp: Function;
private _tStart: Function;
private _tMove: Function;
private _tEnd: Function;
private _events: UIEventManager = new UIEventManager(false);
private _pointerEvents: PointerEvents;
/**
* 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
* 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`.
@ -202,8 +198,7 @@ export class Refresher {
constructor(
@Host() private _content: Content,
private _zone: NgZone,
elementRef: ElementRef
) {
elementRef: ElementRef) {
_content.addCssClass('has-refresher');
// deprecated warning
@ -222,31 +217,29 @@ export class Refresher {
private _onStart(ev: TouchEvent): any {
// if multitouch then get out immediately
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);
console.debug('Pull-to-refresh, onStart', ev.type, 'y:', coord.y);
let now = Date.now();
if (this._lastStart + 100 > now) {
return 2;
}
this._lastStart = now;
if ( ev.type === 'mousedown' && !this._mMove) {
this._mMove = this._content.addMouseMoveListener( this._onMove.bind(this) );
}
this.startY = this.currentY = coord.y;
this.progress = 0;
if (!this.pullMax) {
this.pullMax = (this.pullMin + 60);
}
this.state = STATE_PULLING;
return true;
}
private _onMove(ev: TouchEvent): any {
private _onMove(ev: TouchEvent) {
// this method can get called like a bazillion times per second,
// so it's built to be as efficient as possible, and does its
// best to do any DOM read/writes only when absolutely necessary
@ -396,12 +389,6 @@ export class Refresher {
// reset on any touchend/mouseup
this.startY = null;
if (this._mMove) {
// we don't want to always listen to mousemoves
// remove it if we're still listening
this._mMove();
this._mMove = null;
}
}
private _beginRefresh() {
@ -463,10 +450,8 @@ export class Refresher {
this.state = state;
this._setCss(0, '', true, delay);
if (this._mMove) {
// always remove the mousemove event
this._mMove();
this._mMove = null;
if (this._pointerEvents) {
this._pointerEvents.stop();
}
}
@ -481,43 +466,13 @@ export class Refresher {
}
private _setListeners(shouldListen: boolean) {
const self = this;
const content = self._content;
this._events.unlistenAll();
this._pointerEvents = null;
if (shouldListen) {
// add listener outside of zone
// touch handlers
self._zone.runOutsideAngular(function() {
if (!self._tStart) {
self._tStart = content.addTouchStartListener( self._onStart.bind(self) );
}
if (!self._tMove) {
self._tMove = content.addTouchMoveListener( self._onMove.bind(self) );
}
if (!self._tEnd) {
self._tEnd = content.addTouchEndListener( self._onEnd.bind(self) );
}
// mouse handlers
// mousemove does not get added until mousedown fires
if (!self._mDown) {
self._mDown = content.addMouseDownListener( self._onStart.bind(self) );
}
if (!self._mUp) {
self._mUp = content.addMouseUpListener( self._onEnd.bind(self) );
}
});
} else {
// unregister event listeners from content element
self._mDown && self._mDown();
self._mMove && self._mMove();
self._mUp && self._mUp();
self._tStart && self._tStart();
self._tMove && self._tMove();
self._tEnd && self._tEnd();
self._mDown = self._mMove = self._mUp = self._tStart = self._tMove = self._tEnd = null;
this._pointerEvents = this._events.pointerEvents(this._content.getScrollElement(),
this._onStart.bind(this),
this._onMove.bind(this),
this._onEnd.bind(this));
}
}

View File

@ -3,6 +3,7 @@ import {NgControl} from '@angular/common';
import {Config} from '../../config/config';
import {isPresent} from '../../util/util';
import {Debouncer} from '../../util/debouncer';
/**
@ -46,10 +47,10 @@ import {isPresent} from '../../util/util';
})
export class Searchbar {
private _value: string|number = '';
private _tmr: any;
private _shouldBlur: boolean = true;
private _isActive: boolean = false;
private _searchbarInput: ElementRef;
private _debouncer: Debouncer = new Debouncer(250);
/**
* @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() 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"`.
@ -268,13 +275,11 @@ export class Searchbar {
*/
inputChanged(ev: any) {
let value = ev.target.value;
clearTimeout(this._tmr);
this._tmr = setTimeout(() => {
this._debouncer.debounce(() => {
this._value = value;
this.onChange(this._value);
this.ionInput.emit(ev);
}, Math.round(this.debounce));
});
}
/**

View File

@ -201,6 +201,17 @@ export class Tab extends NavController {
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
*/
@ -222,6 +233,10 @@ export class Tab extends NavController {
parent.add(this);
if (parentTabs.rootNav) {
this._sbEnabled = parentTabs.rootNav.isSwipeBackEnabled();
}
this._panelId = 'tabpanel-' + this.id;
this._btnId = 'tab-' + this.id;
}

View File

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

View File

@ -1,6 +1,6 @@
<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="Settings" tabIcon="settings" [root]="tab3Root"></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-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="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="My Profile" tabIcon="person" [root]="root2"></ion-tab>
</ion-tabs>

View File

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

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