diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index f73b0b3363f..929c8b3cceb 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -167,7 +167,8 @@ export const enum KEYS { DOWN_ARROW = 'ArrowDown', DOWN_ARROW_IE = 'Down', F2 = 'F2', - TAB = 'Tab' + TAB = 'Tab', + SEMICOLON = ';' } /** diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts b/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts index 22d9eba4d71..72434f21d7c 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts @@ -1,4 +1,5 @@ import { isIE } from '../core/utils'; +import { DatePart, DatePartInfo } from '../directives/date-time-editor/date-time-editor.common'; /** * This enum is used to keep the date validation result. @@ -18,6 +19,11 @@ const enum FormatDesc { TwoDigits = '2-digit' } +export interface DateTimeValue { + state: DateState; + value: Date; +} + /** *@hidden */ @@ -27,6 +33,9 @@ const enum DateChars { DayChar = 'd' } +const TimeCharsArr = ['h', 'H', 'm', 's', 'S', 't', 'T']; +const DateCharsArr = ['d', 'D', 'M', 'y', 'Y']; + /** *@hidden */ @@ -37,7 +46,7 @@ const enum DateParts { } /** - *@hidden + * @hidden */ export abstract class DatePickerUtil { private static readonly SHORT_DATE_MASK = 'MM/dd/yy'; @@ -46,6 +55,282 @@ export abstract class DatePickerUtil { private static readonly PROMPT_CHAR = '_'; private static readonly DEFAULT_LOCALE = 'en'; + public static parseDateTimeArray(dateTimeParts: DatePartInfo[], inputData: string): DateTimeValue { + const parts: { [key in DatePart]: number } = {} as any; + dateTimeParts.forEach(dp => { + let value = parseInt(this.getCleanVal(inputData, dp), 10); + if (!value) { + value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0; + } + parts[dp.type] = value; + }); + + if (parts[DatePart.Month] < 1 || 12 < parts[DatePart.Month]) { + return { state: DateState.Invalid, value: new Date(NaN) }; + } + + // TODO: Century threshold + if (parts[DatePart.Year] < 50) { + parts[DatePart.Year] += 2000; + } + + if (parts[DatePart.Date] > DatePickerUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) { + return { state: DateState.Invalid, value: new Date(NaN) }; + } + + if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) { + return { state: DateState.Invalid, value: new Date(NaN) }; + } + + return { + state: DateState.Valid, + value: new Date( + parts[DatePart.Year] || 2000, + parts[DatePart.Month] - 1 || 0, + parts[DatePart.Date] || 1, + parts[DatePart.Hours] || 0, + parts[DatePart.Minutes] || 0, + parts[DatePart.Seconds] || 0 + ) + }; + } + + public static parseDateTimeFormat(mask: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): DatePartInfo[] { + let format = DatePickerUtil.setInputFormat(mask); + let dateTimeData: DatePartInfo[] = []; + if (!format && !isIE()) { + dateTimeData = DatePickerUtil.getDefaultLocaleMask(locale); + } else { + format = (format) ? format : DatePickerUtil.SHORT_DATE_MASK; + const formatArray = Array.from(format); + for (let i = 0; i < formatArray.length; i++) { + const datePartRange = this.getDatePartInfoRange(formatArray[i], format, i); + const dateTimeInfo = { + type: DatePickerUtil.determineDatePart(formatArray[i]), + start: datePartRange.start, + end: datePartRange.end, + format: mask.match(new RegExp(`${format[i]}+`, 'g'))[0], + }; + while (DatePickerUtil.isDateOrTimeChar(formatArray[i])) { + if (dateTimeData.indexOf(dateTimeInfo) === -1) { + dateTimeData.push(dateTimeInfo); + } + i++; + } + } + } + + return dateTimeData; + } + + public static setInputFormat(format: string): string { + if (!format) { return ''; } + let chars = ''; + let newFormat = ''; + for (let i = 0; ; i++) { + while (DatePickerUtil.isDateOrTimeChar(format[i])) { + chars += format[i]; + i++; + } + const datePartType = DatePickerUtil.determineDatePart(chars[0]); + if (datePartType !== DatePart.Year) { + newFormat += chars[0].repeat(2); + } else { + newFormat += chars; + } + + if (i >= format.length) { break; } + + if (!DatePickerUtil.isDateOrTimeChar(format[i])) { + newFormat += format[i]; + } + chars = ''; + } + + return newFormat; + } + + public static isDateOrTimeChar(char: string): boolean { + return TimeCharsArr.indexOf(char) !== -1 || DateCharsArr.indexOf(char) !== -1; + } + + public static calculateDateOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + if (isSpinLoop) { + const maxDate = DatePickerUtil.daysInMonth(currentDate.getFullYear(), currentDate.getMonth() + 1); + const deltaSign = delta > 1 ? delta % (Math.abs(delta) - 1) : delta; + let date = currentDate.getDate(); + for (let i = Math.abs(delta); i > 0; i--) { + if (deltaSign > 0) { + date = date < maxDate ? date + deltaSign : 1; + } else { + date = date > 1 ? date + deltaSign : maxDate; + } + } + + return new Date(currentDate.setDate(date)); + } + newDate = new Date(newDate.setDate(newDate.getDate() + delta)); + if (currentDate.getMonth() === newDate.getMonth()) { + return newDate; + } + + return currentDate; + } + + public static calculateMonthOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + const maxDate = DatePickerUtil.daysInMonth(currentDate.getFullYear(), currentDate.getMonth() + 1 + delta); + if (newDate.getDate() > maxDate) { + newDate.setDate(maxDate); + } + if (isSpinLoop) { + const deltaSign = delta > 1 ? delta % (Math.abs(delta) - 1) : delta; + let month = currentDate.getMonth(); + for (let i = Math.abs(delta); i > 0; i--) { + if (deltaSign > 0) { + month = month < 11 ? month + deltaSign : 0; + } else { + month = month > 0 ? month + deltaSign : 11; + } + } + + return new Date(newDate.setMonth(month)); + } + newDate = new Date(newDate.setMonth(newDate.getMonth() + delta)); + if (currentDate.getFullYear() === newDate.getFullYear()) { + return newDate; + } + + return currentDate; + } + + public static calculateYearOnSpin(delta: number, newDate: Date, currentDate: Date): Date { + const maxDate = DatePickerUtil.daysInMonth(currentDate.getFullYear() + delta, currentDate.getMonth() + 1); + if (newDate.getDate() > maxDate) { + newDate.setDate(maxDate); + } + return new Date(newDate.setFullYear(newDate.getFullYear() + delta)); + } + + public static calculateHoursOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + if (isSpinLoop) { + const deltaSign = delta > 1 ? delta % (Math.abs(delta) - 1) : delta; + let hours = currentDate.getHours(); + for (let i = Math.abs(delta); i > 0; i--) { + if (deltaSign > 0) { + hours = hours < 23 ? hours + deltaSign : 0; + } else { + hours = hours > 0 ? hours + deltaSign : 23; + } + } + + return new Date(currentDate.setHours(hours)); + } + newDate = new Date(newDate.setHours(newDate.getHours() + delta)); + if (currentDate.getDate() === newDate.getDate()) { + return newDate; + } + + return currentDate; + } + + public static calculateMinutesOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + if (isSpinLoop) { + const deltaSign = delta > 1 ? delta % (Math.abs(delta) - 1) : delta; + let minutes = currentDate.getMinutes(); + for (let i = Math.abs(delta); i > 0; i--) { + if (deltaSign > 0) { + minutes = minutes < 59 ? minutes + deltaSign : 0; + } else { + minutes = minutes > 0 ? minutes + deltaSign : 59; + } + } + + return new Date(currentDate.setMinutes(minutes)); + } + newDate = new Date(newDate.setMinutes(newDate.getMinutes() + delta)); + if (currentDate.getHours() === newDate.getHours()) { + return newDate; + } + + return currentDate; + } + + public static calculateSecondsOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + if (isSpinLoop) { + const deltaSign = delta > 1 ? delta % (Math.abs(delta) - 1) : delta; + let seconds = currentDate.getSeconds(); + for (let i = Math.abs(delta); i > 0; i--) { + if (deltaSign > 0) { + seconds = seconds < 59 ? seconds + deltaSign : 0; + } else { + seconds = seconds > 0 ? seconds + deltaSign : 59; + } + } + + return new Date(currentDate.setSeconds(seconds)); + } + newDate = new Date(newDate.setSeconds(newDate.getSeconds() + delta)); + if (currentDate.getMinutes() === newDate.getMinutes()) { + return newDate; + } + + return currentDate; + } + + public static calculateAmPmOnSpin(newDate: Date, currentDate: Date, amPmFromMask: string) { + switch (amPmFromMask) { + case 'AM': + newDate = new Date(newDate.setHours(newDate.getHours() + 12 * 1)); + break; + case 'PM': + newDate = new Date(newDate.setHours(newDate.getHours() + 12 * -1)); + break; + } + if (newDate.getDate() !== currentDate.getDate()) { + return currentDate; + } + + return newDate; + } + + private static getCleanVal(inputData: string, datePart: DatePartInfo): string { + return DatePickerUtil.trimUnderlines(inputData.substring(datePart.start, datePart.end)); + } + + private static getDatePartInfoRange(datePartChars: string, mask: string, index: number): any { + const start = mask.indexOf(datePartChars, index); + let end = start; + while (this.isDateOrTimeChar(mask[end])) { + end++; + } + + return { start, end }; + } + + private static determineDatePart(char: string): DatePart { + switch (char) { + case 'd': + case 'D': + return DatePart.Date; + case 'M': + return DatePart.Month; + case 'y': + case 'Y': + return DatePart.Year; + case 'h': + case 'H': + return DatePart.Hours; + case 'm': + return DatePart.Minutes; + case 's': + case 'S': + return DatePart.Seconds; + case 't': + case 'T': + return DatePart.AmPm; + } + } + /** * This method generates date parts structure based on editor mask and locale. * @param maskValue: string @@ -169,12 +454,12 @@ export abstract class DatePickerUtil { return mask.join(''); } /** - * This method parses an input string base on date parts and returns a date and its validation state. - * @param dateFormatParts - * @param prevDateValue - * @param inputValue - * @returns object containing a date and its validation state - */ + * This method parses an input string base on date parts and returns a date and its validation state. + * @param dateFormatParts + * @param prevDateValue + * @param inputValue + * @returns object containing a date and its validation state + */ public static parseDateArray(dateFormatParts: any[], prevDateValue: Date, inputValue: string): any { const dayStr = DatePickerUtil.getDayValueFromInput(dateFormatParts, inputValue); const monthStr = DatePickerUtil.getMonthValueFromInput(dateFormatParts, inputValue); @@ -339,6 +624,10 @@ export abstract class DatePickerUtil { return ''; } + public static daysInMonth(fullYear: number, month: number): number { + return new Date(fullYear, month, 0).getDate(); + } + private static getYearFormatType(format: string): string { switch (format.match(new RegExp(DateChars.YearChar, 'g')).length) { case 1: { @@ -464,10 +753,6 @@ export abstract class DatePickerUtil { return { min: minValue, max: maxValue }; } - private static daysInMonth(fullYear: number, month: number): number { - return new Date(fullYear, month, 0).getDate(); - } - private static getDateValueFromInput(dateFormatParts: any[], type: DateParts, inputValue: string, trim: boolean = true): string { const partPosition = DatePickerUtil.getDateFormatPart(dateFormatParts, type).position; const result = inputValue.substring(partPosition[0], partPosition[1]); diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts new file mode 100644 index 00000000000..b8ea9caaec8 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.common.ts @@ -0,0 +1,21 @@ +export interface IgxDateTimeEditorEventArgs { + oldValue: Date | string; + newValue: Date | string; +} + +export enum DatePart { + Date = 'date', + Month = 'month', + Year = 'year', + Hours = 'hours', + Minutes = 'minutes', + Seconds = 'seconds', + AmPm = 'ampm' +} + +export interface DatePartInfo { + type: DatePart; + start: number; + end: number; + format: string; +} diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts new file mode 100644 index 00000000000..9a1d8f4809d --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts @@ -0,0 +1,331 @@ +import { IgxDateTimeEditorDirective } from './date-time-editor.directive'; +import { DOCUMENT } from '@angular/common'; +import { DatePart } from '../date-time-editor/date-time-editor.common'; + +let dateTimeEditor: IgxDateTimeEditorDirective; + +describe('IgxDateTimeEditor', () => { + describe('Unit tests', () => { + const maskParsingService = jasmine.createSpyObj('MaskParsingService', ['parseMask', 'restoreValueFromMask', 'parseMaskValue']); + const renderer2 = jasmine.createSpyObj('Renderer2', ['setAttribute']); + let elementRef = { nativeElement: null }; + + it('Should correctly display input format during user input.', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + // TODO + }); + + it('Should spin/evaluate date input if an invalid date is pasted.', () => { + // new Date(3333, 33, 33) + // Wed Nov 02 3335 00:00:00 GMT+0200 (Eastern European Standard Time) + }); + + it('Should correctly show year based on century threshold.', () => { + // TODO + }); + + it('Should not allow invalid dates to be entered.', () => { + // valid for date and month segments + }); + + it('Should autofill missing date/time segments on blur.', () => { + // TODO + // _1/__/___ => 14/01/2000 -> de default date (1) and default year (2000) + }); + + it('Should support different display and input formats.', () => { // ? + // TODO + // have century threshold by default? + // paste/input -"1/1/220 1:1:1:1" - input format/mask "_1/_1/_220 _1:_1:_1:__1" - display format "1/1/220 1:1:1:100" + // input - 10/10/2020 10:10:10:111 - input format/mask - "10/10/2020 10:10:10:111" - display format "10/10/2020 10:10:10:111" + }); + + it('Should apply the display format defined.', () => { + // TODO + // default format + // custom format + }); + + it('Should support long and short date formats', () => { + // TODO + }); + + it('Should correctly display input and display formats, when different ones are defined for the component.', () => { + // TODO + }); + + it('Should disable the input when disabled property is set.', () => { + // TODO + }); + + it('Should set the input as readonly when readonly property is set.', () => { + // TODO + }); + + it('Editor should not be editable when readonly or disabled.', () => { + // TODO + }); + + it('Should move the caret to the start of the same portion if the caret is positioned at the end.', () => { + // TODO + // Ctrl/Cmd + Arrow Left + }); + + it('Should move the caret to the end of the same portion if it is positioned at the beginning.', () => { + // TODO + // Ctrl/Cmd + Arrow Right + }); + + it('Should move the caret to the same position on the next portion.', () => { + // TODO + // beginning of portion + // end of portion + }); + + describe('Should be able to spin the date portions.', () => { + it('Should correctly increment / decrement date portions with passed in DatePart', () => { + elementRef = { nativeElement: { value: '12/10/2015' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.inputFormat = 'dd/M/yy'; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date('12/10/2015'); + const date = dateTimeEditor.value.getDate(); + const month = dateTimeEditor.value.getMonth(); + + dateTimeEditor.increment(DatePart.Date); + expect(dateTimeEditor.value.getDate()).toBeGreaterThan(date); + + dateTimeEditor.decrement(DatePart.Month); + expect(dateTimeEditor.value.getMonth()).toBeLessThan(month); + }); + + it('Should correctly increment / decrement date portions without passed in DatePart', () => { + elementRef = { nativeElement: { value: '12/10/2015' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date('12/10/2015'); + const date = dateTimeEditor.value.getDate(); + + dateTimeEditor.increment(); + expect(dateTimeEditor.value.getDate()).toBeGreaterThan(date); + + dateTimeEditor.decrement(); + expect(dateTimeEditor.value.getDate()).toEqual(date); + }); + + it('Should not loop over to next month when incrementing date', () => { + elementRef = { nativeElement: { value: '29/02/2020' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 1, 29); + + dateTimeEditor.increment(); + expect(dateTimeEditor.value.getDate()).toEqual(1); + expect(dateTimeEditor.value.getMonth()).toEqual(1); + }); + + it('Should not loop over to next year when incrementing month', () => { + elementRef = { nativeElement: { value: '29/12/2020' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 11, 29); + + dateTimeEditor.increment(DatePart.Month); + expect(dateTimeEditor.value.getMonth()).toEqual(0); + expect(dateTimeEditor.value.getFullYear()).toEqual(2020); + }); + + it('Should update date part if next/previous month\'s max date is less than the current one\'s', () => { + elementRef = { nativeElement: { value: '31/01/2020' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 0, 31); + + dateTimeEditor.increment(DatePart.Month); + expect(dateTimeEditor.value.getDate()).toEqual(29); + expect(dateTimeEditor.value.getMonth()).toEqual(1); + }); + + it('Should prioritize Date for spinning, if it is set in format', () => { + elementRef = { nativeElement: { value: '11/03/2020 00:00:00 AM' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.inputFormat = 'dd/M/yy HH:mm:ss tt'; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 2, 11); + + dateTimeEditor.increment(); + expect(dateTimeEditor.value.getDate()).toEqual(12); + + dateTimeEditor.decrement(); + expect(dateTimeEditor.value.getDate()).toEqual(11); + }); + + it('Should not loop over when isSpinLoop is false', () => { + elementRef = { nativeElement: { value: '31/03/2020' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.isSpinLoop = false; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 2, 31); + + dateTimeEditor.increment(DatePart.Date); + expect(dateTimeEditor.value.getDate()).toEqual(31); + + dateTimeEditor.value = new Date(2020, 1, 31); + dateTimeEditor.decrement(DatePart.Month); + expect(dateTimeEditor.value.getMonth()).toEqual(1); + }); + + it('Should loop over when isSpinLoop is true (default)', () => { + elementRef = { nativeElement: { value: '31/03/2019' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2020, 2, 31); + + dateTimeEditor.increment(DatePart.Date); + expect(dateTimeEditor.value.getDate()).toEqual(1); + + dateTimeEditor.value = new Date(2020, 0, 31); + dateTimeEditor.decrement(DatePart.Month); + expect(dateTimeEditor.value.getMonth()).toEqual(11); + }); + }); + + describe('Should be able to spin the time portions.', () => { + it('Should correctly increment / decrement time portions with passed in DatePart', () => { + elementRef = { nativeElement: { value: '10/10/2010 12:10:34' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2010, 11, 10, 12, 10, 34); + const minutes = dateTimeEditor.value.getMinutes(); + const seconds = dateTimeEditor.value.getSeconds(); + + dateTimeEditor.increment(DatePart.Minutes); + expect(dateTimeEditor.value.getMinutes()).toBeGreaterThan(minutes); + + dateTimeEditor.decrement(DatePart.Seconds); + expect(dateTimeEditor.value.getSeconds()).toBeLessThan(seconds); + }); + + it('Should correctly increment / decrement time portions without passed in DatePart', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + /* + * format must be set because the editor will prioritize Date if Hours is not set + * and no DatePart is provided to increment / decrement + */ + dateTimeEditor.inputFormat = 'HH:mm:ss tt'; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(); + const hours = dateTimeEditor.value.getHours(); + + dateTimeEditor.increment(); + expect(dateTimeEditor.value.getHours()).toBeGreaterThan(hours); + + dateTimeEditor.decrement(); + expect(dateTimeEditor.value.getHours()).toEqual(hours); + }); + + it('Should not loop over to next minute when incrementing seconds', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2019, 1, 20, 20, 5, 59); + + dateTimeEditor.increment(DatePart.Seconds); + expect(dateTimeEditor.value.getMinutes()).toEqual(5); + expect(dateTimeEditor.value.getSeconds()).toEqual(0); + }); + + it('Should not loop over to next hour when incrementing minutes', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2019, 1, 20, 20, 59, 12); + + dateTimeEditor.increment(DatePart.Minutes); + expect(dateTimeEditor.value.getHours()).toEqual(20); + expect(dateTimeEditor.value.getMinutes()).toEqual(0); + }); + + it('Should not loop over to next day when incrementing hours', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2019, 1, 20, 23, 13, 12); + + dateTimeEditor.increment(DatePart.Hours); + expect(dateTimeEditor.value.getDate()).toEqual(20); + expect(dateTimeEditor.value.getHours()).toEqual(0); + }); + + it('Should not loop over when isSpinLoop is false', () => { + elementRef = { nativeElement: { value: '20/02/2019 23:00:12' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.isSpinLoop = false; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2019, 1, 20, 23, 0, 12); + + dateTimeEditor.increment(DatePart.Hours); + expect(dateTimeEditor.value.getHours()).toEqual(23); + + dateTimeEditor.decrement(DatePart.Minutes); + expect(dateTimeEditor.value.getMinutes()).toEqual(0); + }); + + it('Should loop over when isSpinLoop is true (default)', () => { + elementRef = { nativeElement: { value: '20/02/2019 23:15:12' } }; + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(2019, 1, 20, 23, 15, 0); + + dateTimeEditor.increment(DatePart.Hours); + expect(dateTimeEditor.value.getHours()).toEqual(0); + + dateTimeEditor.decrement(DatePart.Seconds); + expect(dateTimeEditor.value.getSeconds()).toEqual(59); + }); + }); + + it('Should revert to empty mask on clear()', () => { + // TODO + // should clear inner value and emit valueChanged + }); + + it('Should not block the user from typing/pasting/dragging dates outside of min/max range', () => { + // TODO + }); + + it('Should enter an invalid state if the input does not satisfy min/max props.', () => { + // TODO + // should throw an event containing the arguments + // apply styles? + }); + + // it('Should prevent user input if the input is outside min/max values defined.', () => { + // // TODO + // // clear the date / reset the the date to min/max? -> https://github.com/IgniteUI/igniteui-angular/issues/6286 + // }); + + it('Should display Default "/" separator if none is set.', () => { + // TODO + }); + + it('Should display the Custom separator if such is defined.', () => { + // TODO + }); + + it('Should preserve the separator on paste/drag with other separator', () => { + // TODO + }); + + it('Should preserve the date when pasting with different separator', () => { + // TODO + // 01/01/0220 --> 01/01/0220 + // 01\01\0220 --> 01/01/0220 + // 01%01%0220 --> 01/01/0220 + // 01-01-0220 --> 01/01/0220 + // 01-01-2020 --> 01/01/2020 + }); + }); + + describe('Integration tests', () => { + // TODO + }); +}); diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts new file mode 100644 index 00000000000..a265a16a583 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts @@ -0,0 +1,424 @@ +import { + Directive, Input, ElementRef, OnInit, + Renderer2, NgModule, Output, EventEmitter, Inject, HostListener +} from '@angular/core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor, } from '@angular/forms'; +import { CommonModule, formatDate, DOCUMENT } from '@angular/common'; +import { IgxMaskDirective } from '../mask/mask.directive'; +import { MaskParsingService } from '../mask/mask-parsing.service'; +import { KEYS } from '../../core/utils'; +import { + DatePickerUtil, DateState, DateTimeValue +} from '../../date-picker/date-picker.utils'; +import { IgxDateTimeEditorEventArgs, DatePartInfo, DatePart } from './date-time-editor.common'; + +@Directive({ + selector: '[igxDateTimeEditor]', + exportAs: 'igxDateTimeEditor', + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateTimeEditorDirective, multi: true } + ] +}) +export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnInit, ControlValueAccessor { + @Input() + public value: Date; + + @Input() + public locale = 'en'; + + @Input() + public minValue: string | Date; + + @Input() + public maxValue: string | Date; + + @Input() + public promptChar: string; + + @Input() + public isSpinLoop = true; + + @Input() + public displayFormat: string; + + public get inputFormat(): string { + return this._format; + } + + @Input(`igxDateTimeEditor`) + public set inputFormat(value: string) { + if (value) { + this._format = value; + } + const mask = this.buildMask(this.inputFormat); + this.mask = value.indexOf('tt') !== -1 ? mask.substring(0, mask.length - 2) + 'LL' : mask; + } + + @Output() + public valueChanged = new EventEmitter(); + + @Output() + public validationFailed = new EventEmitter(); + + private _document: Document; + private _isFocused: boolean; + private _format = 'dd/MM/yyyy'; + private _oldValue: Date | string; + private _dateTimeFormatParts: DatePartInfo[]; + private onTouchCallback = (...args: any[]) => { }; + private onChangeCallback = (...args: any[]) => { }; + + private get literals() { + const literals = []; + for (const char of this.mask) { + if (char.match(/[^0lL]/)) { literals.push(char); } + } + + return literals; + } + + private get emptyMask() { + return this.maskParser.applyMask(this.inputFormat, this.maskOptions); + } + + private get targetDatePart(): DatePart { + if (this._document.activeElement === this.nativeElement) { + return this._dateTimeFormatParts.find(p => p.start <= this.selectionStart && this.selectionStart <= p.end).type; + } else { + if (this._dateTimeFormatParts.some(p => p.type === DatePart.Date)) { + return DatePart.Date; + } else if (this._dateTimeFormatParts.some(p => p.type === DatePart.Hours)) { + return DatePart.Hours; + } + } + } + + constructor( + protected elementRef: ElementRef, + protected maskParser: MaskParsingService, + protected renderer: Renderer2, + @Inject(DOCUMENT) private document: any) { + super(elementRef, maskParser, renderer); + this._document = this.document as Document; + } + + /** @hidden */ + public ngOnInit(): void { + this._dateTimeFormatParts = DatePickerUtil.parseDateTimeFormat(this.inputFormat); + this.renderer.setAttribute(this.nativeElement, 'placeholder', this.inputFormat); + this.updateMask(); + } + + public clear(): void { + this.updateValue(null); + this.updateMask(); + } + + public increment(datePart?: DatePart): void { + const newValue = datePart + ? this.calculateValueOnSpin(datePart, 1) + : this.calculateValueOnSpin(this.targetDatePart, 1); + this.updateValue(newValue ? newValue : new Date()); + this.updateMask(); + } + + public decrement(datePart?: DatePart): void { + const newValue = datePart + ? this.calculateValueOnSpin(datePart, -1) + : this.calculateValueOnSpin(this.targetDatePart, -1); + this.updateValue(newValue ? newValue : new Date()); + this.updateMask(); + } + + /** @hidden */ + public writeValue(value: any): void { + this.value = value; + this.updateMask(); + } + + /** @hidden */ + public registerOnChange(fn: any): void { this.onChangeCallback = fn; } + + /** @hidden */ + public registerOnTouched(fn: any): void { this.onTouchCallback = fn; } + + /** @hidden */ + public setDisabledState?(isDisabled: boolean): void { } + + /** @hidden */ + public onKeyDown(event: KeyboardEvent) { + super.onKeyDown(event); + if (event.key === KEYS.UP_ARROW || event.key === KEYS.UP_ARROW_IE || + event.key === KEYS.DOWN_ARROW || event.key === KEYS.DOWN_ARROW_IE) { + this.spin(event); + return; + } + + if (event.ctrlKey && event.key === KEYS.SEMICOLON) { + this.updateValue(new Date()); + this.updateMask(); + } + + this.moveCursor(event); + } + + /** @hidden */ + public onFocus(): void { + this._isFocused = true; + this.onTouchCallback(); + this.updateMask(); + super.onFocus(); + } + + /** @hidden */ + public onBlur(event): void { + this._isFocused = false; + this.updateValue(this.value); + this.updateMask(); + this.onTouchCallback(); + super.onBlur(event); + } + + /** @hidden */ + public onInputChanged(): void { + // the mask must be updated before any date operations + super.onInputChanged(); + if (this.inputValue === this.emptyMask) { + this.updateValue(null); + return; + } + + const parsedDate = this.parseDate(this.inputValue); + if (parsedDate.state === DateState.Valid) { + this.updateValue(parsedDate.value); + } else { + this.validationFailed.emit({ oldValue: this.value, newValue: parsedDate.value }); + this.updateValue(null); + } + } + + private buildMask(format: string): string { + return DatePickerUtil.setInputFormat(format).replace(/\w/g, '0'); + } + + private isDate(value: any): value is Date { + return value instanceof Date && typeof value === 'object'; + } + + private valueInRange(value: Date): boolean { + if (!value) { return false; } + const maxValueAsDate = this.isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue)?.value; + const minValueAsDate = this.isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue)?.value; + if (maxValueAsDate && minValueAsDate) { + return value.getTime() <= maxValueAsDate.getTime() && + minValueAsDate.getTime() <= value.getTime(); + } + + return maxValueAsDate && value.getTime() <= maxValueAsDate.getTime() || + minValueAsDate && minValueAsDate.getTime() <= value.getTime(); + } + + private calculateValueOnSpin(datePart: DatePart, delta: number): Date { + if (!this.value || !this.isValidDate(this.value)) { return null; } + const newDate = new Date(this.value.getFullYear(), this.value.getMonth(), this.value.getDate(), + this.value.getHours(), this.value.getMinutes(), this.value.getSeconds()); + if (this.isValidDate(this.value)) { + switch (datePart) { + case DatePart.Date: + return DatePickerUtil.calculateDateOnSpin(delta, newDate, this.value, this.isSpinLoop); + case DatePart.Month: + return DatePickerUtil.calculateMonthOnSpin(delta, newDate, this.value, this.isSpinLoop); + case DatePart.Year: + return DatePickerUtil.calculateYearOnSpin(delta, newDate, this.value); + case DatePart.Hours: + return DatePickerUtil.calculateHoursOnSpin(delta, newDate, this.value, this.isSpinLoop); + case DatePart.Minutes: + return DatePickerUtil.calculateMinutesOnSpin(delta, newDate, this.value, this.isSpinLoop); + case DatePart.Seconds: + return DatePickerUtil.calculateSecondsOnSpin(delta, newDate, this.value, this.isSpinLoop); + case DatePart.AmPm: + const formatPart = this._dateTimeFormatParts.find(dp => dp.type === DatePart.AmPm); + const amPmFromMask = this.inputValue.substring(formatPart.start, formatPart.end); + return DatePickerUtil.calculateAmPmOnSpin(newDate, this.value, amPmFromMask); + } + } + + return this.value; + } + + private updateValue(newDate: Date) { + this._oldValue = this.value; + this.value = newDate; + if (this.minValue || this.maxValue) { + if (this.valueInRange(this.value)) { + this.onChangeCallback(this.value); + } else { + this.onChangeCallback(null); + } + } else { + this.onChangeCallback(this.value); + } + if (this.inputIsComplete()) { + this.valueChanged.emit({ oldValue: this._oldValue, newValue: this.value }); + } + } + + private updateMask() { + if (!this.value || !this.isValidDate(this.value)) { + this.inputValue = this.emptyMask; + return; + } + if (this._isFocused) { + const cursor = this.selectionEnd; + let mask = this.emptyMask; + this._dateTimeFormatParts.forEach(p => { + const partLength = p.end - p.start; + let targetValue = this.getMaskedValue(p.type, partLength); + + if (p.type === DatePart.Month) { + targetValue = this.prependValue( + parseInt(targetValue.replace(new RegExp(this.promptChar, 'g'), '0'), 10) + 1, partLength, '0'); + } + + if (p.type === DatePart.Hours && p.format.indexOf('h') !== -1) { + targetValue = this.prependValue(this.toTwelveHourFormat(targetValue), partLength, '0'); + } + + if (p.type === DatePart.Year && p.format.length === 2) { + targetValue = this.prependValue(parseInt(targetValue.slice(-2), 10), partLength, '0'); + } + + mask = this.maskParser.replaceInMask(mask, targetValue, this.maskOptions, p.start, p.end).value; + }); + this.inputValue = mask; + this.setSelectionRange(cursor); + } else { + const format = this.displayFormat ? this.displayFormat : this.inputFormat; + this.inputValue = formatDate(this.value, format.replace('tt', 'aa'), this.locale); + } + } + + private toTwelveHourFormat(value: string): number { + let hour = parseInt(value.replace(new RegExp(this.promptChar, 'g'), '0'), 10); + if (hour > 12) { + hour -= 12; + } else if (hour === 0) { + hour = 12; + } + + return hour; + } + + private getMaskedValue(datePart: DatePart, partLength: number): string { + let maskedValue; + switch (datePart) { + case DatePart.Date: + maskedValue = this.value.getDate(); + break; + case DatePart.Month: + maskedValue = this.value.getMonth(); + break; + case DatePart.Year: + maskedValue = this.value.getFullYear(); + break; + case DatePart.Hours: + maskedValue = this.value.getHours(); + break; + case DatePart.Minutes: + maskedValue = this.value.getMinutes(); + break; + case DatePart.Seconds: + maskedValue = this.value.getSeconds(); + break; + case DatePart.AmPm: + maskedValue = this.value.getHours() >= 12 ? 'PM' : 'AM'; + break; + } + + if (datePart !== DatePart.AmPm) { + return this.prependValue(maskedValue, partLength, '0'); + } + + return maskedValue; + } + + private prependValue(value: number, partLength: number, prependChar: string): string { + return (prependChar + value.toString()).slice(-partLength); + } + + private spin(event: KeyboardEvent): void { + event.preventDefault(); + switch (event.key) { + case KEYS.UP_ARROW: + case KEYS.UP_ARROW_IE: + this.increment(); + break; + case KEYS.DOWN_ARROW: + case KEYS.DOWN_ARROW_IE: + this.decrement(); + break; + } + } + + private inputIsComplete(): boolean { + return this.inputValue.indexOf(this.promptChar) === -1; + } + + private isValidDate(date: Date): boolean { + return date && date.getTime && !isNaN(date.getTime()); + } + + private parseDate(val: string): DateTimeValue { + if (!val) { return null; } + return DatePickerUtil.parseDateTimeArray(this._dateTimeFormatParts, val); + } + + private moveCursor(event: KeyboardEvent): void { + const value = (event.target as HTMLInputElement).value; + switch (event.key) { + case KEYS.LEFT_ARROW: + case KEYS.LEFT_ARROW_IE: + if (event.ctrlKey) { + event.preventDefault(); + this.setSelectionRange(this.getNewPosition(value)); + } + break; + case KEYS.RIGHT_ARROW: + case KEYS.RIGHT_ARROW_IE: + if (event.ctrlKey) { + event.preventDefault(); + this.setSelectionRange(this.getNewPosition(value, 1)); + } + break; + } + } + + /** + * Move the cursor in a specific direction until it reaches a date/time separator. + * Then return its index. + * + * @param value The string it operates on. + * @param direction 0 is left, 1 is right. Default is 0. + */ + private getNewPosition(value: string, direction = 0): number { + let cursorPos = this.selectionStart; + if (!direction) { + do { + cursorPos = cursorPos > 0 ? --cursorPos : cursorPos; + } while (!this.literals.includes(value[cursorPos - 1]) && cursorPos > 0); + return cursorPos; + } else { + do { + cursorPos++; + } while (!this.literals.includes(value[cursorPos]) && cursorPos < value.length); + return cursorPos; + } + } +} + +@NgModule({ + declarations: [IgxDateTimeEditorDirective], + exports: [IgxDateTimeEditorDirective], + imports: [CommonModule] +}) +export class IgxDateTimeEditorModule { } diff --git a/projects/igniteui-angular/src/lib/directives/date-time-editor/index.ts b/projects/igniteui-angular/src/lib/directives/date-time-editor/index.ts new file mode 100644 index 00000000000..4a06b380806 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/index.ts @@ -0,0 +1,2 @@ +export * from './date-time-editor.common'; +export * from './date-time-editor.directive'; diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts b/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts index f5b347bf6c0..8632fcb12cc 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts @@ -100,7 +100,9 @@ export class MaskParsingService { } continue; } - if (chars[0] && !this.validateCharOnPosition(chars[0], i, maskOptions.format)) { + if (chars[0] + && !this.validateCharOnPosition(chars[0], i, maskOptions.format) + && chars[0] !== maskOptions.promptChar) { break; } @@ -115,6 +117,12 @@ export class MaskParsingService { return { value: maskedValue, end: cursor }; } + public replaceCharAt(strValue: string, index: number, char: string): string { + if (strValue !== undefined) { + return strValue.substring(0, index) + char + strValue.substring(index + 1); + } + } + /** Validates only non literal positions. */ private validateCharOnPosition(inputChar: string, position: number, mask: string): boolean { let regex: RegExp; @@ -170,11 +178,6 @@ export class MaskParsingService { return isValid; } - private replaceCharAt(strValue: string, index: number, char: string): string { - if (strValue !== undefined) { - return strValue.substring(0, index) + char + strValue.substring(index + 1); - } - } private getMaskLiterals(mask: string): Map { const literals = new Map(); diff --git a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts index 43059a67cd1..073c6737fc1 100644 --- a/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/mask/mask.directive.ts @@ -105,19 +105,32 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA return { format, promptChar }; } - private get selectionStart(): number { + /** @hidden */ + protected get nativeElement(): HTMLInputElement { + return this.elementRef.nativeElement; + } + + /** @hidden */ + protected get selectionStart(): number { // Edge(classic) and FF don't select text on drop return this.nativeElement.selectionStart === this.nativeElement.selectionEnd && this._hasDropAction ? this.nativeElement.selectionEnd - this._droppedData.length : this.nativeElement.selectionStart; } - private get selectionEnd(): number { + /** @hidden */ + protected get selectionEnd(): number { return this.nativeElement.selectionEnd; } - private get nativeElement(): HTMLInputElement { - return this.elementRef.nativeElement; + /** @hidden */ + protected get start(): number { + return this._start; + } + + /** @hidden */ + protected get end(): number { + return this._end; } private _end = 0; @@ -179,7 +192,6 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA return; } - let valueToParse = ''; if (this._hasDropAction) { this._start = this.selectionStart; } @@ -188,6 +200,7 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA this._key = KEYCODES.BACKSPACE; } + let valueToParse = ''; switch (this._key) { case KEYCODES.DELETE: this._end = this._start === this._end ? ++this._end : this._end; @@ -210,6 +223,7 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA this._onChangeCallback(this._dataValue); this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.inputValue }); + this.afterInput(); } @@ -273,19 +287,13 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA this._oldText = this.inputValue; } - private showDisplayValue(value: string) { - if (this.displayValuePipe) { - this.inputValue = this.displayValuePipe.transform(value); - } else if (value === this.maskParser.applyMask(null, this.maskOptions)) { - this.inputValue = ''; - } - } - - private setSelectionRange(start: number, end: number = start): void { + /** @hidden */ + protected setSelectionRange(start: number, end: number = start): void { this.nativeElement.setSelectionRange(start, end); } - private afterInput() { + /** @hidden */ + protected afterInput() { this._oldText = this.inputValue; this._hasDropAction = false; this._start = 0; @@ -293,6 +301,14 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA this._key = null; } + private showDisplayValue(value: string) { + if (this.displayValuePipe) { + this.inputValue = this.displayValuePipe.transform(value); + } else if (value === this.maskParser.applyMask(null, this.maskOptions)) { + this.inputValue = ''; + } + } + /** @hidden */ public writeValue(value: string): void { if (this.promptChar && this.promptChar.length > 1) { diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 720ee06e21b..e5195ace57c 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -29,6 +29,7 @@ export * from './lib/directives/text-highlight/text-highlight.directive'; export * from './lib/directives/text-selection/text-selection.directive'; export * from './lib/directives/toggle/toggle.directive'; export * from './lib/directives/tooltip/tooltip.directive'; +export * from './lib/directives/date-time-editor/index'; /** * Data operations diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e76c796b9f3..221f962c367 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -432,6 +432,11 @@ export class AppComponent implements OnInit { icon: 'view_column', name: 'Mask Directive' }, + { + link: '/date-time-editor', + icon: 'view_column', + name: 'DateTime Editor' + }, { link: '/ripple', icon: 'wifi_tethering', diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 436538a5c65..c7d494ff339 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,7 +6,7 @@ import { NgModule } from '@angular/core'; import { IgxIconModule, IgxGridModule, IgxExcelExporterService, IgxCsvExporterService, IgxOverlayService, IgxDragDropModule, IgxDividerModule, IgxTreeGridModule, IgxHierarchicalGridModule, IgxInputGroupModule, - IgxIconService, DisplayDensityToken, DisplayDensity + IgxIconService, DisplayDensityToken, DisplayDensity, IgxDateTimeEditorModule, IgxButtonModule } from 'igniteui-angular'; import { IgxColumnHidingModule } from 'igniteui-angular'; import { SharedModule } from './shared/shared.module'; @@ -115,6 +115,7 @@ import { GridExternalFilteringComponent } from './grid-external-filtering/grid-e import { AboutComponent } from './grid-state/about.component'; import { GridSaveStateComponent } from './grid-state/grid-state.component'; import { GridMasterDetailSampleComponent } from './grid-master-detail/grid-master-detail.sample'; +import { DateTimeEditorSampleComponent } from './date-time-editor/date-time-editor.sample'; import { GridColumnSelectionSampleComponent, GridColumnSelectionFilterPipe } from './grid-column-selection/grid-column-selection.sample'; import { ReactiveFormSampleComponent } from './reactive-from/reactive-form-sample.component'; import { GridRowPinningSampleComponent } from './grid-row-pinning/grid-row-pinning.sample'; @@ -151,6 +152,7 @@ const components = [ ListPanningSampleComponent, ListPerformanceSampleComponent, MaskSampleComponent, + DateTimeEditorSampleComponent, NavbarSampleComponent, NavdrawerSampleComponent, OverlaySampleComponent, @@ -249,7 +251,9 @@ const components = [ IgxDividerModule, SharedModule, routing, - HammerModule + HammerModule, + IgxDateTimeEditorModule, + IgxButtonModule ], providers: [ LocalService, diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index f0079899757..0f13bb8b87a 100644 --- a/src/app/app.routing.ts +++ b/src/app/app.routing.ts @@ -68,6 +68,7 @@ import { GridAutoSizeSampleComponent } from './grid-auto-size/grid-auto-size.sam import { GridSaveStateComponent } from './grid-state/grid-state.component'; import { AboutComponent } from './grid-state/about.component'; import { GridMasterDetailSampleComponent } from './grid-master-detail/grid-master-detail.sample'; +import { DateTimeEditorSampleComponent } from './date-time-editor/date-time-editor.sample'; import { GridRowPinningSampleComponent } from './grid-row-pinning/grid-row-pinning.sample'; const appRoutes = [ @@ -160,6 +161,10 @@ const appRoutes = [ path: 'mask', component: MaskSampleComponent }, + { + path: 'date-time-editor', + component: DateTimeEditorSampleComponent + }, { path: 'navbar', component: NavbarSampleComponent diff --git a/src/app/date-time-editor/date-time-editor.sample.css b/src/app/date-time-editor/date-time-editor.sample.css new file mode 100644 index 00000000000..aaa64aa325f --- /dev/null +++ b/src/app/date-time-editor/date-time-editor.sample.css @@ -0,0 +1,5 @@ +.content { + display: flex; + flex-flow: row; + justify-content: space-evenly; +} diff --git a/src/app/date-time-editor/date-time-editor.sample.html b/src/app/date-time-editor/date-time-editor.sample.html new file mode 100644 index 00000000000..ddf6fd0e5cb --- /dev/null +++ b/src/app/date-time-editor/date-time-editor.sample.html @@ -0,0 +1,34 @@ + + Allows the user to manipulate date and time strings in a specified format. + + +
+
+
Simple date-time editor
+ + + +
+ +
+
DateTime editor in a form
+
+ + + + + + + + + +
+
+
diff --git a/src/app/date-time-editor/date-time-editor.sample.ts b/src/app/date-time-editor/date-time-editor.sample.ts new file mode 100644 index 00000000000..de50c7c4e3e --- /dev/null +++ b/src/app/date-time-editor/date-time-editor.sample.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; +import { IgxDateTimeEditorEventArgs } from 'igniteui-angular'; + +@Component({ + selector: 'app-date-time-editor', + templateUrl: './date-time-editor.sample.html', + styleUrls: ['./date-time-editor.sample.css'] +}) +export class DateTimeEditorSampleComponent { + public date = new Date(2020, 2, 23); + public date1 = new Date(2021, 3, 24); + public format = 'dd/M/yyyy'; + + public minDate = new Date(2020, 2, 20); + public maxDate = new Date(2020, 2, 25); + + public onValueChanged(event: IgxDateTimeEditorEventArgs) { + console.log('value changed', event.oldValue, event.newValue); + } + + public onValidationFailed(event: IgxDateTimeEditorEventArgs) { + console.log('validation failed', event.oldValue, event.newValue); + } +} diff --git a/src/app/routing.ts b/src/app/routing.ts index 2745466630d..8a3af2963ae 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -93,6 +93,7 @@ import { GridExternalFilteringComponent } from './grid-external-filtering/grid-e import { GridSaveStateComponent } from './grid-state/grid-state.component'; import { AboutComponent } from './grid-state/about.component'; import { GridMasterDetailSampleComponent } from './grid-master-detail/grid-master-detail.sample'; +import { DateTimeEditorSampleComponent } from './date-time-editor/date-time-editor.sample'; import { GridRowPinningSampleComponent } from './grid-row-pinning/grid-row-pinning.sample'; import { ReactiveFormSampleComponent } from './reactive-from/reactive-form-sample.component'; @@ -214,6 +215,10 @@ const appRoutes = [ path: 'mask', component: MaskSampleComponent }, + { + path: 'date-time-editor', + component: DateTimeEditorSampleComponent + }, { path: 'navbar', component: NavbarSampleComponent