Skip to content

Commit 5d873ff

Browse files
committed
feat(item-sliding): two-way item sliding gestures
2 parents f2a9f2d + c28aa53 commit 5d873ff

File tree

8 files changed

+587
-285
lines changed

8 files changed

+587
-285
lines changed
Lines changed: 66 additions & 204 deletions
Original file line numberDiff line numberDiff line change
@@ -1,221 +1,103 @@
1-
import {DIRECTION_RIGHT} from '../../gestures/hammer';
21
import {DragGesture} from '../../gestures/drag-gesture';
2+
import {ItemSliding} from './item-sliding';
33
import {List} from '../list/list';
44

5-
import {CSS, nativeRaf, closest} from '../../util/dom';
5+
import {closest} from '../../util/dom';
66

7+
const DRAG_THRESHOLD = 20;
8+
const MAX_ATTACK_ANGLE = 20;
79

810
export class ItemSlidingGesture extends DragGesture {
9-
canDrag: boolean = true;
10-
data = {};
11-
openItems: number = 0;
1211
onTap: any;
13-
onMouseOut: any;
14-
preventDrag: boolean = false;
15-
dragEnded: boolean = true;
12+
selectedContainer: ItemSliding = null;
13+
openContainer: ItemSliding = null;
1614

1715
constructor(public list: List, public listEle: HTMLElement) {
1816
super(listEle, {
1917
direction: 'x',
2018
threshold: DRAG_THRESHOLD
2119
});
22-
2320
this.listen();
21+
}
2422

25-
this.onTap = (ev: UIEvent) => {
26-
if (!isFromOptionButtons(ev.target)) {
27-
let didClose = this.closeOpened();
28-
if (didClose) {
29-
console.debug('tap close sliding item');
30-
preventDefault(ev);
31-
}
32-
}
33-
};
34-
35-
this.onMouseOut = (ev: any) => {
36-
if (ev.target.tagName === 'ION-ITEM-SLIDING') {
37-
console.debug('tap close sliding item');
38-
this.onDragEnd(ev);
39-
}
40-
};
23+
onTapCallback(ev: any) {
24+
if (isFromOptionButtons(ev.target)) {
25+
return;
26+
}
27+
let didClose = this.closeOpened();
28+
if (didClose) {
29+
console.debug('tap close sliding item, preventDefault');
30+
ev.preventDefault();
31+
}
4132
}
4233

4334
onDragStart(ev: any): boolean {
44-
let itemContainerEle = getItemContainer(ev.target);
45-
if (!itemContainerEle) {
46-
console.debug('onDragStart, no itemContainerEle');
35+
let angle = Math.abs(ev.angle);
36+
if (angle > MAX_ATTACK_ANGLE && Math.abs(angle - 180) > MAX_ATTACK_ANGLE) {
37+
this.closeOpened();
4738
return false;
4839
}
4940

50-
this.closeOpened(itemContainerEle);
41+
if (this.selectedContainer) {
42+
console.debug('onDragStart, another container is already selected');
43+
return false;
44+
}
5145

52-
let openAmout = this.getOpenAmount(itemContainerEle);
53-
let itemData = this.get(itemContainerEle);
54-
this.preventDrag = (openAmout > 0);
46+
let container = getContainer(ev);
47+
if (!container) {
48+
console.debug('onDragStart, no itemContainerEle');
49+
return false;
50+
}
5551

56-
if (this.preventDrag) {
52+
// Close open container if it is not the selected one.
53+
if (container !== this.openContainer) {
5754
this.closeOpened();
58-
console.debug('onDragStart, preventDefault');
59-
preventDefault(ev);
60-
return;
6155
}
6256

63-
itemContainerEle.classList.add('active-slide');
64-
65-
this.set(itemContainerEle, 'offsetX', openAmout);
66-
this.set(itemContainerEle, 'startX', ev.center[this.direction]);
67-
68-
this.dragEnded = false;
57+
// Close all item sliding containers but the selected one
58+
this.selectedContainer = container;
59+
this.openContainer = container;
60+
container.startSliding(ev.center.x);
6961

7062
return true;
7163
}
7264

7365
onDrag(ev: any): boolean {
74-
if (this.dragEnded || this.preventDrag || Math.abs(ev.deltaY) > 30) {
75-
console.debug('onDrag preventDrag, dragEnded:', this.dragEnded, 'preventDrag:', this.preventDrag, 'ev.deltaY:', Math.abs(ev.deltaY));
76-
this.preventDrag = true;
77-
return;
78-
}
79-
80-
let itemContainerEle = getItemContainer(ev.target);
81-
if (!itemContainerEle || !isActive(itemContainerEle)) {
82-
console.debug('onDrag, no itemContainerEle');
83-
return;
84-
}
85-
86-
let itemData = this.get(itemContainerEle);
87-
88-
if (!itemData.optsWidth) {
89-
itemData.optsWidth = getOptionsWidth(itemContainerEle);
90-
if (!itemData.optsWidth) {
91-
console.debug('onDrag, no optsWidth');
92-
return;
93-
}
66+
if (this.selectedContainer) {
67+
this.selectedContainer.moveSliding(ev.center.x);
68+
ev.preventDefault();
9469
}
95-
96-
let x = ev.center[this.direction];
97-
let delta = x - itemData.startX;
98-
99-
let newX = Math.max(0, itemData.offsetX - delta);
100-
101-
if (newX > itemData.optsWidth) {
102-
// Calculate the new X position, capped at the top of the buttons
103-
newX = -Math.min(-itemData.optsWidth, -itemData.optsWidth + (((delta + itemData.optsWidth) * 0.4)));
104-
}
105-
106-
if (newX > 5 && ev.srcEvent.type.indexOf('mouse') > -1 && !itemData.hasMouseOut) {
107-
itemContainerEle.addEventListener('mouseout', this.onMouseOut);
108-
itemData.hasMouseOut = true;
109-
}
110-
111-
nativeRaf(() => {
112-
if (!this.dragEnded && !this.preventDrag) {
113-
isItemActive(itemContainerEle, true);
114-
this.open(itemContainerEle, newX, false);
115-
}
116-
});
70+
return;
11771
}
11872

11973
onDragEnd(ev: any) {
120-
this.preventDrag = false;
121-
this.dragEnded = true;
122-
123-
let itemContainerEle = getItemContainer(ev.target);
124-
if (!itemContainerEle || !isActive(itemContainerEle)) {
125-
console.debug('onDragEnd, no itemContainerEle');
126-
return;
127-
}
128-
129-
// If we are currently dragging, we want to snap back into place
130-
// The final resting point X will be the width of the exposed buttons
131-
let itemData = this.get(itemContainerEle);
132-
133-
var restingPoint = itemData.optsWidth;
134-
135-
// Check if the drag didn't clear the buttons mid-point
136-
// and we aren't moving fast enough to swipe open
137-
138-
if (this.getOpenAmount(itemContainerEle) < (restingPoint / 2)) {
139-
140-
// If we are going left but too slow, or going right, go back to resting
141-
if (ev.direction & DIRECTION_RIGHT || Math.abs(ev.velocityX) < 0.3) {
142-
restingPoint = 0;
143-
}
144-
}
145-
146-
itemContainerEle.removeEventListener('mouseout', this.onMouseOut);
147-
itemData.hasMouseOut = false;
148-
149-
nativeRaf(() => {
150-
this.open(itemContainerEle, restingPoint, true);
151-
});
152-
}
153-
154-
closeOpened(doNotCloseEle?: HTMLElement) {
155-
let didClose = false;
156-
if (this.openItems) {
157-
let openItemElements = this.listEle.querySelectorAll('.active-slide');
158-
for (let i = 0; i < openItemElements.length; i++) {
159-
if (openItemElements[i] !== doNotCloseEle) {
160-
this.open(openItemElements[i], 0, true);
161-
didClose = true;
162-
}
163-
}
164-
}
165-
return didClose;
166-
}
167-
168-
open(itemContainerEle: any, openAmount: number, isFinal: boolean) {
169-
let slidingEle = itemContainerEle.querySelector('ion-item,[ion-item]');
170-
if (!slidingEle) {
171-
console.debug('open, no slidingEle, openAmount:', openAmount);
172-
return;
173-
}
174-
175-
this.set(itemContainerEle, 'openAmount', openAmount);
176-
177-
clearTimeout(this.get(itemContainerEle).timerId);
178-
179-
if (openAmount) {
180-
this.openItems++;
181-
182-
} else {
183-
let timerId = setTimeout(() => {
184-
if (slidingEle.style[CSS.transform] === '') {
185-
isItemActive(itemContainerEle, false);
186-
this.openItems--;
187-
}
188-
}, 400);
189-
this.set(itemContainerEle, 'timerId', timerId);
190-
}
191-
192-
slidingEle.style[CSS.transition] = (isFinal ? '' : 'none');
193-
slidingEle.style[CSS.transform] = (openAmount ? 'translate3d(' + -openAmount + 'px,0,0)' : '');
194-
195-
if (isFinal) {
196-
if (openAmount) {
197-
isItemActive(itemContainerEle, true);
198-
this.on('tap', this.onTap);
199-
200-
} else {
74+
if (this.selectedContainer) {
75+
let openAmount = this.selectedContainer.endSliding(ev.velocityX);
76+
this.selectedContainer = null;
77+
78+
// TODO: I am not sure listening for a tap event is the best idea
79+
// we should try mousedown/touchstart
80+
if (openAmount === 0) {
81+
this.openContainer = null;
20182
this.off('tap', this.onTap);
83+
this.onTap = null;
84+
} else if (!this.onTap) {
85+
this.onTap = (event: any) => this.onTapCallback(event);
86+
this.on('tap', this.onTap);
20287
}
20388
}
20489
}
20590

206-
getOpenAmount(itemContainerEle: any) {
207-
return this.get(itemContainerEle).openAmount || 0;
208-
}
209-
210-
get(itemContainerEle: any) {
211-
return this.data[itemContainerEle && itemContainerEle.$ionSlide] || {};
212-
}
213-
214-
set(itemContainerEle: any, key: any, value: any) {
215-
if (!this.data[itemContainerEle.$ionSlide]) {
216-
this.data[itemContainerEle.$ionSlide] = {};
91+
closeOpened(): boolean {
92+
if (this.openContainer) {
93+
this.openContainer.close();
94+
this.openContainer = null;
95+
this.selectedContainer = null;
96+
this.off('tap', this.onTap);
97+
this.onTap = null;
98+
return true;
21799
}
218-
this.data[itemContainerEle.$ionSlide][key] = value;
100+
return false;
219101
}
220102

221103
unlisten() {
@@ -224,34 +106,14 @@ export class ItemSlidingGesture extends DragGesture {
224106
}
225107
}
226108

227-
function isItemActive(ele: any, isActive: boolean) {
228-
ele.classList[isActive ? 'add' : 'remove']('active-slide');
229-
ele.classList[isActive ? 'add' : 'remove']('active-options');
230-
}
231-
232-
function preventDefault(ev: any) {
233-
console.debug('sliding item preventDefault', ev.type);
234-
ev.preventDefault();
235-
}
236-
237-
function getItemContainer(ele: any) {
238-
return closest(ele, 'ion-item-sliding', true);
239-
}
240-
241-
function isFromOptionButtons(ele: any) {
242-
return !!closest(ele, 'ion-item-options', true);
243-
}
244-
245-
function getOptionsWidth(itemContainerEle: any) {
246-
let optsEle = itemContainerEle.querySelector('ion-item-options');
247-
if (optsEle) {
248-
return optsEle.offsetWidth;
109+
function getContainer(ev: any): ItemSliding {
110+
let ele = closest(ev.target, 'ion-item-sliding', true);
111+
if (ele) {
112+
return ele['$ionComponent'];
249113
}
114+
return null;
250115
}
251116

252-
function isActive(itemContainerEle: any) {
253-
return itemContainerEle.classList.contains('active-slide');
117+
function isFromOptionButtons(ele: HTMLElement): boolean {
118+
return !!closest(ele, 'ion-item-options', true);
254119
}
255-
256-
257-
const DRAG_THRESHOLD = 20;

0 commit comments

Comments
 (0)