Skip to content

Commit eaf78f6

Browse files
feat(*): initial implementation of igx-date-range #5732
1 parent bd3f27e commit eaf78f6

9 files changed

+460
-0
lines changed

Diff for: projects/igniteui-angular/src/lib/date-range/README.md

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Directive, ElementRef, Optional, Input } from '@angular/core';
2+
import { NgControl } from '@angular/forms';
3+
4+
/**
5+
* @hidden
6+
*/
7+
@Directive({
8+
selector: '[igxDateRangeBase]'
9+
})
10+
export class IgxDateRangeBaseDirective {
11+
constructor(protected element: ElementRef, @Optional() protected ngControl: NgControl) {
12+
this.nativeElement.readOnly = true;
13+
}
14+
15+
public get nativeElement(): any {
16+
return this.element.nativeElement;
17+
}
18+
19+
@Input('value')
20+
public set value(value: string | string[]) {
21+
this.nativeElement.value = value;
22+
if (this.ngControl) {
23+
this.ngControl.control.setValue(this.nativeElement.value);
24+
}
25+
}
26+
27+
public get value() {
28+
return this.nativeElement.value;
29+
}
30+
31+
public setFocus(): void {
32+
this.nativeElement.focus();
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!-- TODO: remove all styling -->
2+
<div igxToggle>
3+
<igx-calendar #calendar (keydown)="onKeyDown($event)" selection="range" [weekStart]="weekStart"
4+
[hideOutsideDays]="hideOutsideDays" [monthsViewNumber]="monthsViewNumber"
5+
(onSelection)="handleSelection($event)"></igx-calendar>
6+
<div>
7+
<button igxButton (click)="showToday($event)">Today</button>
8+
</div>
9+
</div>
10+
11+
<ng-container *ngTemplateOutlet="defTemplate"></ng-container>
12+
13+
<ng-template #defTemplate>
14+
<div (click)="showCalendar($event)">
15+
<div class="content-wrap">
16+
<div style="display: flex; flex-flow: row;">
17+
<ng-content select="igx-input-group"></ng-content>
18+
</div>
19+
<ng-content select="[igxDateRangeStart]"></ng-content>
20+
<ng-content select="[igxDateRangeEnd]"></ng-content>
21+
<ng-content select="[igxDateRange]"></ng-content>
22+
</div>
23+
</div>
24+
</ng-template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { IgxDateRangeComponent } from './igx-date-range.component';
2+
import { ComponentFixture, async, TestBed } from '@angular/core/testing';
3+
4+
describe('IgxDateRangeComponent', () => {
5+
let component: IgxDateRangeComponent;
6+
let fixture: ComponentFixture<IgxDateRangeComponent>;
7+
8+
beforeEach(async(() => {
9+
TestBed.configureTestingModule({
10+
declarations: [IgxDateRangeComponent]
11+
})
12+
.compileComponents();
13+
}));
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(IgxDateRangeComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import { Component, Input, ContentChild, ViewChild, AfterViewInit, AfterContentInit, OnDestroy } from '@angular/core';
2+
import { InteractionMode } from '../core/enums';
3+
import { IgxToggleDirective } from '../directives/toggle/toggle.directive';
4+
import { IgxCalendarComponent, WEEKDAYS } from '../calendar/index';
5+
import { OverlaySettings, ConnectedPositioningStrategy, GlobalPositionStrategy } from '../services/index';
6+
import { Subject, fromEvent } from 'rxjs';
7+
import { takeUntil } from 'rxjs/operators';
8+
import { KEYS, isIE } from '../core/utils';
9+
import { IgxDateRangeStartDirective, IgxDateRangeEndDirective, IgxDateRangeDirective } from './igx-date-range.directives';
10+
11+
@Component({
12+
selector: 'igx-date-range',
13+
templateUrl: './igx-date-range.component.html'
14+
})
15+
export class IgxDateRangeComponent implements AfterViewInit, AfterContentInit, OnDestroy {
16+
// TODO: docs
17+
@Input()
18+
public mode: InteractionMode;
19+
20+
@Input()
21+
public monthsViewNumber: number;
22+
23+
@Input()
24+
public hideOutsideDays: boolean;
25+
26+
@Input()
27+
public weekStart: number;
28+
29+
@Input()
30+
public locale: string;
31+
32+
@Input()
33+
public formatter: (val: Date) => string;
34+
35+
/**
36+
* @hidden
37+
*/
38+
@ContentChild(IgxDateRangeStartDirective, { read: IgxDateRangeStartDirective, static: false })
39+
protected startInput: IgxDateRangeStartDirective;
40+
41+
/**
42+
* @hidden
43+
*/
44+
@ContentChild(IgxDateRangeEndDirective, { read: IgxDateRangeEndDirective, static: false })
45+
protected endInput: IgxDateRangeEndDirective;
46+
47+
/**
48+
* @hidden
49+
*/
50+
@ContentChild(IgxDateRangeDirective, { read: IgxDateRangeDirective, static: false })
51+
protected singleInput: IgxDateRangeDirective;
52+
53+
/**
54+
* @hidden
55+
*/
56+
@ViewChild(IgxCalendarComponent, { read: IgxCalendarComponent, static: false })
57+
protected calendar: IgxCalendarComponent;
58+
59+
/**
60+
* @hidden
61+
*/
62+
@ViewChild(IgxToggleDirective, { read: IgxToggleDirective, static: false })
63+
protected toggle: IgxToggleDirective;
64+
65+
private dropDownOverlaySettings: OverlaySettings;
66+
private dialogOverlaySettings: OverlaySettings;
67+
private destroy: Subject<boolean>;
68+
69+
constructor() {
70+
this.mode = InteractionMode.Dialog;
71+
this.monthsViewNumber = 1;
72+
this.weekStart = WEEKDAYS.SUNDAY;
73+
this.locale = 'en';
74+
this.destroy = new Subject<boolean>();
75+
}
76+
77+
public showToday(event: KeyboardEvent): void {
78+
const today = new Date();
79+
event.stopPropagation();
80+
this.calendar.selectDate(today);
81+
}
82+
83+
public selectRange(startDate: Date, endDate: Date): void {
84+
const dateRange = [startDate, endDate];
85+
this.calendar.selectDate(dateRange);
86+
this.handleSelection(dateRange);
87+
}
88+
89+
/**
90+
* @hidden
91+
*/
92+
public ngAfterContentInit(): void {
93+
this.validateNgContent();
94+
this.dropDownOverlaySettings = {
95+
closeOnOutsideClick: true,
96+
modal: false,
97+
positionStrategy: new ConnectedPositioningStrategy({
98+
target: this.getPositionTarget()
99+
})
100+
};
101+
this.dialogOverlaySettings = {
102+
closeOnOutsideClick: true,
103+
modal: true,
104+
positionStrategy: new GlobalPositionStrategy()
105+
};
106+
}
107+
108+
/**
109+
* @hidden
110+
*/
111+
public ngAfterViewInit(): void {
112+
if (this.mode === InteractionMode.DropDown) {
113+
if (this.singleInput) {
114+
fromEvent(this.singleInput.nativeElement, 'keydown').pipe(
115+
takeUntil(this.destroy)
116+
).subscribe((evt: KeyboardEvent) => this.onKeyDown(evt));
117+
118+
this.toggle.onClosed.pipe(
119+
takeUntil(this.destroy)
120+
).subscribe(() => this.singleInput.setFocus());
121+
}
122+
if (this.startInput && this.endInput) {
123+
fromEvent(this.startInput.nativeElement, 'keydown').pipe(
124+
takeUntil(this.destroy)
125+
).subscribe((evt: KeyboardEvent) => this.onKeyDown(evt));
126+
127+
this.toggle.onClosed.pipe(
128+
takeUntil(this.destroy)
129+
).subscribe(() => this.startInput.setFocus());
130+
131+
fromEvent(this.endInput.nativeElement, 'keydown').pipe(
132+
takeUntil(this.destroy)
133+
).subscribe((evt: KeyboardEvent) => this.onKeyDown(evt));
134+
}
135+
}
136+
}
137+
138+
/**
139+
* @hidden
140+
*/
141+
public ngOnDestroy(): void {
142+
this.destroy.next(true);
143+
this.destroy.complete();
144+
}
145+
146+
/**
147+
* @hidden
148+
*/
149+
public onKeyDown(event: KeyboardEvent): void {
150+
switch (event.key) {
151+
case KEYS.UP_ARROW:
152+
case KEYS.UP_ARROW_IE:
153+
if (event.altKey) {
154+
this.hideCalendar(event);
155+
}
156+
break;
157+
case KEYS.DOWN_ARROW:
158+
case KEYS.DOWN_ARROW_IE:
159+
if (event.altKey) {
160+
this.showDropDown(event);
161+
}
162+
break;
163+
case KEYS.ESCAPE:
164+
case KEYS.ESCAPE_IE:
165+
this.hideCalendar(event);
166+
this.clearSelection();
167+
break;
168+
}
169+
}
170+
171+
/**
172+
* @hidden
173+
*/
174+
public handleSelection(selectionData: Date[]): void {
175+
if (selectionData.length > 1) {
176+
this.startInput || this.endInput ?
177+
this.handleTwoInputSelection(selectionData) :
178+
this.handleSingleInputSelection(selectionData);
179+
}
180+
}
181+
182+
/**
183+
* @hidden
184+
*/
185+
public showCalendar(event: MouseEvent): void {
186+
switch (this.mode) {
187+
case InteractionMode.Dialog:
188+
this.showDialog(event);
189+
break;
190+
case InteractionMode.DropDown:
191+
this.showDropDown(event);
192+
break;
193+
default:
194+
// TODO: better error message
195+
throw new Error('Unknown mode.');
196+
}
197+
}
198+
199+
/**
200+
* @hidden
201+
*/
202+
protected clearSelection(): void {
203+
this.calendar.deselectDate();
204+
if (this.startInput) {
205+
this.startInput.value = null;
206+
}
207+
if (this.endInput) {
208+
this.endInput.value = null;
209+
}
210+
if (this.singleInput) {
211+
this.singleInput.value = null;
212+
}
213+
}
214+
215+
/**
216+
* @hidden
217+
*/
218+
protected showDialog(event: MouseEvent | KeyboardEvent): void {
219+
event.stopPropagation();
220+
event.preventDefault();
221+
this.activateToggleOpen(this.dialogOverlaySettings);
222+
}
223+
224+
/**
225+
* @hidden
226+
*/
227+
protected showDropDown(event: MouseEvent | KeyboardEvent): void {
228+
event.stopPropagation();
229+
event.preventDefault();
230+
this.activateToggleOpen(this.dropDownOverlaySettings);
231+
}
232+
233+
private handleSingleInputSelection(selectionData: Date[]) {
234+
if (this.singleInput) {
235+
this.singleInput.value = this.extractRange(selectionData);
236+
}
237+
}
238+
239+
private handleTwoInputSelection(selectionData: Date[]) {
240+
const selectionRange = this.extractRange(selectionData);
241+
if (this.startInput) {
242+
this.startInput.value = selectionRange[0];
243+
}
244+
if (this.endInput) {
245+
this.endInput.value = selectionRange[selectionRange.length - 1];
246+
}
247+
}
248+
249+
private getPositionTarget(): HTMLElement {
250+
if (this.startInput && this.endInput) {
251+
return this.startInput.nativeElement;
252+
}
253+
if (this.startInput) {
254+
return this.startInput.nativeElement;
255+
}
256+
if (this.endInput) {
257+
return this.endInput.nativeElement;
258+
}
259+
260+
return this.singleInput.nativeElement;
261+
}
262+
263+
private applyFormatting(date: Date): string {
264+
return this.formatter ? this.formatter(date) : this.applyLocaleToDate(date);
265+
}
266+
267+
private applyLocaleToDate(value: Date): string {
268+
if (isIE()) {
269+
const localeDateStrIE = new Date(value.getFullYear(), value.getMonth(), value.getDate(),
270+
value.getHours(), value.getMinutes(), value.getSeconds(), value.getMilliseconds());
271+
return localeDateStrIE.toLocaleDateString(this.locale);
272+
}
273+
274+
return value.toLocaleDateString(this.locale);
275+
}
276+
277+
private extractRange(selection: Date[]): string[] {
278+
return [this.applyFormatting(selection[0]), this.applyFormatting(selection[selection.length - 1])];
279+
}
280+
281+
private activateToggleOpen(overlaySettings: OverlaySettings): void {
282+
if (this.toggle.collapsed) {
283+
this.toggle.open(overlaySettings);
284+
}
285+
}
286+
287+
private hideCalendar(event: MouseEvent | KeyboardEvent) {
288+
event.stopPropagation();
289+
event.preventDefault();
290+
if (!this.toggle.collapsed) {
291+
const element = event.target as HTMLElement;
292+
this.toggle.close();
293+
element.focus();
294+
}
295+
}
296+
297+
private validateNgContent() {
298+
if (this.startInput && !this.endInput || !this.startInput && this.endInput) {
299+
// TODO: better error message
300+
throw new Error('You must apply both igxDateRangeStart and igxDateRangeEnd if you are using two input elements.');
301+
}
302+
}
303+
}

0 commit comments

Comments
 (0)