feat(item): sliding items work with list reorder

This commit is contained in:
Manu Mtz.-Almeida
2016-06-23 21:17:25 +02:00
parent b7826ba1c1
commit bfdc898945
15 changed files with 493 additions and 348 deletions

View File

@@ -1,5 +1,5 @@
import {Item} from './item';
import {List} from '../list/list';
import {Reorder} from '../item/item-reorder';
import {UIEventManager} from '../../util/ui-event-manager';
import {closest, Coordinates, pointerCoord, CSS, nativeRaf} from '../../util/dom';
@@ -12,7 +12,6 @@ const ITEM_REORDER_ACTIVE = 'reorder-active';
* @private
*/
export class ItemReorderGesture {
private selectedItem: Item = null;
private selectedItemEle: HTMLElement = null;
private selectedItemHeight: number;
@@ -26,7 +25,7 @@ export class ItemReorderGesture {
private events: UIEventManager = new UIEventManager(false);
constructor(public list: List) {
constructor(public list: Reorder) {
let element = this.list.getNativeElement();
this.events.pointerEvents(element,
this.onDragStart.bind(this),
@@ -35,24 +34,22 @@ export class ItemReorderGesture {
}
private onDragStart(ev: any): boolean {
let itemEle = ev.target;
if (itemEle.nodeName !== 'ION-REORDER') {
let reorderElement = ev.target;
if (reorderElement.nodeName !== 'ION-REORDER') {
return false;
}
let item = <Item> itemEle['$ionComponent'];
let item = reorderElement['$ionReorderNode'];
if (!item) {
console.error('item does not contain ion component');
console.error('item does not contain ion ionReorderNode');
return false;
}
ev.preventDefault();
// Preparing state
this.selectedItem = item;
this.selectedItemEle = item.getNativeElement();
this.selectedItemHeight = item.height();
this.lastToIndex = item.index;
this.lastYcoord = -100;
this.selectedItemEle = item;
this.selectedItemHeight = item.offsetHeight;
this.lastYcoord = this.lastToIndex = -100;
this.windowHeight = window.innerHeight - AUTO_SCROLL_MARGIN;
this.lastScrollPosition = this.list.scrollContent(0);
@@ -60,7 +57,7 @@ export class ItemReorderGesture {
this.offset = pointerCoord(ev);
this.offset.y += this.lastScrollPosition;
item.setCssClass(ITEM_REORDER_ACTIVE, true);
item.classList.add(ITEM_REORDER_ACTIVE);
this.list.reorderStart();
return true;
}
@@ -83,9 +80,9 @@ export class ItemReorderGesture {
if (Math.abs(posY - this.lastYcoord) > 30) {
let overItem = this.itemForCoord(coord);
if (overItem) {
let toIndex = overItem.index;
if (toIndex !== this.lastToIndex || this.emptyZone) {
let fromIndex = this.selectedItem.index;
let toIndex = indexForItem(overItem);
if (toIndex && (toIndex !== this.lastToIndex || this.emptyZone)) {
let fromIndex = indexForItem(this.selectedItemEle);
this.lastToIndex = toIndex;
this.lastYcoord = posY;
this.emptyZone = false;
@@ -96,40 +93,25 @@ export class ItemReorderGesture {
}
}
// Update selected item position
// Update selected item position
let ydiff = Math.round(posY - this.offset.y + scrollPosition);
selectedItem.style[CSS.transform] = `translateY(${ydiff}px)`;
}
private onDragEnd() {
if (!this.selectedItemEle) {
return;
}
nativeRaf(() => {
let toIndex = this.lastToIndex;
let fromIndex = this.selectedItem.index;
this.selectedItem.setCssClass(ITEM_REORDER_ACTIVE, false);
this.selectedItem = null;
this.selectedItemEle = null;
this.list.reorderEmit(fromIndex, toIndex);
});
let toIndex = this.lastToIndex;
let fromIndex = indexForItem(this.selectedItemEle);
this.selectedItemEle.classList.remove(ITEM_REORDER_ACTIVE);
this.selectedItemEle = null;
this.list.reorderEmit(fromIndex, toIndex);
}
private itemForCoord(coord: Coordinates): Item {
let element = <HTMLElement>document.elementFromPoint(this.offset.x - 100, coord.y);
if (!element) {
return null;
}
if (element.nodeName !== 'ION-ITEM') {
return null;
}
let item = <Item>(<any>element)['$ionComponent'];
if (!item) {
console.error('item does not have $ionComponent');
return null;
}
return item;
private itemForCoord(coord: Coordinates): HTMLElement {
return itemForPosition(this.offset.x - 100, coord.y);
}
private scroll(posY: number): number {
@@ -141,8 +123,6 @@ export class ItemReorderGesture {
return this.lastScrollPosition;
}
/**
* @private
*/
@@ -153,3 +133,25 @@ export class ItemReorderGesture {
this.list = null;
}
}
function itemForPosition(x: number, y: number): HTMLElement {
let element = <HTMLElement>document.elementFromPoint(x, y);
if (!element) {
return null;
}
if (element.nodeName !== 'ION-ITEM' && !element.hasAttribute('ion-item')) {
return null;
}
if (indexForItem(element)) {
return element;
}
let parent = element.parentNode;
if (indexForItem(parent)) {
return <HTMLElement>parent;
}
return null;
}
function indexForItem(element: any): number {
return element['$ionIndex'];
}

View File

@@ -24,7 +24,9 @@ ion-reorder {
.reorder-enabled {
.item {
.item, .item-wrapper {
transition: transform 300ms;
will-change: transform;
}
@@ -40,6 +42,7 @@ ion-reorder {
}
.item-wrapper.reorder-active,
.item.reorder-active {
z-index: 4;

View File

@@ -1,6 +1,171 @@
import { Component, ElementRef, forwardRef, Inject } from '@angular/core';
import { Component, Directive, ElementRef, EventEmitter, forwardRef, Input, NgZone, Renderer, Inject, Optional, Output } from '@angular/core';
import { Content } from '../content/content';
import { CSS } from '../../util/dom';
import { Item } from './item';
import { ItemReorderGesture } from '../item/item-reorder-gesture';
import { isTrueProperty } from '../../util/util';
export interface ReorderIndexes {
from: number;
to: number;
}
/**
* @private
*/
@Directive({
selector: '[reorder]',
host: {
'[class.reorder-enabled]': '_enableReorder',
}
})
export class Reorder {
private _enableReorder: boolean = false;
private _reorderGesture: ItemReorderGesture;
private _lastToIndex: number = -1;
private _element: HTMLElement;
@Output() ionItemReorder: EventEmitter<ReorderIndexes> = new EventEmitter<ReorderIndexes>();
constructor(
elementRef: ElementRef,
private _rendered: Renderer,
private _zone: NgZone,
@Optional() private _content: Content) {
this._element = elementRef.nativeElement;
}
/**
* @private
*/
ngOnDestroy() {
this._element = null;
this._reorderGesture && this._reorderGesture.destroy();
}
@Input()
get reorder(): boolean {
return this._enableReorder;
}
set reorder(val: boolean) {
this._enableReorder = isTrueProperty(val);
if (!this._enableReorder) {
this._reorderGesture && this._reorderGesture.destroy();
this._reorderGesture = null;
} else if (!this._reorderGesture) {
console.debug('enableReorderItems');
this._reorderGesture = new ItemReorderGesture(this);
}
}
/**
* @private
*/
reorderStart() {
let children = this._element.children;
let len = children.length;
this.setCssClass('reorder-active', true);
for (let i = 0; i < len; i++) {
children[i]['$ionIndex'] = i;
}
}
/**
* @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._element.children;
let len = children.length;
this.setCssClass('reorder-active', false);
let transform = CSS.transform;
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;
// TODO: I think both loops can be merged into a single one
// but I had no luck last time I tried
/********* DOM READ ********** */
let children = this._element.children;
/********* DOM WRITE ********* */
let transform = CSS.transform;
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)` : '';
}
}
}
}
/**
* @private
*/
setCssClass(classname: string, add: boolean) {
this._rendered.setElementClass(this._element, classname, add);
}
/**
* @private
*/
getNativeElement(): HTMLElement {
return this._element;
}
}
/**
* @private
@@ -11,8 +176,18 @@ import { Item } from './item';
})
export class ItemReorder {
constructor(
@Inject(forwardRef(() => Item)) item: Item,
elementRef: ElementRef) {
elementRef.nativeElement['$ionComponent'] = item;
@Inject(forwardRef(() => Item)) private item: Item,
private elementRef: ElementRef) {
}
ngAfterContentInit() {
let item = this.item.getNativeElement();
if (item.parentNode.nodeName === 'ION-ITEM-SLIDING') {
this.elementRef.nativeElement['$ionReorderNode'] = item.parentNode;
} else {
this.elementRef.nativeElement['$ionReorderNode'] = item;
}
}
}

View File

@@ -70,38 +70,40 @@ export class ItemSlidingGesture extends DragGesture {
}
onDragEnd(ev: any) {
if (this.selectedContainer) {
ev.preventDefault();
if (!this.selectedContainer) {
return;
}
ev.preventDefault();
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
this.selectedContainer = null;
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
this.selectedContainer = null;
// TODO: I am not sure listening for a tap event is the best idea
// we should try mousedown/touchstart
if (openAmount === 0) {
this.openContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
} else if (!this.onTap) {
this.onTap = (event: any) => this.onTapCallback(event);
this.on('tap', this.onTap);
}
// TODO: I am not sure listening for a tap event is the best idea
// we should try mousedown/touchstart
if (openAmount === 0) {
this.openContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
} else if (!this.onTap) {
this.onTap = (event: any) => this.onTapCallback(event);
this.on('tap', this.onTap);
}
}
closeOpened(): boolean {
if (this.openContainer) {
this.openContainer.close();
this.openContainer = null;
this.selectedContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
return true;
if (!this.openContainer) {
return false;
}
return false;
this.openContainer.close();
this.openContainer = null;
this.selectedContainer = null;
this.off('tap', this.onTap);
this.onTap = null;
return true;
}
unlisten() {
this.closeOpened();
super.unlisten();
this.list = null;
}

View File

@@ -71,9 +71,11 @@ ion-item-sliding.active-slide {
z-index: $z-index-item-options + 1;
opacity: 1;
transition: all 300ms cubic-bezier(.36, .66, .04, 1);
transition: transform 500ms cubic-bezier(.36, .66, .04, 1);
pointer-events: none;
will-change: transform;
}
ion-item-options {

View File

@@ -1,11 +1,11 @@
import { ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, Input, Optional, Output, QueryList, Renderer, ViewEncapsulation } from '@angular/core';
import { ChangeDetectionStrategy, Component, ContentChildren, ContentChild, Directive, ElementRef, EventEmitter, Input, Optional, Output, QueryList, Renderer, ViewEncapsulation } from '@angular/core';
import { CSS, nativeRaf, nativeTimeout } from '../../util/dom';
import { CSS, nativeRaf, nativeTimeout, clearNativeTimeout } from '../../util/dom';
import { Item } from './item';
import { isPresent } from '../../util/util';
import { List } from '../list/list';
const SWIPE_FACTOR = 1.1;
const SWIPE_MARGIN = 20;
const ELASTIC_FACTOR = 0.55;
export const enum ItemSideFlags {
@@ -52,8 +52,7 @@ export class ItemOptions {
*/
@Output() ionSwipe: EventEmitter<ItemSliding> = new EventEmitter<ItemSliding>();
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {
}
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
/**
* @private
@@ -83,10 +82,13 @@ export class ItemOptions {
}
const enum SlidingState {
Disabled = 0,
Enabled = 1,
Right = 2,
Left = 3
Disabled = 1 << 1,
Enabled = 1 << 2,
Right = 1 << 3,
Left = 1 << 4,
SwipeRight = 1 << 5,
SwipeLeft = 1 << 6,
}
@@ -194,23 +196,15 @@ export class ItemSliding {
private _optsDirty: boolean = true;
private _state: SlidingState = SlidingState.Disabled;
/**
* @private
* */
slidingPercent: number = 0;
/**
* @private
* */
@ContentChild(Item) private item: Item;
/**
* @output {event} Expression to evaluate when the sliding position changes.
* It reports the relative position.
*
* ```ts
* ondrag(percent) {
* ondrag(item) {
* let percent = item.getSlidingPercent();
* if (percent > 0) {
* // positive
* console.log('right side');
@@ -225,20 +219,16 @@ export class ItemSliding {
* ```
*
*/
@Output() ionDrag: EventEmitter<number> = new EventEmitter<number>();
@Output() ionDrag: EventEmitter<ItemSliding> = new EventEmitter<ItemSliding>();
constructor(@Optional() private _list: List, private _renderer: Renderer, private _elementRef: ElementRef) {
_list.enableSlidingItems(true);
constructor( @Optional() list: List, private _renderer: Renderer, private _elementRef: ElementRef) {
list && list.containsSlidingItem(true);
_elementRef.nativeElement.$ionComponent = this;
_renderer.setElementClass(_elementRef.nativeElement, 'item-wrapper', true);
this.setCssClass('item-wrapper', true);
}
/**
* @private
*/
@ContentChildren(ItemOptions)
set _itemOptions(itemOptions: QueryList<ItemOptions>) {
private set _itemOptions(itemOptions: QueryList<ItemOptions>) {
let sides = 0;
for (var item of itemOptions.toArray()) {
var side = item.getSides();
@@ -253,12 +243,33 @@ export class ItemSliding {
this._sides = sides;
}
/**
* @private
*/
getOpenAmount(): number {
return this._openAmount;
}
/**
* @private
*/
getSlidingPercent(): number {
let openAmount = this._openAmount;
if (openAmount > 0) {
return openAmount / this._optsWidthRightSide;
} else if (openAmount < 0) {
return openAmount / this._optsWidthLeftSide;
} else {
return 0;
}
}
/**
* @private
*/
startSliding(startX: number) {
if (this._timer) {
clearTimeout(this._timer);
clearNativeTimeout(this._timer);
this._timer = null;
}
if (this._openAmount === 0) {
@@ -278,7 +289,7 @@ export class ItemSliding {
return;
}
let openAmount = this._startX - x;
let openAmount = (this._startX - x);
switch (this._sides) {
case ItemSideFlags.Right: openAmount = Math.max(0, openAmount); break;
case ItemSideFlags.Left: openAmount = Math.min(0, openAmount); break;
@@ -316,105 +327,88 @@ export class ItemSliding {
restingPoint = 0;
}
this.fireSwipeEvent();
this._setOpenAmount(restingPoint, true);
this.fireSwipeEvent();
return restingPoint;
}
/**
* @private
* */
fireSwipeEvent() {
if (this.slidingPercent > SWIPE_FACTOR) {
private fireSwipeEvent() {
if (this._state & SlidingState.SwipeRight) {
this._rightOptions.ionSwipe.emit(this);
} else if (this.slidingPercent < -SWIPE_FACTOR) {
} else if (this._state & SlidingState.SwipeLeft) {
this._leftOptions.ionSwipe.emit(this);
}
}
/**
* @private
* */
calculateOptsWidth() {
private calculateOptsWidth() {
nativeRaf(() => {
if (this._optsDirty) {
this._optsWidthRightSide = 0;
if (this._rightOptions) {
this._optsWidthRightSide = this._rightOptions.width();
}
this._optsWidthLeftSide = 0;
if (this._leftOptions) {
this._optsWidthLeftSide = this._leftOptions.width();
}
this._optsDirty = false;
if (!this._optsDirty) {
return;
}
this._optsWidthRightSide = 0;
if (this._rightOptions) {
this._optsWidthRightSide = this._rightOptions.width();
}
this._optsWidthLeftSide = 0;
if (this._leftOptions) {
this._optsWidthLeftSide = this._leftOptions.width();
}
this._optsDirty = false;
});
}
/**
* @private
*/
private _setOpenAmount(openAmount: number, isFinal: boolean) {
if (this._timer) {
clearTimeout(this._timer);
clearNativeTimeout(this._timer);
this._timer = null;
}
this._openAmount = openAmount;
if (isFinal) {
this.item.setCssStyle(CSS.transition, '');
let didEnd = openAmount === 0;
if (didEnd) {
// TODO: refactor. there must exist a better way
// if sliding ended, we wait 400ms until animation finishes
} else {
if (openAmount > 0) {
let state = (openAmount >= (this._optsWidthRightSide + SWIPE_MARGIN))
? SlidingState.Right | SlidingState.SwipeRight
: SlidingState.Right;
this._setState(state);
} else if (openAmount < 0) {
let state = (openAmount <= (-this._optsWidthLeftSide - SWIPE_MARGIN))
? SlidingState.Left | SlidingState.SwipeLeft
: SlidingState.Left;
this._setState(state);
}
}
if (openAmount === 0) {
this._timer = nativeTimeout(() => {
this._setState(SlidingState.Disabled);
this._timer = null;
}, 400);
this.slidingPercent = 0;
} else if (openAmount > 0) {
this._setState(SlidingState.Right);
this.slidingPercent = openAmount / this._optsWidthRightSide;
} else if (openAmount < 0) {
this._setState(SlidingState.Left);
this.slidingPercent = openAmount / this._optsWidthLeftSide;
}, 600);
this.item.setCssStyle(CSS.transform, '');
return;
}
if (!isFinal) {
this.setClass('active-swipe-right', this.slidingPercent > SWIPE_FACTOR);
this.setClass('active-swipe-left', this.slidingPercent < -SWIPE_FACTOR);
} else {
this.item.setCssStyle(CSS.transition, '');
}
this.ionDrag.emit(this.slidingPercent);
this.item.setCssStyle(CSS.transform, (didEnd ? '' : 'translate3d(' + -openAmount + 'px,0,0)'));
this.item.setCssStyle(CSS.transform, `translate3d(${-openAmount}px,0,0)`);
this.ionDrag.emit(this);
}
private _setState(state: SlidingState) {
if (state !== this._state) {
this._state = state;
this.setClass('active-slide', state !== SlidingState.Disabled);
this.setClass('active-options-right', state === SlidingState.Right);
this.setClass('active-options-left', state === SlidingState.Left);
if (state === SlidingState.Disabled || state === SlidingState.Enabled) {
this.setClass('active-swipe-right', false);
this.setClass('active-swipe-left', false);
}
if (state === this._state) {
return;
}
this.setCssClass('active-slide', (state !== SlidingState.Disabled));
this.setCssClass('active-options-right', !!(state & SlidingState.Right));
this.setCssClass('active-options-left', !!(state & SlidingState.Left));
this.setCssClass('active-swipe-right', !!(state & SlidingState.SwipeRight));
this.setCssClass('active-swipe-left', !!(state & SlidingState.SwipeLeft));
this._state = state;
}
/**
* @private
*/
setClass(className: string, add: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, className, add);
}
/**
* @private
*/
getOpenAmount(): number {
return this._openAmount;
}
/**
* Close the sliding item. Items can also be closed from the [List](../../list/List).
@@ -454,24 +448,37 @@ export class ItemSliding {
this._setOpenAmount(0, true);
}
/**
* @private
*/
setCssClass(cssClass: string, shouldAdd: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
}
/**
* @private
*/
setCssStyle(property: string, value: string) {
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
}
}
function shouldClose(isCloseDirection: boolean, isMovingFast: boolean, isOnCloseZone: boolean): boolean {
// The logic required to know when the sliding item should close (openAmount=0)
// depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone)
// and it ended up being too complicated to be written manually without errors
// so the truth table is attached below: (0=false, 1=true)
// isCloseDirection | isMovingFast | isOnCloseZone || shouldClose
// 0 | 0 | 0 || 0
// 0 | 0 | 1 || 1
// 0 | 1 | 0 || 0
// 0 | 1 | 1 || 0
// 1 | 0 | 0 || 0
// 1 | 0 | 1 || 1
// 1 | 1 | 0 || 1
// 1 | 1 | 1 || 1
// The resulting expression was generated by resolving the K-map (Karnaugh map):
let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast);
return shouldClose;
// The logic required to know when the sliding item should close (openAmount=0)
// depends on three booleans (isCloseDirection, isMovingFast, isOnCloseZone)
// and it ended up being too complicated to be written manually without errors
// so the truth table is attached below: (0=false, 1=true)
// isCloseDirection | isMovingFast | isOnCloseZone || shouldClose
// 0 | 0 | 0 || 0
// 0 | 0 | 1 || 1
// 0 | 1 | 0 || 0
// 0 | 1 | 1 || 0
// 1 | 0 | 0 || 0
// 1 | 0 | 1 || 1
// 1 | 1 | 0 || 1
// 1 | 1 | 1 || 1
// The resulting expression was generated by resolving the K-map (Karnaugh map):
let shouldClose = (!isMovingFast && isOnCloseZone) || (isCloseDirection && isMovingFast);
return shouldClose;
}

View File

@@ -291,11 +291,6 @@ export class Item {
private _label: Label;
private _viewLabel: boolean = true;
/**
* @private
*/
@Input() index: number;
/**
* @private
*/
@@ -308,7 +303,6 @@ export class Item {
constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
this.id = form.nextId().toString();
_elementRef.nativeElement['$ionComponent'] = this;
}
/**
@@ -333,20 +327,6 @@ export class Item {
}
}
/**
* @private
*/
setCssClass(cssClass: string, shouldAdd: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
}
/**
* @private
*/
setCssStyle(property: string, value: string) {
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
}
/**
* @private
*/
@@ -406,15 +386,22 @@ export class Item {
/**
* @private
*/
getNativeElement() {
return this._elementRef.nativeElement;
setCssClass(cssClass: string, shouldAdd: boolean) {
this._renderer.setElementClass(this._elementRef.nativeElement, cssClass, shouldAdd);
}
/**
* @private
*/
height(): number {
return this._elementRef.nativeElement.offsetHeight;
setCssStyle(property: string, value: string) {
this._renderer.setElementStyle(this._elementRef.nativeElement, property, value);
}
/**
* @private
*/
getNativeElement(): HTMLElement {
return this._elementRef.nativeElement;
}
}

View File

@@ -1,5 +1,5 @@
import {Component, ChangeDetectorRef} from '@angular/core';
import {ionicBootstrap} from '../../../../../src';
import {ionicBootstrap, reorderArray} from '../../../../../src';
@Component({
@@ -21,9 +21,7 @@ class E2EPage {
}
reorder(indexes: any) {
let element = this.items[indexes.from];
this.items.splice(indexes.from, 1);
this.items.splice(indexes.to, 0, element);
this.items = reorderArray(this.items, indexes);
}
}

View File

@@ -10,8 +10,7 @@
<ion-content>
<ion-list [reorder]="isReordering" (ionItemReorder)="reorder($event)">
<ion-item *ngFor="let item of items; let index=index"
[index]="index"
<ion-item *ngFor="let item of items"
[style.background]="'rgb('+(255-item*4)+','+(255-item*4)+','+(255-item*4)+')'"
[style.height]="item*2+35+'px'">
{{item}}

View File

@@ -4,8 +4,6 @@ import { Content } from '../content/content';
import { Ion } from '../ion';
import { isTrueProperty } from '../../util/util';
import { ItemSlidingGesture } from '../item/item-sliding-gesture';
import { ItemReorderGesture } from '../item/item-reorder-gesture';
import { nativeTimeout } from '../../util/dom';
/**
* The List is a widely used interface element in almost any mobile app,
@@ -25,20 +23,13 @@ import { nativeTimeout } from '../../util/dom';
*/
@Directive({
selector: 'ion-list',
host: {
'[class.reorder-enabled]': '_enableReorder',
}
})
export class List extends Ion {
private _enableReorder: boolean = false;
private _enableSliding: boolean = false;
private _enableSliding: boolean = true;
private _containsSlidingItems: boolean = false;
private _slidingGesture: ItemSlidingGesture;
private _reorderGesture: ItemReorderGesture;
private _lastToIndex: number = -1;
@Output() ionItemReorder: EventEmitter<{ from: number, to: number }> = new EventEmitter<{ from: number, to: number }>();
constructor(elementRef: ElementRef, private _rendered: Renderer, private _zone: NgZone, @Optional() private _content: Content) {
constructor(elementRef: ElementRef, private _rendered: Renderer) {
super(elementRef);
}
@@ -47,7 +38,6 @@ export class List extends Ion {
*/
ngOnDestroy() {
this._slidingGesture && this._slidingGesture.destroy();
this._reorderGesture && this._reorderGesture.destroy();
}
/**
@@ -70,20 +60,38 @@ export class List extends Ion {
* ```
* @param {boolean} shouldEnable whether the item-sliding should be enabled or not
*/
enableSlidingItems(shouldEnable: boolean) {
if (this._enableSliding === shouldEnable) {
return;
}
@Input()
get sliding(): boolean {
return this._enableSliding;
}
set sliding(val: boolean) {
this._enableSliding = isTrueProperty(val);
this._updateSlidingState();
}
this._enableSliding = shouldEnable;
if (shouldEnable) {
console.debug('enableSlidingItems');
nativeTimeout(() => this._slidingGesture = new ItemSlidingGesture(this));
} else {
/**
* @private
*/
containsSlidingItem(contains: boolean) {
this._containsSlidingItems = contains;
this._updateSlidingState();
}
private _updateSlidingState() {
let shouldSlide = this._enableSliding && this._containsSlidingItems;
if (!shouldSlide) {
this._slidingGesture && this._slidingGesture.unlisten();
this._slidingGesture = null;
} else if (!this._slidingGesture) {
console.debug('enableSlidingItems');
this._slidingGesture = new ItemSlidingGesture(this);
}
}
/**
* Close the open sliding item.
*
@@ -106,111 +114,6 @@ export class List extends Ion {
closeSlidingItems() {
this._slidingGesture && this._slidingGesture.closeOpened();
}
setCssClass(classname: string, add: boolean) {
this._rendered.setElementClass(this.getNativeElement(), classname, add);
}
reorderStart() {
this.setCssClass('reorder-active', true);
}
/**
* @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;
this.setCssClass('reorder-active', false);
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;
// TODO: I think both loops can be merged into a single one
// but I had no luck last time I tried
/********* DOM READ ********** */
let children = this.elementRef.nativeElement.children;
/********* DOM WRITE ********* */
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

@@ -19,6 +19,7 @@ import { Tabs } from '../components/tabs/tabs';
import { Tab } from '../components/tabs/tab';
import { List, ListHeader } from '../components/list/list';
import { Item, ItemContent } from '../components/item/item';
import { Reorder } from '../components/item/item-reorder';
import { ItemSliding, ItemOptions } from '../components/item/item-sliding';
import { VirtualScroll } from '../components/virtual-scroll/virtual-scroll';
import { VirtualItem, VirtualHeader, VirtualFooter } from '../components/virtual-scroll/virtual-item';
@@ -145,6 +146,7 @@ export const IONIC_DIRECTIVES: any[] = [
ItemContent,
ItemSliding,
ItemOptions,
Reorder,
VirtualScroll,
VirtualItem,
VirtualHeader,

View File

@@ -19,6 +19,7 @@ export * from './util/click-block';
export * from './util/events';
export * from './util/keyboard';
export * from './util/form';
export { reorderArray } from './util/util';
export * from './animations/animation';
export * from './transitions/page-transition';

View File

@@ -175,3 +175,11 @@ export function getQuerystring(url: string): any {
}
return queryParams;
}
export function reorderArray(array: any[], indexes: {from: number, to: number}): any[] {
let element = array[indexes.from];
array.splice(indexes.from, 1);
array.splice(indexes.to, 0, element);
return array;
}