Skip to content

Commit 3b1fe1c

Browse files
refactor(date-time-editor): allow AM/PM to be placed anywhere in the mask
- clean up getMaskedValue - add lessThanMinValue and greaterThanMaxValue to DatePickerUtil - add value check in updateMask
1 parent 95875ff commit 3b1fe1c

File tree

3 files changed

+153
-66
lines changed

3 files changed

+153
-66
lines changed

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

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ export abstract class DatePickerUtil {
5050
private static readonly PROMPT_CHAR = '_';
5151
private static readonly DEFAULT_LOCALE = 'en';
5252

53+
54+
55+
/**
56+
* TODO: Unit tests for all public methods.
57+
*/
58+
59+
60+
5361
/**
5462
* Parse a Date value from masked string input based on determined date parts
5563
* @param inputData masked value to parse
@@ -92,20 +100,6 @@ export abstract class DatePickerUtil {
92100
);
93101
}
94102

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-
109103
/**
110104
* Parse the mask into date/time and literal parts
111105
*/
@@ -256,6 +250,72 @@ export abstract class DatePickerUtil {
256250
return newDate;
257251
}
258252

253+
/**
254+
* Determines whether the provided value is less than the provided min value.
255+
* @param includeTime set to false if you want to exclude time portion of the two dates
256+
* @param includeDate set to false if you want to exclude the date portion of the two dates
257+
*/
258+
public static greaterThanMaxValue(value: Date, maxValue: Date, includeTime = true, includeDate = true): boolean {
259+
if (includeTime && includeDate) {
260+
return value.getTime() > maxValue.getTime();
261+
}
262+
263+
let _value = new Date(value.getTime());
264+
let _maxValue = new Date(maxValue.getTime());
265+
if (includeTime) {
266+
_value.setHours(0, 0, 0, 0);
267+
_maxValue.setHours(0, 0, 0, 0);
268+
return _value.getTime() > maxValue.getTime();
269+
}
270+
if (includeDate) {
271+
_value = new Date(0, 0, 0, _value.getHours(), _value.getMinutes(), _value.getSeconds());
272+
_maxValue = new Date(0, 0, 0, _maxValue.getHours(), _maxValue.getMinutes(), _maxValue.getSeconds());
273+
return _value.getTime() > _maxValue.getTime();
274+
}
275+
276+
// throw?
277+
}
278+
279+
/**
280+
* Determines whether the provided value is greater than the provided min value.
281+
* @param includeTime set to false if you want to exclude time portion of the two dates
282+
* @param includeDate set to false if you want to exclude the date portion of the two dates
283+
*/
284+
public static lessThanMinValue(value: Date, minValue: Date, includeTime = true, includeDate = true): boolean {
285+
if (includeTime && includeDate) {
286+
return value.getTime() < minValue.getTime();
287+
}
288+
289+
let _value = new Date(value.getTime());
290+
let _minValue = new Date(minValue.getTime());
291+
if (includeTime) {
292+
_value.setHours(0, 0, 0, 0);
293+
_minValue.setHours(0, 0, 0, 0);
294+
return _value.getTime() < _minValue.getTime();
295+
}
296+
if (includeDate) {
297+
_value = new Date(0, 0, 0, _value.getHours(), _value.getMinutes(), _value.getSeconds());
298+
_minValue = new Date(0, 0, 0, _minValue.getHours(), _minValue.getMinutes(), _minValue.getSeconds());
299+
return _value.getTime() > _minValue.getTime();
300+
}
301+
302+
// throw?
303+
}
304+
305+
private static ensureLeadingZero(part: DatePartInfo) {
306+
switch (part.type) {
307+
case DatePart.Date:
308+
case DatePart.Month:
309+
case DatePart.Hours:
310+
case DatePart.Minutes:
311+
case DatePart.Seconds:
312+
if (part.format.length === 1) {
313+
part.format = part.format.repeat(2);
314+
}
315+
break;
316+
}
317+
}
318+
259319
private static getCleanVal(inputData: string, datePart: DatePartInfo, promptChar?: string): string {
260320
return DatePickerUtil.trimEmptyPlaceholders(inputData.substring(datePart.start, datePart.end), promptChar);
261321
}

projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,22 @@ describe('IgxDateTimeEditor', () => {
311311
dateTimeEditor.decrement(DatePart.Seconds);
312312
expect(dateTimeEditor.value.getSeconds()).toEqual(59);
313313
});
314+
315+
it('should properly parse AM/PM no matter where it is in the format', () => {
316+
inputFormat = 'dd tt yyyy-MM mm-ss-hh';
317+
inputDate = '12 AM 2020-06 14-15-11';
318+
elementRef = { nativeElement: { value: inputDate } };
319+
initializeDateTimeEditor();
320+
321+
dateTimeEditor.inputFormat = inputFormat;
322+
expect(dateTimeEditor.mask).toEqual('00 LL 0000-00 00-00-00');
323+
324+
dateTimeEditor.value = new Date(2020, 5, 12, 11, 15, 14);
325+
spyOnProperty((dateTimeEditor as any), 'inputValue', 'get').and.returnValue(inputDate);
326+
327+
dateTimeEditor.increment(DatePart.AmPm);
328+
expect(dateTimeEditor.value).toEqual(new Date(2020, 5, 12, 23, 15, 14));
329+
});
314330
});
315331
});
316332

projects/igniteui-angular/src/lib/directives/date-time-editor/date-time-editor.directive.ts

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
141141
if (value) {
142142
this._format = value;
143143
}
144-
const mask = this.inputFormat?.replace(/\w/g, '0');
145-
this.mask = value.indexOf('tt') !== -1 ? mask.substring(0, mask.length - 2) + 'LL' : mask;
144+
const mask = this.inputFormat?.replace(new RegExp(/(?=[^t])[\w]/, 'g'), '0');
145+
this.mask = mask.indexOf('tt') !== -1 ? mask.replace(new RegExp('tt', 'g'), 'LL') : mask;
146146
}
147147

148148
public get inputFormat(): string {
@@ -160,9 +160,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
160160
public set value(value: Date) {
161161
this._value = value;
162162
this.onChangeCallback(value);
163-
if (value) {
164-
this.updateMask();
165-
}
163+
this.updateMask();
166164
}
167165

168166
public get value() {
@@ -219,6 +217,20 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
219217
}
220218
}
221219

220+
private get hasDateParts() {
221+
return this._inputDateParts.some(
222+
p => p.type === DatePart.Date
223+
|| p.type === DatePart.Month
224+
|| p.type === DatePart.Year);
225+
}
226+
227+
private get hasTimeParts() {
228+
return this._inputDateParts.some(
229+
p => p.type === DatePart.Hours
230+
|| p.type === DatePart.Minutes
231+
|| p.type === DatePart.Seconds);
232+
}
233+
222234
constructor(
223235
protected renderer: Renderer2,
224236
protected elementRef: ElementRef,
@@ -241,9 +253,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
241253
this.renderer.setAttribute(this.nativeElement, 'placeholder', defPlaceholder);
242254
}
243255
// TODO: fill in partial dates?
244-
if (this.value) {
245-
this.updateMask();
246-
}
256+
this.updateMask();
247257
}
248258
}
249259

@@ -255,7 +265,6 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
255265
/** Clear the input element value. */
256266
public clear(): void {
257267
this.updateValue(null);
258-
this.inputValue = this._isFocused ? this.emptyMask : '';
259268
}
260269

261270
/**
@@ -292,15 +301,22 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
292301

293302
/** @hidden @internal */
294303
public validate(control: AbstractControl): ValidationErrors | null {
295-
if (this.minValue && !this.valueInRange(control.value)) {
304+
if (!this.inputIsComplete() || !control.value) {
305+
return { 'value': true };
306+
}
307+
308+
const maxValueAsDate = this.isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue);
309+
const minValueAsDate = this.isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue);
310+
if (minValueAsDate
311+
&& DatePickerUtil.lessThanMinValue(
312+
control.value, minValueAsDate, this.hasTimeParts, this.hasDateParts)) {
296313
return { 'minValue': true };
297314
}
298-
if (this.maxValue && !this.valueInRange(control.value)) {
315+
if (maxValueAsDate
316+
&& DatePickerUtil.greaterThanMaxValue(
317+
control.value, maxValueAsDate, this.hasTimeParts, this.hasDateParts)) {
299318
return { 'maxValue': true };
300319
}
301-
if (!this.inputIsComplete()) {
302-
return { 'value': true };
303-
}
304320

305321
return null;
306322
}
@@ -360,11 +376,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
360376
public onFocus(): void {
361377
this._isFocused = true;
362378
this.onTouchCallback();
363-
if (this.value) {
364-
this.updateMask();
365-
} else {
366-
this.inputValue = this.emptyMask;
367-
}
379+
this.updateMask();
368380
super.onFocus();
369381
}
370382

@@ -373,24 +385,26 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
373385
this._isFocused = false;
374386
if (!this.inputIsComplete() && this.inputValue !== this.emptyMask) {
375387
this.updateValue(this.parseDate(this.inputValue));
376-
}
377-
if (!this.isValidDate(this.value)) {
378-
this.inputValue = '';
379-
return;
388+
} else {
389+
this.updateMask();
380390
}
381391

382-
this.updateMask();
383392
super.onBlur(value);
384393
}
385394

386395
/** @hidden @internal */
387396
public updateMask(): void {
388397
if (this._isFocused) {
398+
if (!this.value) { return; }
389399
// store the cursor position as it will be moved during masking
390400
const cursor = this.selectionEnd;
391401
this.inputValue = this.getMaskedValue();
392402
this.setSelectionRange(cursor);
393403
} else {
404+
if (!this.value || !this.isValidDate(this.value)) {
405+
this.inputValue = '';
406+
return;
407+
}
394408
const format = this.displayFormat || this.inputFormat;
395409
if (format) {
396410
this.inputValue = formatDate(this.value, format.replace('tt', 'aa'), this.locale);
@@ -405,19 +419,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
405419
let mask = this.emptyMask;
406420
for (const part of this._inputDateParts) {
407421
if (part.type === DatePart.Literal) { continue; }
408-
const partLength = part.format.length;
409-
let targetValue = this.getPartValue(part.type, partLength);
410-
if (part.type === DatePart.Month) {
411-
targetValue = this.prependValue(
412-
parseInt(targetValue.replace(new RegExp(this.promptChar, 'g'), '0'), 10) + 1, partLength, '0');
413-
}
414-
if (part.type === DatePart.Hours && part.format.indexOf('h') !== -1) {
415-
targetValue = this.prependValue(this.toTwelveHourFormat(targetValue), partLength, '0');
416-
}
417-
if (part.type === DatePart.Year && partLength === 2) {
418-
targetValue = this.prependValue(parseInt(targetValue.slice(-2), 10), partLength, '0');
419-
}
420-
422+
const targetValue = this.getPartValue(part, part.format.length);
421423
mask = this.maskParser.replaceInMask(mask, targetValue, this.maskOptions, part.start, part.end).value;
422424
}
423425

@@ -432,23 +434,20 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
432434
if (!value) { return false; }
433435
const maxValueAsDate = this.isDate(this.maxValue) ? this.maxValue : this.parseDate(this.maxValue);
434436
const minValueAsDate = this.isDate(this.minValue) ? this.minValue : this.parseDate(this.minValue);
435-
if (minValueAsDate && value && this.compareDates(value, minValueAsDate)) {
437+
if (minValueAsDate
438+
&& DatePickerUtil.lessThanMinValue(
439+
value, minValueAsDate, this.hasTimeParts, this.hasDateParts)) {
436440
return false;
437-
} else if (maxValueAsDate && value && this.compareDates(maxValueAsDate, value)) {
441+
}
442+
if (maxValueAsDate
443+
&& DatePickerUtil.greaterThanMaxValue(
444+
value, maxValueAsDate, this.hasTimeParts, this.hasDateParts)) {
438445
return false;
439446
}
440447

441448
return true;
442449
}
443450

444-
private compareDates(firstDate: Date, secondDate: Date) {
445-
const _firstDate = new Date(firstDate.getTime());
446-
_firstDate.setHours(0, 0, 0, 0);
447-
const _secondDate = new Date(secondDate.getTime());
448-
_secondDate.setHours(0, 0, 0, 0);
449-
return _firstDate.getTime() < _secondDate.getTime();
450-
}
451-
452451
private spinValue(datePart: DatePart, delta: number): Date {
453452
if (!this.value || !this.isValidDate(this.value)) { return null; }
454453
const newDate = new Date(this.value.getTime());
@@ -507,20 +506,32 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
507506
return hour;
508507
}
509508

510-
private getPartValue(datePart: DatePart, partLength: number): string {
509+
private getPartValue(datePartInfo: DatePartInfo, partLength: number): string {
511510
let maskedValue;
511+
const datePart = datePartInfo.type;
512512
switch (datePart) {
513513
case DatePart.Date:
514514
maskedValue = this.value.getDate();
515515
break;
516516
case DatePart.Month:
517-
maskedValue = this.value.getMonth();
517+
// months are zero based
518+
maskedValue = this.value.getMonth() + 1;
518519
break;
519520
case DatePart.Year:
520-
maskedValue = this.value.getFullYear();
521+
if (partLength === 2) {
522+
maskedValue = this.prependValue(
523+
parseInt(this.value.getFullYear().toString().slice(-2), 10), partLength, '0');
524+
} else {
525+
maskedValue = this.value.getFullYear();
526+
}
521527
break;
522528
case DatePart.Hours:
523-
maskedValue = this.value.getHours();
529+
if (datePartInfo.format.indexOf('h') !== -1) {
530+
maskedValue = this.prependValue(
531+
this.toTwelveHourFormat(this.value.getHours().toString()), partLength, '0');
532+
} else {
533+
maskedValue = this.value.getHours();
534+
}
524535
break;
525536
case DatePart.Minutes:
526537
maskedValue = this.value.getMinutes();
@@ -533,7 +544,7 @@ export class IgxDateTimeEditorDirective extends IgxMaskDirective implements OnCh
533544
break;
534545
}
535546

536-
if (datePart !== DatePart.AmPm) {
547+
if (datePartInfo.type !== DatePart.AmPm) {
537548
return this.prependValue(maskedValue, partLength, '0');
538549
}
539550

0 commit comments

Comments
 (0)