Skip to content

Commit

Permalink
feat(date-time-editor): initial implementation #6271
Browse files Browse the repository at this point in the history
- ngModel binding
- value spinning
- min/max range
- dev demo (initial)
- Unit tests for spinning; isSpinLoop
  • Loading branch information
jackofdiamond5 committed Mar 16, 2020
1 parent 8ca9c0e commit 0dee1ca
Show file tree
Hide file tree
Showing 14 changed files with 1,122 additions and 71 deletions.
8 changes: 7 additions & 1 deletion projects/igniteui-angular/src/lib/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ';'
}

/**
Expand Down
293 changes: 281 additions & 12 deletions projects/igniteui-angular/src/lib/date-picker/date-picker.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand All @@ -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';
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 === '') {
Expand All @@ -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) };
}

Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]);
Expand Down
Loading

0 comments on commit 0dee1ca

Please sign in to comment.