@rifansi/angular-datetime-picker
Version:
Angular Date Time Picker
852 lines (755 loc) • 26.5 kB
text/typescript
/**
* date-time-picker-input.directive
*/
import {
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
forwardRef,
Inject,
Input,
OnDestroy,
OnInit,
Optional,
Output,
Renderer2
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { DOWN_ARROW } from '@angular/cdk/keycodes';
import { OwlDateTimeComponent } from './date-time-picker.component';
import { DateTimeAdapter } from './adapter/date-time-adapter.class';
import {
OWL_DATE_TIME_FORMATS,
OwlDateTimeFormats
} from './adapter/date-time-format.class';
import { Subscription } from 'rxjs';
import { SelectMode } from './date-time.class';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
export const OWL_DATETIME_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OwlDateTimeInputDirective),
multi: true
};
export const OWL_DATETIME_VALIDATORS: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OwlDateTimeInputDirective),
multi: true
};
({
selector: 'input[owlDateTime]',
exportAs: 'owlDateTimeInput',
standalone: false,
host: {
'(keydown)': 'handleKeydownOnHost($event)',
'(blur)': 'handleBlurOnHost($event)',
'(input)': 'handleInputOnHost($event)',
'(change)': 'handleChangeOnHost($event)',
'[attr.aria-haspopup]': 'owlDateTimeInputAriaHaspopup',
'[attr.aria-owns]': 'owlDateTimeInputAriaOwns',
'[attr.min]': 'minIso8601',
'[attr.max]': 'maxIso8601',
'[disabled]': 'owlDateTimeInputDisabled'
},
providers: [
OWL_DATETIME_VALUE_ACCESSOR,
OWL_DATETIME_VALIDATORS,
],
})
export class OwlDateTimeInputDirective<T>
implements
OnInit,
AfterContentInit,
OnDestroy,
ControlValueAccessor,
Validator {
static ngAcceptInputType_disabled: boolean|'';
/**
* Required flag to be used for range of [null, null]
* */
private _required: boolean;
()
get required() {
return this._required;
}
set required(value: any) {
this._required = value === '' || value;
this.validatorOnChange();
}
/**
* The date time picker that this input is associated with.
* */
()
set owlDateTime(value: OwlDateTimeComponent<T>) {
this.registerDateTimePicker(value);
}
/**
* A function to filter date time
*/
()
set owlDateTimeFilter(filter: (date: T | null) => boolean) {
this._dateTimeFilter = filter;
this.validatorOnChange();
}
private _dateTimeFilter: (date: T | null) => boolean;
get dateTimeFilter() {
return this._dateTimeFilter;
}
/** Whether the date time picker's input is disabled. */
()
private _disabled: boolean;
get disabled() {
return !!this._disabled;
}
set disabled(value: boolean) {
const newValue = coerceBooleanProperty(value);
const element = this.elmRef.nativeElement;
if (this._disabled !== newValue) {
this._disabled = newValue;
this.disabledChange.emit(newValue);
}
// We need to null check the `blur` method, because it's undefined during SSR.
if (newValue && element.blur) {
// Normally, native input elements automatically blur if they turn disabled. This behavior
// is problematic, because it would mean that it triggers another change detection cycle,
// which then causes a changed after checked error if the input element was focused before.
element.blur();
}
}
/** The minimum valid date. */
private _min: T | null;
()
get min(): T | null {
return this._min;
}
set min(value: T | null) {
this._min = this.getValidDate(this.dateTimeAdapter.deserialize(value));
this.validatorOnChange();
}
/** The maximum valid date. */
private _max: T | null;
()
get max(): T | null {
return this._max;
}
set max(value: T | null) {
this._max = this.getValidDate(this.dateTimeAdapter.deserialize(value));
this.validatorOnChange();
}
/**
* The picker's select mode
*/
private _selectMode: SelectMode = 'single';
()
get selectMode() {
return this._selectMode;
}
set selectMode(mode: SelectMode) {
if (
mode !== 'single' &&
mode !== 'range' &&
mode !== 'rangeFrom' &&
mode !== 'rangeTo'
) {
throw Error('OwlDateTime Error: invalid selectMode value!');
}
this._selectMode = mode;
}
/**
* The character to separate the 'from' and 'to' in input value
*/
()
rangeSeparator = '-';
private _value: T | null;
()
get value() {
return this._value;
}
set value(value: T | null) {
value = this.dateTimeAdapter.deserialize(value);
this.lastValueValid = !value || this.dateTimeAdapter.isValid(value);
value = this.getValidDate(value);
const oldDate = this._value;
this._value = value;
// set the input property 'value'
this.formatNativeInputValue();
// check if the input value changed
if (!this.dateTimeAdapter.isEqual(oldDate, value)) {
this.valueChange.emit(value);
}
}
private _values: T[] = [];
()
get values() {
return this._values;
}
set values(values: T[]) {
if (values && values.length > 0) {
this._values = values.map(v => {
v = this.dateTimeAdapter.deserialize(v);
return this.getValidDate(v);
});
this.lastValueValid =
(!this._values[0] ||
this.dateTimeAdapter.isValid(this._values[0])) &&
(!this._values[1] ||
this.dateTimeAdapter.isValid(this._values[1]));
} else {
this._values = [];
this.lastValueValid = true;
}
// set the input property 'value'
this.formatNativeInputValue();
this.valueChange.emit(this._values);
}
/**
* Callback to invoke when `change` event is fired on this `<input>`
* */
()
dateTimeChange = new EventEmitter<any>();
/**
* Callback to invoke when an `input` event is fired on this `<input>`.
* */
()
dateTimeInput = new EventEmitter<any>();
get elementRef(): ElementRef {
return this.elmRef;
}
get isInSingleMode(): boolean {
return this._selectMode === 'single';
}
get isInRangeMode(): boolean {
return (
this._selectMode === 'range' ||
this._selectMode === 'rangeFrom' ||
this._selectMode === 'rangeTo'
);
}
/** The date-time-picker that this input is associated with. */
public dtPicker: OwlDateTimeComponent<T>;
private dtPickerSub: Subscription = Subscription.EMPTY;
private localeSub: Subscription = Subscription.EMPTY;
private lastValueValid = true;
private onModelChange: Function = () => {};
private onModelTouched: Function = () => {};
private validatorOnChange: Function = () => {};
/** The form control validator for whether the input parses. */
private parseValidator: ValidatorFn = (): ValidationErrors | null => {
return this.lastValueValid
? null
: { owlDateTimeParse: { text: this.elmRef.nativeElement.value } };
}
/** The form control validator for the min date. */
private minValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
if (this.isInSingleMode) {
const controlValue = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value)
);
return !this.min ||
!controlValue ||
this.dateTimeAdapter.compare(this.min, controlValue) <= 0
? null
: { owlDateTimeMin: { min: this.min, actual: controlValue } };
} else if (this.isInRangeMode && control.value) {
const controlValueFrom = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[0])
);
const controlValueTo = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[1])
);
return !this.min ||
!controlValueFrom ||
!controlValueTo ||
this.dateTimeAdapter.compare(this.min, controlValueFrom) <= 0
? null
: {
owlDateTimeMin: {
min: this.min,
actual: [controlValueFrom, controlValueTo]
}
};
}
}
/** The form control validator for the max date. */
private maxValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
if (this.isInSingleMode) {
const controlValue = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value)
);
return !this.max ||
!controlValue ||
this.dateTimeAdapter.compare(this.max, controlValue) >= 0
? null
: { owlDateTimeMax: { max: this.max, actual: controlValue } };
} else if (this.isInRangeMode && control.value) {
const controlValueFrom = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[0])
);
const controlValueTo = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[1])
);
return !this.max ||
!controlValueFrom ||
!controlValueTo ||
this.dateTimeAdapter.compare(this.max, controlValueTo) >= 0
? null
: {
owlDateTimeMax: {
max: this.max,
actual: [controlValueFrom, controlValueTo]
}
};
}
}
/** The form control validator for the date filter. */
private filterValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
const controlValue = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value)
);
return !this._dateTimeFilter ||
!controlValue ||
this._dateTimeFilter(controlValue)
? null
: { owlDateTimeFilter: true };
}
/**
* The form control validator for the range.
* Check whether the 'before' value is before the 'to' value
* */
private rangeValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
if (this.isInSingleMode || !control.value) {
return null;
}
const controlValueFrom = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[0])
);
const controlValueTo = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[1])
);
return !controlValueFrom ||
!controlValueTo ||
this.dateTimeAdapter.compare(controlValueFrom, controlValueTo) <= 0
? null
: { owlDateTimeRange: true };
}
/**
* The form control validator for the range when required.
* Check whether the 'before' and 'to' values are present
* */
private requiredRangeValidator: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
if (this.isInSingleMode || !control.value || !this.required) {
return null;
}
const controlValueFrom = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[0])
);
const controlValueTo = this.getValidDate(
this.dateTimeAdapter.deserialize(control.value[1])
);
return !controlValueFrom ||
!controlValueTo
? { owlRequiredDateTimeRange: [controlValueFrom, controlValueTo] }
: null;
}
/** The combined form control validator for this input. */
private validator: ValidatorFn | null = Validators.compose([
this.parseValidator,
this.minValidator,
this.maxValidator,
this.filterValidator,
this.rangeValidator,
this.requiredRangeValidator
]);
/** Emits when the value changes (either due to user input or programmatic change). */
public valueChange = new EventEmitter<T[] | T | null>();
/** Emits when the disabled state has changed */
public disabledChange = new EventEmitter<boolean>();
get owlDateTimeInputAriaHaspopup(): boolean {
return true;
}
get owlDateTimeInputAriaOwns(): string {
return (this.dtPicker.opened && this.dtPicker.id) || null;
}
get minIso8601(): string {
return this.min ? this.dateTimeAdapter.toIso8601(this.min) : null;
}
get maxIso8601(): string {
return this.max ? this.dateTimeAdapter.toIso8601(this.max) : null;
}
get owlDateTimeInputDisabled(): boolean {
return this.disabled;
}
constructor(
private elmRef: ElementRef,
private renderer: Renderer2,
()
private dateTimeAdapter: DateTimeAdapter<T>,
() (OWL_DATE_TIME_FORMATS)
private dateTimeFormats: OwlDateTimeFormats ) {
if (!this.dateTimeAdapter) {
throw Error(
`OwlDateTimePicker: No provider found for DateTimePicker. You must import one of the following ` +
`modules at your application root: OwlNativeDateTimeModule, OwlMomentDateTimeModule, or provide a ` +
`custom implementation.`
);
}
if (!this.dateTimeFormats) {
throw Error(
`OwlDateTimePicker: No provider found for OWL_DATE_TIME_FORMATS. You must import one of the following ` +
`modules at your application root: OwlNativeDateTimeModule, OwlMomentDateTimeModule, or provide a ` +
`custom implementation.`
);
}
this.localeSub = this.dateTimeAdapter.localeChanges.subscribe(() => {
this.value = this.value;
});
}
public ngOnInit(): void {
if (!this.dtPicker) {
throw Error(
`OwlDateTimePicker: the picker input doesn't have any associated owl-date-time component`
);
}
}
public ngAfterContentInit(): void {
this.dtPickerSub = this.dtPicker.confirmSelectedChange.subscribe(
(selecteds: T[] | T) => {
if (Array.isArray(selecteds)) {
this.values = selecteds;
} else {
this.value = selecteds;
}
this.onModelChange(selecteds);
this.onModelTouched();
this.dateTimeChange.emit({
source: this,
value: selecteds,
input: this.elmRef.nativeElement
});
this.dateTimeInput.emit({
source: this,
value: selecteds,
input: this.elmRef.nativeElement
});
}
);
}
public ngOnDestroy(): void {
this.dtPickerSub.unsubscribe();
this.localeSub.unsubscribe();
this.valueChange.complete();
this.disabledChange.complete();
}
public writeValue(value: any): void {
if (this.isInSingleMode) {
this.value = value;
} else {
this.values = value;
}
}
public registerOnChange(fn: any): void {
this.onModelChange = fn;
}
public registerOnTouched(fn: any): void {
this.onModelTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
public validate(c: AbstractControl): { [key: string]: any } {
return this.validator ? this.validator(c) : null;
}
public registerOnValidatorChange(fn: () => void): void {
this.validatorOnChange = fn;
}
/**
* Open the picker when user hold alt + DOWN_ARROW
* */
public handleKeydownOnHost(event: KeyboardEvent ): void {
if (event.altKey && event.keyCode === DOWN_ARROW) {
this.dtPicker.open();
event.preventDefault();
}
}
public handleBlurOnHost(event: Event ): void {
this.onModelTouched();
}
public handleInputOnHost(event: any ): void {
const value = event.target.value;
if (this._selectMode === 'single') {
this.changeInputInSingleMode(value);
} else if (this._selectMode === 'range') {
this.changeInputInRangeMode(value);
} else {
this.changeInputInRangeFromToMode(value);
}
}
public handleChangeOnHost(event: any ): void {
let v;
if (this.isInSingleMode) {
v = this.value;
} else if (this.isInRangeMode) {
v = this.values;
}
this.dateTimeChange.emit({
source: this,
value: v,
input: this.elmRef.nativeElement
});
}
/**
* Set the native input property 'value'
*/
public formatNativeInputValue(): void {
if (this.isInSingleMode) {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
this._value
? this.dateTimeAdapter.format(
this._value,
this.dtPicker.formatString
)
: ''
);
} else if (this.isInRangeMode) {
if (this._values && this.values.length > 0) {
const from = this._values[0];
const to = this._values[1];
const fromFormatted = from
? this.dateTimeAdapter.format(
from,
this.dtPicker.formatString
)
: '';
const toFormatted = to
? this.dateTimeAdapter.format(
to,
this.dtPicker.formatString
)
: '';
if (!fromFormatted && !toFormatted) {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
null
);
} else {
if (this._selectMode === 'range') {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
fromFormatted +
' ' +
this.rangeSeparator +
' ' +
toFormatted
);
} else if (this._selectMode === 'rangeFrom') {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
fromFormatted
);
} else if (this._selectMode === 'rangeTo') {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
toFormatted
);
}
}
} else {
this.renderer.setProperty(
this.elmRef.nativeElement,
'value',
''
);
}
}
return;
}
/**
* Register the relationship between this input and its picker component
*/
private registerDateTimePicker(picker: OwlDateTimeComponent<T>) {
if (picker) {
this.dtPicker = picker;
this.dtPicker.registerInput(this);
}
}
/**
* Convert a given obj to a valid date object
*/
private getValidDate(obj: any): T | null {
return this.dateTimeAdapter.isDateInstance(obj) &&
this.dateTimeAdapter.isValid(obj)
? obj
: null;
}
/**
* Convert a time string to a date-time string
* When pickerType is 'timer', the value in the picker's input is a time string.
* The dateTimeAdapter parse fn could not parse a time string to a Date Object.
* Therefore we need this fn to convert a time string to a date-time string.
*/
private convertTimeStringToDateTimeString(
timeString: string,
dateTime: T
): string | null {
if (timeString) {
const v = dateTime || this.dateTimeAdapter.now();
const dateString = this.dateTimeAdapter.format(
v,
this.dateTimeFormats.datePickerInput
);
return dateString + ' ' + timeString;
} else {
return null;
}
}
/**
* Handle input change in single mode
*/
private changeInputInSingleMode(inputValue: string): void {
let value = inputValue;
if (this.dtPicker.pickerType === 'timer') {
value = this.convertTimeStringToDateTimeString(value, this.value);
}
let result = this.dateTimeAdapter.parse(
value,
this.dateTimeFormats.parseInput
);
this.lastValueValid = !result || this.dateTimeAdapter.isValid(result);
result = this.getValidDate(result);
// if the newValue is the same as the oldValue, we intend to not fire the valueChange event
// result equals to null means there is input event, but the input value is invalid
if (!this.isSameValue(result, this._value) || result === null) {
this._value = result;
this.valueChange.emit(result);
this.onModelChange(result);
this.dateTimeInput.emit({
source: this,
value: result,
input: this.elmRef.nativeElement
});
}
}
/**
* Handle input change in rangeFrom or rangeTo mode
*/
private changeInputInRangeFromToMode(inputValue: string): void {
const originalValue =
this._selectMode === 'rangeFrom'
? this._values[0]
: this._values[1];
if (this.dtPicker.pickerType === 'timer') {
inputValue = this.convertTimeStringToDateTimeString(
inputValue,
originalValue
);
}
let result = this.dateTimeAdapter.parse(
inputValue,
this.dateTimeFormats.parseInput
);
this.lastValueValid = !result || this.dateTimeAdapter.isValid(result);
result = this.getValidDate(result);
// if the newValue is the same as the oldValue, we intend to not fire the valueChange event
if (
(this._selectMode === 'rangeFrom' &&
this.isSameValue(result, this._values[0]) &&
result) ||
(this._selectMode === 'rangeTo' &&
this.isSameValue(result, this._values[1]) &&
result)
) {
return;
}
this._values =
this._selectMode === 'rangeFrom'
? [result, this._values[1]]
: [this._values[0], result];
this.valueChange.emit(this._values);
this.onModelChange(this._values);
this.dateTimeInput.emit({
source: this,
value: this._values,
input: this.elmRef.nativeElement
});
}
/**
* Handle input change in range mode
*/
private changeInputInRangeMode(inputValue: string): void {
const selecteds = inputValue.split(this.rangeSeparator);
let fromString = selecteds[0];
let toString = selecteds[1];
if (this.dtPicker.pickerType === 'timer') {
fromString = this.convertTimeStringToDateTimeString(
fromString,
this.values[0]
);
toString = this.convertTimeStringToDateTimeString(
toString,
this.values[1]
);
}
let from = this.dateTimeAdapter.parse(
fromString,
this.dateTimeFormats.parseInput
);
let to = this.dateTimeAdapter.parse(
toString,
this.dateTimeFormats.parseInput
);
this.lastValueValid =
(!from || this.dateTimeAdapter.isValid(from)) &&
(!to || this.dateTimeAdapter.isValid(to));
from = this.getValidDate(from);
to = this.getValidDate(to);
// if the newValue is the same as the oldValue, we intend to not fire the valueChange event
if (
!this.isSameValue(from, this._values[0]) ||
!this.isSameValue(to, this._values[1]) ||
(from === null && to === null)
) {
this._values = [from, to];
this.valueChange.emit(this._values);
this.onModelChange(this._values);
this.dateTimeInput.emit({
source: this,
value: this._values,
input: this.elmRef.nativeElement
});
}
}
/**
* Check if the two value is the same
*/
private isSameValue(first: T | null, second: T | null): boolean {
if (first && second) {
return this.dateTimeAdapter.compare(first, second) === 0;
}
return first === second;
}
}