diff --git a/projects/igniteui-angular/src/lib/core/utils.ts b/projects/igniteui-angular/src/lib/core/utils.ts index f73b0b3363f..e27bb334161 100644 --- a/projects/igniteui-angular/src/lib/core/utils.ts +++ b/projects/igniteui-angular/src/lib/core/utils.ts @@ -167,7 +167,13 @@ export const enum KEYS { DOWN_ARROW = 'ArrowDown', DOWN_ARROW_IE = 'Down', F2 = 'F2', - TAB = 'Tab' + TAB = 'Tab', + Z = 'z', + Y = 'y', + X = 'x', + BACKSPACE = 'Backspace', + DELETE = 'Delete', + 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..7539c76b77d 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 @@ -18,6 +18,28 @@ const enum FormatDesc { TwoDigits = '2-digit' } +export interface DateTimeValue { + state: DateState; + value: Date; +} + +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; +} + /** *@hidden */ @@ -27,6 +49,9 @@ const enum DateChars { DayChar = 'd' } +const TimeCharsArr = ['h', 'H', 'm', 's', 'S', 't', 'T']; +const DateCharsArr = ['d', 'D', 'M', 'y', 'Y']; + /** *@hidden */ @@ -36,8 +61,16 @@ const enum DateParts { Year = 'year' } +/** @hidden */ +const enum TimeParts { + Hour = 'hour', + Minute = 'minute', + Second = 'second', + AmPm = 'ampm' +} + /** - *@hidden + * @hidden1 */ export abstract class DatePickerUtil { private static readonly SHORT_DATE_MASK = 'MM/dd/yy'; @@ -46,6 +79,226 @@ 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: null }; + } + + // 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: null }; + } + + if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) { + return { state: DateState.Invalid, value: null }; + } + + return { + state: DateState.Valid, + value: new Date( + parts[DatePart.Year], + parts[DatePart.Month] - 1, + parts[DatePart.Date], + parts[DatePart.Hours], + parts[DatePart.Minutes], + parts[DatePart.Seconds] + ) + }; + } + + public static parseDateTimeFormat(mask: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): DatePartInfo[] { + let dateTimeData: DatePartInfo[] = []; + if ((mask === undefined || mask === '') && !isIE()) { + dateTimeData = DatePickerUtil.getDefaultLocaleMask(locale); + } else { + const format = (mask) ? mask : 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: formatArray[i], + }; + while (DatePickerUtil.isDateOrTimeChar(formatArray[i])) { + if (dateTimeData.indexOf(dateTimeInfo) === -1) { + dateTimeData.push(dateTimeInfo); + } + i++; + } + } + } + + return dateTimeData; + } + + public static setInputFormat(format: string) { + let chars = ''; + let newFormat = ''; + for (let i = 0; ; i++) { + while (DatePickerUtil.isDateOrTimeChar(format[i])) { + chars += format[i]; + i++; + } + + if (chars.length === 1 || chars.length === 3) { + 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.includes(char) || DateCharsArr.includes(char); + } + + public static calculateDateOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + newDate = new Date(newDate.setDate(newDate.getDate() + delta)); + if (isSpinLoop) { + if (currentDate.getMonth() > newDate.getMonth()) { + return new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); + } else if (currentDate.getMonth() < newDate.getMonth()) { + return new Date(currentDate.setDate(1)); + } + } + 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(), newDate.getMonth() + 1 + delta); + if (newDate.getDate() > maxDate) { + newDate.setDate(maxDate); + } + newDate = new Date(newDate.setMonth(newDate.getMonth() + delta)); + if (isSpinLoop) { + if (currentDate.getFullYear() < newDate.getFullYear()) { + return new Date(currentDate.setMonth(0)); + } else if (currentDate.getFullYear() > newDate.getFullYear()) { + return new Date(currentDate.setMonth(11)); + } + } + if (currentDate.getFullYear() === newDate.getFullYear()) { + return newDate; + } + + return currentDate; + } + + public static calculateHoursOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + newDate = new Date(newDate.setHours(newDate.getHours() + delta)); + if (isSpinLoop) { + if (newDate.getDate() > currentDate.getDate()) { + return new Date(currentDate.setHours(0)); + } else if (newDate.getDate() < currentDate.getDate()) { + return new Date(currentDate.setHours(23)); + } + } + if (currentDate.getDate() === newDate.getDate()) { + return newDate; + } + + return currentDate; + } + + public static calculateMinutesOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + newDate = new Date(newDate.setMinutes(newDate.getMinutes() + delta)); + if (isSpinLoop) { + if (newDate.getHours() > currentDate.getHours()) { + return new Date(currentDate.setMinutes(0)); + } else if (newDate.getHours() < currentDate.getHours()) { + return new Date(currentDate.setMinutes(59)); + } + } + + if (currentDate.getHours() === newDate.getHours()) { + return newDate; + } + + return currentDate; + } + + public static calculateSecondsOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date { + newDate = new Date(newDate.setSeconds(newDate.getSeconds() + delta)); + if (isSpinLoop) { + if (newDate.getMinutes() > currentDate.getMinutes()) { + return new Date(currentDate.setSeconds(0)); + } else if (newDate.getMinutes() < currentDate.getMinutes()) { + return new Date(currentDate.setSeconds(59)); + } + } + if (currentDate.getMinutes() === newDate.getMinutes()) { + return newDate; + } + + return currentDate; + } + + 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 @@ -54,7 +307,7 @@ export abstract class DatePickerUtil { */ public static parseDateFormat(maskValue: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): any[] { let dateStruct = []; - if (maskValue === undefined && !isIE()) { + if ((maskValue === undefined || maskValue === '') && !isIE()) { dateStruct = DatePickerUtil.getDefaultLocaleMask(locale); } else { const mask = (maskValue) ? maskValue : DatePickerUtil.SHORT_DATE_MASK; @@ -88,7 +341,7 @@ export abstract class DatePickerUtil { } for (let i = 0; i < maskArray.length; i++) { - if (!DatePickerUtil.isDateChar(maskArray[i])) { + if (!DatePickerUtil.isDateTimeChar(maskArray[i])) { dateStruct.push({ type: DatePickerUtil.SEPARATOR, initialPosition: i, @@ -180,8 +433,9 @@ export abstract class DatePickerUtil { const monthStr = DatePickerUtil.getMonthValueFromInput(dateFormatParts, inputValue); const yearStr = DatePickerUtil.getYearValueFromInput(dateFormatParts, inputValue); const yearFormat = DatePickerUtil.getDateFormatPart(dateFormatParts, DateParts.Year).formatType; - const day = (dayStr !== '') ? parseInt(dayStr, 10) : 1; - const month = (monthStr !== '') ? parseInt(monthStr, 10) - 1 : 0; + const today = new Date(); + const day = (dayStr !== '') ? parseInt(dayStr, 10) : today.getDate(); + const month = (monthStr !== '') ? parseInt(monthStr, 10) - 1 : today.getMonth(); let year; if (yearStr === '') { @@ -198,16 +452,21 @@ export abstract class DatePickerUtil { } else { yearPrefix = '20'; } - const fullYear = (yearFormat === FormatDesc.TwoDigits) ? yearPrefix.concat(year) : year; if ((month < 0) || (month > 11) || (month === NaN)) { return { state: DateState.Invalid, value: inputValue }; } + let fullYear = (yearFormat === FormatDesc.TwoDigits) ? yearPrefix.concat(year) : year; if ((day < 1) || (day > DatePickerUtil.daysInMonth(fullYear, month + 1)) || (day === NaN)) { return { state: DateState.Invalid, value: inputValue }; } + if (yearStr !== '') { + fullYear = parseInt(fullYear, 10); + fullYear = fullYear < 50 ? fullYear + 2000 : fullYear + 1900; + } + return { state: DateState.Valid, date: new Date(fullYear, month, day) }; } @@ -339,6 +598,20 @@ export abstract class DatePickerUtil { return ''; } + public static daysInMonth(fullYear: number, month: number): number { + return new Date(fullYear, month, 0).getDate(); + } + + private static getFormatType(format: string, targetChar: string) { + switch (format.match(new RegExp(targetChar, 'g')).length) { + case 1: + case 4: + return FormatDesc.Numeric; + case 2: + return FormatDesc.TwoDigits; + } + } + private static getYearFormatType(format: string): string { switch (format.match(new RegExp(DateChars.YearChar, 'g')).length) { case 1: { @@ -419,8 +692,8 @@ export abstract class DatePickerUtil { return dateStruct; } - private static isDateChar(char: string): boolean { - return (char === DateChars.YearChar || char === DateChars.MonthChar || char === DateChars.DayChar); + private static isDateTimeChar(char: string): boolean { + return (char === DateChars.YearChar || char === DateChars.MonthChar || char === DateChars.DayChar || TimeCharsArr.includes(char)); } private static getNumericFormatPrefix(formatType: string): string { @@ -464,10 +737,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.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..ecdc002ca7c --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts @@ -0,0 +1,321 @@ +import { IgxDateTimeEditorDirective } from './date-time-editor.directive'; +import { DOCUMENT } from '@angular/common'; +import { DatePart } from '../../date-picker/date-picker.utils'; + +let fixture, + dateTimeEditor: IgxDateTimeEditorDirective; + +fdescribe('IgxDateTimeEditor', () => { + describe('Unit tests', () => { + const maskParsingService = jasmine.createSpyObj('MaskParsingService', ['parseMask', 'restoreValueFromMask', 'parseMaskValue']); + const renderer2 = jasmine.createSpyObj('Renderer2', ['setAttribute']); + const 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 + }); + + fdescribe('Should be able to spin the date portions.', () => { + it('Should correctly increment / decrement date portions with passed in DatePart', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.format = 'dd/M/yy'; + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(); + 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', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(); + 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', () => { + 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', () => { + 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', () => { + 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', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.format = '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', () => { + 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)', () => { + 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); + }); + }); + + fdescribe('Should be able to spin the time portions.', () => { + it('Should correctly increment / decrement time portions with passed in DatePart', () => { + dateTimeEditor = new IgxDateTimeEditorDirective(elementRef, maskParsingService, renderer2, DOCUMENT); + dateTimeEditor.ngOnInit(); + dateTimeEditor.value = new Date(); + 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.format = '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', () => { + 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)', () => { + 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..76e2670c0a0 --- /dev/null +++ b/projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts @@ -0,0 +1,360 @@ +import { Directive, Input, ElementRef, OnInit, AfterViewInit, Renderer2, NgModule, Output, EventEmitter, Inject } from '@angular/core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor, } from '@angular/forms'; +import { IgxMaskDirective } from '../mask/mask.directive'; +import { MaskParsingService } from '../mask/mask-parsing.service'; +import { CommonModule, formatDate, DOCUMENT } from '@angular/common'; +import { KEYS, isEdge } from '../../core/utils'; +import { DatePickerUtil, DateState, DateTimeValue as InputDateTime, DatePartInfo, DatePart } from '../../date-picker/date-picker.utils'; + +export interface IgxDateTimeEditorEventArgs { + oldValue: Date | string; + newValue: Date | string; +} + +@Directive({ + selector: '[igxDateTimeEditor]', + providers: [ + { provide: NG_VALUE_ACCESSOR, useExisting: IgxDateTimeEditorDirective, multi: true } + ] +}) +export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnInit, ControlValueAccessor { + @Input() + public value: Date; + + // TODO + // locale date formats should be the same sa igxdatepicker's - shortDate, longDate, etc + @Input() + public locale: string; + + @Input() + public minValue: string | Date; + + @Input() + public maxValue: string | Date; + + @Input() + public promptChar: string; + + @Input() + public isSpinLoop = true; + + @Output() + public valueChanged = new EventEmitter(); + + @Output() + public validationFailed = new EventEmitter(); + + public get format(): string { + return this._format; + } + + @Input(`igxDateTimeEditor`) + public set format(value: string) { + this._format = value; + const mask = this.buildMask(this.format); + this.mask = value.indexOf('tt') !== -1 ? mask.substring(0, mask.length - 2) + 'LL' : mask; + } + + private _document: Document; + 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 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(DatePickerUtil.setInputFormat(this.format)); + this.renderer.setAttribute(this.nativeElement, 'placeholder', this.format); + } + + public clear(): void { + this.showMask(''); + this.value = null; + this.valueChanged.emit({ oldValue: this._oldValue, newValue: this.value }); + } + + public increment(datePart?: DatePart): void { + const newValue = datePart ? this.calculateValueOnSpin(datePart, 1) : this.calculateValueOnSpin(this.targetDatePart, 1); + if (newValue && this.value && newValue !== this.value) { + this.updateValue(newValue); + } + + // TODO: update mask + // this.updateMask(); + } + + public decrement(datePart?: DatePart): void { + const newValue = datePart ? this.calculateValueOnSpin(datePart, -1) : this.calculateValueOnSpin(this.targetDatePart, -1); + if (newValue && this.value && newValue !== this.value) { + this.updateValue(newValue); + + // TODO: update mask + // this.updateMask(true); + } + } + + /** @hidden */ + public writeValue(value: any): void { + this.value = value; + } + + /** @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) { + // TODO: emit success & update mask? + this.value = new Date(); + this.valueChanged.emit({ oldValue: this._oldValue, newValue: this.value }); + this.updateMask(); + } + + this.moveCursor(event); + } + + /** @hidden */ + public onFocus(): void { + this.onTouchCallback(); + super.onFocus(); + } + + /** @hidden */ + public onBlur(event): void { + // if inputted string does not fit in the editor, show as many chars as possible followed by "..." ? + if (!this.valueInRange(this.value)) { + this.validationFailed.emit({ oldValue: this._oldValue, newValue: this.value }); + // this.updateMask(); TODO: set empty mask + } + + // TODO: display value pipe + // this.updateMask(); TODO: fill in any empty date parts + this.onTouchCallback(); + super.onBlur(event); + } + + /** @hidden */ + protected handleInputChanged(): void { + // the mask must be updated before any date operations + super.handleInputChanged(); + const parsedDate = this.parseDate(this.inputValue); + if (parsedDate.state === DateState.Valid && this.inputValue.indexOf(this.promptChar) === -1) { + this.updateValue(parsedDate.value); + } + + super.afterInput(); + } + + 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 { + 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) { return; } + const currentDate = this.value as Date; + const newDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate(), + currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds()); + if (this.value && this.isValidDate(currentDate)) { + switch (datePart) { + case DatePart.Date: + return DatePickerUtil.calculateDateOnSpin(delta, newDate, currentDate, this.isSpinLoop); + case DatePart.Month: + return DatePickerUtil.calculateMonthOnSpin(delta, newDate, currentDate, this.isSpinLoop); + case DatePart.Year: + return new Date(newDate.setFullYear(newDate.getFullYear() + delta)); + case DatePart.Hours: + return DatePickerUtil.calculateHoursOnSpin(delta, newDate, currentDate, this.isSpinLoop); + case DatePart.Minutes: + return DatePickerUtil.calculateMinutesOnSpin(delta, newDate, currentDate, this.isSpinLoop); + case DatePart.Seconds: + return DatePickerUtil.calculateSecondsOnSpin(delta, newDate, currentDate, this.isSpinLoop); + } + } + + return currentDate; + } + + private updateValue(newDate: Date) { + this._oldValue = this.value; + this.value = newDate; + this.onChangeCallback(this.value); + this.valueChanged.emit({ oldValue: this._oldValue, newValue: this.value }); + } + + private updateMask() { + const cursor = this.selectionEnd; + this._dateTimeFormatParts.forEach(p => { + // TODO: cycle through the parts and update the mask based on their indices + let value: number = this.breakUpDate(p.type); + // TODO: append all date parts from the date object to one another + // if a part of that date object's length is less than the expected length (taken from the format) -> prepend prompt chars + value = p.type === DatePart.Month ? value + 1 : value; + this.inputValue = this.maskParser.replaceInMask(this.inputValue, `${value}`, this.maskOptions, p.start, p.end).value; + }); + this.setSelectionRange(cursor); + } + + private breakUpDate(datePart: DatePart): number { + const valueAsDate = this.value as Date; + switch (datePart) { + case DatePart.Date: + return valueAsDate.getDate(); + case DatePart.Month: + return valueAsDate.getMonth(); + case DatePart.Year: + return valueAsDate.getFullYear(); + case DatePart.Hours: + return valueAsDate.getHours(); + case DatePart.Minutes: + return valueAsDate.getMinutes(); + case DatePart.Seconds: + return valueAsDate.getSeconds(); + } + } + + private updateMaskOnSpin(parsedValue: number, editedParts: DatePartInfo[], addPromptChar?: boolean) { + let start = editedParts[0].start; + const end = editedParts[editedParts.length - 1].start; + if (parsedValue.toString().length < editedParts.length) { + start += editedParts.length - parsedValue.toString().length; + if (addPromptChar) { + this.inputValue = this.maskParser.replaceCharAt(this.inputValue, start - 1, this.promptChar); + } + } + + this.inputValue = this.maskParser.replaceInMask( + this.inputValue, `${parsedValue} `, this.maskOptions, start, end).value; + this.setSelectionRange(this.end); + } + + 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 isValidDate(date: Date): boolean { + return date && date.getTime && !isNaN(date.getTime()); + } + + private parseDate(val: string): InputDateTime { + 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, 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/mask/mask-parsing.service.ts b/projects/igniteui-angular/src/lib/directives/mask/mask-parsing.service.ts index f5b347bf6c0..22fb4d56b3e 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 @@ -115,6 +115,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 +176,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..100f216c45b 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; @@ -174,43 +187,7 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA /** @hidden */ @HostListener('input') public onInputChanged(): void { - if (isIE() && this._stopPropagation) { - this._stopPropagation = false; - return; - } - - let valueToParse = ''; - if (this._hasDropAction) { - this._start = this.selectionStart; - } - if (this.inputValue.length < this._oldText.length && this._key === KEYCODES.INPUT_METHOD) { - // software keyboard input delete - this._key = KEYCODES.BACKSPACE; - } - - switch (this._key) { - case KEYCODES.DELETE: - this._end = this._start === this._end ? ++this._end : this._end; - break; - case KEYCODES.BACKSPACE: - this._start = this.selectionStart; - break; - default: - valueToParse = this.inputValue.substring(this._start, this.selectionEnd); - break; - } - - const replacedData = this.maskParser.replaceInMask(this._oldText, valueToParse, this.maskOptions, this._start, this._end); - this.inputValue = replacedData.value; - if (this._key === KEYCODES.BACKSPACE) { replacedData.end = this._start; } - this.setSelectionRange(replacedData.end); - - const rawVal = this.maskParser.parseValueFromMask(this.inputValue, this.maskOptions); - this._dataValue = this.includeLiterals ? this.inputValue : rawVal; - this._onChangeCallback(this._dataValue); - - this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.inputValue }); - this.afterInput(); + this.handleInputChanged(true); } /** @hidden */ @@ -258,6 +235,50 @@ export class IgxMaskDirective implements OnInit, AfterViewChecked, ControlValueA this._droppedData = event.dataTransfer.getData('text'); } + /** @hidden */ + protected handleInputChanged(reset?: boolean) { + if (isIE() && this._stopPropagation) { + this._stopPropagation = false; + return; + } + + if (this._hasDropAction) { + this._start = this.selectionStart; + } + if (this.inputValue.length < this._oldText.length && this._key === KEYCODES.INPUT_METHOD) { + // software keyboard input delete + this._key = KEYCODES.BACKSPACE; + } + + let valueToParse = ''; + switch (this._key) { + case KEYCODES.DELETE: + this._end = this._start === this._end ? ++this._end : this._end; + break; + case KEYCODES.BACKSPACE: + this._start = this.selectionStart; + break; + default: + valueToParse = this.inputValue.substring(this._start, this.selectionEnd); + break; + } + + const replacedData = this.maskParser.replaceInMask(this._oldText, valueToParse, this.maskOptions, this._start, this._end); + this.inputValue = replacedData.value; + if (this._key === KEYCODES.BACKSPACE) { replacedData.end = this._start; } + this.setSelectionRange(replacedData.end); + + const rawVal = this.maskParser.parseValueFromMask(this.inputValue, this.maskOptions); + this._dataValue = this.includeLiterals ? this.inputValue : rawVal; + this._onChangeCallback(this._dataValue); + + this.onValueChange.emit({ rawValue: rawVal, formattedValue: this.inputValue }); + + if (reset) { + this.afterInput(); + } + } + /** @hidden */ protected showMask(value: string) { if (this.focusedValuePipe) { @@ -273,19 +294,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 +308,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..748a6f4bde9 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/date-time-editor.directive'; /** * Data operations diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 67f4576df73..157170900d4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -417,6 +417,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 48a06552208..fce2a4a4c57 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 } 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'; const components = [ AppComponent, @@ -148,6 +149,7 @@ const components = [ ListPanningSampleComponent, ListPerformanceSampleComponent, MaskSampleComponent, + DateTimeEditorSampleComponent, NavbarSampleComponent, NavdrawerSampleComponent, OverlaySampleComponent, @@ -242,7 +244,8 @@ const components = [ IgxDividerModule, SharedModule, routing, - HammerModule + HammerModule, + IgxDateTimeEditorModule ], providers: [ LocalService, diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts index 0874c9b458f..356d279b46a 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'; const appRoutes = [ { @@ -159,6 +160,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..e69de29bb2d 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..657153b6db4 --- /dev/null +++ b/src/app/date-time-editor/date-time-editor.sample.html @@ -0,0 +1,30 @@ +
+

+ DateTime editor with date and time +

+ + + +
+ +
+

+ DateTime Editor with date only +

+ + + +
+ +
+

+ DateTime Editor with time only +

+ + + +
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..c14f419f1f4 --- /dev/null +++ b/src/app/date-time-editor/date-time-editor.sample.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } 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 implements OnInit { + public date = new Date(); + + ngOnInit(): void { + } + + 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 d238554ca90..d67c7b07993 100644 --- a/src/app/routing.ts +++ b/src/app/routing.ts @@ -92,6 +92,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'; const appRoutes = [ { @@ -211,6 +212,10 @@ const appRoutes = [ path: 'mask', component: MaskSampleComponent }, + { + path: 'date-time-editor', + component: DateTimeEditorSampleComponent + }, { path: 'navbar', component: NavbarSampleComponent