Skip to content

Commit

Permalink
feat(list): list items can be reordered
Browse files Browse the repository at this point in the history
references ionic-team#5595
  • Loading branch information
manucorporat committed Jun 18, 2016
1 parent 0c88589 commit 99bd47f
Show file tree
Hide file tree
Showing 13 changed files with 549 additions and 67 deletions.
145 changes: 145 additions & 0 deletions src/components/item/item-reorder-gesture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {Item} from './item';
import {List} from '../list/list';
import {Listener} from '../../util/listener';
import {closest, Coordinates, pointerCoord, CSS, nativeRaf, raf} 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 itemHeight: number;
private lastToIndex: number;
private lastYcoord: number;
private emptyZone: boolean;
private events: Listener = new Listener();

constructor(public list: List) {
let element = this.list.getNativeElement();
this.events.pointerEvents(element,
(ev: any) => this.onDragStart(ev),
(ev: any) => this.onDrag(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;
item.setCssClass(ITEM_REORDER_ACTIVE, true);
return true;
}

private onDrag(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 item = this.itemForCoord(coord);
if (!item) {
this.emptyZone = true;
return;
}

// Move surrounding items if needed
let toIndex = item.index;
if (item.index !== this.lastToIndex || this.emptyZone) {
let fromIndex = this.selectedItem.index;
this.lastToIndex = item.index;
this.lastYcoord = coord.y;
this.emptyZone = false;
nativeRaf(() => {
this.list.reorderMove(fromIndex, toIndex, this.itemHeight);
});
}
}

private onDragEnd(ev: any) {
if (!this.selectedItem) {
return;
}

var toIndex = this.lastToIndex;
var fromIndex = this.selectedItem.index;

nativeRaf(() => {
this.selectedItem.setCssClass(ITEM_REORDER_ACTIVE, false);
this.selectedItem = null;
this.list.reorderEmit(fromIndex, toIndex);
});
}

private itemForCoord(coord: Coordinates): Item {
let element = <any>document.elementFromPoint(this.offset.x - 100, coord.y);
if (!element) {
return null;
}
element = closest(element, 'ion-item', true);
if (!element) {
return null;
}
let item = <Item>(<any>element)['$ionComponent'];
if (!item) {
console.error('item does not have $ionComponent');
return null;
}
return item;
}

private scroll(coord: Coordinates): number {
let scrollDiff = 0;
if (coord.y < AUTO_SCROLL_MARGIN) {
scrollDiff = -SCROLL_JUMP;
} else if (coord.y > (window.innerHeight - AUTO_SCROLL_MARGIN)) {
scrollDiff = SCROLL_JUMP;
}
return this.list.scrollContent(scrollDiff);
}

/**
* @private
*/
destroy() {
this.events.unlistenAll();
this.events = null;
this.list = null;
}
}
48 changes: 48 additions & 0 deletions src/components/item/item-reorder.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
17 changes: 17 additions & 0 deletions src/components/item/item-reorder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Component, ElementRef, Inject, forwardRef} from '@angular/core';
import {Item} from './item';

/**
* @private
*/
@Component({
selector: 'ion-reorder',
template: `<ion-icon name="menu"></ion-icon>`
})
export class ItemReorder {
constructor(
@Inject(forwardRef(() => Item)) item: Item,
elementRef: ElementRef) {
elementRef.nativeElement['$ionComponent'] = item;
}
}
4 changes: 2 additions & 2 deletions src/components/item/item-sliding-gesture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand Down
1 change: 1 addition & 0 deletions src/components/item/item.scss
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ ion-input.item {

@import "item-media";
@import "item-sliding";
@import "item-reorder";
18 changes: 17 additions & 1 deletion src/components/item/item.ts
Original file line number Diff line number Diff line change
@@ -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';


/**
Expand Down Expand Up @@ -235,11 +236,13 @@ import {Label} from '../label/label';
'<ng-content select="ion-select,ion-input,ion-textarea,ion-datetime,ion-range,[item-content]"></ng-content>' +
'</div>' +
'<ng-content select="[item-right],ion-radio,ion-toggle"></ng-content>' +
'<ion-reorder></ion-reorder>' +
'</div>' +
'<ion-button-effect></ion-button-effect>',
host: {
'class': 'item'
},
directives: [forwardRef(() => ItemReorder)],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
Expand All @@ -249,6 +252,11 @@ export class Item {
private _label: Label;
private _viewLabel: boolean = true;

/**
* @private
*/
@Input() index: number;

/**
* @private
*/
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -354,4 +363,11 @@ export class Item {
icon.addClass('item-icon');
});
}

/**
* @private
*/
height(): number {
return this._elementRef.nativeElement.offsetHeight;
}
}
1 change: 1 addition & 0 deletions src/components/item/test/reorder/e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

30 changes: 30 additions & 0 deletions src/components/item/test/reorder/index.ts
Original file line number Diff line number Diff line change
@@ -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);
21 changes: 21 additions & 0 deletions src/components/item/test/reorder/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<ion-toolbar primary>
<ion-title>Reorder items</ion-title>
<ion-buttons end>
<button (click)="toggle()">
Edit
</button>
</ion-buttons>
</ion-toolbar>

<ion-content>

<ion-list [reorder]="isReordering" (ionItemReorder)="reorder($event)">
<ion-item *ngFor="let item of items; let index=index"
[index]="index"
[style.background]="'rgb('+(255-item*4)+','+(255-item*4)+','+(255-item*4)+')'">
{{item}}
</ion-item>
</ion-list>

</ion-content>

4 changes: 2 additions & 2 deletions src/components/label/label.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ ion-label {
font-size: inherit;
text-overflow: ellipsis;
white-space: nowrap;

pointer-events: none;
}

.item-input ion-label {
flex: initial;

max-width: 200px;

pointer-events: none;
}

[text-wrap] ion-label {
Expand Down
Loading

0 comments on commit 99bd47f

Please sign in to comment.