diff --git a/src/components/item/item-reorder-gesture.ts b/src/components/item/item-reorder-gesture.ts new file mode 100644 index 00000000000..afbdfdb4a79 --- /dev/null +++ b/src/components/item/item-reorder-gesture.ts @@ -0,0 +1,131 @@ +import {Item} from './item'; +import {List} from '../list/list'; +import {closest, Coordinates, pointerCoord, CSS, raf} from '../../util/dom'; + +/** + * @private + */ +export class ItemReorderGesture { + private selectedItem: Item = null; + private offset: Coordinates; + private itemHeight: number; + private lastToIndex: number; + private emptyZone: boolean; + + private onTouchstart: Function; + private onTouchmove: Function; + private onTouchend: Function; + + constructor(public list: List) { + this.listen(); + } + + private onDragStart(ev: any) { + let itemEle = ev.target; + if (itemEle.nodeName === 'ION-REORDER') { + let item = itemEle['$ionComponent']; + if (!item) { + console.error('item does not contain ion component'); + return; + } + + this.offset = pointerCoord(ev); + this.selectedItem = item; + this.itemHeight = item.height(); + this.lastToIndex = item.index; + item.setCssClass('reorder-active', true); + ev.preventDefault(); + } + } + + private onDrag(ev: any) { + if (!this.selectedItem) { + return; + } + + let coord = pointerCoord(ev); + let ydiff = Math.round(coord.y - this.offset.y); + + this.selectedItem.setCssStyle(CSS.transform, 'translateY(' + ydiff + 'px)'); + ev.preventDefault(); + + let item = this.itemForCoord(coord); + if (!item) { + this.emptyZone = true; + return; + } + let fromIndex = this.selectedItem.index; + let toIndex = item.index; + if (toIndex !== this.lastToIndex || this.emptyZone) { + this.lastToIndex = toIndex; + this.emptyZone = false; + this.list.reorderMove(fromIndex, toIndex, this.itemHeight); + } + } + + private onDragEnd(ev: any) { + if (!this.selectedItem) { + return; + } + + let item = this.selectedItem; + let fromIndex = item.index; + + item.setCssClass('reorder-active', false); + this.selectedItem = null; + + let toIndex = this.lastToIndex; + 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) { + return null; + } + return item; + } + + /** + * @private + */ + listen() { + let ele = this.list.getNativeElement(); + this.onTouchstart = (ev: any) => this.onDragStart(ev); + this.onTouchmove = (ev: any) => this.onDrag(ev); + this.onTouchend = (ev: any) => this.onDragEnd(ev); + + ele.addEventListener('touchstart', this.onTouchstart); + ele.addEventListener('touchmove', this.onTouchmove); + ele.addEventListener('touchend', this.onTouchend); + } + + /** + * @private + */ + unlisten() { + let ele = this.list.getNativeElement(); + ele.removeEventListener('touchstart', this.onTouchstart); + ele.removeEventListener('touchmove', this.onTouchmove); + ele.removeEventListener('touchend', this.onTouchend); + this.onTouchstart = null; + this.onTouchmove = null; + this.onTouchend = null; + } + + /** + * @private + */ + destroy() { + this.unlisten(); + this.list = null; + } +} diff --git a/src/components/item/item-sliding-gesture.ts b/src/components/item/item-sliding-gesture.ts index 8d150c3fc1e..1c9a41920b9 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.scss b/src/components/item/item.scss index 7c043ce39e5..b52f235d8f6 100644 --- a/src/components/item/item.scss +++ b/src/components/item/item.scss @@ -83,5 +83,46 @@ ion-input.item { align-items: flex-start; } +// Item reorder +// -------------------------------------------------- + +ion-reorder { + display: none; + flex: 1; + justify-content: center; + align-items: 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; + } +} + +.reorder-active { + pointer-events: none; + transition: none; + box-shadow: 0 0 10px rgba(0, 0, 0, .5); + opacity: 0.8; + z-index: 4; + ion-reorder { + pointer-events: none; + } +} + + @import "item-media"; @import "item-sliding"; diff --git a/src/components/item/item.ts b/src/components/item/item.ts index e56416d1940..ee02282500e 100644 --- a/src/components/item/item.ts +++ b/src/components/item/item.ts @@ -1,4 +1,4 @@ -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'; @@ -235,11 +235,13 @@ import {Label} from '../label/label'; '' + '' + '' + + '' + '' + '', host: { 'class': 'item' }, + directives: [forwardRef(() => ItemReorder)], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) @@ -249,6 +251,11 @@ export class Item { private _label: Label; private _viewLabel: boolean = true; + /** + * @private + */ + @Input() index: number; + /** * @private */ @@ -261,6 +268,7 @@ export class Item { constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) { this.id = form.nextId().toString(); + _elementRef.nativeElement['$ionComponent'] = this; } /** @@ -354,4 +362,24 @@ export class Item { icon.addClass('item-icon'); }); } + + /** + * @private + */ + height(): number { + return this._elementRef.nativeElement.offsetHeight; + } +} + +/** + * @private + */ +@Component({ + selector: 'ion-reorder', + template: `` +}) +class ItemReorder { + constructor(item: Item, elementRef: ElementRef) { + elementRef.nativeElement['$ionComponent'] = item; + } } 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..4c2eedcfae5 --- /dev/null +++ b/src/components/item/test/reorder/index.ts @@ -0,0 +1,32 @@ +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); + // console.log(this.items); + // this.d.detectChanges(); + } +} + +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..5ddd83487ca --- /dev/null +++ b/src/components/item/test/reorder/main.html @@ -0,0 +1,21 @@ + + Reorder items + + + + + + + + + + {{item}} + + + + + diff --git a/src/components/list/list.ts b/src/components/list/list.ts index 2abc477af6a..765727defb7 100644 --- a/src/components/list/list.ts +++ b/src/components/list/list.ts @@ -1,7 +1,10 @@ -import {Directive, ElementRef, Renderer, Attribute, NgZone} from '@angular/core'; +import {Directive, ElementRef, EventEmitter, Renderer, Input, Output, Attribute, NgZone} from '@angular/core'; 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,24 +23,22 @@ 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; - - /** - * @private - */ - slidingGesture: ItemSlidingGesture; + @Output() ionItemReorder: EventEmitter<{ from: number, to: number }> = new EventEmitter(); constructor(elementRef: ElementRef, private _zone: NgZone) { super(elementRef); - this.ele = elementRef.nativeElement; } /** @@ -45,7 +46,7 @@ export class List extends Ion { */ ngOnDestroy() { this.slidingGesture && this.slidingGesture.destroy(); - this.ele = this.slidingGesture = null; + this.reorderGesture && this.reorderGesture.destroy(); } /** @@ -76,9 +77,7 @@ 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(); @@ -108,6 +107,84 @@ export class List extends Ion { 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 + */ + reorderReset() { + let children = this.elementRef.nativeElement.children; + let len = children.length; + for (var 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 children = this.elementRef.nativeElement.children; + if (toIndex >= this.lastToIndex) { + for (var i = this.lastToIndex; i <= toIndex; i++) { + if (i !== fromIndex) { + children[i].style.transform = (i > fromIndex) + ? 'translateY(' + -itemHeight + 'px)' + : ''; + } + } + } + + if (toIndex <= this.lastToIndex) { + for (var i = toIndex; i <= this.lastToIndex; i++) { + if (i !== fromIndex) { + children[i].style.transform = (i < fromIndex) + ? 'translateY(' + itemHeight + 'px)' + : ''; + } + } + } + + this.lastToIndex = toIndex; + } + + @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) { + nativeTimeout(() => this.reorderGesture = new ItemReorderGesture(this)); + } else { + this.reorderGesture && this.reorderGesture.unlisten(); + } + } + }