Skip to content

Commit 5e33a4d

Browse files
committed
mobile touch support (dragging)
Part II for #1757 * added support for mobile touch events, and tested two.html to work great on Android 10 Chrome - dragging between grids and insert/remove TODO: do sizing (though handle. idealy we could support 2 finger at a later time ! :)
1 parent 2917acc commit 5e33a4d

File tree

6 files changed

+235
-7
lines changed

6 files changed

+235
-7
lines changed

demo/two.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ <h1>Two grids demo</h1>
3737

3838
<div class="row" style="margin-top: 20px">
3939
<div class="col-md-6">
40-
<a onClick="toggleFloat(this, 0)" class="btn btn-primary" href="#">float: false</a>
40+
<a onClick="toggleFloat(this, 0)" class="btn btn-primary" href="#">float: true</a>
4141
<a onClick="compact(0)" class="btn btn-primary" href="#">Compact</a>
4242
<div class="grid-stack" id="left_grid"></div>
4343
</div>
4444
<div class="col-md-6">
45-
<a onClick="toggleFloat(this, 1)" class="btn btn-primary" href="#">float: true</a>
45+
<a onClick="toggleFloat(this, 1)" class="btn btn-primary" href="#">float: false</a>
4646
<a onClick="compact(1)" class="btn btn-primary" href="#">Compact</a>
4747
<div class="grid-stack" id="right_grid"></div>
4848
</div>
@@ -55,14 +55,14 @@ <h1>Two grids demo</h1>
5555
minRow: 1, // don't collapse when empty
5656
cellHeight: 70,
5757
disableOneColumnMode: true,
58-
float: false,
58+
float: true,
5959
// dragIn: '.sidebar .grid-stack-item', // add draggable to class
6060
// dragInOptions: { revert: 'invalid', scroll: false, appendTo: 'body', helper: 'clone' }, // clone
6161
removable: '.trash', // true or drag-out delete class
6262
acceptWidgets: function(el) { return true; } // function example, but can also be: true | false | '.someClass' value
6363
};
6464
let grids = GridStack.initAll(options);
65-
grids[1].float(true);
65+
grids[1].float(false);
6666

6767
// new 4.x static method instead of setting up options on every grid (never been per grid really) but old options still works
6868
GridStack.setupDragIn('.sidebar .grid-stack-item', { revert: 'invalid', scroll: false, appendTo: 'body', helper: myClone });

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"build": "yarn --no-progress && rm -rf dist/* && grunt && yarn build:es6 && yarn build:es5 && yarn doc",
2323
"build:es6": "webpack && tsc --stripInternal",
2424
"build:es5": "webpack --config es5/webpack.config.js && tsc --stripInternal --project es5/tsconfig.json",
25-
"w": "rm -rf dist/* && grunt && webpack",
25+
"w": "webpack",
2626
"t": "rm -rf dist/* && grunt && tsc --stripInternal",
2727
"doc": "doctoc ./README.md && doctoc ./doc/README.md && doctoc ./doc/CHANGES.md",
2828
"test": "yarn lint && karma start karma.conf.js",

src/h5/dd-draggable.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DDUtils } from './dd-utils';
88
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
99
import { GridItemHTMLElement, DDUIData } from '../types';
1010
import { DDElementHost } from './dd-element';
11+
import { isTouch, touchend, touchmove, touchstart, pointerdown } from './touch';
1112

1213
// TODO: merge with DDDragOpt ?
1314
export interface DDDraggableOpt {
@@ -31,6 +32,8 @@ interface DragOffset {
3132
offsetTop: number;
3233
}
3334

35+
type DDDragEvent = 'drag' | 'dragstart' | 'dragstop';
36+
3437
// let count = 0; // TEST
3538

3639
export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt<DDDraggableOpt> {
@@ -69,18 +72,23 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
6972
this.enable();
7073
}
7174

72-
public on(event: 'drag' | 'dragstart' | 'dragstop', callback: (event: DragEvent) => void): void {
75+
public on(event: DDDragEvent, callback: (event: DragEvent) => void): void {
7376
super.on(event, callback);
7477
}
7578

76-
public off(event: 'drag' | 'dragstart' | 'dragstop'): void {
79+
public off(event: DDDragEvent): void {
7780
super.off(event);
7881
}
7982

8083
public enable(): void {
8184
if (this.disabled === false) return;
8285
super.enable();
8386
this.dragEl.addEventListener('mousedown', this._mouseDown);
87+
if (isTouch) {
88+
this.dragEl.addEventListener('touchstart', touchstart);
89+
this.dragEl.addEventListener('pointerdown', pointerdown);
90+
// this.dragEl.style.touchAction = 'none'; // not needed unlike pointerdown doc comment
91+
}
8492
this.el.classList.remove('ui-draggable-disabled');
8593
this.el.classList.add('ui-draggable');
8694
}
@@ -89,6 +97,9 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
8997
if (this.disabled === true) return;
9098
super.disable();
9199
this.dragEl.removeEventListener('mousedown', this._mouseDown);
100+
if (isTouch) {
101+
this.dragEl.removeEventListener('touchstart', touchstart);
102+
}
92103
this.el.classList.remove('ui-draggable');
93104
if (!forDestroy) this.el.classList.add('ui-draggable-disabled');
94105
}
@@ -126,6 +137,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
126137
// document handler so we can continue receiving moves as the item is 'fixed' position, and capture=true so WE get a first crack
127138
document.addEventListener('mousemove', this._mouseMove, true); // true=capture, not bubble
128139
document.addEventListener('mouseup', this._mouseUp, true);
140+
if (isTouch) {
141+
this.dragEl.addEventListener('touchmove', touchmove);
142+
this.dragEl.addEventListener('touchend', touchend);
143+
}
129144

130145
e.preventDefault();
131146
DDManager.mouseHandled = true;
@@ -177,6 +192,10 @@ export class DDDraggable extends DDBaseImplement implements HTMLElementExtendOpt
177192
protected _mouseUp(e: MouseEvent): void {
178193
document.removeEventListener('mousemove', this._mouseMove, true);
179194
document.removeEventListener('mouseup', this._mouseUp, true);
195+
if (isTouch) {
196+
this.dragEl.removeEventListener('touchmove', touchmove, true);
197+
this.dragEl.removeEventListener('touchend', touchend, true);
198+
}
180199
if (this.dragging) {
181200
delete this.dragging;
182201
this.helper.classList.remove('ui-draggable-dragging');

src/h5/dd-droppable.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DDManager } from './dd-manager';
88
import { DDBaseImplement, HTMLElementExtendOpt } from './dd-base-impl';
99
import { DDUtils } from './dd-utils';
1010
import { DDElementHost } from './dd-element';
11+
import { isTouch, pointerenter, pointerleave } from './touch';
1112

1213
export interface DDDroppableOpt {
1314
accept?: string | ((el: HTMLElement) => boolean);
@@ -50,6 +51,10 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
5051
this.el.classList.remove('ui-droppable-disabled');
5152
this.el.addEventListener('mouseenter', this._mouseEnter);
5253
this.el.addEventListener('mouseleave', this._mouseLeave);
54+
if (isTouch) {
55+
this.el.addEventListener('pointerenter', pointerenter);
56+
this.el.addEventListener('pointerleave', pointerleave);
57+
}
5358
}
5459

5560
public disable(forDestroy = false): void {
@@ -59,6 +64,10 @@ export class DDDroppable extends DDBaseImplement implements HTMLElementExtendOpt
5964
if (!forDestroy) this.el.classList.add('ui-droppable-disabled');
6065
this.el.removeEventListener('mouseenter', this._mouseEnter);
6166
this.el.removeEventListener('mouseleave', this._mouseLeave);
67+
if (isTouch) {
68+
this.el.removeEventListener('pointerenter', pointerenter);
69+
this.el.removeEventListener('pointerleave', pointerleave);
70+
}
6271
}
6372

6473
public destroy(): void {

src/h5/dd-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { DDDroppable } from './dd-droppable';
1010
* globals that are shared across Drag & Drop instances
1111
*/
1212
export class DDManager {
13+
1314
/** true if a mouse down event was handled */
1415
public static mouseHandled: boolean;
1516

src/h5/touch.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* touch.ts 5.1.1
3+
* Copyright (c) 2021 Alain Dumesny - see GridStack root license
4+
*/
5+
6+
import { DDManager } from "./dd-manager";
7+
8+
/**
9+
* Detect touch support - Windows Surface devices and other touch devices
10+
*/
11+
export const isTouch: boolean = ( 'ontouchstart' in document
12+
|| 'ontouchstart' in window
13+
|| !!window.TouchEvent
14+
|| ((window as any).DocumentTouch && document instanceof (window as any).DocumentTouch)
15+
|| navigator.maxTouchPoints > 0
16+
|| (navigator as any).msMaxTouchPoints > 0
17+
);
18+
19+
20+
// interface TouchCoord {x: number, y: number};
21+
22+
class DDTouch {
23+
public static touchHandled: boolean;
24+
public static pointerLeaveTimeout: number;
25+
}
26+
27+
/**
28+
* Get the x,y position of a touch event
29+
*/
30+
// function getTouchCoords(e: TouchEvent): TouchCoord {
31+
// return {
32+
// x: e.changedTouches[0].pageX,
33+
// y: e.changedTouches[0].pageY
34+
// };
35+
// }
36+
37+
/**
38+
* Simulate a mouse event based on a corresponding touch event
39+
* @param {Object} e A touch event
40+
* @param {String} simulatedType The corresponding mouse event
41+
*/
42+
function simulateMouseEvent(e: TouchEvent, simulatedType: string) {
43+
44+
// Ignore multi-touch events
45+
if (e.touches.length > 1) return;
46+
47+
// Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors
48+
if (e.cancelable) e.preventDefault();
49+
50+
const touch = e.changedTouches[0],
51+
simulatedEvent = document.createEvent('MouseEvents');
52+
53+
// Initialize the simulated mouse event using the touch event's coordinates
54+
simulatedEvent.initMouseEvent(
55+
simulatedType, // type
56+
true, // bubbles
57+
true, // cancelable
58+
window, // view
59+
1, // detail
60+
touch.screenX, // screenX
61+
touch.screenY, // screenY
62+
touch.clientX, // clientX
63+
touch.clientY, // clientY
64+
false, // ctrlKey
65+
false, // altKey
66+
false, // shiftKey
67+
false, // metaKey
68+
0, // button
69+
null // relatedTarget
70+
);
71+
72+
// Dispatch the simulated event to the target element
73+
e.target.dispatchEvent(simulatedEvent);
74+
}
75+
76+
/**
77+
* Simulate a mouse event based on a corresponding Pointer event
78+
* @param {Object} e A pointer event
79+
* @param {String} simulatedType The corresponding mouse event
80+
*/
81+
function simulatePointerMouseEvent(e: PointerEvent, simulatedType: string) {
82+
83+
// Prevent "Ignored attempt to cancel a touchmove event with cancelable=false" errors
84+
if (e.cancelable) e.preventDefault();
85+
86+
const simulatedEvent = document.createEvent('MouseEvents');
87+
88+
// Initialize the simulated mouse event using the touch event's coordinates
89+
simulatedEvent.initMouseEvent(
90+
simulatedType, // type
91+
true, // bubbles
92+
true, // cancelable
93+
window, // view
94+
1, // detail
95+
e.screenX, // screenX
96+
e.screenY, // screenY
97+
e.clientX, // clientX
98+
e.clientY, // clientY
99+
false, // ctrlKey
100+
false, // altKey
101+
false, // shiftKey
102+
false, // metaKey
103+
0, // button
104+
null // relatedTarget
105+
);
106+
107+
// Dispatch the simulated event to the target element
108+
e.target.dispatchEvent(simulatedEvent);
109+
}
110+
111+
112+
/**
113+
* Handle the touchstart events
114+
* @param {Object} e The widget element's touchstart event
115+
*/
116+
export function touchstart(e: TouchEvent) {
117+
// Ignore the event if another widget is already being handled
118+
if (DDTouch.touchHandled) return; DDTouch.touchHandled = true;
119+
120+
// Simulate the mouse events
121+
// simulateMouseEvent(e, 'mouseover');
122+
// simulateMouseEvent(e, 'mousemove');
123+
simulateMouseEvent(e, 'mousedown');
124+
};
125+
126+
/**
127+
* Handle the touchmove events
128+
* @param {Object} e The document's touchmove event
129+
*/
130+
export function touchmove(e: TouchEvent) {
131+
// Ignore event if not handled by us
132+
if (!DDTouch.touchHandled) return;
133+
134+
simulateMouseEvent(e, 'mousemove');
135+
};
136+
137+
/**
138+
* Handle the touchend events
139+
* @param {Object} e The document's touchend event
140+
*/
141+
export function touchend(e: TouchEvent) {
142+
143+
// Ignore event if not handled
144+
if (!DDTouch.touchHandled) return;
145+
146+
// cancel delayed leave event when we release on ourself which happens BEFORE we get this!
147+
if (DDTouch.pointerLeaveTimeout) {
148+
window.clearTimeout(DDTouch.pointerLeaveTimeout);
149+
delete DDTouch.pointerLeaveTimeout;
150+
}
151+
152+
const wasDragging = !!DDManager.dragElement;
153+
154+
// Simulate the mouseup event
155+
simulateMouseEvent(e, 'mouseup');
156+
// simulateMouseEvent(event, 'mouseout');
157+
158+
// If the touch interaction did not move, it should trigger a click
159+
if (!wasDragging) {
160+
simulateMouseEvent(e, 'click');
161+
}
162+
163+
// Unset the flag to allow other widgets to inherit the touch event
164+
DDTouch.touchHandled = false;
165+
};
166+
167+
/**
168+
* Note we don't get touchenter/touchleave (which are deprecated)
169+
* see https://stackoverflow.com/questions/27908339/js-touch-equivalent-for-mouseenter
170+
* so instead of PointerEvent to still get enter/leave and send the matching mouse event.
171+
*/
172+
export function pointerdown(e: PointerEvent) {
173+
(e.target as HTMLElement).releasePointerCapture(e.pointerId) // <- Important!
174+
}
175+
176+
export function pointerenter(e: PointerEvent) {
177+
// ignore the initial one we get on pointerdown on ourself
178+
if (!DDManager.dragElement) {
179+
// console.log('pointerenter ignored');
180+
return;
181+
}
182+
// console.log('pointerenter');
183+
simulatePointerMouseEvent(e, 'mouseenter');
184+
}
185+
186+
export function pointerleave(e: PointerEvent) {
187+
// ignore the leave on ourself we get before releasing the mouse over ourself
188+
// by delaying sending the event and having the up event cancel us
189+
if (!DDManager.dragElement) {
190+
// console.log('pointerleave ignored');
191+
return;
192+
}
193+
DDTouch.pointerLeaveTimeout = window.setTimeout(() => {
194+
delete DDTouch.pointerLeaveTimeout;
195+
// console.log('pointerleave delayed');
196+
simulatePointerMouseEvent(e, 'mouseleave');
197+
}, 10);
198+
}
199+

0 commit comments

Comments
 (0)