@@ -70,7 +70,7 @@ let calendarBodyId = 1;
70
70
encapsulation : ViewEncapsulation . None ,
71
71
changeDetection : ChangeDetectionStrategy . OnPush ,
72
72
} )
73
- export class MatCalendarBody implements OnChanges , OnDestroy , AfterViewChecked {
73
+ export class MatCalendarBody < D = any > implements OnChanges , OnDestroy , AfterViewChecked {
74
74
/**
75
75
* Used to skip the next focus event when rendering the preview range.
76
76
* We need a flag like this, because some browsers fire focus events asynchronously.
@@ -150,6 +150,12 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
150
150
151
151
@Output ( ) readonly activeDateChange = new EventEmitter < MatCalendarUserEvent < number > > ( ) ;
152
152
153
+ /** Emits the date at the possible start of a drag event. */
154
+ @Output ( ) readonly dragStarted = new EventEmitter < MatCalendarUserEvent < D > > ( ) ;
155
+
156
+ /** Emits the date at the conclusion of a drag, or null if mouse was not released on a date. */
157
+ @Output ( ) readonly dragEnded = new EventEmitter < MatCalendarUserEvent < D | null > > ( ) ;
158
+
153
159
/** The number of blank cells to put at the beginning for the first row. */
154
160
_firstRowOffset : number ;
155
161
@@ -159,18 +165,31 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
159
165
/** Width of an individual cell. */
160
166
_cellWidth : string ;
161
167
168
+ private _didDragSinceMouseDown = false ;
169
+
162
170
constructor ( private _elementRef : ElementRef < HTMLElement > , private _ngZone : NgZone ) {
163
171
_ngZone . runOutsideAngular ( ( ) => {
164
172
const element = _elementRef . nativeElement ;
165
173
element . addEventListener ( 'mouseenter' , this . _enterHandler , true ) ;
174
+ element . addEventListener ( 'touchmove' , this . _touchmoveHandler , true ) ;
166
175
element . addEventListener ( 'focus' , this . _enterHandler , true ) ;
167
176
element . addEventListener ( 'mouseleave' , this . _leaveHandler , true ) ;
168
177
element . addEventListener ( 'blur' , this . _leaveHandler , true ) ;
178
+ element . addEventListener ( 'mousedown' , this . _mousedownHandler ) ;
179
+ element . addEventListener ( 'touchstart' , this . _mousedownHandler ) ;
180
+ window . addEventListener ( 'mouseup' , this . _mouseupHandler ) ;
181
+ window . addEventListener ( 'touchend' , this . _touchendHandler ) ;
169
182
} ) ;
170
183
}
171
184
172
185
/** Called when a cell is clicked. */
173
186
_cellClicked ( cell : MatCalendarCell , event : MouseEvent ) : void {
187
+ // Ignore "clicks" that are actually canceled drags (eg the user dragged
188
+ // off and then went back to this cell to undo).
189
+ if ( this . _didDragSinceMouseDown ) {
190
+ return ;
191
+ }
192
+
174
193
if ( cell . enabled ) {
175
194
this . selectedValueChange . emit ( { value : cell . value , event} ) ;
176
195
}
@@ -207,9 +226,14 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
207
226
ngOnDestroy ( ) {
208
227
const element = this . _elementRef . nativeElement ;
209
228
element . removeEventListener ( 'mouseenter' , this . _enterHandler , true ) ;
229
+ element . removeEventListener ( 'touchmove' , this . _touchmoveHandler , true ) ;
210
230
element . removeEventListener ( 'focus' , this . _enterHandler , true ) ;
211
231
element . removeEventListener ( 'mouseleave' , this . _leaveHandler , true ) ;
212
232
element . removeEventListener ( 'blur' , this . _leaveHandler , true ) ;
233
+ element . removeEventListener ( 'mousedown' , this . _mousedownHandler ) ;
234
+ element . removeEventListener ( 'touchstart' , this . _mousedownHandler ) ;
235
+ window . removeEventListener ( 'mouseup' , this . _mouseupHandler ) ;
236
+ window . removeEventListener ( 'touchend' , this . _touchendHandler ) ;
213
237
}
214
238
215
239
/** Returns whether a cell is active. */
@@ -400,32 +424,112 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
400
424
}
401
425
} ;
402
426
427
+ private _touchmoveHandler = ( event : TouchEvent ) => {
428
+ if ( ! this . isRange ) return ;
429
+
430
+ const target = getActualTouchTarget ( event ) ;
431
+ const cell = target ? this . _getCellFromElement ( target as HTMLElement ) : null ;
432
+
433
+ if ( target !== event . target ) {
434
+ this . _didDragSinceMouseDown = true ;
435
+ }
436
+
437
+ // If the initial target of the touch is a date cell, prevent default so
438
+ // that the move is not handled as a scroll.
439
+ if ( getCellElement ( event . target as HTMLElement ) ) {
440
+ event . preventDefault ( ) ;
441
+ }
442
+
443
+ this . _ngZone . run ( ( ) => this . previewChange . emit ( { value : cell ?. enabled ? cell : null , event} ) ) ;
444
+ } ;
445
+
403
446
/**
404
447
* Event handler for when the user's pointer leaves an element
405
448
* inside the calendar body (e.g. by hovering out or blurring).
406
449
*/
407
450
private _leaveHandler = ( event : Event ) => {
408
451
// We only need to hit the zone when we're selecting a range.
409
452
if ( this . previewEnd !== null && this . isRange ) {
453
+ if ( event . type !== 'blur' ) {
454
+ this . _didDragSinceMouseDown = true ;
455
+ }
456
+
410
457
// Only reset the preview end value when leaving cells. This looks better, because
411
458
// we have a gap between the cells and the rows and we don't want to remove the
412
459
// range just for it to show up again when the user moves a few pixels to the side.
413
- if ( event . target && this . _getCellFromElement ( event . target as HTMLElement ) ) {
460
+ if (
461
+ event . target &&
462
+ this . _getCellFromElement ( event . target as HTMLElement ) &&
463
+ ! (
464
+ ( event as MouseEvent ) . relatedTarget &&
465
+ this . _getCellFromElement ( ( event as MouseEvent ) . relatedTarget as HTMLElement )
466
+ )
467
+ ) {
414
468
this . _ngZone . run ( ( ) => this . previewChange . emit ( { value : null , event} ) ) ;
415
469
}
416
470
}
417
471
} ;
418
472
419
- /** Finds the MatCalendarCell that corresponds to a DOM node. */
420
- private _getCellFromElement ( element : HTMLElement ) : MatCalendarCell | null {
421
- let cell : HTMLElement | undefined ;
473
+ /**
474
+ * Triggered on mousedown or touchstart on a date cell.
475
+ * Respsonsible for starting a drag sequence.
476
+ */
477
+ private _mousedownHandler = ( event : Event ) => {
478
+ if ( ! this . isRange ) return ;
422
479
423
- if ( isTableCell ( element ) ) {
424
- cell = element ;
425
- } else if ( isTableCell ( element . parentNode ! ) ) {
426
- cell = element . parentNode as HTMLElement ;
480
+ this . _didDragSinceMouseDown = false ;
481
+ // Begin a drag if a cell within the current range was targeted.
482
+ const cell = event . target && this . _getCellFromElement ( event . target as HTMLElement ) ;
483
+ if ( ! cell || ! this . _isInRange ( cell . rawValue ) ) {
484
+ return ;
427
485
}
428
486
487
+ this . _ngZone . run ( ( ) => {
488
+ this . dragStarted . emit ( {
489
+ value : cell . rawValue ,
490
+ event,
491
+ } ) ;
492
+ } ) ;
493
+ } ;
494
+
495
+ /** Triggered on mouseup anywhere. Respsonsible for ending a drag sequence. */
496
+ private _mouseupHandler = ( event : Event ) => {
497
+ if ( ! this . isRange ) return ;
498
+
499
+ const cellElement = getCellElement ( event . target as HTMLElement ) ;
500
+ if ( ! cellElement ) {
501
+ // Mouseup happened outside of datepicker. Cancel drag.
502
+ this . _ngZone . run ( ( ) => {
503
+ this . dragEnded . emit ( { value : null , event} ) ;
504
+ } ) ;
505
+ return ;
506
+ }
507
+
508
+ if ( cellElement . closest ( '.mat-calendar-body' ) !== this . _elementRef . nativeElement ) {
509
+ // Mouseup happened inside a different month instance.
510
+ // Allow it to handle the event.
511
+ return ;
512
+ }
513
+
514
+ this . _ngZone . run ( ( ) => {
515
+ const cell = this . _getCellFromElement ( cellElement ) ;
516
+ this . dragEnded . emit ( { value : cell ?. rawValue ?? null , event} ) ;
517
+ } ) ;
518
+ } ;
519
+
520
+ /** Triggered on touchend anywhere. Respsonsible for ending a drag sequence. */
521
+ private _touchendHandler = ( event : TouchEvent ) => {
522
+ const target = getActualTouchTarget ( event ) ;
523
+
524
+ if ( target ) {
525
+ this . _mouseupHandler ( { target} as unknown as Event ) ;
526
+ }
527
+ } ;
528
+
529
+ /** Finds the MatCalendarCell that corresponds to a DOM node. */
530
+ private _getCellFromElement ( element : HTMLElement ) : MatCalendarCell | null {
531
+ const cell = getCellElement ( element ) ;
532
+
429
533
if ( cell ) {
430
534
const row = cell . getAttribute ( 'data-mat-row' ) ;
431
535
const col = cell . getAttribute ( 'data-mat-col' ) ;
@@ -446,8 +550,25 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
446
550
}
447
551
448
552
/** Checks whether a node is a table cell element. */
449
- function isTableCell ( node : Node ) : node is HTMLTableCellElement {
450
- return node . nodeName === 'TD' ;
553
+ function isTableCell ( node : Node | undefined | null ) : node is HTMLTableCellElement {
554
+ return node ?. nodeName === 'TD' ;
555
+ }
556
+
557
+ /**
558
+ * Gets the date table cell element that is or contains the specified element.
559
+ * Or returns null if element is not part of a date cell.
560
+ */
561
+ function getCellElement ( element : HTMLElement ) : HTMLElement | null {
562
+ let cell : HTMLElement | undefined ;
563
+ if ( isTableCell ( element ) ) {
564
+ cell = element ;
565
+ } else if ( isTableCell ( element . parentNode ) ) {
566
+ cell = element . parentNode as HTMLElement ;
567
+ } else if ( isTableCell ( element . parentNode ?. parentNode ) ) {
568
+ cell = element . parentNode ! . parentNode as HTMLElement ;
569
+ }
570
+
571
+ return cell ?. getAttribute ( 'data-mat-row' ) != null ? cell : null ;
451
572
}
452
573
453
574
/** Checks whether a value is the start of a range. */
@@ -476,3 +597,12 @@ function isInRange(
476
597
value <= end
477
598
) ;
478
599
}
600
+
601
+ /**
602
+ * Extracts the element that actually corresponds to a touch event's location
603
+ * (rather than the element that initiated the sequence of touch events).
604
+ */
605
+ function getActualTouchTarget ( event : TouchEvent ) : Element | null {
606
+ const touchLocation = event . changedTouches [ 0 ] ;
607
+ return document . elementFromPoint ( touchLocation . clientX , touchLocation . clientY ) ;
608
+ }
0 commit comments