UNPKG

igniteui-angular-sovn

Version:

Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps

1,059 lines (948 loc) 36.7 kB
import { AfterViewInit, ChangeDetectorRef, Component, ContentChild, ContentChildren, ElementRef, EventEmitter, HostBinding, HostListener, Inject, Injector, Input, LOCALE_ID, OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; import { NgTemplateOutlet, NgIf } from '@angular/common'; import { AbstractControl, ControlValueAccessor, NgControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms'; import { fromEvent, merge, MonoTypeOperatorFunction, noop, Subscription } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { fadeIn, fadeOut } from '../animations/fade'; import { CalendarSelection, IgxCalendarComponent } from '../calendar/public_api'; import { DateRangeType } from '../core/dates'; import { DisplayDensityToken, IDisplayDensityOptions } from '../core/density'; import { IDateRangePickerResourceStrings } from '../core/i18n/date-range-picker-resources'; import { CurrentResourceStrings } from '../core/i18n/resources'; import { IBaseCancelableBrowserEventArgs, isDate, parseDate, PlatformUtil } from '../core/utils'; import { IgxCalendarContainerComponent } from '../date-common/calendar-container/calendar-container.component'; import { PickerBaseDirective } from '../date-common/picker-base.directive'; import { IgxPickerActionsDirective } from '../date-common/picker-icons.common'; import { DateTimeUtil } from '../date-common/util/date-time.util'; import { IgxOverlayOutletDirective } from '../directives/toggle/toggle.directive'; import { IgxInputDirective, IgxInputGroupComponent, IgxInputGroupType, IgxInputState, IgxLabelDirective, IGX_INPUT_GROUP_TYPE } from '../input-group/public_api'; import { AutoPositionStrategy, IgxOverlayService, OverlayCancelableEventArgs, OverlayEventArgs, OverlaySettings, PositionSettings } from '../services/public_api'; import { DateRange, IgxDateRangeEndComponent, IgxDateRangeInputsBaseComponent, IgxDateRangeSeparatorDirective, IgxDateRangeStartComponent, DateRangePickerFormatPipe } from './date-range-picker-inputs.common'; import { IgxPrefixDirective } from '../directives/prefix/prefix.directive'; import { IgxIconComponent } from '../icon/icon.component'; const SingleInputDatesConcatenationString = ' - '; /** * Provides the ability to select a range of dates from a calendar UI or editable inputs. * * @igxModule IgxDateRangeModule * * @igxTheme igx-input-group-theme, igx-calendar-theme, igx-date-range-picker-theme * * @igxKeywords date, range, date range, date picker * * @igxGroup scheduling * * @remarks * It displays the range selection in a single or two input fields. * The default template displays a single *readonly* input field * while projecting `igx-date-range-start` and `igx-date-range-end` * displays two *editable* input fields. * * @example * ```html * <igx-date-range-picker mode="dropdown"></igx-date-range-picker> * ``` */ @Component({ selector: 'igx-date-range-picker', templateUrl: './date-range-picker.component.html', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateRangePickerComponent, multi: true }, { provide: NG_VALIDATORS, useExisting: IgxDateRangePickerComponent, multi: true } ], standalone: true, imports: [ NgIf, NgTemplateOutlet, IgxIconComponent, IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective, DateRangePickerFormatPipe ] }) export class IgxDateRangePickerComponent extends PickerBaseDirective implements OnChanges, OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, Validator { /** * The number of displayed month views. * * @remarks * Default is `2`. * * @example * ```html * <igx-date-range-picker [displayMonthsCount]="3"></igx-date-range-picker> * ``` */ @Input() public displayMonthsCount = 2; /** * Gets/Sets whether dates that are not part of the current month will be displayed. * * @remarks * Default value is `false`. * * @example * ```html * <igx-date-range-picker [hideOutsideDays]="true"></igx-date-range-picker> * ``` */ @Input() public hideOutsideDays: boolean; /** * A custom formatter function, applied on the selected or passed in date. * * @example * ```typescript * private dayFormatter = new Intl.DateTimeFormat("en", { weekday: "long" }); * private monthFormatter = new Intl.DateTimeFormat("en", { month: "long" }); * * public formatter(date: Date): string { * return `${this.dayFormatter.format(date)} - ${this.monthFormatter.format(date)} - ${date.getFullYear()}`; * } * ``` * ```html * <igx-date-range-picker [formatter]="formatter"></igx-date-range-picker> * ``` */ @Input() public formatter: (val: DateRange) => string; /** * The default text of the calendar dialog `done` button. * * @remarks * Default value is `Done`. * An @Input property that renders Done button with custom text. By default `doneButtonText` is set to Done. * The button will only show up in `dialog` mode. * * @example * ```html * <igx-date-range-picker doneButtonText="完了"></igx-date-range-picker> * ``` */ @Input() public set doneButtonText(value: string) { this._doneButtonText = value; } public get doneButtonText(): string { if (this._doneButtonText === null) { return this.resourceStrings.igx_date_range_picker_done_button; } return this._doneButtonText; } /** * Custom overlay settings that should be used to display the calendar. * * @example * ```html * <igx-date-range-picker [overlaySettings]="customOverlaySettings"></igx-date-range-picker> * ``` */ @Input() public override overlaySettings: OverlaySettings; /** * The format used when editable inputs are not focused. * * @remarks * Uses Angular's DatePipe. * * @example * ```html * <igx-date-range-picker displayFormat="EE/M/yy"></igx-date-range-picker> * ``` * */ @Input() public override displayFormat: string; /** * The expected user input format and placeholder. * * @remarks * Default is `"'MM/dd/yyyy'"` * * @example * ```html * <igx-date-range-picker inputFormat="dd/MM/yy"></igx-date-range-picker> * ``` */ @Input() public override inputFormat: string; /** * The minimum value in a valid range. * * @example * <igx-date-range-picker [minValue]="minDate"></igx-date-range-picker> */ @Input() public set minValue(value: Date | string) { this._minValue = value; this.onValidatorChange(); } public get minValue(): Date | string { return this._minValue; } /** * The maximum value in a valid range. * * @example * <igx-date-range-picker [maxValue]="maxDate"></igx-date-range-picker> */ @Input() public set maxValue(value: Date | string) { this._maxValue = value; this.onValidatorChange(); } public get maxValue(): Date | string { return this._maxValue; } /** * An accessor that sets the resource strings. * By default it uses EN resources. */ @Input() public set resourceStrings(value: IDateRangePickerResourceStrings) { this._resourceStrings = Object.assign({}, this._resourceStrings, value); } /** * An accessor that returns the resource strings. */ public get resourceStrings(): IDateRangePickerResourceStrings { return this._resourceStrings; } /** * Sets the `placeholder` for single-input `IgxDateRangePickerComponent`. * * @example * ```html * <igx-date-range-picker [placeholder]="'Choose your dates'"></igx-date-range-picker> * ``` */ @Input() public override placeholder = ''; /** * Gets/Sets the container used for the popup element. * * @remarks * `outlet` is an instance of `IgxOverlayOutletDirective` or an `ElementRef`. * @example * ```html * <div igxOverlayOutlet #outlet="overlay-outlet"></div> * //.. * <igx-date-range-picker [outlet]="outlet"></igx-date-range-picker> * //.. * ``` */ @Input() public override outlet: IgxOverlayOutletDirective | ElementRef<any>; /** * Show/hide week numbers * * @remarks * Default is `false`. * * @example * ```html * <igx-date-range-picker [showWeekNumbers]="true"></igx-date-range-picker> * `` */ @Input() public showWeekNumbers = false; /** * Emitted when the picker's value changes. Used for two-way binding. * * @example * ```html * <igx-date-range-picker [(value)]="date"></igx-date-range-picker> * ``` */ @Output() public valueChange = new EventEmitter<DateRange>(); /** @hidden @internal */ @HostBinding('class.igx-date-range-picker') public cssClass = 'igx-date-range-picker'; @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef }) private viewContainerRef: ViewContainerRef; /** @hidden @internal */ @ViewChild(IgxInputDirective) public inputDirective: IgxInputDirective; /** @hidden @internal */ @ContentChildren(IgxDateRangeInputsBaseComponent) public projectedInputs: QueryList<IgxDateRangeInputsBaseComponent>; @ContentChild(IgxLabelDirective) public label: IgxLabelDirective; @ContentChild(IgxPickerActionsDirective) public pickerActions: IgxPickerActionsDirective; /** @hidden @internal */ @ContentChild(IgxDateRangeSeparatorDirective, { read: TemplateRef }) public dateSeparatorTemplate: TemplateRef<any>; /** @hidden @internal */ public get dateSeparator(): string { if (this._dateSeparator === null) { return this.resourceStrings.igx_date_range_picker_date_separator; } return this._dateSeparator; } /** @hidden @internal */ public get appliedFormat(): string { return DateTimeUtil.getLocaleDateFormat(this.locale, this.displayFormat) || DateTimeUtil.DEFAULT_INPUT_FORMAT; } /** @hidden @internal */ public get singleInputFormat(): string { if (this.placeholder !== '') { return this.placeholder; } const format = this.appliedFormat; return `${format}${SingleInputDatesConcatenationString}${format}`; } /** * Gets calendar state. * * ```typescript * let state = this.dateRange.collapsed; * ``` */ public override get collapsed(): boolean { return this._collapsed; } /** * The currently selected value / range from the calendar * * @remarks * The current value is of type `DateRange` * * @example * ```typescript * const newValue: DateRange = { start: new Date("2/2/2012"), end: new Date("3/3/2013")}; * this.dateRangePicker.value = newValue; * ``` */ public get value(): DateRange | null { return this._value; } @Input() public set value(value: DateRange | null) { this.updateValue(value); this.onChangeCallback(value); this.valueChange.emit(value); } /** @hidden @internal */ public get hasProjectedInputs(): boolean { return this.projectedInputs?.length > 0; } /** @hidden @internal */ public get separatorClass(): string { return this.getComponentDensityClass('igx-date-range-picker__label'); } private get required(): boolean { if (this._ngControl && this._ngControl.control && this._ngControl.control.validator) { const error = this._ngControl.control.validator({} as AbstractControl); return (error && error.required) ? true : false; } return false; } private get calendar(): IgxCalendarComponent { return this._calendar; } private get dropdownOverlaySettings(): OverlaySettings { return Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings); } private get dialogOverlaySettings(): OverlaySettings { return Object.assign({}, this._dialogOverlaySettings, this.overlaySettings); } private _resourceStrings = CurrentResourceStrings.DateRangePickerResStrings; private _doneButtonText = null; private _dateSeparator = null; private _value: DateRange | null; private _overlayId: string; private _ngControl: NgControl; private _statusChanges$: Subscription; private _calendar: IgxCalendarComponent; private _positionSettings: PositionSettings; private _focusedInput: IgxDateRangeInputsBaseComponent; private _overlaySubFilter: [MonoTypeOperatorFunction<OverlayEventArgs>, MonoTypeOperatorFunction<OverlayEventArgs | OverlayCancelableEventArgs>] = [ filter(x => x.id === this._overlayId), takeUntil(merge(this._destroy$, this.closed)) ]; private _dialogOverlaySettings: OverlaySettings = { closeOnOutsideClick: true, modal: true, closeOnEscape: true }; private _dropDownOverlaySettings: OverlaySettings = { closeOnOutsideClick: true, modal: false, closeOnEscape: true }; private onChangeCallback: (dateRange: DateRange) => void = noop; private onTouchCallback: () => void = noop; private onValidatorChange: () => void = noop; constructor(element: ElementRef, @Inject(LOCALE_ID) _localeId: string, protected platform: PlatformUtil, private _injector: Injector, private _cdr: ChangeDetectorRef, @Inject(IgxOverlayService) private _overlayService: IgxOverlayService, @Optional() @Inject(DisplayDensityToken) _displayDensityOptions?: IDisplayDensityOptions, @Optional() @Inject(IGX_INPUT_GROUP_TYPE) _inputGroupType?: IgxInputGroupType) { super(element, _localeId, _displayDensityOptions, _inputGroupType); this.locale = this.locale || this._localeId; } /** @hidden @internal */ @HostListener('keydown', ['$event']) /** @hidden @internal */ public onKeyDown(event: KeyboardEvent): void { switch (event.key) { case this.platform.KEYMAP.ARROW_UP: if (event.altKey) { this.close(); } break; case this.platform.KEYMAP.ARROW_DOWN: if (event.altKey) { this.open(); } break; } } /** * Opens the date range picker's dropdown or dialog. * * @example * ```html * <igx-date-range-picker #dateRange></igx-date-range-picker> * * <button (click)="dateRange.open()">Open Dialog</button * ``` */ public open(overlaySettings?: OverlaySettings): void { if (!this.collapsed || this.disabled) { return; } const settings = Object.assign({}, this.isDropdown ? this.dropdownOverlaySettings : this.dialogOverlaySettings , overlaySettings); this._overlayId = this._overlayService .attach(IgxCalendarContainerComponent, this.viewContainerRef, settings); this.subscribeToOverlayEvents(); this._overlayService.show(this._overlayId); } /** * Closes the date range picker's dropdown or dialog. * * @example * ```html * <igx-date-range-picker #dateRange></igx-date-range-picker> * * <button (click)="dateRange.close()">Close Dialog</button> * ``` */ public close(): void { if (!this.collapsed) { this._overlayService.hide(this._overlayId); } } /** * Toggles the date range picker's dropdown or dialog * * @example * ```html * <igx-date-range-picker #dateRange></igx-date-range-picker> * * <button (click)="dateRange.toggle()">Toggle Dialog</button> * ``` */ public toggle(overlaySettings?: OverlaySettings): void { if (!this.collapsed) { this.close(); } else { this.open(overlaySettings); } } /** * Selects a range of dates. If no `endDate` is passed, range is 1 day (only `startDate`) * * @example * ```typescript * public selectFiveDayRange() { * const today = new Date(); * const inFiveDays = new Date(new Date().setDate(today.getDate() + 5)); * this.dateRange.select(today, inFiveDays); * } * ``` */ public select(startDate: Date, endDate?: Date): void { endDate = endDate ?? startDate; const dateRange = [startDate, endDate]; this.handleSelection(dateRange); } /** @hidden @internal */ public writeValue(value: DateRange): void { this.updateValue(value); } /** @hidden @internal */ public registerOnChange(fn: any): void { this.onChangeCallback = fn; } /** @hidden @internal */ public registerOnTouched(fn: any): void { this.onTouchCallback = fn; } /** @hidden @internal */ public validate(control: AbstractControl): ValidationErrors | null { const value: DateRange = control.value; const errors = {}; if (value) { if (this.hasProjectedInputs) { const startInput = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent; const endInput = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent; if (!startInput.dateTimeEditor.value) { Object.assign(errors, { startValue: true }); } if (!endInput.dateTimeEditor.value) { Object.assign(errors, { endValue: true }); } } const min = parseDate(this.minValue); const max = parseDate(this.maxValue); const start = parseDate(value.start); const end = parseDate(value.end); if ((min && start && DateTimeUtil.lessThanMinValue(start, min, false)) || (min && end && DateTimeUtil.lessThanMinValue(end, min, false))) { Object.assign(errors, { minValue: true }); } if ((max && start && DateTimeUtil.greaterThanMaxValue(start, max, false)) || (max && end && DateTimeUtil.greaterThanMaxValue(end, max, false))) { Object.assign(errors, { maxValue: true }); } } return Object.keys(errors).length > 0 ? errors : null; } /** @hidden @internal */ public registerOnValidatorChange?(fn: any): void { this.onValidatorChange = fn; } /** @hidden @internal */ public setDisabledState?(isDisabled: boolean): void { this.disabled = isDisabled; } /** @hidden */ public override ngOnInit(): void { this._ngControl = this._injector.get<NgControl>(NgControl, null); this.locale = this.locale || this._localeId; super.ngOnInit(); } /** @hidden */ public override ngAfterViewInit(): void { super.ngAfterViewInit(); this.subscribeToDateEditorEvents(); this.configPositionStrategy(); this.configOverlaySettings(); this.cacheFocusedInput(); this.attachOnTouched(); this.setRequiredToInputs(); if (this._ngControl) { this._statusChanges$ = this._ngControl.statusChanges.subscribe(this.onStatusChanged.bind(this)); } // delay invocations until the current change detection cycle has completed Promise.resolve().then(() => { this.updateDisabledState(); this.initialSetValue(); this.updateInputs(); // B.P. 07 July 2021 - IgxDateRangePicker not showing initial disabled state with ChangeDetectionStrategy.OnPush #9776 /** * if disabled is placed on the range picker element and there are projected inputs * run change detection since igxInput will initially set the projected inputs' disabled to false */ if (this.hasProjectedInputs && this.disabled) { this._cdr.markForCheck(); } }); this.updateDisplayFormat(); this.updateInputFormat(); } /** @hidden @internal */ public ngOnChanges(changes: SimpleChanges): void { if (changes['displayFormat'] && this.hasProjectedInputs) { this.updateDisplayFormat(); } if (changes['inputFormat'] && this.hasProjectedInputs) { this.updateInputFormat(); } if (changes['disabled']) { this.updateDisabledState(); } } /** @hidden @internal */ public override ngOnDestroy(): void { super.ngOnDestroy(); if (this._statusChanges$) { this._statusChanges$.unsubscribe(); } if (this._overlayId) { this._overlayService.detach(this._overlayId); } } /** @hidden @internal */ public getEditElement() { return this.inputDirective.nativeElement; } protected onStatusChanged = () => { if (this.inputGroup) { this.setValidityState(this.inputDirective, this.inputGroup.isFocused); } else if (this.hasProjectedInputs) { this.projectedInputs .forEach((i) => { this.setValidityState(i.inputDirective, i.isFocused); }); } this.setRequiredToInputs(); }; private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) { if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) { if (this.hasValidators && isFocused) { inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID; } else { inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID; } } else { inputDirective.valid = IgxInputState.INITIAL; } } private get isTouchedOrDirty(): boolean { return (this._ngControl.control.touched || this._ngControl.control.dirty); } private get hasValidators(): boolean { return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator); } private handleSelection(selectionData: Date[]): void { let newValue = this.extractRange(selectionData); if (!newValue.start && !newValue.end) { newValue = null; } this.value = newValue; if (this.isDropdown && selectionData?.length > 1) { this.close(); } } private handleClosing(e: IBaseCancelableBrowserEventArgs): void { const args = { owner: this, cancel: e?.cancel, event: e?.event }; this.closing.emit(args); e.cancel = args.cancel; if (args.cancel) { return; } if (this.isDropdown && e?.event && !this.element.nativeElement.contains(e.event.target)) { // outside click this.updateValidityOnBlur(); } else { this.onTouchCallback(); // input click if (this.hasProjectedInputs && this._focusedInput) { this._focusedInput.setFocus(); this._focusedInput = null; } if (this.inputDirective) { this.inputDirective.focus(); } } } private subscribeToOverlayEvents() { this._overlayService.opening.pipe(...this._overlaySubFilter).subscribe((e) => { const overlayEvent = e as OverlayCancelableEventArgs; const args = { owner: this, cancel: overlayEvent?.cancel, event: e.event }; this.opening.emit(args); if (args.cancel) { this._overlayService.detach(this._overlayId); overlayEvent.cancel = true; return; } this._initializeCalendarContainer(e.componentRef.instance); this._collapsed = false; this.updateCalendar(); }); this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => { this.calendar?.daysView?.focusActiveDate(); this.opened.emit({ owner: this }); }); this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e) => { this.handleClosing(e as OverlayCancelableEventArgs); }); this._overlayService.closed.pipe(...this._overlaySubFilter).subscribe(() => { this._overlayService.detach(this._overlayId); this._collapsed = true; this._overlayId = null; this.closed.emit({ owner: this }); }); } private updateValue(value: DateRange) { this._value = value ? value : null; this.updateInputs(); this.updateCalendar(); } private updateValidityOnBlur() { this.onTouchCallback(); if (this._ngControl) { if (this.hasProjectedInputs) { this.projectedInputs.forEach(i => { if (!this._ngControl.valid) { i.updateInputValidity(IgxInputState.INVALID); } else { i.updateInputValidity(IgxInputState.INITIAL); } }); } if (this.inputDirective) { if (!this._ngControl.valid) { this.inputDirective.valid = IgxInputState.INVALID; } else { this.inputDirective.valid = IgxInputState.INITIAL; } } } } private updateDisabledState() { if (this.hasProjectedInputs) { const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent; const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent; start.inputDirective.disabled = this.disabled; end.inputDirective.disabled = this.disabled; return; } } private setRequiredToInputs(): void { // workaround for igxInput setting required Promise.resolve().then(() => { const isRequired = this.required; if (this.inputGroup && this.inputGroup.isRequired !== isRequired) { this.inputGroup.isRequired = isRequired; } else if (this.hasProjectedInputs && this._ngControl) { this.projectedInputs.forEach(i => i.isRequired = isRequired); } }); } private parseMinValue(value: string | Date): Date | null { let minValue: Date = parseDate(value); if (!minValue && this.hasProjectedInputs) { const start = this.projectedInputs.filter(i => i instanceof IgxDateRangeStartComponent)[0]; if (start) { minValue = parseDate(start.dateTimeEditor.minValue); } } return minValue; } private parseMaxValue(value: string | Date): Date | null { let maxValue: Date = parseDate(value); if (!maxValue && this.projectedInputs) { const end = this.projectedInputs.filter(i => i instanceof IgxDateRangeEndComponent)[0]; if (end) { maxValue = parseDate(end.dateTimeEditor.maxValue); } } return maxValue; } private updateCalendar(): void { if (!this.calendar) { return; } this.calendar.disabledDates = []; const minValue = this.parseMinValue(this.minValue); if (minValue) { this.calendar.disabledDates.push({ type: DateRangeType.Before, dateRange: [minValue] }); } const maxValue = this.parseMaxValue(this.maxValue); if (maxValue) { this.calendar.disabledDates.push({ type: DateRangeType.After, dateRange: [maxValue] }); } const range: Date[] = []; if (this.value?.start && this.value?.end) { const _value = this.toRangeOfDates(this.value); if (DateTimeUtil.greaterThanMaxValue(_value.start, _value.end)) { this.swapEditorDates(); } if (this.valueInRange(this.value, minValue, maxValue)) { range.push(_value.start, _value.end); } } if (range.length > 0) { this.calendar.selectDate(range); } else if (range.length === 0 && this.calendar.monthViews) { this.calendar.deselectDate(); } this.calendar.viewDate = range[0] || new Date(); } private swapEditorDates(): void { if (this.hasProjectedInputs) { const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent; const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent; [start.dateTimeEditor.value, end.dateTimeEditor.value] = [end.dateTimeEditor.value, start.dateTimeEditor.value]; [this.value.start, this.value.end] = [this.value.end, this.value.start]; } } private valueInRange(value: DateRange, minValue?: Date, maxValue?: Date): boolean { const _value = this.toRangeOfDates(value); if (minValue && DateTimeUtil.lessThanMinValue(_value.start, minValue, false)) { return false; } if (maxValue && DateTimeUtil.greaterThanMaxValue(_value.end, maxValue, false)) { return false; } return true; } private extractRange(selection: Date[]): DateRange { return { start: selection[0] || null, end: selection.length > 0 ? selection[selection.length - 1] : null }; } private toRangeOfDates(range: DateRange): { start: Date; end: Date } { let start; let end; if (!isDate(range.start)) { start = DateTimeUtil.parseIsoDate(range.start); } if (!isDate(range.end)) { end = DateTimeUtil.parseIsoDate(range.end); } if (start || end) { return { start, end }; } return { start: range.start as Date, end: range.end as Date }; } private subscribeToDateEditorEvents(): void { if (this.hasProjectedInputs) { const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent; const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent; if (start && end) { start.dateTimeEditor.valueChange .pipe(takeUntil(this._destroy$)) .subscribe(value => { if (this.value) { this.value = { start: value, end: this.value.end }; } else { this.value = { start: value, end: null }; } }); end.dateTimeEditor.valueChange .pipe(takeUntil(this._destroy$)) .subscribe(value => { if (this.value) { this.value = { start: this.value.start, end: value as Date }; } else { this.value = { start: null, end: value as Date }; } }); } } } private attachOnTouched(): void { if (this.hasProjectedInputs) { this.projectedInputs.forEach(i => { fromEvent(i.dateTimeEditor.nativeElement, 'blur') .pipe(takeUntil(this._destroy$)) .subscribe(() => { if (this.collapsed) { this.updateValidityOnBlur(); } }); }); } else { fromEvent(this.inputDirective.nativeElement, 'blur') .pipe(takeUntil(this._destroy$)) .subscribe(() => { if (this.collapsed) { this.updateValidityOnBlur(); } }); } } private cacheFocusedInput(): void { if (this.hasProjectedInputs) { this.projectedInputs.forEach(i => { fromEvent(i.dateTimeEditor.nativeElement, 'focus') .pipe(takeUntil(this._destroy$)) .subscribe(() => this._focusedInput = i); }); } } private configPositionStrategy(): void { this._positionSettings = { openAnimation: fadeIn, closeAnimation: fadeOut }; this._dropDownOverlaySettings.positionStrategy = new AutoPositionStrategy(this._positionSettings); this._dropDownOverlaySettings.target = this.element.nativeElement; } private configOverlaySettings(): void { if (this.overlaySettings !== null) { this._dropDownOverlaySettings = Object.assign({}, this._dropDownOverlaySettings, this.overlaySettings); this._dialogOverlaySettings = Object.assign({}, this._dialogOverlaySettings, this.overlaySettings); } } private initialSetValue() { // if there is no value and no ngControl on the picker but we have inputs we may have value set through // their ngModels - we should generate our initial control value if ((!this.value || (!this.value.start && !this.value.end)) && this.hasProjectedInputs && !this._ngControl) { const start = this.projectedInputs.find(i => i instanceof IgxDateRangeStartComponent); const end = this.projectedInputs.find(i => i instanceof IgxDateRangeEndComponent); this._value = { start: start.dateTimeEditor.value as Date, end: end.dateTimeEditor.value as Date }; } } private updateInputs(): void { const start = this.projectedInputs?.find(i => i instanceof IgxDateRangeStartComponent) as IgxDateRangeStartComponent; const end = this.projectedInputs?.find(i => i instanceof IgxDateRangeEndComponent) as IgxDateRangeEndComponent; if (start && end) { const _value = this.value ? this.toRangeOfDates(this.value) : null; start.updateInputValue(_value?.start || null); end.updateInputValue(_value?.end || null); } } private updateDisplayFormat(): void { this.projectedInputs.forEach(i => { const input = i as IgxDateRangeInputsBaseComponent; input.dateTimeEditor.displayFormat = this.displayFormat; }); } private updateInputFormat(): void { this.projectedInputs.forEach(i => { const input = i as IgxDateRangeInputsBaseComponent; if (input.dateTimeEditor.inputFormat !== this.inputFormat) { input.dateTimeEditor.inputFormat = this.inputFormat; } }); } private _initializeCalendarContainer(componentInstance: IgxCalendarContainerComponent) { this._calendar = componentInstance.calendar; this.calendar.hasHeader = false; this.calendar.locale = this.locale; this.calendar.selection = CalendarSelection.RANGE; this.calendar.weekStart = this.weekStart; this.calendar.hideOutsideDays = this.hideOutsideDays; this.calendar.monthsViewNumber = this.displayMonthsCount; this.calendar.showWeekNumbers = this.showWeekNumbers; this.calendar.selected.pipe(takeUntil(this._destroy$)).subscribe((ev: Date[]) => this.handleSelection(ev)); componentInstance.mode = this.mode; componentInstance.closeButtonLabel = !this.isDropdown ? this.doneButtonText : null; componentInstance.pickerActions = this.pickerActions; componentInstance.calendarClose.pipe(takeUntil(this._destroy$)).subscribe(() => this.close()); } }