diff --git a/CHANGELOG.md b/CHANGELOG.md index a2ce62f74ea..327a7ab5f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,61 @@ + +# [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)) + + + # [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: ``` ``` - + becomes - + ``` ``` @@ -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). diff --git a/package.json b/package.json index 8aac04698fa..47841b05b4b 100644 --- a/package.json +++ b/package.json @@ -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" } } -} +} \ No newline at end of file diff --git a/scripts/npm/README.md b/scripts/npm/README.md index 4bfe8247943..8be85c12403 100644 --- a/scripts/npm/README.md +++ b/scripts/npm/README.md @@ -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 diff --git a/src/components/content/content.ts b/src/components/content/content.ts index e298daa4398..da8ee2213b3 100644 --- a/src/components/content/content.ts +++ b/src/components/content/content.ts @@ -185,6 +185,13 @@ export class Content extends Ion { } }; } + + /** + * @private + */ + getScrollElement(): HTMLElement { + return this._scrollEle; + } /** * @private diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts index 4e5684e273e..fff46fb429f 100644 --- a/src/components/icon/icon.ts +++ b/src/components/icon/icon.ts @@ -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'; } diff --git a/src/components/input/native-input.ts b/src/components/input/native-input.ts index 5eeb9c273c9..473de4477f9 100644 --- a/src/components/input/native-input.ts +++ b/src/components/input/native-input.ts @@ -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 = 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); diff --git a/src/components/item/item-reorder-gesture.ts b/src/components/item/item-reorder-gesture.ts new file mode 100644 index 00000000000..13d534ea12e --- /dev/null +++ b/src/components/item/item-reorder-gesture.ts @@ -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 = document.elementFromPoint(this.offset.x - 100, coord.y); + if (!element) { + return null; + } + element = closest(element, 'ion-item', true); + if (!element) { + return null; + } + let item = (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; + } +} diff --git a/src/components/item/item-reorder.scss b/src/components/item/item-reorder.scss new file mode 100644 index 00000000000..ebf1f5dd904 --- /dev/null +++ b/src/components/item/item-reorder.scss @@ -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; + } +} diff --git a/src/components/item/item-reorder.ts b/src/components/item/item-reorder.ts new file mode 100644 index 00000000000..36268b8f4c7 --- /dev/null +++ b/src/components/item/item-reorder.ts @@ -0,0 +1,17 @@ +import {Component, ElementRef, Inject, forwardRef} from '@angular/core'; +import {Item} from './item'; + +/** + * @private + */ +@Component({ + selector: 'ion-reorder', + template: `` +}) +export class ItemReorder { + constructor( + @Inject(forwardRef(() => Item)) item: Item, + elementRef: ElementRef) { + elementRef.nativeElement['$ionComponent'] = item; + } +} diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index 025ec1ac5ab..3495ecbcc58 100644 --- a/src/components/item/item-sliding-gesture.ts +++ b/src/components/item/item-sliding-gesture.ts @@ -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 }); diff --git a/src/components/item/item-sliding.scss b/src/components/item/item-sliding.scss index 66a6c0689b9..eb8997c0f5b 100644 --- a/src/components/item/item-sliding.scss +++ b/src/components/item/item-sliding.scss @@ -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 { diff --git a/src/components/item/item-sliding.ts b/src/components/item/item-sliding.ts index 82b3c8190b1..46bf3167c4e 100644 --- a/src/components/item/item-sliding.ts +++ b/src/components/item/item-sliding.ts @@ -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 + * + * + * Item 1 + * + * + * + * + * + *``` */ @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 = 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 `` element. * + * @usage + * ```html + * + * + * + * Item + * + * + * + * + * + + * + * + * + * + * + * ``` + * * ### 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 { * * - * + * * - * - - * - * - * + * + * Item + * + * + * + * * ``` * * ### Button Layout @@ -118,33 +159,16 @@ const enum SlidingState { * `` element. * * ```html - * - * Item - * - * - * - * - * ``` + * + * + * * - * @usage - * ```html - * - * - * - * Item - * - * - * - * - * - - * - * - * - * - * * ``` * + * * @demo /docs/v2/demos/item-sliding/ * @see {@link /docs/v2/components#lists List Component Docs} * @see {@link ../Item Item API 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; } - \ No newline at end of file + diff --git a/src/components/item/item.scss b/src/components/item/item.scss index 7c043ce39e5..3954a82872d 100644 --- a/src/components/item/item.scss +++ b/src/components/item/item.scss @@ -85,3 +85,4 @@ ion-input.item { @import "item-media"; @import "item-sliding"; +@import "item-reorder"; diff --git a/src/components/item/item.ts b/src/components/item/item.ts index e56416d1940..e64d79f812a 100644 --- a/src/components/item/item.ts +++ b/src/components/item/item.ts @@ -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'; '' + '' + '' + + '' + '' + '', 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; + } } diff --git a/src/components/item/test/reorder/e2e.ts b/src/components/item/test/reorder/e2e.ts new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/src/components/item/test/reorder/e2e.ts @@ -0,0 +1 @@ + diff --git a/src/components/item/test/reorder/index.ts b/src/components/item/test/reorder/index.ts new file mode 100644 index 00000000000..cf5c190cb7e --- /dev/null +++ b/src/components/item/test/reorder/index.ts @@ -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); diff --git a/src/components/item/test/reorder/main.html b/src/components/item/test/reorder/main.html new file mode 100644 index 00000000000..b8f19189630 --- /dev/null +++ b/src/components/item/test/reorder/main.html @@ -0,0 +1,22 @@ + + Reorder items + + + + + + + + + + {{item}} + + + + + diff --git a/src/components/list/list.ts b/src/components/list/list.ts index 2abc477af6a..58e1a415171 100644 --- a/src/components/list/list.ts +++ b/src/components/list/list.ts @@ -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(); + } } } diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts index a5e936a3d97..80ca524260d 100644 --- a/src/components/nav/nav-controller.ts +++ b/src/components/nav/nav-controller.ts @@ -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} diff --git a/src/components/nav/nav.ts b/src/components/nav/nav.ts index 718b6e89d24..59569b06775 100644 --- a/src/components/nav/nav.ts +++ b/src/components/nav/nav.ts @@ -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); } diff --git a/src/components/picker/picker.ts b/src/components/picker/picker.ts index 23ebc728255..c18e26d0eba 100644 --- a/src/components/picker/picker.ts +++ b/src/components/picker/picker.ts @@ -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 = 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; - } - } diff --git a/src/components/range/range.ts b/src/components/range/range.ts index 3bcd6ed47c5..d9b78f1fd40 100644 --- a/src/components/range/range.ts +++ b/src/components/range/range.ts @@ -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(); } } diff --git a/src/components/range/test/basic/page1.html b/src/components/range/test/basic/page1.html index dcc8dec1a0c..2066f1a1f08 100644 --- a/src/components/range/test/basic/page1.html +++ b/src/components/range/test/basic/page1.html @@ -19,7 +19,7 @@ - + diff --git a/src/components/refresher/refresher.ts b/src/components/refresher/refresher.ts index 19afeef3a7d..06a9c729274 100644 --- a/src/components/refresher/refresher.ts +++ b/src/components/refresher/refresher.ts @@ -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; } - - 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; + if (this.state !== STATE_INACTIVE) { + return false; } - this._lastStart = now; - if ( ev.type === 'mousedown' && !this._mMove) { - this._mMove = this._content.addMouseMoveListener( this._onMove.bind(this) ); + 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); + 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)); } } diff --git a/src/components/searchbar/searchbar.ts b/src/components/searchbar/searchbar.ts index 44237c589ce..0c05acaaa0c 100644 --- a/src/components/searchbar/searchbar.ts +++ b/src/components/searchbar/searchbar.ts @@ -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)); + }); } /** diff --git a/src/components/tabs/tab.ts b/src/components/tabs/tab.ts index 693f821b7fa..4f0f8db1cb2 100644 --- a/src/components/tabs/tab.ts +++ b/src/components/tabs/tab.ts @@ -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; } diff --git a/src/components/tabs/test/advanced/index.ts b/src/components/tabs/test/advanced/index.ts index 4757cff4079..63b914aa6b2 100644 --- a/src/components/tabs/test/advanced/index.ts +++ b/src/components/tabs/test/advanced/index.ts @@ -324,7 +324,7 @@ class Tab3Page1 { @Component({ - template: `` + template: '' }) class E2EApp { root = SignIn; diff --git a/src/components/tabs/test/advanced/tabs.html b/src/components/tabs/test/advanced/tabs.html index 338c2b32fff..53e15a20ce1 100644 --- a/src/components/tabs/test/advanced/tabs.html +++ b/src/components/tabs/test/advanced/tabs.html @@ -1,6 +1,6 @@ - + diff --git a/src/components/tabs/test/basic/index.ts b/src/components/tabs/test/basic/index.ts index bd97deab376..4b19f5eb4b8 100644 --- a/src/components/tabs/test/basic/index.ts +++ b/src/components/tabs/test/basic/index.ts @@ -235,7 +235,7 @@ export class Tab3 { - + diff --git a/src/components/toggle/toggle.ts b/src/components/toggle/toggle.ts index 6504e00b15f..13041180fb0 100644 --- a/src/components/toggle/toggle.ts +++ b/src/components/toggle/toggle.ts @@ -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(); } } diff --git a/src/util/debouncer.ts b/src/util/debouncer.ts new file mode 100644 index 00000000000..327de2eeddf --- /dev/null +++ b/src/util/debouncer.ts @@ -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); + } + } + +} diff --git a/src/util/ui-event-manager.ts b/src/util/ui-event-manager.ts new file mode 100644 index 00000000000..9f886a92dc9 --- /dev/null +++ b/src/util/ui-event-manager.ts @@ -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); + } +}