@@ -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,13 +424,28 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
400
424
}
401
425
} ;
402
426
427
+ private _touchmoveHandler = ( event : TouchEvent ) => {
428
+ const target = getActualTouchTarget ( event ) ;
429
+ const cell = target ? this . _getCellFromElement ( target as HTMLElement ) : null ;
430
+
431
+ if ( target !== event . target ) {
432
+ this . _didDragSinceMouseDown = true ;
433
+ }
434
+
435
+ this . _ngZone . run ( ( ) => this . previewChange . emit ( { value : cell ?. enabled ? cell : null , event} ) ) ;
436
+ } ;
437
+
403
438
/**
404
439
* Event handler for when the user's pointer leaves an element
405
440
* inside the calendar body (e.g. by hovering out or blurring).
406
441
*/
407
442
private _leaveHandler = ( event : Event ) => {
408
443
// We only need to hit the zone when we're selecting a range.
409
444
if ( this . previewEnd !== null && this . isRange ) {
445
+ if ( event . type !== 'blur' ) {
446
+ this . _didDragSinceMouseDown = true ;
447
+ }
448
+
410
449
// Only reset the preview end value when leaving cells. This looks better, because
411
450
// we have a gap between the cells and the rows and we don't want to remove the
412
451
// range just for it to show up again when the user moves a few pixels to the side.
@@ -416,16 +455,56 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
416
455
}
417
456
} ;
418
457
419
- /** Finds the MatCalendarCell that corresponds to a DOM node. */
420
- private _getCellFromElement ( element : HTMLElement ) : MatCalendarCell | null {
421
- let cell : HTMLElement | undefined ;
458
+ private _mousedownHandler = ( event : Event ) => {
459
+ this . _didDragSinceMouseDown = false ;
460
+ // Begin a drag if a cell within the current range was targeted.
461
+ const cell = event . target && this . _getCellFromElement ( event . target as HTMLElement ) ;
462
+ if ( ! cell || ! this . _isInRange ( cell . rawValue ) ) {
463
+ return ;
464
+ }
465
+
466
+ this . _ngZone . run ( ( ) => {
467
+ this . dragStarted . emit ( {
468
+ value : cell . rawValue ,
469
+ event,
470
+ } ) ;
471
+ } ) ;
472
+ } ;
422
473
423
- if ( isTableCell ( element ) ) {
424
- cell = element ;
425
- } else if ( isTableCell ( element . parentNode ! ) ) {
426
- cell = element . parentNode as HTMLElement ;
474
+ private _mouseupHandler = ( event : Event ) => {
475
+ const cellElement = getCellElement ( event . target as HTMLElement ) ;
476
+ if ( ! cellElement ) {
477
+ // Mouseup happened outside of datepicker. Cancel drag.
478
+ this . _ngZone . run ( ( ) => {
479
+ this . dragEnded . emit ( { value : null , event} ) ;
480
+ } ) ;
481
+ return ;
482
+ }
483
+
484
+ if ( cellElement . closest ( '.mat-calendar-body' ) !== this . _elementRef . nativeElement ) {
485
+ // Mouseup happened inside a different month instance.
486
+ // Allow it to handle the event.
487
+ return ;
427
488
}
428
489
490
+ this . _ngZone . run ( ( ) => {
491
+ const cell = this . _getCellFromElement ( cellElement ) ;
492
+ this . dragEnded . emit ( { value : cell ?. rawValue ?? null , event} ) ;
493
+ } ) ;
494
+ } ;
495
+
496
+ private _touchendHandler = ( event : TouchEvent ) => {
497
+ const target = getActualTouchTarget ( event ) ;
498
+
499
+ if ( target ) {
500
+ this . _mouseupHandler ( { target} as unknown as Event ) ;
501
+ }
502
+ } ;
503
+
504
+ /** Finds the MatCalendarCell that corresponds to a DOM node. */
505
+ private _getCellFromElement ( element : HTMLElement ) : MatCalendarCell | null {
506
+ const cell = getCellElement ( element ) ;
507
+
429
508
if ( cell ) {
430
509
const row = cell . getAttribute ( 'data-mat-row' ) ;
431
510
const col = cell . getAttribute ( 'data-mat-col' ) ;
@@ -446,8 +525,21 @@ export class MatCalendarBody implements OnChanges, OnDestroy, AfterViewChecked {
446
525
}
447
526
448
527
/** Checks whether a node is a table cell element. */
449
- function isTableCell ( node : Node ) : node is HTMLTableCellElement {
450
- return node . nodeName === 'TD' ;
528
+ function isTableCell ( node : Node | undefined | null ) : node is HTMLTableCellElement {
529
+ return node ?. nodeName === 'TD' ;
530
+ }
531
+
532
+ function getCellElement ( element : HTMLElement ) : HTMLElement | null {
533
+ let cell : HTMLElement | undefined ;
534
+ if ( isTableCell ( element ) ) {
535
+ cell = element ;
536
+ } else if ( isTableCell ( element . parentNode ) ) {
537
+ cell = element . parentNode as HTMLElement ;
538
+ } else if ( isTableCell ( element . parentNode ?. parentNode ) ) {
539
+ cell = element . parentNode ! . parentNode as HTMLElement ;
540
+ }
541
+
542
+ return cell ?. getAttribute ( 'data-mat-row' ) != null ? cell : null ;
451
543
}
452
544
453
545
/** Checks whether a value is the start of a range. */
@@ -476,3 +568,8 @@ function isInRange(
476
568
value <= end
477
569
) ;
478
570
}
571
+
572
+ function getActualTouchTarget ( event : TouchEvent ) : Element | null {
573
+ const touchLocation = event . changedTouches [ 0 ] ;
574
+ return document . elementFromPoint ( touchLocation . clientX , touchLocation . clientY ) ;
575
+ }
0 commit comments