Skip to content

Commit 0dee1ca

Browse files
feat(date-time-editor): initial implementation #6271
- ngModel binding - value spinning - min/max range - dev demo (initial) - Unit tests for spinning; isSpinLoop
1 parent 8ca9c0e commit 0dee1ca

File tree

14 files changed

+1122
-71
lines changed

14 files changed

+1122
-71
lines changed

projects/igniteui-angular/src/lib/core/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,13 @@ export const enum KEYS {
167167
DOWN_ARROW = 'ArrowDown',
168168
DOWN_ARROW_IE = 'Down',
169169
F2 = 'F2',
170-
TAB = 'Tab'
170+
TAB = 'Tab',
171+
Z = 'z',
172+
Y = 'y',
173+
X = 'x',
174+
BACKSPACE = 'Backspace',
175+
DELETE = 'Delete',
176+
SEMICOLON = ';'
171177
}
172178

173179
/**

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

Lines changed: 281 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,28 @@ const enum FormatDesc {
1818
TwoDigits = '2-digit'
1919
}
2020

21+
export interface DateTimeValue {
22+
state: DateState;
23+
value: Date;
24+
}
25+
26+
export enum DatePart {
27+
Date = 'date',
28+
Month = 'month',
29+
Year = 'year',
30+
Hours = 'hours',
31+
Minutes = 'minutes',
32+
Seconds = 'seconds',
33+
AmPm = 'ampm'
34+
}
35+
36+
export interface DatePartInfo {
37+
type: DatePart;
38+
start: number;
39+
end: number;
40+
format: string;
41+
}
42+
2143
/**
2244
*@hidden
2345
*/
@@ -27,6 +49,9 @@ const enum DateChars {
2749
DayChar = 'd'
2850
}
2951

52+
const TimeCharsArr = ['h', 'H', 'm', 's', 'S', 't', 'T'];
53+
const DateCharsArr = ['d', 'D', 'M', 'y', 'Y'];
54+
3055
/**
3156
*@hidden
3257
*/
@@ -36,8 +61,16 @@ const enum DateParts {
3661
Year = 'year'
3762
}
3863

64+
/** @hidden */
65+
const enum TimeParts {
66+
Hour = 'hour',
67+
Minute = 'minute',
68+
Second = 'second',
69+
AmPm = 'ampm'
70+
}
71+
3972
/**
40-
*@hidden
73+
* @hidden1
4174
*/
4275
export abstract class DatePickerUtil {
4376
private static readonly SHORT_DATE_MASK = 'MM/dd/yy';
@@ -46,6 +79,226 @@ export abstract class DatePickerUtil {
4679
private static readonly PROMPT_CHAR = '_';
4780
private static readonly DEFAULT_LOCALE = 'en';
4881

82+
public static parseDateTimeArray(dateTimeParts: DatePartInfo[], inputData: string): DateTimeValue {
83+
const parts: { [key in DatePart]: number } = {} as any;
84+
dateTimeParts.forEach(dp => {
85+
let value = parseInt(this.getCleanVal(inputData, dp), 10);
86+
if (!value) {
87+
value = dp.type === DatePart.Date || dp.type === DatePart.Month ? 1 : 0;
88+
}
89+
parts[dp.type] = value;
90+
});
91+
92+
if (parts[DatePart.Month] < 1 || 12 < parts[DatePart.Month]) {
93+
return { state: DateState.Invalid, value: null };
94+
}
95+
96+
// TODO: Century threshold
97+
if (parts[DatePart.Year] < 50) {
98+
parts[DatePart.Year] += 2000;
99+
}
100+
101+
if (parts[DatePart.Date] > DatePickerUtil.daysInMonth(parts[DatePart.Year], parts[DatePart.Month])) {
102+
return { state: DateState.Invalid, value: null };
103+
}
104+
105+
if (parts[DatePart.Hours] > 23 || parts[DatePart.Minutes] > 59 || parts[DatePart.Seconds] > 59) {
106+
return { state: DateState.Invalid, value: null };
107+
}
108+
109+
return {
110+
state: DateState.Valid,
111+
value: new Date(
112+
parts[DatePart.Year],
113+
parts[DatePart.Month] - 1,
114+
parts[DatePart.Date],
115+
parts[DatePart.Hours],
116+
parts[DatePart.Minutes],
117+
parts[DatePart.Seconds]
118+
)
119+
};
120+
}
121+
122+
public static parseDateTimeFormat(mask: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): DatePartInfo[] {
123+
let dateTimeData: DatePartInfo[] = [];
124+
if ((mask === undefined || mask === '') && !isIE()) {
125+
dateTimeData = DatePickerUtil.getDefaultLocaleMask(locale);
126+
} else {
127+
const format = (mask) ? mask : DatePickerUtil.SHORT_DATE_MASK;
128+
const formatArray = Array.from(format);
129+
for (let i = 0; i < formatArray.length; i++) {
130+
const datePartRange = this.getDatePartInfoRange(formatArray[i], format, i);
131+
const dateTimeInfo = {
132+
type: DatePickerUtil.determineDatePart(formatArray[i]),
133+
start: datePartRange.start,
134+
end: datePartRange.end,
135+
format: formatArray[i],
136+
};
137+
while (DatePickerUtil.isDateOrTimeChar(formatArray[i])) {
138+
if (dateTimeData.indexOf(dateTimeInfo) === -1) {
139+
dateTimeData.push(dateTimeInfo);
140+
}
141+
i++;
142+
}
143+
}
144+
}
145+
146+
return dateTimeData;
147+
}
148+
149+
public static setInputFormat(format: string) {
150+
let chars = '';
151+
let newFormat = '';
152+
for (let i = 0; ; i++) {
153+
while (DatePickerUtil.isDateOrTimeChar(format[i])) {
154+
chars += format[i];
155+
i++;
156+
}
157+
158+
if (chars.length === 1 || chars.length === 3) {
159+
newFormat += chars[0].repeat(2);
160+
} else {
161+
newFormat += chars;
162+
}
163+
164+
if (i >= format.length) { break; }
165+
166+
if (!DatePickerUtil.isDateOrTimeChar(format[i])) {
167+
newFormat += format[i];
168+
}
169+
chars = '';
170+
}
171+
172+
return newFormat;
173+
}
174+
175+
public static isDateOrTimeChar(char: string): boolean {
176+
return TimeCharsArr.includes(char) || DateCharsArr.includes(char);
177+
}
178+
179+
public static calculateDateOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date {
180+
newDate = new Date(newDate.setDate(newDate.getDate() + delta));
181+
if (isSpinLoop) {
182+
if (currentDate.getMonth() > newDate.getMonth()) {
183+
return new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
184+
} else if (currentDate.getMonth() < newDate.getMonth()) {
185+
return new Date(currentDate.setDate(1));
186+
}
187+
}
188+
if (currentDate.getMonth() === newDate.getMonth()) {
189+
return newDate;
190+
}
191+
192+
return currentDate;
193+
}
194+
195+
public static calculateMonthOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date {
196+
const maxDate = DatePickerUtil.daysInMonth(currentDate.getFullYear(), newDate.getMonth() + 1 + delta);
197+
if (newDate.getDate() > maxDate) {
198+
newDate.setDate(maxDate);
199+
}
200+
newDate = new Date(newDate.setMonth(newDate.getMonth() + delta));
201+
if (isSpinLoop) {
202+
if (currentDate.getFullYear() < newDate.getFullYear()) {
203+
return new Date(currentDate.setMonth(0));
204+
} else if (currentDate.getFullYear() > newDate.getFullYear()) {
205+
return new Date(currentDate.setMonth(11));
206+
}
207+
}
208+
if (currentDate.getFullYear() === newDate.getFullYear()) {
209+
return newDate;
210+
}
211+
212+
return currentDate;
213+
}
214+
215+
public static calculateHoursOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date {
216+
newDate = new Date(newDate.setHours(newDate.getHours() + delta));
217+
if (isSpinLoop) {
218+
if (newDate.getDate() > currentDate.getDate()) {
219+
return new Date(currentDate.setHours(0));
220+
} else if (newDate.getDate() < currentDate.getDate()) {
221+
return new Date(currentDate.setHours(23));
222+
}
223+
}
224+
if (currentDate.getDate() === newDate.getDate()) {
225+
return newDate;
226+
}
227+
228+
return currentDate;
229+
}
230+
231+
public static calculateMinutesOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date {
232+
newDate = new Date(newDate.setMinutes(newDate.getMinutes() + delta));
233+
if (isSpinLoop) {
234+
if (newDate.getHours() > currentDate.getHours()) {
235+
return new Date(currentDate.setMinutes(0));
236+
} else if (newDate.getHours() < currentDate.getHours()) {
237+
return new Date(currentDate.setMinutes(59));
238+
}
239+
}
240+
241+
if (currentDate.getHours() === newDate.getHours()) {
242+
return newDate;
243+
}
244+
245+
return currentDate;
246+
}
247+
248+
public static calculateSecondsOnSpin(delta: number, newDate: Date, currentDate: Date, isSpinLoop: boolean): Date {
249+
newDate = new Date(newDate.setSeconds(newDate.getSeconds() + delta));
250+
if (isSpinLoop) {
251+
if (newDate.getMinutes() > currentDate.getMinutes()) {
252+
return new Date(currentDate.setSeconds(0));
253+
} else if (newDate.getMinutes() < currentDate.getMinutes()) {
254+
return new Date(currentDate.setSeconds(59));
255+
}
256+
}
257+
if (currentDate.getMinutes() === newDate.getMinutes()) {
258+
return newDate;
259+
}
260+
261+
return currentDate;
262+
}
263+
264+
private static getCleanVal(inputData: string, datePart: DatePartInfo): string {
265+
return DatePickerUtil.trimUnderlines(inputData.substring(datePart.start, datePart.end));
266+
}
267+
268+
private static getDatePartInfoRange(datePartChars: string, mask: string, index: number): any {
269+
const start = mask.indexOf(datePartChars, index);
270+
let end = start;
271+
while (this.isDateOrTimeChar(mask[end])) {
272+
end++;
273+
}
274+
275+
return { start, end };
276+
}
277+
278+
private static determineDatePart(char: string): DatePart {
279+
switch (char) {
280+
case 'd':
281+
case 'D':
282+
return DatePart.Date;
283+
case 'M':
284+
return DatePart.Month;
285+
case 'y':
286+
case 'Y':
287+
return DatePart.Year;
288+
case 'h':
289+
case 'H':
290+
return DatePart.Hours;
291+
case 'm':
292+
return DatePart.Minutes;
293+
case 's':
294+
case 'S':
295+
return DatePart.Seconds;
296+
case 't':
297+
case 'T':
298+
return DatePart.AmPm;
299+
}
300+
}
301+
49302
/**
50303
* This method generates date parts structure based on editor mask and locale.
51304
* @param maskValue: string
@@ -54,7 +307,7 @@ export abstract class DatePickerUtil {
54307
*/
55308
public static parseDateFormat(maskValue: string, locale: string = DatePickerUtil.DEFAULT_LOCALE): any[] {
56309
let dateStruct = [];
57-
if (maskValue === undefined && !isIE()) {
310+
if ((maskValue === undefined || maskValue === '') && !isIE()) {
58311
dateStruct = DatePickerUtil.getDefaultLocaleMask(locale);
59312
} else {
60313
const mask = (maskValue) ? maskValue : DatePickerUtil.SHORT_DATE_MASK;
@@ -88,7 +341,7 @@ export abstract class DatePickerUtil {
88341
}
89342

90343
for (let i = 0; i < maskArray.length; i++) {
91-
if (!DatePickerUtil.isDateChar(maskArray[i])) {
344+
if (!DatePickerUtil.isDateTimeChar(maskArray[i])) {
92345
dateStruct.push({
93346
type: DatePickerUtil.SEPARATOR,
94347
initialPosition: i,
@@ -180,8 +433,9 @@ export abstract class DatePickerUtil {
180433
const monthStr = DatePickerUtil.getMonthValueFromInput(dateFormatParts, inputValue);
181434
const yearStr = DatePickerUtil.getYearValueFromInput(dateFormatParts, inputValue);
182435
const yearFormat = DatePickerUtil.getDateFormatPart(dateFormatParts, DateParts.Year).formatType;
183-
const day = (dayStr !== '') ? parseInt(dayStr, 10) : 1;
184-
const month = (monthStr !== '') ? parseInt(monthStr, 10) - 1 : 0;
436+
const today = new Date();
437+
const day = (dayStr !== '') ? parseInt(dayStr, 10) : today.getDate();
438+
const month = (monthStr !== '') ? parseInt(monthStr, 10) - 1 : today.getMonth();
185439

186440
let year;
187441
if (yearStr === '') {
@@ -198,16 +452,21 @@ export abstract class DatePickerUtil {
198452
} else {
199453
yearPrefix = '20';
200454
}
201-
const fullYear = (yearFormat === FormatDesc.TwoDigits) ? yearPrefix.concat(year) : year;
202455

203456
if ((month < 0) || (month > 11) || (month === NaN)) {
204457
return { state: DateState.Invalid, value: inputValue };
205458
}
206459

460+
let fullYear = (yearFormat === FormatDesc.TwoDigits) ? yearPrefix.concat(year) : year;
207461
if ((day < 1) || (day > DatePickerUtil.daysInMonth(fullYear, month + 1)) || (day === NaN)) {
208462
return { state: DateState.Invalid, value: inputValue };
209463
}
210464

465+
if (yearStr !== '') {
466+
fullYear = parseInt(fullYear, 10);
467+
fullYear = fullYear < 50 ? fullYear + 2000 : fullYear + 1900;
468+
}
469+
211470
return { state: DateState.Valid, date: new Date(fullYear, month, day) };
212471
}
213472

@@ -339,6 +598,20 @@ export abstract class DatePickerUtil {
339598
return '';
340599
}
341600

601+
public static daysInMonth(fullYear: number, month: number): number {
602+
return new Date(fullYear, month, 0).getDate();
603+
}
604+
605+
private static getFormatType(format: string, targetChar: string) {
606+
switch (format.match(new RegExp(targetChar, 'g')).length) {
607+
case 1:
608+
case 4:
609+
return FormatDesc.Numeric;
610+
case 2:
611+
return FormatDesc.TwoDigits;
612+
}
613+
}
614+
342615
private static getYearFormatType(format: string): string {
343616
switch (format.match(new RegExp(DateChars.YearChar, 'g')).length) {
344617
case 1: {
@@ -419,8 +692,8 @@ export abstract class DatePickerUtil {
419692
return dateStruct;
420693
}
421694

422-
private static isDateChar(char: string): boolean {
423-
return (char === DateChars.YearChar || char === DateChars.MonthChar || char === DateChars.DayChar);
695+
private static isDateTimeChar(char: string): boolean {
696+
return (char === DateChars.YearChar || char === DateChars.MonthChar || char === DateChars.DayChar || TimeCharsArr.includes(char));
424697
}
425698

426699
private static getNumericFormatPrefix(formatType: string): string {
@@ -464,10 +737,6 @@ export abstract class DatePickerUtil {
464737
return { min: minValue, max: maxValue };
465738
}
466739

467-
private static daysInMonth(fullYear: number, month: number): number {
468-
return new Date(fullYear, month, 0).getDate();
469-
}
470-
471740
private static getDateValueFromInput(dateFormatParts: any[], type: DateParts, inputValue: string, trim: boolean = true): string {
472741
const partPosition = DatePickerUtil.getDateFormatPart(dateFormatParts, type).position;
473742
const result = inputValue.substring(partPosition[0], partPosition[1]);

0 commit comments

Comments
 (0)