Skip to content

Commit

Permalink
#7997 Add time support into datetime mask type
Browse files Browse the repository at this point in the history
  • Loading branch information
OlgaLarina committed Apr 8, 2024
1 parent 261fa3f commit d785443
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 28 deletions.
102 changes: 75 additions & 27 deletions src/mask/mask_datetime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,17 @@ export function getDateTimeLexems(pattern: string): Array<IDateTimeMaskLexem> {
* [View Demo](https://surveyjs.io/form-library/examples/masked-input-fields/ (linkStyle))
*/
export class InputMaskDateTime extends InputMaskPattern {
private defaultDate = "1970-01-01T";
private turnOfTheCentury = 68;
private lexems: Array<IDateTimeMaskLexem> = [];
private inputDateTimeData: Array<IInputDateTimeData> = [];
private validBeginningOfNumbers: { [key: string]: any } = {
hour: 2,
minute: 6,
second: 6,
day: 3,
month: 1,
};
/**
* A minimum date and time value that respondents can enter.
* @see max
Expand All @@ -149,6 +157,13 @@ export class InputMaskDateTime extends InputMaskPattern {
*/
@property() max: string;

public get hasDatePart(): boolean {
return this.lexems.some(l => l.type === "day" || l.type === "month" || l.type === "year");
}
public get hasTimePart(): boolean {
return this.lexems.some(l => l.type === "hour" || l.type === "minute" || l.type === "second");
}

public getType(): string {
return "datetimemask";
}
Expand All @@ -168,8 +183,11 @@ export class InputMaskDateTime extends InputMaskPattern {
}

private getMaskedStrFromISO(str: string): string {
const date = new Date(str);
let date = new Date(str);
this.initInputDateTimeData();
if(!this.hasDatePart) {
date = new Date(this.defaultDate + str);
}

if(!isNaN(date as any)) {
this.lexems.forEach((lexem, index) => {
Expand Down Expand Up @@ -253,9 +271,9 @@ export class InputMaskDateTime extends InputMaskPattern {
result.push(date.join("-"));
}
if(time.length > 0) {
time.push(time.join(":"));
result.push(time.join(":"));
}
return result.join(" ");
return result.join("T");
}

private isYearValid(dateTime: IDateTimeComposition): boolean {
Expand All @@ -267,23 +285,35 @@ export class InputMaskDateTime extends InputMaskPattern {
return dateTime.year >= parseInt(minYearPart) && dateTime.year <= parseInt(maxYearPart);
}

private createIDateTimeCompositionWithDefaults(dateTime: IDateTimeComposition, isUpperLimit: boolean): IDateTimeComposition {
const min = dateTime.min;
const max = dateTime.max;
const year = dateTime.year !== undefined ? dateTime.year : getDefaultYearForValidation(min.getFullYear(), max.getFullYear());
const month = dateTime.month !== undefined ? dateTime.month : (isUpperLimit && this.hasDatePart ? 12 : 1);
const day = dateTime.day !== undefined ? dateTime.day : (isUpperLimit && this.hasDatePart ? 31 : 1);
const hour = dateTime.hour !== undefined ? dateTime.hour : (isUpperLimit ? 23 : 0);
const minute = dateTime.minute !== undefined ? dateTime.minute : (isUpperLimit ? 59 : 0);
const second = dateTime.second !== undefined ? dateTime.second : (isUpperLimit ? 59 : 0);

return { year: year, month: month, day: day, hour: hour, minute: minute, second: second };
}

private isDateValid(dateTime: IDateTimeComposition): boolean {
const min = dateTime.min;
const max = dateTime.max;
const year = dateTime.year !== undefined ? dateTime.year : getDefaultYearForValidation(min.getFullYear(), max.getFullYear());
const month = dateTime.month !== undefined ? dateTime.month : 1;
const day = dateTime.day !== undefined ? dateTime.day : 1;
const hour = dateTime.hour !== undefined ? dateTime.hour : 0;
const minute = dateTime.minute !== undefined ? dateTime.minute : 0;
const second = dateTime.second !== undefined ? dateTime.second : 0;
const date = new Date(this.getISO_8601Format({ year: year, month: month, day: day, hour: hour, minute: minute, second: second }));
const monthIndex = month - 1;

const date = new Date(this.getISO_8601Format(this.createIDateTimeCompositionWithDefaults(dateTime, false)));
const dateH = new Date(this.getISO_8601Format(this.createIDateTimeCompositionWithDefaults(dateTime, true)));

return !isNaN(date as any) &&
date.getDate() === day &&
date.getMonth() === monthIndex &&
date.getFullYear() === year &&
date >= dateTime.min && date <= dateTime.max;
dateH >= dateTime.min && date <= dateTime.max;
}

private getPlaceholder(lexemLength: number, str: string, char: string) {
Expand All @@ -307,38 +337,45 @@ export class InputMaskDateTime extends InputMaskPattern {
}

(dateTime as any)[propertyName] = parseInt(data);
const firstDigit = parseInt(data[0]);
const validBeginningOfNumber = this.validBeginningOfNumbers[propertyName];
if (propertyName === "year" && !this.isYearValid(dateTime)) {
data = data.slice(0, data.length - 1);
} else if((propertyName === "day" && parseInt(data[0]) > 3) || (propertyName === "month" && parseInt(data[0]) > 1)) {
} else if(validBeginningOfNumber !== undefined && firstDigit > validBeginningOfNumber) {
if(this.isDateValid(dateTime)) {
newItem.isCompleted = true;
} else {
data = data.slice(0, data.length - 1);
}
} else if((propertyName === "day" && parseInt(data[0]) <= 3 && parseInt(data[0]) !== 0) || (propertyName === "month" && parseInt(data[0]) <= 1 && parseInt(data[0]) !== 0)) {
const prevValue = (dateTime as any)[propertyName];
let tempValue = prevValue * 10;
const maxValue = propertyName === "month" ? 3 : 10;
newItem.isCompleted = true;

for(let index = 0; index < maxValue; index++) {
(dateTime as any)[propertyName] = tempValue + index;
if(this.isDateValid(dateTime)) {
newItem.isCompleted = false;
break;
}
}
(dateTime as any)[propertyName] = prevValue;
} else if(validBeginningOfNumber !== undefined && firstDigit !== 0 && firstDigit <= validBeginningOfNumber) {
this.checkValidationDateTimePart(dateTime, propertyName, newItem);
if(newItem.isCompleted && !this.isDateValid(dateTime)) {
data = data.slice(0, data.length - 1);
}
} else if((propertyName === "hour" && parseInt(data[0]) > 2) || ((propertyName === "minute" || propertyName === "second") && parseInt(data[0]) > 6)) {
newItem.isCompleted = true;
}
newItem.value = data || undefined;
(dateTime as any)[propertyName] = parseInt(data) > 0 ? parseInt(data) : undefined;
}

private checkValidationDateTimePart(dateTime: IDateTimeComposition, propertyName: string, newItem: IInputDateTimeData) {
const prevValue = (dateTime as any)[propertyName];
let tempValue = prevValue * 10;
let maxValue = 10;
if (propertyName === "month") maxValue = 3;
if (propertyName === "hour") maxValue = 5;

newItem.isCompleted = true;

for (let index = 0; index < maxValue; index++) {
(dateTime as any)[propertyName] = tempValue + index;
if (this.isDateValid(dateTime)) {
newItem.isCompleted = false;
break;
}
}
(dateTime as any)[propertyName] = prevValue;
}

private getCorrectDatePartFormat(inputData: IInputDateTimeData, matchWholeMask: boolean): string {
const lexem = inputData.lexem;
let dataStr = inputData.value || "";
Expand All @@ -359,15 +396,26 @@ export class InputMaskDateTime extends InputMaskPattern {
}

private createIDateTimeComposition(): IDateTimeComposition {
let isoMin: string, isoMax: string;

if(this.hasDatePart) {
isoMin = this.min || "0001-01-01";
isoMax = this.max || "9999-12-31";
} else {
isoMin = this.defaultDate + (this.min || "00:00:00");
isoMax = this.defaultDate + (this.max || "23:59:59");

}

const tempDateTime: IDateTimeComposition = {
hour: undefined,
minute: undefined,
second: undefined,
day: undefined,
month: undefined,
year: undefined,
min: new Date(this.min || "0001-01-01"),
max: new Date(this.max || "9999-12-31")
min: new Date(isoMin),
max: new Date(isoMax)
};
return tempDateTime;
}
Expand Down
116 changes: 115 additions & 1 deletion tests/mask/mask_datetime_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,25 @@ QUnit.test("get getMaskedValue value from ISO m/d/yy", function(assert) {
assert.equal(maskInstance.getMaskedValue("2024-13-05"), "m/d/yy");
});

QUnit.test("get getMaskedValue value from ISO", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "yyyy";
assert.equal(maskInstance.getMaskedValue("2024"), "2024");

maskInstance.pattern = "mm/yyyy";
assert.equal(maskInstance.getMaskedValue("2024-09"), "09/2024");

maskInstance.pattern = "m/yyyy";
assert.equal(maskInstance.getMaskedValue("2024-09"), "9/2024");

maskInstance.pattern = "m/yy";
assert.equal(maskInstance.getMaskedValue("2024-09"), "9/24");

maskInstance.pattern = "HH:MM";
assert.equal(maskInstance.getMaskedValue("12:45"), "12:45");
assert.equal(maskInstance.getMaskedValue("05:05"), "05:05");
});

QUnit.test("getISO_8601Format", function(assert) {
const maskInstance = new InputMaskDateTime();

Expand All @@ -246,6 +265,10 @@ QUnit.test("getISO_8601Format", function(assert) {

maskInstance.pattern = "m/yy";
assert.equal(maskInstance.getUnmaskedValue("9/24"), "2024-09");

maskInstance.pattern = "HH:MM";
assert.equal(maskInstance.getUnmaskedValue("12:45"), "12:45");
assert.equal(maskInstance.getUnmaskedValue("05:05"), "05:05");
});

QUnit.test("get masked date if text with dots mm/dd/yyyy", function(assert) {
Expand Down Expand Up @@ -634,7 +657,77 @@ QUnit.test("dateTime process: cursor position", function(assert) {
assert.equal(result.caretPosition, 7, "insert 2");
});

QUnit.test("dateTime processInput: min", function(assert) {
QUnit.test("dateTime processInput: min for datetime", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "mm/dd/yyyy HH:MM";
maskInstance.min = "05/04/1982 09:15";
let result = maskInstance.processInput({ insertedChars: "2", selectionStart: 9, selectionEnd: 9, prevValue: "05/04/198y HH:MM", inputDirection: "forward" });
assert.equal(result.value, "05/04/1982 HH:MM", "type 2");
assert.equal(result.caretPosition, 11, "type 2");

result = maskInstance.processInput({ insertedChars: "8", selectionStart: 11, selectionEnd: 11, prevValue: "05/04/1982 HH:MM", inputDirection: "forward" });
assert.equal(result.value, "05/04/1982 HH:MM", "try type 8");
assert.equal(result.caretPosition, 11, "try type 8");

result = maskInstance.processInput({ insertedChars: "1", selectionStart: 11, selectionEnd: 11, prevValue: "05/04/1982 HH:MM", inputDirection: "forward" });
assert.equal(result.value, "05/04/1982 1H:MM", "type 1");
assert.equal(result.caretPosition, 12, "type 1");
});

QUnit.test("dateTime processInput: min for time", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "HH:MM";
maskInstance.min = "09:15";

let result = maskInstance.processInput({ insertedChars: "8", selectionStart: 0, selectionEnd: 0, prevValue: "HH:MM", inputDirection: "forward" });
assert.equal(result.value, "HH:MM", "try type 8");
assert.equal(result.caretPosition, 0, "try type 8");

result = maskInstance.processInput({ insertedChars: "1", selectionStart: 0, selectionEnd: 0, prevValue: "HH:MM", inputDirection: "forward" });
assert.equal(result.value, "1H:MM", "type 1");
assert.equal(result.caretPosition, 1, "type 1");

result = maskInstance.processInput({ insertedChars: "2", selectionStart: 1, selectionEnd: 1, prevValue: "1H:MM", inputDirection: "forward" });
assert.equal(result.value, "12:MM", "type 2");
assert.equal(result.caretPosition, 3, "type 2");

result = maskInstance.processInput({ insertedChars: "8", selectionStart: 3, selectionEnd: 3, prevValue: "12:MM", inputDirection: "forward" });
assert.equal(result.value, "12:08", "type 8");
assert.equal(result.caretPosition, 5, "type 8");

result = maskInstance.processInput({ insertedChars: "8", selectionStart: 3, selectionEnd: 3, prevValue: "09:MM", inputDirection: "forward" });
assert.equal(result.value, "09:MM", "try type 8");
assert.equal(result.caretPosition, 3, "try type 8");

result = maskInstance.processInput({ insertedChars: "3", selectionStart: 3, selectionEnd: 3, prevValue: "09:MM", inputDirection: "forward" });
assert.equal(result.value, "09:3M", "type 3");
assert.equal(result.caretPosition, 4, "type 3");
});

QUnit.test("dateTime processInput: min & max for time", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "HH:MM";
maskInstance.min = "09:15";
maskInstance.max = "17:45";

let result = maskInstance.processInput({ insertedChars: "1", selectionStart: 0, selectionEnd: 0, prevValue: "HH:MM", inputDirection: "forward" });
assert.equal(result.value, "1H:MM", "type 1");
assert.equal(result.caretPosition, 1, "type 1");

result = maskInstance.processInput({ insertedChars: "9", selectionStart: 1, selectionEnd: 1, prevValue: "1H:MM", inputDirection: "forward" });
assert.equal(result.value, "1H:MM", "try type 9");
assert.equal(result.caretPosition, 1, "try type 9");

result = maskInstance.processInput({ insertedChars: "7", selectionStart: 1, selectionEnd: 1, prevValue: "1H:MM", inputDirection: "forward" });
assert.equal(result.value, "17:MM", "type 7");
assert.equal(result.caretPosition, 3, "type 7");

result = maskInstance.processInput({ insertedChars: "5", selectionStart: 3, selectionEnd: 3, prevValue: "17:MM", inputDirection: "forward" });
assert.equal(result.value, "17:05", "type 5");
assert.equal(result.caretPosition, 5, "type 5");
});

QUnit.test("dateTime processInput: min for date", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "mm/dd/yyyy";
maskInstance.min = "1972-02-01";
Expand Down Expand Up @@ -723,4 +816,25 @@ QUnit.test("dateTime processInput: min & max small range", function(assert) {
result = maskInstance.processInput({ insertedChars: "1", prevValue: "05/dd/yyyy", selectionStart: 3, selectionEnd: 3, inputDirection: "forward" });
assert.equal(result.value, "05/01/yyyy", "type 1");
assert.equal(result.caretPosition, 6, "type 1");
});

QUnit.test("dateTime processInput: time", function(assert) {
const maskInstance = new InputMaskDateTime();
maskInstance.pattern = "HH:MM";

let result = maskInstance.processInput({ insertedChars: "1", prevValue: "HH:MM", selectionStart: 0, selectionEnd: 0, inputDirection: "forward" });
assert.equal(result.value, "1H:MM", "type 1");
assert.equal(result.caretPosition, 1, "type 1");

result = maskInstance.processInput({ insertedChars: "2", prevValue: "1H:MM", selectionStart: 1, selectionEnd: 1, inputDirection: "forward" });
assert.equal(result.value, "12:MM", "type 2");
assert.equal(result.caretPosition, 3, "type 2");

result = maskInstance.processInput({ insertedChars: "4", prevValue: "12:MM", selectionStart: 3, selectionEnd: 3, inputDirection: "forward" });
assert.equal(result.value, "12:4M", "type 4");
assert.equal(result.caretPosition, 4, "type 4");

result = maskInstance.processInput({ insertedChars: "5", prevValue: "12:4M", selectionStart: 4, selectionEnd: 4, inputDirection: "forward" });
assert.equal(result.value, "12:45", "type 5");
assert.equal(result.caretPosition, 5, "type 5");
});

0 comments on commit d785443

Please sign in to comment.