Skip to content

Commit 9959fc4

Browse files
ColinFrickMarkColeman1
authored andcommitted
feat: add touch support for reorderable and resizable features (#181)
* feat: port touch events to newest version * test(draggable): use MouseEvent constructor to simulate dragging * style(draggable): run prettier
1 parent 233c6a3 commit 9959fc4

File tree

9 files changed

+107
-52
lines changed

9 files changed

+107
-52
lines changed

projects/swimlane/ngx-datatable/src/lib/components/header/header-cell.component.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@
4949
visibility: visible;
5050
}
5151
}
52+
:host {
53+
@media (hover: none) {
54+
touch-action: none;
55+
56+
.resize-handle {
57+
visibility: visible;
58+
}
59+
60+
.datatable-header-cell-label.draggable {
61+
user-select: none;
62+
}
63+
}
64+
}
5265

5366
.resize-handle--not-resizable {
5467
:host(:hover) {

projects/swimlane/ngx-datatable/src/lib/components/header/header-cell.component.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { NgTemplateOutlet } from '@angular/common';
2525
import { InnerSortEvent, TableColumnInternal } from '../../types/internal.types';
2626
import { fromEvent, Subscription, takeUntil } from 'rxjs';
27+
import { getPositionFromEvent } from '../../utils/events';
2728

2829
@Component({
2930
selector: 'datatable-header-cell',
@@ -55,7 +56,11 @@ import { fromEvent, Subscription, takeUntil } from 'rxjs';
5556
<span (click)="onSort()" [class]="sortClass"> </span>
5657
</div>
5758
@if (column.resizeable) {
58-
<span class="resize-handle" (mousedown)="onMousedown($event)"></span>
59+
<span
60+
class="resize-handle"
61+
(mousedown)="onMousedown($event)"
62+
(touchstart)="onMousedown($event)"
63+
></span>
5964
}
6065
`,
6166
host: {
@@ -217,6 +222,9 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
217222
@HostListener('contextmenu', ['$event'])
218223
onContextmenu($event: MouseEvent): void {
219224
this.columnContextmenu.emit({ event: $event, column: this.column });
225+
if (this.column.draggable) {
226+
$event.preventDefault();
227+
}
220228
}
221229

222230
@HostListener('keydown.enter')
@@ -281,17 +289,21 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
281289
}
282290
}
283291

284-
protected onMousedown(event: MouseEvent): void {
292+
protected onMousedown(event: MouseEvent | TouchEvent): void {
293+
const isMouse = event instanceof MouseEvent;
285294
const initialWidth = this.element.clientWidth;
286-
const mouseDownScreenX = event.screenX;
295+
const { screenX } = getPositionFromEvent(event);
287296
event.stopPropagation();
288297

289-
const mouseup = fromEvent(document, 'mouseup');
298+
const mouseup = fromEvent<MouseEvent | TouchEvent>(document, isMouse ? 'mouseup' : 'touchend');
290299
this.subscription = mouseup.subscribe(() => this.onMouseup());
291300

292-
const mouseMoveSub = fromEvent(document, 'mousemove')
301+
const mouseMoveSub = fromEvent<MouseEvent | TouchEvent>(
302+
document,
303+
isMouse ? 'mousemove' : 'touchmove'
304+
)
293305
.pipe(takeUntil(mouseup))
294-
.subscribe((e: Event) => this.move(e, initialWidth, mouseDownScreenX));
306+
.subscribe((e: MouseEvent | TouchEvent) => this.move(e, initialWidth, screenX));
295307

296308
this.subscription.add(mouseMoveSub);
297309
}
@@ -303,8 +315,12 @@ export class DataTableHeaderCellComponent implements OnInit, OnDestroy {
303315
}
304316
}
305317

306-
private move(event: Event, initialWidth: number, mouseDownScreenX: number): void {
307-
const movementX = (event as MouseEvent).screenX - mouseDownScreenX;
318+
private move(
319+
event: MouseEvent | TouchEvent,
320+
initialWidth: number,
321+
mouseDownScreenX: number
322+
): void {
323+
const movementX = getPositionFromEvent(event).screenX - mouseDownScreenX;
308324
const newWidth = initialWidth + movementX;
309325
this.resizing.emit({ width: newWidth, column: this.column });
310326
}

projects/swimlane/ngx-datatable/src/lib/components/header/header.component.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export class DataTableHeaderComponent implements OnDestroy, OnChanges {
133133
@Input() reorderable: boolean;
134134
@Input() verticalScrollVisible = false;
135135

136-
dragEventTarget?: MouseEvent;
136+
dragEventTarget?: MouseEvent | TouchEvent;
137137

138138
@HostBinding('style.height')
139139
@Input()
@@ -212,7 +212,13 @@ export class DataTableHeaderComponent implements OnDestroy, OnChanges {
212212
this.destroyed = true;
213213
}
214214

215-
onLongPressStart({ event, model }: { event: MouseEvent; model: TableColumnInternal<unknown> }) {
215+
onLongPressStart({
216+
event,
217+
model
218+
}: {
219+
event: MouseEvent | TouchEvent;
220+
model: TableColumnInternal<unknown>;
221+
}) {
216222
model.dragging = true;
217223
this.dragEventTarget = event;
218224
}

projects/swimlane/ngx-datatable/src/lib/directives/draggable.directive.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,8 @@ describe('DraggableDirective', () => {
4444

4545
beforeEach(() => {
4646
element.classList.add('draggable');
47-
mouseDown = <MouseEvent>{
48-
target: element,
49-
preventDefault: () => {}
50-
};
47+
mouseDown = new MouseEvent('mousedown');
48+
Object.defineProperty(mouseDown, 'target', { value: element });
5149
});
5250

5351
// or else the document:mouseup event can fire again when resizing.

projects/swimlane/ngx-datatable/src/lib/directives/draggable.directive.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { fromEvent, Subscription } from 'rxjs';
1414
import { takeUntil } from 'rxjs/operators';
1515
import { DraggableDragEvent, TableColumnInternal } from '../types/internal.types';
16+
import { getPositionFromEvent } from '../utils/events';
1617

1718
/**
1819
* Draggable Directive for Angular2
@@ -53,7 +54,7 @@ export class DraggableDirective implements OnDestroy, OnChanges {
5354
this._destroySubscription();
5455
}
5556

56-
onMouseup(event: MouseEvent): void {
57+
onMouseup(event: MouseEvent | TouchEvent): void {
5758
if (!this.isDragging) {
5859
return;
5960
}
@@ -71,20 +72,27 @@ export class DraggableDirective implements OnDestroy, OnChanges {
7172
}
7273
}
7374

74-
onMousedown(event: MouseEvent): void {
75+
onMousedown(event: MouseEvent | TouchEvent): void {
76+
const isMouse = event instanceof MouseEvent;
7577
// we only want to drag the inner header text
7678
const isDragElm = (<HTMLElement>event.target).classList.contains('draggable');
7779

7880
if (isDragElm && (this.dragX || this.dragY)) {
7981
event.preventDefault();
8082
this.isDragging = true;
8183

82-
const mouseDownPos = { x: event.clientX, y: event.clientY };
84+
const mouseDownPos = getPositionFromEvent(event);
8385

84-
const mouseup = fromEvent<MouseEvent>(document, 'mouseup');
86+
const mouseup = fromEvent<MouseEvent | TouchEvent>(
87+
document,
88+
isMouse ? 'mouseup' : 'touchend'
89+
);
8590
this.subscription = mouseup.subscribe(ev => this.onMouseup(ev));
8691

87-
const mouseMoveSub = fromEvent<MouseEvent>(document, 'mousemove')
92+
const mouseMoveSub = fromEvent<MouseEvent | TouchEvent>(
93+
document,
94+
isMouse ? 'mousemove' : 'touchmove'
95+
)
8896
.pipe(takeUntil(mouseup))
8997
.subscribe(ev => this.move(ev, mouseDownPos));
9098

@@ -98,13 +106,14 @@ export class DraggableDirective implements OnDestroy, OnChanges {
98106
}
99107
}
100108

101-
move(event: MouseEvent, mouseDownPos: { x: number; y: number }): void {
109+
move(event: MouseEvent | TouchEvent, mouseDownPos: { clientX: number; clientY: number }): void {
102110
if (!this.isDragging) {
103111
return;
104112
}
105113

106-
const x = event.clientX - mouseDownPos.x;
107-
const y = event.clientY - mouseDownPos.y;
114+
const { clientX, clientY } = getPositionFromEvent(event);
115+
const x = clientX - mouseDownPos.clientX;
116+
const y = clientY - mouseDownPos.clientY;
108117

109118
if (this.dragX) {
110119
this.element.style.left = `${x}px`;

projects/swimlane/ngx-datatable/src/lib/directives/long-press.directive.ts

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,46 @@ import {
22
booleanAttribute,
33
Directive,
44
EventEmitter,
5-
HostBinding,
6-
HostListener,
75
Input,
86
numberAttribute,
97
OnDestroy,
10-
Output
8+
Output,
9+
signal
1110
} from '@angular/core';
1211
import { fromEvent, Subscription } from 'rxjs';
1312
import { TableColumnInternal } from '../types/internal.types';
1413

1514
@Directive({
16-
selector: '[long-press]'
15+
selector: '[long-press]',
16+
host: {
17+
'(touchstart)': 'onMouseDown($event)',
18+
'(mousedown)': 'onMouseDown($event)',
19+
'[class.press]': 'pressing()',
20+
'[class.longpress]': 'isLongPressing()'
21+
}
1722
})
1823
export class LongPressDirective implements OnDestroy {
1924
@Input({ transform: booleanAttribute }) pressEnabled = true;
2025
@Input() pressModel: TableColumnInternal;
2126
@Input({ transform: numberAttribute }) duration = 500;
2227

23-
@Output() longPressStart = new EventEmitter<{ event: MouseEvent; model: TableColumnInternal }>();
28+
@Output() longPressStart = new EventEmitter<{
29+
event: MouseEvent | TouchEvent;
30+
model: TableColumnInternal;
31+
}>();
2432
@Output() longPressEnd = new EventEmitter<{ model: TableColumnInternal }>();
2533

26-
pressing: boolean;
27-
isLongPressing: boolean;
34+
pressing = signal(false);
35+
isLongPressing = signal(false);
2836
timeout: any;
2937

3038
subscription: Subscription;
3139

32-
@HostBinding('class.press')
33-
get press(): boolean {
34-
return this.pressing;
35-
}
36-
37-
@HostBinding('class.longpress')
38-
get isLongPress(): boolean {
39-
return this.isLongPressing;
40-
}
40+
onMouseDown(event: MouseEvent | TouchEvent): void {
41+
const isMouse = event instanceof MouseEvent;
4142

42-
@HostListener('mousedown', ['$event'])
43-
onMouseDown(event: MouseEvent): void {
4443
// don't do right/middle clicks
45-
if (event.which !== 1 || !this.pressEnabled) {
44+
if (!this.pressEnabled || (isMouse && event.button !== 0)) {
4645
return;
4746
}
4847

@@ -52,14 +51,14 @@ export class LongPressDirective implements OnDestroy {
5251
return;
5352
}
5453

55-
this.pressing = true;
56-
this.isLongPressing = false;
54+
this.pressing.set(true);
55+
this.isLongPressing.set(false);
5756

58-
const mouseup = fromEvent(document, 'mouseup');
57+
const mouseup = fromEvent(document, isMouse ? 'mouseup' : 'touchend');
5958
this.subscription = mouseup.subscribe(() => this.endPress());
6059

6160
this.timeout = setTimeout(() => {
62-
this.isLongPressing = true;
61+
this.isLongPressing.set(true);
6362
this.longPressStart.emit({
6463
event,
6564
model: this.pressModel
@@ -69,8 +68,8 @@ export class LongPressDirective implements OnDestroy {
6968

7069
endPress(): void {
7170
clearTimeout(this.timeout);
72-
this.isLongPressing = false;
73-
this.pressing = false;
71+
this.isLongPressing.set(false);
72+
this.pressing.set(false);
7473
this._destroySubscription();
7574

7675
this.longPressEnd.emit({

projects/swimlane/ngx-datatable/src/lib/directives/orderable.directive.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
TableColumnInternal,
2020
TargetChangedEvent
2121
} from '../types/internal.types';
22+
import { getPositionFromEvent } from '../utils/events';
2223

2324
interface OrderPosition {
2425
left: number;
@@ -140,11 +141,10 @@ export class OrderableDirective implements AfterContentInit, OnDestroy {
140141
element.style.left = 'auto';
141142
}
142143

143-
isTarget(model: TableColumnInternal, event: MouseEvent) {
144+
isTarget(model: TableColumnInternal, event: MouseEvent | TouchEvent) {
144145
let i = 0;
145-
const x = event.x || event.clientX;
146-
const y = event.y || event.clientY;
147-
const targets = this.document.elementsFromPoint(x, y);
146+
const { clientX, clientY } = getPositionFromEvent(event);
147+
const targets = this.document.elementsFromPoint(clientX, clientY);
148148

149149
for (const id in this.positions) {
150150
// current column position which throws event.

projects/swimlane/ngx-datatable/src/lib/types/internal.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface Page {
4040
}
4141

4242
export interface DraggableDragEvent {
43-
event: MouseEvent;
43+
event: MouseEvent | TouchEvent;
4444
element: HTMLElement;
4545
model: TableColumnInternal;
4646
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Extracts the position (x, y coordinates) from a MouseEvent or TouchEvent.
3+
*
4+
* @param {MouseEvent | TouchEvent} event - The event object from which to extract the position. Can be either a MouseEvent or a TouchEvent.
5+
* @return {{ x: number, y: number }} An object containing the x and y coordinates of the event relative to the viewport.
6+
*/
7+
export function getPositionFromEvent(event: MouseEvent | TouchEvent): {
8+
clientX: number;
9+
clientY: number;
10+
screenX: number;
11+
screenY: number;
12+
} {
13+
return event instanceof MouseEvent ? (event as MouseEvent) : (event.changedTouches[0] as Touch);
14+
}

0 commit comments

Comments
 (0)