Skip to content

Commit 884dfbb

Browse files
authored
feat(calendar): multi/range selection with shift click (#12207)
* feat(calendar): multi/range selection with shift click * test(calendar): should select/deselect multiple dates * chore(*): update CHANGELOG with new feature * fix(calendar): address comments * fix(calendar): deselect multiple dates in multi view * fix(calendar): address comments * test(calendar-multi-view): should select/deselect multiple dates in multi-view * fix(calendar): apply requested changes
1 parent a74130e commit 884dfbb

File tree

5 files changed

+357
-26
lines changed

5 files changed

+357
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ All notable changes for each version of this project will be documented in this
2626
- `IgxGrid`, `IgxTreeGrid`, `IgxHierarchicalGrid`
2727
- **Behavioral Change** - When editing a row, `rowChangesCount` and `hiddenColumnsCount`would be displayed.
2828

29+
- `IgxCalendar`
30+
31+
Added support for shift key + mouse click interactions.
32+
- `multi` mode - select/deselect all dates between the last selected/deselected and the one clicked while holding `Shift`.
33+
- `range` mode - extend/shorten the range from the last selected date to the one clicked while holding `Shift`.
34+
2935
## 14.2.0
3036

3137
### New Features

projects/igniteui-angular/src/lib/calendar/calendar-base.ts

Lines changed: 144 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Input, Output, EventEmitter, Directive, Inject, LOCALE_ID } from '@angular/core';
1+
import { Input, Output, EventEmitter, Directive, Inject, LOCALE_ID, HostListener } from '@angular/core';
22
import { WEEKDAYS, Calendar, isDateInRanges, IFormattingOptions, IFormattingViews } from './calendar';
33
import { ControlValueAccessor } from '@angular/forms';
44
import { DateRangeDescriptor } from '../core/dates';
@@ -115,6 +115,16 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
115115
*/
116116
public selectedDates;
117117

118+
/**
119+
* @hidden
120+
*/
121+
public shiftKey: boolean = false;
122+
123+
/**
124+
* @hidden
125+
*/
126+
public lastSelectedDate: Date;
127+
118128
/**
119129
* @hidden
120130
*/
@@ -154,6 +164,11 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
154164
*/
155165
protected _onChangeCallback: (_: Date) => void = noop;
156166

167+
/**
168+
* @hidden
169+
*/
170+
protected _deselectDate: boolean;
171+
157172
/**
158173
* @hidden
159174
*/
@@ -174,6 +189,16 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
174189
*/
175190
private _viewDate: Date;
176191

192+
/**
193+
* @hidden
194+
*/
195+
private _startDate: Date;
196+
197+
/**
198+
* @hidden
199+
*/
200+
private _endDate: Date;
201+
177202
/**
178203
* @hidden
179204
*/
@@ -462,16 +487,45 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
462487
}
463488

464489
/**
465-
* Performs deselection of a single value, when selection is multi
490+
* Multi/Range selection with shift key
491+
*
492+
* @hidden
493+
* @internal
494+
*/
495+
@HostListener('pointerdown', ['$event'])
496+
public onPointerdown(event: MouseEvent) {
497+
this.shiftKey = event.button === 0 && event.shiftKey;
498+
}
499+
500+
/**
501+
* Performs deselection of date/dates, when selection is multi
466502
* Usually performed by the selectMultiple method, but leads to bug when multiple months are in view
467503
*
468504
* @hidden
469505
*/
470506
public deselectMultipleInMonth(value: Date) {
471-
const valueDateOnly = this.getDateOnly(value);
472-
this.selectedDates = this.selectedDates.filter(
473-
(date: Date) => date.getTime() !== valueDateOnly.getTime()
474-
);
507+
// deselect multiple dates from last clicked to shift clicked date (excluding)
508+
if (this.shiftKey) {
509+
let start: Date;
510+
let end: Date;
511+
512+
[start, end] = this.lastSelectedDate.getTime() < value.getTime()
513+
? [this.lastSelectedDate, value]
514+
: [value, this.lastSelectedDate];
515+
516+
this.selectedDates = this.selectedDates.filter(
517+
(date: Date) => date.getTime() < start.getTime() || date.getTime() > end.getTime()
518+
);
519+
520+
this.selectedDates.push(value);
521+
522+
} else {
523+
// deselect a single date
524+
const valueDateOnly = this.getDateOnly(value);
525+
this.selectedDates = this.selectedDates.filter(
526+
(date: Date) => date.getTime() !== valueDateOnly.getTime()
527+
);
528+
}
475529
}
476530

477531
/**
@@ -641,20 +695,55 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
641695

642696
this.selectedDates = Array.from(new Set([...newDates, ...selDates])).map(v => new Date(v));
643697
} else {
644-
const valueDateOnly = this.getDateOnly(value);
645-
const newSelection = [];
646-
if (this.selectedDates.every((date: Date) => date.getTime() !== valueDateOnly.getTime())) {
647-
newSelection.push(valueDateOnly);
698+
let newSelection = [];
699+
700+
if (this.shiftKey && this.lastSelectedDate) {
701+
702+
[this._startDate, this._endDate] = this.lastSelectedDate.getTime() < value.getTime()
703+
? [this.lastSelectedDate, value]
704+
: [value, this.lastSelectedDate];
705+
706+
const unselectedDates = [this._startDate, ...this.generateDateRange(this._startDate, this._endDate)]
707+
.filter(date => !this.isDateDisabled(date)
708+
&& this.selectedDates.every((d: Date) => d.getTime() !== date.getTime())
709+
);
710+
711+
// select all dates from last selected to shift clicked date
712+
if (this.selectedDates.some((date: Date) => date.getTime() === this.lastSelectedDate.getTime())
713+
&& unselectedDates.length) {
714+
715+
newSelection = unselectedDates;
716+
} else {
717+
// delesect all dates from last clicked to shift clicked date (excluding)
718+
this.selectedDates = this.selectedDates.filter((date: Date) =>
719+
date.getTime() < this._startDate.getTime() || date.getTime() > this._endDate.getTime()
720+
);
721+
722+
this.selectedDates.push(value);
723+
this._deselectDate = true;
724+
}
725+
726+
this._startDate = this._endDate = undefined;
727+
728+
} else if (this.selectedDates.every((date: Date) => date.getTime() !== value.getTime())) {
729+
newSelection.push(value);
730+
648731
} else {
649732
this.selectedDates = this.selectedDates.filter(
650-
(date: Date) => date.getTime() !== valueDateOnly.getTime()
733+
(date: Date) => date.getTime() !== value.getTime()
651734
);
735+
736+
this._deselectDate = true;
652737
}
653738

654739
if (newSelection.length > 0) {
655740
this.selectedDates = this.selectedDates.concat(newSelection);
741+
this._deselectDate = false;
656742
}
743+
744+
this.lastSelectedDate = value;
657745
}
746+
658747
this.selectedDates = this.selectedDates.filter(d => !this.isDateDisabled(d));
659748
this.selectedDates.sort((a: Date, b: Date) => a.valueOf() - b.valueOf());
660749
this._onChangeCallback(this.selectedDates);
@@ -664,19 +753,46 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
664753
* @hidden
665754
*/
666755
private selectRange(value: Date | Date[], excludeDisabledDates: boolean = false) {
667-
let start: Date;
668-
let end: Date;
669-
670756
if (Array.isArray(value)) {
671-
// this.rangeStarted = false;
672757
value.sort((a: Date, b: Date) => a.valueOf() - b.valueOf());
673-
start = this.getDateOnly(value[0]);
674-
end = this.getDateOnly(value[value.length - 1]);
675-
this.selectedDates = [start, ...this.generateDateRange(start, end)];
758+
this._startDate = this.getDateOnly(value[0]);
759+
this._endDate = this.getDateOnly(value[value.length - 1]);
676760
} else {
677-
if (!this.rangeStarted) {
761+
762+
if (this.shiftKey && this.lastSelectedDate) {
763+
764+
if (this.lastSelectedDate.getTime() === value.getTime()) {
765+
this.selectedDates = this.selectedDates.length === 1 ? [] : [value];
766+
this.rangeStarted = !!this.selectedDates.length;
767+
this._onChangeCallback(this.selectedDates);
768+
return;
769+
}
770+
771+
// shortens the range when selecting a date inside of it
772+
if (this.selectedDates.some((date: Date) => date.getTime() === value.getTime())) {
773+
774+
this.lastSelectedDate.getTime() < value.getTime()
775+
? this._startDate = value
776+
: this._endDate = value;
777+
778+
} else {
779+
// extends the range when selecting a date outside of it
780+
// allows selection from last deselected to current selected date
781+
if (this.lastSelectedDate.getTime() < value.getTime()) {
782+
this._startDate = this._startDate ?? this.lastSelectedDate;
783+
this._endDate = value;
784+
} else {
785+
this._startDate = value;
786+
this._endDate = this._endDate ?? this.lastSelectedDate;
787+
}
788+
}
789+
790+
this.rangeStarted = false;
791+
792+
} else if (!this.rangeStarted) {
678793
this.rangeStarted = true;
679794
this.selectedDates = [value];
795+
this._startDate = this._endDate = undefined;
680796
} else {
681797
this.rangeStarted = false;
682798

@@ -686,13 +802,16 @@ export class IgxCalendarBaseDirective implements ControlValueAccessor {
686802
return;
687803
}
688804

689-
this.selectedDates.push(value);
690-
this.selectedDates.sort((a: Date, b: Date) => a.valueOf() - b.valueOf());
691-
692-
start = this.selectedDates.shift();
693-
end = this.selectedDates.pop();
694-
this.selectedDates = [start, ...this.generateDateRange(start, end)];
805+
[this._startDate, this._endDate] = this.lastSelectedDate.getTime() < value.getTime()
806+
? [this.lastSelectedDate, value]
807+
: [value, this.lastSelectedDate];
695808
}
809+
810+
this.lastSelectedDate = value;
811+
}
812+
813+
if (this._startDate && this._endDate) {
814+
this.selectedDates = [this._startDate, ...this.generateDateRange(this._startDate, this._endDate)];
696815
}
697816

698817
if (excludeDisabledDates) {

projects/igniteui-angular/src/lib/calendar/calendar-multi-view.component.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,28 @@ describe('Multi-View Calendar - ', () => {
10471047
expect(HelperTestFunctions.getMonthViewSelectedDates(fixture, 1).length).toBe(0);
10481048
});
10491049

1050+
it('Multi Selection - Select/Deselect dates with Shift key from one view should also select/deselect dates in the other', () => {
1051+
calendar.selection = 'multi';
1052+
fixture.detectChanges();
1053+
1054+
const octoberDates = HelperTestFunctions.getMonthViewDates(fixture, 1);
1055+
const october27th = octoberDates[26];
1056+
const october31st = octoberDates[30];
1057+
1058+
UIInteractions.simulateClickAndSelectEvent(october27th);
1059+
UIInteractions.simulateClickAndSelectEvent(october31st, true);
1060+
fixture.detectChanges();
1061+
1062+
expect(HelperTestFunctions.getMonthViewSelectedDates(fixture, 1).length).toBe(5);
1063+
expect(HelperTestFunctions.getMonthViewSelectedDates(fixture, 2).length).toBe(5);
1064+
1065+
UIInteractions.simulateClickAndSelectEvent(october27th, true);
1066+
fixture.detectChanges();
1067+
1068+
expect(HelperTestFunctions.getMonthViewSelectedDates(fixture, 1).length).toBe(1);
1069+
expect(HelperTestFunctions.getMonthViewSelectedDates(fixture, 2).length).toBe(1);
1070+
});
1071+
10501072
it('Multi/Single Selection - select multiple dates should not create range', () => {
10511073
expect(calendar.hideOutsideDays).toBe(false);
10521074
calendar.selection = 'multi';

0 commit comments

Comments
 (0)