Skip to content

Commit 5c38921

Browse files
manucorporatadamdbradley
authored andcommitted
feat(list): reorder list items
References #5595
1 parent af22287 commit 5c38921

File tree

11 files changed

+545
-23
lines changed

11 files changed

+545
-23
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {Item} from './item';
2+
import {List} from '../list/list';
3+
import {UIEventManager} from '../../util/ui-event-manager';
4+
import {closest, Coordinates, pointerCoord, CSS, nativeRaf} from '../../util/dom';
5+
6+
7+
const AUTO_SCROLL_MARGIN = 60;
8+
const SCROLL_JUMP = 10;
9+
const ITEM_REORDER_ACTIVE = 'reorder-active';
10+
11+
/**
12+
* @private
13+
*/
14+
export class ItemReorderGesture {
15+
private selectedItem: Item = null;
16+
private offset: Coordinates;
17+
private lastToIndex: number;
18+
private lastYcoord: number;
19+
private emptyZone: boolean;
20+
21+
private itemHeight: number;
22+
private windowHeight: number;
23+
24+
private events: UIEventManager = new UIEventManager(false);
25+
26+
constructor(public list: List) {
27+
let element = this.list.getNativeElement();
28+
this.events.pointerEvents(element,
29+
(ev: any) => this.onDragStart(ev),
30+
(ev: any) => this.onDragMove(ev),
31+
(ev: any) => this.onDragEnd(ev));
32+
}
33+
34+
private onDragStart(ev: any): boolean {
35+
let itemEle = ev.target;
36+
if (itemEle.nodeName !== 'ION-REORDER') {
37+
return false;
38+
}
39+
40+
let item = itemEle['$ionComponent'];
41+
if (!item) {
42+
console.error('item does not contain ion component');
43+
return false;
44+
}
45+
ev.preventDefault();
46+
47+
// Preparing state
48+
this.offset = pointerCoord(ev);
49+
this.offset.y += this.list.scrollContent(0);
50+
this.selectedItem = item;
51+
this.itemHeight = item.height();
52+
this.lastToIndex = item.index;
53+
this.windowHeight = window.innerHeight - AUTO_SCROLL_MARGIN;
54+
item.setCssClass(ITEM_REORDER_ACTIVE, true);
55+
return true;
56+
}
57+
58+
private onDragMove(ev: any) {
59+
if (!this.selectedItem) {
60+
return;
61+
}
62+
ev.preventDefault();
63+
64+
// Get coordinate
65+
var coord = pointerCoord(ev);
66+
67+
// Scroll if we reach the scroll margins
68+
let scrollPosition = this.scroll(coord);
69+
70+
// Update selected item position
71+
let ydiff = Math.round(coord.y - this.offset.y + scrollPosition);
72+
this.selectedItem.setCssStyle(CSS.transform, `translateY(${ydiff}px)`);
73+
74+
// Only perform hit test if we moved at least 30px from previous position
75+
if (Math.abs(coord.y - this.lastYcoord) < 30) {
76+
return;
77+
}
78+
79+
// Hit test
80+
let overItem = this.itemForCoord(coord);
81+
if (!overItem) {
82+
this.emptyZone = true;
83+
return;
84+
}
85+
86+
// Move surrounding items if needed
87+
let toIndex = overItem.index;
88+
if (toIndex !== this.lastToIndex || this.emptyZone) {
89+
let fromIndex = this.selectedItem.index;
90+
this.lastToIndex = overItem.index;
91+
this.lastYcoord = coord.y;
92+
this.emptyZone = false;
93+
nativeRaf(() => {
94+
this.list.reorderMove(fromIndex, toIndex, this.itemHeight);
95+
});
96+
}
97+
}
98+
99+
private onDragEnd(ev: any) {
100+
if (!this.selectedItem) {
101+
return;
102+
}
103+
104+
nativeRaf(() => {
105+
let toIndex = this.lastToIndex;
106+
let fromIndex = this.selectedItem.index;
107+
this.selectedItem.setCssClass(ITEM_REORDER_ACTIVE, false);
108+
this.selectedItem = null;
109+
this.list.reorderEmit(fromIndex, toIndex);
110+
});
111+
}
112+
113+
private itemForCoord(coord: Coordinates): Item {
114+
let element = <any>document.elementFromPoint(this.offset.x - 100, coord.y);
115+
if (!element) {
116+
return null;
117+
}
118+
element = closest(element, 'ion-item', true);
119+
if (!element) {
120+
return null;
121+
}
122+
let item = <Item>(<any>element)['$ionComponent'];
123+
if (!item) {
124+
console.error('item does not have $ionComponent');
125+
return null;
126+
}
127+
return item;
128+
}
129+
130+
private scroll(coord: Coordinates): number {
131+
let scrollDiff = 0;
132+
if (coord.y < AUTO_SCROLL_MARGIN) {
133+
scrollDiff = -SCROLL_JUMP;
134+
} else if (coord.y > this.windowHeight) {
135+
scrollDiff = SCROLL_JUMP;
136+
}
137+
return this.list.scrollContent(scrollDiff);
138+
}
139+
140+
/**
141+
* @private
142+
*/
143+
destroy() {
144+
this.events.unlistenAll();
145+
this.events = null;
146+
this.list = null;
147+
}
148+
}

src/components/item/item-reorder.scss

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
// Item reorder
3+
// --------------------------------------------------
4+
5+
ion-reorder {
6+
display: none;
7+
8+
flex: 1;
9+
align-items: center;
10+
justify-content: center;
11+
12+
max-width: 40px;
13+
height: 100%;
14+
15+
font-size: 1.6em;
16+
17+
pointer-events: all;
18+
touch-action: manipulation;
19+
20+
ion-icon {
21+
pointer-events: none;
22+
}
23+
}
24+
25+
.reorder-enabled {
26+
27+
ion-item {
28+
will-change: transform;
29+
}
30+
31+
ion-reorder {
32+
display: flex;
33+
}
34+
}
35+
36+
ion-item.reorder-active {
37+
z-index: 4;
38+
39+
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
40+
opacity: .8;
41+
transition: none;
42+
43+
pointer-events: none;
44+
45+
ion-reorder {
46+
pointer-events: none;
47+
}
48+
}

src/components/item/item-reorder.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {Component, ElementRef, Inject, forwardRef} from '@angular/core';
2+
import {Item} from './item';
3+
4+
/**
5+
* @private
6+
*/
7+
@Component({
8+
selector: 'ion-reorder',
9+
template: `<ion-icon name="menu"></ion-icon>`
10+
})
11+
export class ItemReorder {
12+
constructor(
13+
@Inject(forwardRef(() => Item)) item: Item,
14+
elementRef: ElementRef) {
15+
elementRef.nativeElement['$ionComponent'] = item;
16+
}
17+
}

src/components/item/item-sliding-gesture.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export class ItemSlidingGesture extends DragGesture {
1212
selectedContainer: ItemSliding = null;
1313
openContainer: ItemSliding = null;
1414

15-
constructor(public list: List, public listEle: HTMLElement) {
16-
super(listEle, {
15+
constructor(public list: List) {
16+
super(list.getNativeElement(), {
1717
direction: 'x',
1818
threshold: DRAG_THRESHOLD
1919
});

src/components/item/item.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,4 @@ ion-input.item {
8585

8686
@import "item-media";
8787
@import "item-sliding";
88+
@import "item-reorder";

src/components/item/item.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import {Component, ContentChildren, forwardRef, ViewChild, ContentChild, Renderer, ElementRef, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
1+
import {Component, ContentChildren, forwardRef, Input, ViewChild, ContentChild, Renderer, ElementRef, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
22

33
import {Button} from '../button/button';
44
import {Form} from '../../util/form';
55
import {Icon} from '../icon/icon';
66
import {Label} from '../label/label';
7+
import {ItemReorder} from './item-reorder';
78

89

910
/**
@@ -235,11 +236,13 @@ import {Label} from '../label/label';
235236
'<ng-content select="ion-select,ion-input,ion-textarea,ion-datetime,ion-range,[item-content]"></ng-content>' +
236237
'</div>' +
237238
'<ng-content select="[item-right],ion-radio,ion-toggle"></ng-content>' +
239+
'<ion-reorder></ion-reorder>' +
238240
'</div>' +
239241
'<ion-button-effect></ion-button-effect>',
240242
host: {
241243
'class': 'item'
242244
},
245+
directives: [forwardRef(() => ItemReorder)],
243246
changeDetection: ChangeDetectionStrategy.OnPush,
244247
encapsulation: ViewEncapsulation.None,
245248
})
@@ -249,6 +252,11 @@ export class Item {
249252
private _label: Label;
250253
private _viewLabel: boolean = true;
251254

255+
/**
256+
* @private
257+
*/
258+
@Input() index: number;
259+
252260
/**
253261
* @private
254262
*/
@@ -261,6 +269,7 @@ export class Item {
261269

262270
constructor(form: Form, private _renderer: Renderer, private _elementRef: ElementRef) {
263271
this.id = form.nextId().toString();
272+
_elementRef.nativeElement['$ionComponent'] = this;
264273
}
265274

266275
/**
@@ -354,4 +363,11 @@ export class Item {
354363
icon.addClass('item-icon');
355364
});
356365
}
366+
367+
/**
368+
* @private
369+
*/
370+
height(): number {
371+
return this._elementRef.nativeElement.offsetHeight;
372+
}
357373
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Component, ChangeDetectorRef} from '@angular/core';
2+
import {ionicBootstrap} from '../../../../../src';
3+
4+
5+
@Component({
6+
templateUrl: 'main.html'
7+
})
8+
class E2EPage {
9+
items: any[] = [];
10+
isReordering: boolean = false;
11+
12+
constructor(private d: ChangeDetectorRef) {
13+
let nu = 30;
14+
for (let i = 0; i < nu; i++) {
15+
this.items.push(i);
16+
}
17+
}
18+
19+
toggle() {
20+
this.isReordering = !this.isReordering;
21+
}
22+
23+
reorder(indexes: any) {
24+
let element = this.items[indexes.from];
25+
this.items.splice(indexes.from, 1);
26+
this.items.splice(indexes.to, 0, element);
27+
}
28+
}
29+
30+
ionicBootstrap(E2EPage);
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<ion-toolbar primary>
2+
<ion-title>Reorder items</ion-title>
3+
<ion-buttons end>
4+
<button (click)="toggle()">
5+
Edit
6+
</button>
7+
</ion-buttons>
8+
</ion-toolbar>
9+
10+
<ion-content>
11+
12+
<ion-list [reorder]="isReordering" (ionItemReorder)="reorder($event)">
13+
<ion-item *ngFor="let item of items; let index=index"
14+
[index]="index"
15+
[style.background]="'rgb('+(255-item*4)+','+(255-item*4)+','+(255-item*4)+')'"
16+
[style.height]="item*2+35+'px'">
17+
{{item}}
18+
</ion-item>
19+
</ion-list>
20+
21+
</ion-content>
22+

0 commit comments

Comments
 (0)