Skip to content

Commit 31abc2e

Browse files
authored
Merge pull request #6923 from IgniteUI/bpenkov/date-time-editor
DateTime Editor implementation
2 parents 01e2ceb + b759cd3 commit 31abc2e

20 files changed

+2167
-36
lines changed

Diff for: projects/igniteui-angular/src/lib/core/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const enum KEYS {
168168
DOWN_ARROW_IE = 'Down',
169169
F2 = 'F2',
170170
TAB = 'Tab',
171+
SEMICOLON = ';',
171172
HOME = 'Home',
172173
END = 'End'
173174
}

Diff for: projects/igniteui-angular/src/lib/date-picker/date-picker.pipes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class DatePickerDisplayValuePipe implements PipeTransform {
1616
return '';
1717
}
1818
this._datePicker.rawDateString = value;
19-
return DatePickerUtil.trimUnderlines(value);
19+
return DatePickerUtil.trimEmptyPlaceholders(value);
2020
}
2121
return '';
2222
}

Diff for: projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts

+250-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isIE } from '../core/utils';
2+
import { DatePart, DatePartInfo } from '../directives/date-time-editor/date-time-editor.common';
23

34
/**
45
* This enum is used to keep the date validation result.
@@ -27,6 +28,9 @@ const enum DateChars {
2728
DayChar = 'd'
2829
}
2930

31+
const DATE_CHARS = ['h', 'H', 'm', 's', 'S', 't', 'T'];
32+
const TIME_CHARS = ['d', 'D', 'M', 'y', 'Y'];
33+
3034
/**
3135
* @hidden
3236
*/
@@ -46,6 +50,242 @@ export abstract class DatePickerUtil {
4650
private static readonly PROMPT_CHAR = '_';
4751
private static readonly DEFAULT_LOCALE = 'en';
4852

53+
/**
54+
* Parse a Date value from masked string input based on determined date parts
55+
* @param inputData masked value to parse
56+
* @param dateTimeParts Date parts array for the mask
57+
*/
58+
public static parseValueFromMask(inputData: string, dateTimeParts: DatePartInfo[], promptChar?: string): Date | null {
59+
const parts: { [key in DatePart]: number } = {} as any;
60+
dateTimeParts.forEach(dp => {
61+
let value = parseInt(this.getCleanVal(inputData, dp, promptChar), 10);
62+
if (!value) {
63+
value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0;
64+
}
65+
parts[dp.type] = value;
66+
});
67+
68+
if (parts[DatePart.Month] < 1 || 12 < parts[DatePart.Month]) {
69+
return null;
70+
}
71+
72+
// TODO: Century threshold
73+
if (parts[DatePart.Year] < 50) {
74+
parts[DatePart.Year] += 2000;
75+
}
76+
77+
if (parts[DatePart.Date] > DatePickerUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) {
78+
return null;
79+
}
80+
81+
if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) {
82+
return null;
83+
}
84+
85+
return new Date(
86+
parts[DatePart.Year] || 2000,
87+
parts[DatePart.Month] - 1 || 0,
88+
parts[DatePart.Date] || 1,
89+
parts[DatePart.Hours] || 0,
90+
parts[DatePart.Minutes] || 0,
91+
parts[DatePart.Seconds] || 0
92+
);
93+
}
94+
95+
private static ensureLeadingZero(part: DatePartInfo) {
96+
switch (part.type) {
97+
case DatePart.Date:
98+
case DatePart.Month:
99+
case DatePart.Hours:
100+
case DatePart.Minutes:
101+
case DatePart.Seconds:
102+
if (part.format.length === 1) {
103+
part.format = part.format.repeat(2);
104+
}
105+
break;
106+
}
107+
}
108+
109+
/**
110+
* Parse the mask into date/time and literal parts
111+
*/
112+
public static parseDateTimeFormat(mask: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): DatePartInfo[] {
113+
const format = mask || DatePickerUtil.getDefaultInputFormat(locale);
114+
const dateTimeParts: DatePartInfo[] = [];
115+
const formatArray = Array.from(format);
116+
let currentPart: DatePartInfo = null;
117+
let position = 0;
118+
119+
for (let i = 0; i < formatArray.length; i++, position++) {
120+
const type = DatePickerUtil.determineDatePart(formatArray[i]);
121+
if (currentPart) {
122+
if (currentPart.type === type) {
123+
currentPart.format += formatArray[i];
124+
if (i < formatArray.length - 1) {
125+
continue;
126+
}
127+
}
128+
129+
DatePickerUtil.ensureLeadingZero(currentPart);
130+
currentPart.end = currentPart.start + currentPart.format.length;
131+
position = currentPart.end;
132+
dateTimeParts.push(currentPart);
133+
}
134+
135+
currentPart = {
136+
start: position,
137+
end: position + formatArray[i].length,
138+
type: type,
139+
format: formatArray[i]
140+
};
141+
}
142+
143+
return dateTimeParts;
144+
}
145+
146+
public static getDefaultInputFormat(locale: string): string {
147+
if (!Intl || !Intl.DateTimeFormat || !Intl.DateTimeFormat.prototype.formatToParts) {
148+
// TODO: fallback with Intl.format for IE?
149+
return DatePickerUtil.SHORT_DATE_MASK;
150+
}
151+
const parts = DatePickerUtil.getDefaultLocaleMask(locale);
152+
parts.forEach(p => {
153+
if (p.type !== DatePart.Year && p.type !== DatePickerUtil.SEPARATOR) {
154+
p.formatType = FormatDesc.TwoDigits;
155+
}
156+
});
157+
158+
return DatePickerUtil.getMask(parts);
159+
}
160+
161+
public static isDateOrTimeChar(char: string): boolean {
162+
return DATE_CHARS.indexOf(char) !== -1 || TIME_CHARS.indexOf(char) !== -1;
163+
}
164+
165+
public static spinDate(delta: number, newDate: Date, isSpinLoop: boolean): void {
166+
const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth());
167+
let date = newDate.getDate() + delta;
168+
if (date > maxDate) {
169+
date = isSpinLoop ? date % maxDate : maxDate;
170+
} else if (date < 1) {
171+
date = isSpinLoop ? maxDate + (date % maxDate) : 1;
172+
}
173+
174+
newDate.setDate(date);
175+
}
176+
177+
public static spinMonth(delta: number, newDate: Date, isSpinLoop: boolean): void {
178+
const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear(), newDate.getMonth() + delta);
179+
if (newDate.getDate() > maxDate) {
180+
newDate.setDate(maxDate);
181+
}
182+
183+
const maxMonth = 11;
184+
const minMonth = 0;
185+
let month = newDate.getMonth() + delta;
186+
if (month > maxMonth) {
187+
month = isSpinLoop ? (month % maxMonth) - 1 : maxMonth;
188+
} else if (month < minMonth) {
189+
month = isSpinLoop ? maxMonth + (month % maxMonth) + 1 : minMonth;
190+
}
191+
192+
newDate.setMonth(month);
193+
}
194+
195+
public static spinYear(delta: number, newDate: Date): void {
196+
const maxDate = DatePickerUtil.daysInMonth(newDate.getFullYear() + delta, newDate.getMonth());
197+
if (newDate.getDate() > maxDate) {
198+
// clip to max to avoid leap year change shifting the entire value
199+
newDate.setDate(maxDate);
200+
}
201+
newDate.setFullYear(newDate.getFullYear() + delta);
202+
}
203+
204+
public static spinHours(delta: number, newDate: Date, isSpinLoop: boolean): void {
205+
const maxHour = 23;
206+
const minHour = 0;
207+
let hours = newDate.getHours() + delta;
208+
if (hours > maxHour) {
209+
hours = isSpinLoop ? hours % maxHour - 1 : maxHour;
210+
} else if (hours < minHour) {
211+
hours = isSpinLoop ? maxHour + (hours % maxHour) + 1 : minHour;
212+
}
213+
214+
newDate.setHours(hours);
215+
}
216+
217+
public static spinMinutes(delta: number, newDate: Date, isSpinLoop: boolean): void {
218+
const maxMinutes = 59;
219+
const minMinutes = 0;
220+
let minutes = newDate.getMinutes() + delta;
221+
if (minutes > maxMinutes) {
222+
minutes = isSpinLoop ? minutes % maxMinutes - 1 : maxMinutes;
223+
} else if (minutes < minMinutes) {
224+
minutes = isSpinLoop ? maxMinutes + (minutes % maxMinutes) + 1 : minMinutes;
225+
}
226+
227+
newDate.setMinutes(minutes);
228+
}
229+
230+
public static spinSeconds(delta: number, newDate: Date, isSpinLoop: boolean): void {
231+
const maxSeconds = 59;
232+
const minSeconds = 0;
233+
let seconds = newDate.getSeconds() + delta;
234+
if (seconds > maxSeconds) {
235+
seconds = isSpinLoop ? seconds % maxSeconds - 1 : maxSeconds;
236+
} else if (seconds < minSeconds) {
237+
seconds = isSpinLoop ? maxSeconds + (seconds % maxSeconds) + 1 : minSeconds;
238+
}
239+
240+
newDate.setSeconds(seconds);
241+
}
242+
243+
public static spinAmPm(newDate: Date, currentDate: Date, amPmFromMask: string): Date {
244+
switch (amPmFromMask) {
245+
case 'AM':
246+
newDate = new Date(newDate.setHours(newDate.getHours() + 12));
247+
break;
248+
case 'PM':
249+
newDate = new Date(newDate.setHours(newDate.getHours() - 12));
250+
break;
251+
}
252+
if (newDate.getDate() !== currentDate.getDate()) {
253+
return currentDate;
254+
}
255+
256+
return newDate;
257+
}
258+
259+
private static getCleanVal(inputData: string, datePart: DatePartInfo, promptChar?: string): string {
260+
return DatePickerUtil.trimEmptyPlaceholders(inputData.substring(datePart.start, datePart.end), promptChar);
261+
}
262+
263+
private static determineDatePart(char: string): DatePart {
264+
switch (char) {
265+
case 'd':
266+
case 'D':
267+
return DatePart.Date;
268+
case 'M':
269+
return DatePart.Month;
270+
case 'y':
271+
case 'Y':
272+
return DatePart.Year;
273+
case 'h':
274+
case 'H':
275+
return DatePart.Hours;
276+
case 'm':
277+
return DatePart.Minutes;
278+
case 's':
279+
case 'S':
280+
return DatePart.Seconds;
281+
case 't':
282+
case 'T':
283+
return DatePart.AmPm;
284+
default:
285+
return DatePart.Literal;
286+
}
287+
}
288+
49289
/**
50290
* This method generates date parts structure based on editor mask and locale.
51291
* @param maskValue: string
@@ -204,7 +444,7 @@ export abstract class DatePickerUtil {
204444
return { state: DateState.Invalid, value: inputValue };
205445
}
206446

207-
if ((day < 1) || (day > DatePickerUtil.daysInMonth(fullYear, month + 1)) || (day === NaN)) {
447+
if ((day < 1) || (day > DatePickerUtil.daysInMonth(fullYear, month)) || (day === NaN)) {
208448
return { state: DateState.Invalid, value: inputValue };
209449
}
210450

@@ -220,8 +460,8 @@ export abstract class DatePickerUtil {
220460
* This method replaces prompt chars with empty string.
221461
* @param value
222462
*/
223-
public static trimUnderlines(value: string): string {
224-
const result = value.replace(/_/g, '');
463+
public static trimEmptyPlaceholders(value: string, promptChar?: string): string {
464+
const result = value.replace(new RegExp(promptChar || '_', 'g'), '');
225465
return result;
226466
}
227467

@@ -339,6 +579,10 @@ export abstract class DatePickerUtil {
339579
return '';
340580
}
341581

582+
public static daysInMonth(fullYear: number, month: number): number {
583+
return new Date(fullYear, month + 1, 0).getDate();
584+
}
585+
342586
private static getYearFormatType(format: string): string {
343587
switch (format.match(new RegExp(DateChars.YearChar, 'g')).length) {
344588
case 1: {
@@ -394,7 +638,7 @@ export abstract class DatePickerUtil {
394638
});
395639
} else {
396640
dateStruct.push({
397-
type: formatToParts[i].type,
641+
type: formatToParts[i].type
398642
});
399643
}
400644
}
@@ -410,7 +654,7 @@ export abstract class DatePickerUtil {
410654
break;
411655
}
412656
case DateParts.Year: {
413-
dateStruct[i].formatType = formatterOptions.month;
657+
dateStruct[i].formatType = formatterOptions.year;
414658
break;
415659
}
416660
}
@@ -464,14 +708,10 @@ export abstract class DatePickerUtil {
464708
return { min: minValue, max: maxValue };
465709
}
466710

467-
private static daysInMonth(fullYear: number, month: number): number {
468-
return new Date(fullYear, month, 0).getDate();
469-
}
470-
471711
private static getDateValueFromInput(dateFormatParts: any[], type: DateParts, inputValue: string, trim: boolean = true): string {
472712
const partPosition = DatePickerUtil.getDateFormatPart(dateFormatParts, type).position;
473713
const result = inputValue.substring(partPosition[0], partPosition[1]);
474-
return (trim) ? DatePickerUtil.trimUnderlines(result) : result;
714+
return (trim) ? DatePickerUtil.trimEmptyPlaceholders(result) : result;
475715
}
476716

477717
private static getDayValueFromInput(dateFormatParts: any[], inputValue: string, trim: boolean = true): string {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export interface IgxDateTimeEditorEventArgs {
2+
oldValue: Date;
3+
newValue: Date;
4+
userInput: string;
5+
}
6+
7+
/**
8+
* An @Enum that allows you to specify a particular date, time or AmPm part.
9+
*/
10+
export enum DatePart {
11+
Date = 'date',
12+
Month = 'month',
13+
Year = 'year',
14+
Hours = 'hour',
15+
Minutes = 'minute',
16+
Seconds = 'second',
17+
AmPm = 'ampm',
18+
Literal = 'literal'
19+
}
20+
21+
export interface DatePartInfo {
22+
type: DatePart;
23+
start: number;
24+
end: number;
25+
format: string;
26+
}

0 commit comments

Comments
 (0)