From c85ca2355cb0b6fcef73f3e7497f7c31fa82c87c Mon Sep 17 00:00:00 2001 From: Vladimir Potekhin <46284632+vladimirpotekhin@users.noreply.github.com> Date: Thu, 16 Mar 2023 18:44:17 +0300 Subject: [PATCH] feat(kit): use 1 as min segment value in `Date`-related masks (#197) --- .../kit/date-range/date-range-basic.cy.ts | 12 +++++----- .../tests/kit/date-time/date-time-basic.cy.ts | 6 +++-- .../cypress/tests/kit/date/date-basic.cy.ts | 22 +++++++++++++++-- .../lib/masks/date-range/date-range-mask.ts | 1 + .../min-max-date-time-postprocessor.ts | 15 +++++++++--- projects/kit/src/lib/masks/date/date-mask.ts | 7 +++++- .../processors/min-max-date-postprocessor.ts | 15 ++++++++++-- .../utils/date/date-segment-value-length.ts | 6 ++--- .../utils/date/raise-segment-value-to-min.ts | 24 +++++++++++++++++++ .../get-date-segment-value-length.spec.ts | 15 ++++++++++++ .../lib/utils/date/validate-date-string.ts | 7 +++++- 11 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts create mode 100644 projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts diff --git a/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts index 8188de8f0..6126259b7 100644 --- a/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/date-range/date-range-basic.cy.ts @@ -166,7 +166,7 @@ describe('DateRange | Basic', () => { .should('have.prop', 'selectionEnd', '25.02.18'.length); }); - it('13.06.1736 - 14.09|.1821 => Backspace => 13.06.1736 - 14.0|0.1821 => Type "3" => 13.06.1736 - 14.03|.1821', () => { + it('13.06.1736 - 14.09|.1821 => Backspace => 13.06.1736 - 14.0|1.1821 => Type "3" => 13.06.1736 - 14.03|.1821', () => { cy.get('@input') .type('13.06.1736-14.09.1821') .should('have.value', '13.06.1736 – 14.09.1821') @@ -174,7 +174,7 @@ describe('DateRange | Basic', () => { .should('have.prop', 'selectionStart', '13.06.1736 - 14.09'.length) .should('have.prop', 'selectionEnd', '13.06.1736 - 14.09'.length) .type('{backspace}') - .should('have.value', '13.06.1736 – 14.00.1821') + .should('have.value', '13.06.1736 – 14.01.1821') .should('have.prop', 'selectionStart', '13.06.1736 - 14.0'.length) .should('have.prop', 'selectionEnd', '13.06.1736 - 14.0'.length) .type('3') @@ -300,7 +300,7 @@ describe('DateRange | Basic', () => { describe('Text selection', () => { describe('Select range and press Backspace / Delete', () => { - it('10.|12|.2005 - 16.12.2007 => Backspace => 10.|00.2005 - 16.12.2007', () => { + it('10.|12|.2005 - 16.12.2007 => Backspace => 10.|01.2005 - 16.12.2007', () => { cy.get('@input') .type('10122005-16122007') .should('have.value', '10.12.2005 – 16.12.2007') @@ -312,12 +312,12 @@ describe('DateRange | Basic', () => { ]); cy.get('@input') - .should('have.value', '10.00.2005 – 16.12.2007') + .should('have.value', '10.01.2005 – 16.12.2007') .should('have.prop', 'selectionStart', '10.'.length) .should('have.prop', 'selectionEnd', '10.'.length); }); - it('10.12.2005 - |16|.12.2007 => Backspace => 10.12.2005 - |00.12.2007', () => { + it('10.12.2005 - |16|.12.2007 => Backspace => 10.12.2005 - |01.12.2007', () => { cy.get('@input') .type('10122005-16122007') .should('have.value', '10.12.2005 – 16.12.2007') @@ -329,7 +329,7 @@ describe('DateRange | Basic', () => { ]); cy.get('@input') - .should('have.value', '10.12.2005 – 00.12.2007') + .should('have.value', '10.12.2005 – 01.12.2007') .should('have.prop', 'selectionStart', '10.12.2005 - '.length) .should('have.prop', 'selectionEnd', '10.12.2005 - '.length); }); diff --git a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts index 52a3b214e..3f2b8cd2d 100644 --- a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts @@ -24,6 +24,8 @@ describe('DateTime | Basic', () => { ['1811201623152', '18.11.2016, 23:15:2'], ['18112016231522', '18.11.2016, 23:15:22'], ['18112016231522123', '18.11.2016, 23:15:22.123'], + ['0', '0'], + ['00', '0'], ] as const; tests.forEach(([typedValue, maskedValue]) => { @@ -219,7 +221,7 @@ describe('DateTime | Basic', () => { describe('Text selection', () => { describe('Select range and press Backspace / Delete', () => { - it('10.|12|.2005, 12:30 => Backspace => 10.|00.2005, 12:30', () => { + it('10.|12|.2005, 12:30 => Backspace => 10.|01.2005, 12:30', () => { cy.get('@input') .type('101220051230') .should('have.value', '10.12.2005, 12:30') @@ -231,7 +233,7 @@ describe('DateTime | Basic', () => { ]); cy.get('@input') - .should('have.value', '10.00.2005, 12:30') + .should('have.value', '10.01.2005, 12:30') .should('have.prop', 'selectionStart', '10.'.length) .should('have.prop', 'selectionEnd', '10.'.length); }); diff --git a/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts b/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts index df7cd1743..0102466ea 100644 --- a/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/date/date-basic.cy.ts @@ -18,6 +18,8 @@ describe('Date', () => { ['12', '12', '12'.length], ['121', '12.1', '12.1'.length], ['1211', '12.11', '12.11'.length], + ['0', '0', 1], + ['00', '0', '0'.length], ] as const; tests.forEach(([typedValue, maskedValue, caretIndex]) => { @@ -176,6 +178,22 @@ describe('Date', () => { .should('have.prop', 'selectionStart', '3'.length) .should('have.prop', 'selectionEnd', '3'.length); }); + + it('02|.01.2008 => Backspace => 0|1.01.2008 => Type "5" => 05|.01.2008', () => { + cy.get('@input') + .type('02012008') + .type('{leftArrow}'.repeat('.01.2008'.length)) + .should('have.prop', 'selectionStart', '02'.length) + .should('have.prop', 'selectionEnd', '02'.length) + .type('{backspace}') + .should('have.value', '01.01.2008') + .should('have.prop', 'selectionStart', '0'.length) + .should('have.prop', 'selectionEnd', '0'.length) + .type('5') + .should('have.value', '05.01.2008') + .should('have.prop', 'selectionStart', '05.'.length) + .should('have.prop', 'selectionEnd', '05.'.length); + }); }); describe('Fixed values', () => { @@ -206,7 +224,7 @@ describe('Date', () => { describe('Text selection', () => { describe('Select range and press Backspace', () => { - it('10.|12|.2022 => Backspace => 10.|00.2022', () => { + it('10.|12|.2022 => Backspace => 10.|01.2022', () => { cy.get('@input') .type('10122022') .type('{leftArrow}'.repeat('.2022'.length)) @@ -217,7 +235,7 @@ describe('Date', () => { ]); cy.get('@input') - .should('have.value', '10.00.2022') + .should('have.value', '10.01.2022') .should('have.prop', 'selectionStart', '10.'.length) .should('have.prop', 'selectionEnd', '10.'.length); }); diff --git a/projects/kit/src/lib/masks/date-range/date-range-mask.ts b/projects/kit/src/lib/masks/date-range/date-range-mask.ts index 70f46d015..47297d0d2 100644 --- a/projects/kit/src/lib/masks/date-range/date-range-mask.ts +++ b/projects/kit/src/lib/masks/date-range/date-range-mask.ts @@ -48,6 +48,7 @@ export function maskitoDateRangeOptionsGenerator({ max, dateModeTemplate, datesSeparator: DATE_RANGE_SEPARATOR, + dateSegmentSeparator: separator, }), createMinMaxRangeLengthPostprocessor({ dateModeTemplate, diff --git a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts index b2ca28e05..6ed4a04c1 100644 --- a/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts @@ -9,6 +9,7 @@ import { segmentsToDate, toDateString, } from '../../../utils'; +import {raiseSegmentValueToMin} from '../../../utils/date/raise-segment-value-to-min'; import {parseTimeString} from '../../../utils/time'; import {isDateTimeStringComplete, parseDateTimeString} from '../utils'; @@ -25,16 +26,24 @@ export function createMinMaxDateTimePostprocessor({ }): NonNullable<MaskitoOptions['postprocessor']> { return ({value, selection}) => { const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate); + const parsedDate = parseDateString(dateString, dateModeTemplate); + const parsedTime = parseTimeString(timeString); if (!isDateTimeStringComplete(value, dateModeTemplate, timeMode)) { + const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate); + const fixedValue = toDateString( + {...fixedDate, ...parsedTime}, + dateModeTemplate, + timeMode, + ); + const tail = value.slice(fixedValue.length); + return { selection, - value: value, + value: fixedValue + tail, }; } - const parsedDate = parseDateString(dateString, dateModeTemplate); - const parsedTime = parseTimeString(timeString); const date = segmentsToDate(parsedDate, parsedTime); const clampedDate = clamp(date, min, max); diff --git a/projects/kit/src/lib/masks/date/date-mask.ts b/projects/kit/src/lib/masks/date/date-mask.ts index adb0428f3..12ec88558 100644 --- a/projects/kit/src/lib/masks/date/date-mask.ts +++ b/projects/kit/src/lib/masks/date/date-mask.ts @@ -32,6 +32,11 @@ export function maskitoDateOptionsGenerator({ dateSegmentsSeparator: separator, }), ), - postprocessor: createMinMaxDatePostprocessor({min, max, dateModeTemplate}), + postprocessor: createMinMaxDatePostprocessor({ + min, + max, + dateModeTemplate, + dateSegmentSeparator: separator, + }), }; } diff --git a/projects/kit/src/lib/processors/min-max-date-postprocessor.ts b/projects/kit/src/lib/processors/min-max-date-postprocessor.ts index c7ded16b3..c66892d54 100644 --- a/projects/kit/src/lib/processors/min-max-date-postprocessor.ts +++ b/projects/kit/src/lib/processors/min-max-date-postprocessor.ts @@ -10,17 +10,20 @@ import { segmentsToDate, toDateString, } from '../utils'; +import {raiseSegmentValueToMin} from '../utils/date/raise-segment-value-to-min'; export function createMinMaxDatePostprocessor({ dateModeTemplate, min = DEFAULT_MIN_DATE, max = DEFAULT_MAX_DATE, datesSeparator = '', + dateSegmentSeparator = '.', }: { dateModeTemplate: string; min?: Date; max?: Date; datesSeparator?: string; + dateSegmentSeparator?: string; }): NonNullable<MaskitoOptions['postprocessor']> { return ({value, selection}) => { const endsWithDatesSeparator = datesSeparator && value.endsWith(datesSeparator); @@ -31,12 +34,20 @@ export function createMinMaxDatePostprocessor({ for (const dateString of dateStrings) { validatedValue += validatedValue ? datesSeparator : ''; + const parsedDate = parseDateString(dateString, dateModeTemplate); + if (!isDateStringComplete(dateString, dateModeTemplate)) { - validatedValue += dateString; + const fixedDate = raiseSegmentValueToMin(parsedDate, dateModeTemplate); + + const fixedValue = toDateString(fixedDate, dateModeTemplate); + const tail = dateString.endsWith(dateSegmentSeparator) + ? dateSegmentSeparator + : ''; + + validatedValue += fixedValue + tail; continue; } - const parsedDate = parseDateString(dateString, dateModeTemplate); const date = segmentsToDate(parsedDate); const clampedDate = clamp(date, min, max); diff --git a/projects/kit/src/lib/utils/date/date-segment-value-length.ts b/projects/kit/src/lib/utils/date/date-segment-value-length.ts index f69ab93f6..72222f141 100644 --- a/projects/kit/src/lib/utils/date/date-segment-value-length.ts +++ b/projects/kit/src/lib/utils/date/date-segment-value-length.ts @@ -3,7 +3,7 @@ import {MaskitoDateSegments} from '../../types'; export const getDateSegmentValueLength: ( dateString: string, ) => MaskitoDateSegments<number> = (dateString: string) => ({ - day: dateString.match('/d/g')?.length || 2, - month: dateString.match('/m/g')?.length || 2, - year: dateString.match('/y/g')?.length || 4, + day: dateString.match(/d/g)?.length || 0, + month: dateString.match(/m/g)?.length || 0, + year: dateString.match(/y/g)?.length || 0, }); diff --git a/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts b/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts new file mode 100644 index 000000000..7d7a66d63 --- /dev/null +++ b/projects/kit/src/lib/utils/date/raise-segment-value-to-min.ts @@ -0,0 +1,24 @@ +import {MaskitoDateSegments} from '../../types'; +import {getObjectFromEntries} from '../get-object-from-entries'; +import {getDateSegmentValueLength} from './date-segment-value-length'; + +export function raiseSegmentValueToMin( + segments: Partial<MaskitoDateSegments>, + fullMode: string, +): Partial<MaskitoDateSegments> { + const segmentsLength = getDateSegmentValueLength(fullMode); + + return getObjectFromEntries( + Object.entries<string>(segments).map(([key, value]: [string, string]) => { + const segmentLength = + segmentsLength[key as keyof Partial<MaskitoDateSegments>]; + + return [ + key, + value.length === segmentLength && value.match(/^0+$/) + ? '1'.padStart(segmentLength, '0') + : value, + ]; + }), + ); +} diff --git a/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts b/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts new file mode 100644 index 000000000..e9f41da73 --- /dev/null +++ b/projects/kit/src/lib/utils/date/tests/get-date-segment-value-length.spec.ts @@ -0,0 +1,15 @@ +import {getDateSegmentValueLength} from '../date-segment-value-length'; + +describe('getDateSegmentValueLength', () => { + it('short date', () => { + expect(getDateSegmentValueLength('mm.yy')).toEqual({day: 0, month: 2, year: 2}); + }); + + it('full date', () => { + expect(getDateSegmentValueLength('dd.mm.yyyy')).toEqual({ + day: 2, + month: 2, + year: 4, + }); + }); +}); diff --git a/projects/kit/src/lib/utils/date/validate-date-string.ts b/projects/kit/src/lib/utils/date/validate-date-string.ts index 4f82150a8..c39145dee 100644 --- a/projects/kit/src/lib/utils/date/validate-date-string.ts +++ b/projects/kit/src/lib/utils/date/validate-date-string.ts @@ -39,7 +39,7 @@ export function validateDateString({ offset + validatedDate.length + fantomSeparator + - getDateSegmentValueLength(dateString)[segmentName]; + getDateSegmentValueLength(dateModeTemplate)[segmentName]; const isLastSegmentDigitAdded = lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to; @@ -48,6 +48,11 @@ export function validateDateString({ return {validatedDateString: '', updatedSelection: [from, to]}; // prevent changes } + if (isLastSegmentDigitAdded && Number(segmentValue) < 1) { + // 31.0|1.2010 => Type 0 => 31.0|1.2010 + return {validatedDateString: '', updatedSelection: [from, to]}; // prevent changes + } + const {validatedSegmentValue, prefixedZeroesCount} = padWithZeroesUntilValid( segmentValue, `${maxSegmentValue}`,