From 70dd6bf19cf34c01d102048a1adafe0d4f0bd1d8 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Wed, 8 May 2024 14:06:03 +0300 Subject: [PATCH 01/11] feat(kit): added suppoer of SS.MSS and MM:SS.MSS --- .../src/lib/masks/date-time/date-time-mask.ts | 1 + .../min-max-date-time-postprocessor.ts | 2 +- .../valid-date-time-preprocessor.ts | 3 ++ .../processors/max-validation-preprocessor.ts | 3 ++ .../tests/max-validation-preprocessor.spec.ts | 5 ++- projects/kit/src/lib/masks/time/time-mask.ts | 2 +- projects/kit/src/lib/types/time-mode.ts | 8 +++- .../src/lib/utils/time/parse-time-string.ts | 43 ++++++++++++++++--- .../kit/src/lib/utils/time/to-time-string.ts | 7 +-- .../lib/utils/time/validate-time-string.ts | 4 +- 10 files changed, 65 insertions(+), 13 deletions(-) diff --git a/projects/kit/src/lib/masks/date-time/date-time-mask.ts b/projects/kit/src/lib/masks/date-time/date-time-mask.ts index 2cad6759b..a30500f19 100644 --- a/projects/kit/src/lib/masks/date-time/date-time-mask.ts +++ b/projects/kit/src/lib/masks/date-time/date-time-mask.ts @@ -64,6 +64,7 @@ export function maskitoDateTimeOptionsGenerator({ dateModeTemplate, dateSegmentsSeparator: dateSeparator, dateTimeSeparator, + timeMode, }), ], postprocessors: [ 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 61a807fd5..d52fbc8d8 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 @@ -33,7 +33,7 @@ export function createMinMaxDateTimePostprocessor({ dateTimeSeparator, }); const parsedDate = parseDateString(dateString, dateModeTemplate); - const parsedTime = parseTimeString(timeString); + const parsedTime = parseTimeString(timeString, timeMode); if ( !isDateTimeStringComplete(value, { diff --git a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts index e504bee13..834cdc95f 100644 --- a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts @@ -9,10 +9,12 @@ export function createValidDateTimePreprocessor({ dateModeTemplate, dateSegmentsSeparator, dateTimeSeparator, + timeMode, }: { dateModeTemplate: string; dateSegmentsSeparator: string; dateTimeSeparator: string; + timeMode: string; }): MaskitoPreprocessor { const invalidCharsRegExp = new RegExp( `[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp( @@ -69,6 +71,7 @@ export function createValidDateTimePreprocessor({ paddedMaxValues, offset: validatedValue.length + dateTimeSeparator.length, selection: [from, to], + timeMode, }); if (timeString && !validatedTimeString) { diff --git a/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts b/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts index ffb010fbd..1f6353c94 100644 --- a/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts +++ b/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts @@ -7,6 +7,7 @@ import {padTimeSegments, validateTimeString} from '../../../utils/time'; export function createMaxValidationPreprocessor( timeSegmentMaxValues: MaskitoTimeSegments, + timeMode: string, ): MaskitoPreprocessor { const paddedMaxValues = padTimeSegments(timeSegmentMaxValues); const invalidCharsRegExp = new RegExp( @@ -26,6 +27,7 @@ export function createMaxValidationPreprocessor( paddedMaxValues, offset: 0, selection, + timeMode, }); return { @@ -47,6 +49,7 @@ export function createMaxValidationPreprocessor( paddedMaxValues, offset: 0, selection: [from, to], + timeMode, }); if (newPossibleValue && !validatedTimeString) { diff --git a/projects/kit/src/lib/masks/time/processors/tests/max-validation-preprocessor.spec.ts b/projects/kit/src/lib/masks/time/processors/tests/max-validation-preprocessor.spec.ts index daa824454..a33bd2f8d 100644 --- a/projects/kit/src/lib/masks/time/processors/tests/max-validation-preprocessor.spec.ts +++ b/projects/kit/src/lib/masks/time/processors/tests/max-validation-preprocessor.spec.ts @@ -2,7 +2,10 @@ import {DEFAULT_TIME_SEGMENT_MAX_VALUES} from '../../../../constants'; import {createMaxValidationPreprocessor} from '../max-validation-preprocessor'; describe('createMaxValidationPreprocessor', () => { - const processor = createMaxValidationPreprocessor(DEFAULT_TIME_SEGMENT_MAX_VALUES); + const processor = createMaxValidationPreprocessor( + DEFAULT_TIME_SEGMENT_MAX_VALUES, + 'HH:MM:SS', + ); describe('Paste from clipboard', () => { const process = (data: string): string => diff --git a/projects/kit/src/lib/masks/time/time-mask.ts b/projects/kit/src/lib/masks/time/time-mask.ts index 50608ad67..d017f19cf 100644 --- a/projects/kit/src/lib/masks/time/time-mask.ts +++ b/projects/kit/src/lib/masks/time/time-mask.ts @@ -31,7 +31,7 @@ export function maskitoTimeOptionsGenerator({ createFullWidthToHalfWidthPreprocessor(), createColonConvertPreprocessor(), createZeroPlaceholdersPreprocessor(), - createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues), + createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues, mode), ], overwriteMode: 'replace', }; diff --git a/projects/kit/src/lib/types/time-mode.ts b/projects/kit/src/lib/types/time-mode.ts index 20d0af8fb..364d77815 100644 --- a/projects/kit/src/lib/types/time-mode.ts +++ b/projects/kit/src/lib/types/time-mode.ts @@ -1 +1,7 @@ -export type MaskitoTimeMode = 'HH:MM:SS.MSS' | 'HH:MM:SS' | 'HH:MM' | 'HH'; +export type MaskitoTimeMode = + | 'HH:MM:SS.MSS' + | 'HH:MM:SS' + | 'HH:MM' + | 'HH' + | 'MM:SS.MSS' + | 'SS.MSS'; diff --git a/projects/kit/src/lib/utils/time/parse-time-string.ts b/projects/kit/src/lib/utils/time/parse-time-string.ts index f9660ea19..95516a870 100644 --- a/projects/kit/src/lib/utils/time/parse-time-string.ts +++ b/projects/kit/src/lib/utils/time/parse-time-string.ts @@ -3,17 +3,50 @@ import type {MaskitoTimeSegments} from '../../types'; /** * @param timeString can be with/without fixed characters */ -export function parseTimeString(timeString: string): Partial { +export function parseTimeString( + timeString: string, + timeMode: string, +): Partial { const onlyDigits = timeString.replaceAll(/\D+/g, ''); + const sliceIndexes = createSliceIndexes({ + isStartsWithMilliseconds: timeMode.startsWith('MSS'), + isStartsWithMinutes: timeMode.startsWith('MM'), + isStartsWithSeconds: timeMode.startsWith('SS'), + }); + const timeSegments = { - hours: onlyDigits.slice(0, 2), - minutes: onlyDigits.slice(2, 4), - seconds: onlyDigits.slice(4, 6), - milliseconds: onlyDigits.slice(6, 9), + hours: onlyDigits.slice(...sliceIndexes.hours), + minutes: onlyDigits.slice(...sliceIndexes.minutes), + seconds: onlyDigits.slice(...sliceIndexes.seconds), + milliseconds: onlyDigits.slice(...sliceIndexes.milliseconds), }; return Object.fromEntries( Object.entries(timeSegments).filter(([_, value]) => Boolean(value)), ); } + +function createSliceIndexes({ + isStartsWithMinutes, + isStartsWithSeconds, + isStartsWithMilliseconds, +}: { + isStartsWithMinutes: boolean; + isStartsWithSeconds: boolean; + isStartsWithMilliseconds: boolean; +}): MaskitoTimeSegments<[number, number]> { + const offset = + Number(isStartsWithMinutes) * 2 + + Number(isStartsWithSeconds) * 4 + + Number(isStartsWithMilliseconds) * 6; + + const changeSelection = (index: number): number => Math.max(index - offset, 0); + + return { + hours: [changeSelection(0), changeSelection(2)], + minutes: [changeSelection(2), changeSelection(4)], + seconds: [changeSelection(4), changeSelection(6)], + milliseconds: [changeSelection(6), changeSelection(9)], + }; +} diff --git a/projects/kit/src/lib/utils/time/to-time-string.ts b/projects/kit/src/lib/utils/time/to-time-string.ts index e7a6ad17c..4b5536086 100644 --- a/projects/kit/src/lib/utils/time/to-time-string.ts +++ b/projects/kit/src/lib/utils/time/to-time-string.ts @@ -6,9 +6,10 @@ export function toTimeString({ seconds = '', milliseconds = '', }: Partial): string { - const mm = minutes && `:${minutes}`; - const ss = seconds && `:${seconds}`; - const ms = milliseconds && `.${milliseconds}`; + const mm = hours ? minutes && `:${minutes}` : minutes; + const ss = hours || minutes ? seconds && `:${seconds}` : seconds; + const ms = + hours || minutes || seconds ? milliseconds && `.${milliseconds}` : milliseconds; return `${hours}${mm}${ss}${ms}`; } diff --git a/projects/kit/src/lib/utils/time/validate-time-string.ts b/projects/kit/src/lib/utils/time/validate-time-string.ts index 1e76990f7..b6a34e5e7 100644 --- a/projects/kit/src/lib/utils/time/validate-time-string.ts +++ b/projects/kit/src/lib/utils/time/validate-time-string.ts @@ -14,13 +14,15 @@ export function validateTimeString({ paddedMaxValues, offset, selection: [from, to], + timeMode, }: { timeString: string; paddedMaxValues: MaskitoTimeSegments; offset: number; selection: readonly [number, number]; + timeMode: string; }): {validatedTimeString: string; updatedTimeSelection: [number, number]} { - const parsedTime = parseTimeString(timeString); + const parsedTime = parseTimeString(timeString, timeMode); const possibleTimeSegments = Object.entries(parsedTime) as Array< [keyof MaskitoTimeSegments, string] From fbb43157dcbfcbeb45f434d1e757bce1fa1fc9f4 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 11 May 2024 09:12:04 +0300 Subject: [PATCH 02/11] refactor(kit): parseTimeString --- .../supported-input-types.template.html | 13 ++++++++--- .../src/lib/utils/time/parse-time-string.ts | 22 +++++-------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/projects/demo/src/pages/documentation/supported-input-types/supported-input-types.template.html b/projects/demo/src/pages/documentation/supported-input-types/supported-input-types.template.html index 101adbc8b..b020b9a4d 100644 --- a/projects/demo/src/pages/documentation/supported-input-types/supported-input-types.template.html +++ b/projects/demo/src/pages/documentation/supported-input-types/supported-input-types.template.html @@ -13,15 +13,22 @@

    -
  • selectionStart
  • -
  • selectionEnd
  • -
  • setSelectionRange
  • +
  • + selectionStart +
  • +
  • + selectionEnd +
  • +
  • + setSelectionRange +

According to the diff --git a/projects/kit/src/lib/utils/time/parse-time-string.ts b/projects/kit/src/lib/utils/time/parse-time-string.ts index 95516a870..c361bfcb2 100644 --- a/projects/kit/src/lib/utils/time/parse-time-string.ts +++ b/projects/kit/src/lib/utils/time/parse-time-string.ts @@ -9,11 +9,7 @@ export function parseTimeString( ): Partial { const onlyDigits = timeString.replaceAll(/\D+/g, ''); - const sliceIndexes = createSliceIndexes({ - isStartsWithMilliseconds: timeMode.startsWith('MSS'), - isStartsWithMinutes: timeMode.startsWith('MM'), - isStartsWithSeconds: timeMode.startsWith('SS'), - }); + const sliceIndexes = createSliceIndexes(timeMode); const timeSegments = { hours: onlyDigits.slice(...sliceIndexes.hours), @@ -27,19 +23,11 @@ export function parseTimeString( ); } -function createSliceIndexes({ - isStartsWithMinutes, - isStartsWithSeconds, - isStartsWithMilliseconds, -}: { - isStartsWithMinutes: boolean; - isStartsWithSeconds: boolean; - isStartsWithMilliseconds: boolean; -}): MaskitoTimeSegments<[number, number]> { +function createSliceIndexes(timeMode: string): MaskitoTimeSegments<[number, number]> { const offset = - Number(isStartsWithMinutes) * 2 + - Number(isStartsWithSeconds) * 4 + - Number(isStartsWithMilliseconds) * 6; + Number(timeMode.startsWith('MM')) * 2 + + Number(timeMode.startsWith('SS')) * 4 + + Number(timeMode.startsWith('MSS')) * 6; const changeSelection = (index: number): number => Math.max(index - offset, 0); From 7a487d086f0484941a0ff3de5909cf2b96f66149 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 11 May 2024 09:17:07 +0300 Subject: [PATCH 03/11] docs(demo): added new time modes to api --- projects/demo/src/pages/kit/time/time-mask-doc.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/demo/src/pages/kit/time/time-mask-doc.component.ts b/projects/demo/src/pages/kit/time/time-mask-doc.component.ts index bde0aff1e..2b8067932 100644 --- a/projects/demo/src/pages/kit/time/time-mask-doc.component.ts +++ b/projects/demo/src/pages/kit/time/time-mask-doc.component.ts @@ -49,6 +49,8 @@ export class TimeMaskDocComponent implements GeneratorOptions { 'HH:MM:SS', 'HH:MM:SS.MSS', 'HH', + 'MM:SS.MSS', + 'SS.MSS', ]; protected readonly timeSegmentMaxValuesOptions: Array< From b4eed66f8ef5c2c6382bb4d079f7a1cc9733cd2b Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 11 May 2024 10:24:56 +0300 Subject: [PATCH 04/11] test(demo-integrations): added tests for MM:SS.MSS and SS.MSS --- .../src/tests/kit/time/time-mode.cy.ts | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts b/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts index 7b7c3aa94..f213625e2 100644 --- a/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts +++ b/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts @@ -412,5 +412,241 @@ describe('Time', () => { }); }); }); + + describe('MM:SS.MSS', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.Time}/API?mode=MM:SS.MSS`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .clear() + .as('input'); + }); + + describe('Typing new character overwrite character after cursor', () => { + it('new character is different from the next one', () => { + cy.get('@input') + .type('5959999') + .type('{moveToStart}') + .type('0') + .should('have.value', '09:59.999') + .should('have.prop', 'selectionStart', '0'.length) + .should('have.prop', 'selectionEnd', '0'.length) + .type('000') + .should('have.value', '00:00.999') + .should('have.prop', 'selectionStart', '00:00.'.length) + .should('have.prop', 'selectionEnd', '00:00.'.length) + .type('00') + .should('have.value', '00:00.009') + .should('have.prop', 'selectionStart', '00:00.00'.length) + .should('have.prop', 'selectionEnd', '00:00.00'.length); + }); + + it('moves cursor behind next character if new character is the same with the next one', () => { + cy.get('@input') + .type('59:59.999') + .type('{moveToStart}') + .type('{rightArrow}'.repeat('59:59.'.length)) + .type('9') + .should('have.value', '59:59.999') + .should('have.prop', 'selectionStart', '59:59.9'.length) + .should('have.prop', 'selectionEnd', '59:59.9'.length); + }); + }); + + it('Pad typed value with zero if digit exceeds the first digit of time segment', () => { + cy.get('@input') + .type('6') + .should('have.value', '06') + .should('have.prop', 'selectionStart', '06'.length) + .should('have.prop', 'selectionEnd', '06'.length) + .type('6') + .should('have.value', '06:06') + .should('have.prop', 'selectionStart', '06:06'.length) + .should('have.prop', 'selectionEnd', '06:06'.length) + .type('999') + .should('have.value', '06:06.999') + .should('have.prop', 'selectionStart', '06:06.999'.length) + .should('have.prop', 'selectionEnd', '06:06:999'.length); + }); + + describe('Select range and press new digit', () => { + it( + '|59|:59.999 => Type 2 => 2|0:59.999', + BROWSER_SUPPORTS_REAL_EVENTS, + () => { + cy.get('@input') + .type('5959999') + .should('have.value', '59:59.999') + .realPress([ + ...new Array(':59.999'.length).fill('ArrowLeft'), + 'Shift', + ...new Array('59'.length).fill('ArrowLeft'), + ]); + + cy.get('@input') + .type('2') + .should('have.value', '20:59.999') + .should('have.prop', 'selectionStart', '2'.length) + .should('have.prop', 'selectionEnd', '2'.length); + }, + ); + + it( + '|59|:59.999 => Type 6 => 06:|59.999', + BROWSER_SUPPORTS_REAL_EVENTS, + () => { + cy.get('@input') + .type('5959999') + .should('have.value', '59:59.999') + .realPress([ + ...new Array(':59.999'.length).fill('ArrowLeft'), + 'Shift', + ...new Array('59'.length).fill('ArrowLeft'), + ]); + + cy.get('@input') + .type('6') + .should('have.value', '06:59.999') + .should('have.prop', 'selectionStart', '06:'.length) + .should('have.prop', 'selectionEnd', '06:'.length); + }, + ); + }); + + describe('accepts time segment separators typed by user', () => { + it('59 => Type : => 59:', () => { + cy.get('@input') + .type('59') + .should('have.value', '59') + .type(':') + .should('have.value', '59:') + .should('have.prop', 'selectionStart', '59:'.length) + .should('have.prop', 'selectionEnd', '59:'.length); + }); + + it('59:59 => Type . => 59:59.', () => { + cy.get('@input') + .type('5959') + .should('have.value', '59:59') + .type('.') + .should('have.value', '59:59.') + .should('have.prop', 'selectionStart', '59:59.'.length) + .should('have.prop', 'selectionEnd', '59:59.'.length); + }); + }); + + it('type 5959999 => 59:59.999', () => { + cy.get('@input').type('5959999').should('have.value', '59:59.999'); + }); + }); + + describe('SS.MSS', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.Time}/API?mode=SS.MSS`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .clear() + .as('input'); + }); + + describe('Typing new character overwrite character after cursor', () => { + it('new character is different from the next one', () => { + cy.get('@input') + .type('59999') + .type('{moveToStart}') + .type('0') + .should('have.value', '09.999') + .should('have.prop', 'selectionStart', '0'.length) + .should('have.prop', 'selectionEnd', '0'.length) + .type('0') + .should('have.value', '00.999') + .should('have.prop', 'selectionStart', '00.'.length) + .should('have.prop', 'selectionEnd', '00.'.length) + .type('00') + .should('have.value', '00.009') + .should('have.prop', 'selectionStart', '00.00'.length) + .should('have.prop', 'selectionEnd', '00.00'.length); + }); + + it('moves cursor behind next character if new character is the same with the next one', () => { + cy.get('@input') + .type('59.999') + .type('{moveToStart}') + .type('{rightArrow}'.repeat('59.'.length)) + .type('9') + .should('have.value', '59.999') + .should('have.prop', 'selectionStart', '59.9'.length) + .should('have.prop', 'selectionEnd', '59.9'.length); + }); + }); + + it('Pad typed value with zero if digit exceeds the first digit of time segment', () => { + cy.get('@input') + .type('6') + .should('have.value', '06') + .should('have.prop', 'selectionStart', '06'.length) + .should('have.prop', 'selectionEnd', '06'.length) + .type('999') + .should('have.value', '06.999') + .should('have.prop', 'selectionStart', '06.999'.length) + .should('have.prop', 'selectionEnd', '06:999'.length); + }); + + describe('Select range and press new digit', () => { + it('|59|.999 => Type 2 => 2|0.999', BROWSER_SUPPORTS_REAL_EVENTS, () => { + cy.get('@input') + .type('59999') + .should('have.value', '59.999') + .realPress([ + ...new Array('.999'.length).fill('ArrowLeft'), + 'Shift', + ...new Array('59'.length).fill('ArrowLeft'), + ]); + + cy.get('@input') + .type('2') + .should('have.value', '20.999') + .should('have.prop', 'selectionStart', '2'.length) + .should('have.prop', 'selectionEnd', '2'.length); + }); + + it('|59|.999 => Type 6 => 06.|999', BROWSER_SUPPORTS_REAL_EVENTS, () => { + cy.get('@input') + .type('59999') + .should('have.value', '59.999') + .realPress([ + ...new Array('.999'.length).fill('ArrowLeft'), + 'Shift', + ...new Array('59'.length).fill('ArrowLeft'), + ]); + + cy.get('@input') + .type('6') + .should('have.value', '06.999') + .should('have.prop', 'selectionStart', '06.'.length) + .should('have.prop', 'selectionEnd', '06.'.length); + }); + }); + + describe('accepts time segment separators typed by user', () => { + it('59 => Type . => 59.', () => { + cy.get('@input') + .type('59') + .should('have.value', '59') + .type('.') + .should('have.value', '59.') + .should('have.prop', 'selectionStart', '59.'.length) + .should('have.prop', 'selectionEnd', '59.'.length); + }); + }); + + it('type 59999 => 59.999', () => { + cy.get('@input').type('59999').should('have.value', '59.999'); + }); + }); }); }); From 39aedb0816f7549cce27f6361239f62967f4979e Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 11 May 2024 10:28:02 +0300 Subject: [PATCH 05/11] fix(kit): changed MM:SS.MSS to MM.SS.MSS --- projects/demo/src/pages/kit/time/time-mask-doc.component.ts | 2 +- projects/kit/src/lib/types/time-mode.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/demo/src/pages/kit/time/time-mask-doc.component.ts b/projects/demo/src/pages/kit/time/time-mask-doc.component.ts index 2b8067932..994666a61 100644 --- a/projects/demo/src/pages/kit/time/time-mask-doc.component.ts +++ b/projects/demo/src/pages/kit/time/time-mask-doc.component.ts @@ -49,7 +49,7 @@ export class TimeMaskDocComponent implements GeneratorOptions { 'HH:MM:SS', 'HH:MM:SS.MSS', 'HH', - 'MM:SS.MSS', + 'MM.SS.MSS', 'SS.MSS', ]; diff --git a/projects/kit/src/lib/types/time-mode.ts b/projects/kit/src/lib/types/time-mode.ts index 364d77815..6c247a2e9 100644 --- a/projects/kit/src/lib/types/time-mode.ts +++ b/projects/kit/src/lib/types/time-mode.ts @@ -3,5 +3,5 @@ export type MaskitoTimeMode = | 'HH:MM:SS' | 'HH:MM' | 'HH' - | 'MM:SS.MSS' + | 'MM.SS.MSS' | 'SS.MSS'; From a68386535093764c75878baea7aba95c2720b495 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 11 May 2024 10:33:20 +0300 Subject: [PATCH 06/11] refactor(demo-integrations): changed MM:SS.MSS to MM.SS.MSS --- .../src/tests/kit/time/time-mode.cy.ts | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts b/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts index f213625e2..d415d996a 100644 --- a/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts +++ b/projects/demo-integrations/src/tests/kit/time/time-mode.cy.ts @@ -413,9 +413,9 @@ describe('Time', () => { }); }); - describe('MM:SS.MSS', () => { + describe('MM.SS.MSS', () => { beforeEach(() => { - cy.visit(`/${DemoPath.Time}/API?mode=MM:SS.MSS`); + cy.visit(`/${DemoPath.Time}/API?mode=MM.SS.MSS`); cy.get('#demo-content input') .should('be.visible') .first() @@ -430,28 +430,28 @@ describe('Time', () => { .type('5959999') .type('{moveToStart}') .type('0') - .should('have.value', '09:59.999') + .should('have.value', '09.59.999') .should('have.prop', 'selectionStart', '0'.length) .should('have.prop', 'selectionEnd', '0'.length) .type('000') - .should('have.value', '00:00.999') - .should('have.prop', 'selectionStart', '00:00.'.length) - .should('have.prop', 'selectionEnd', '00:00.'.length) + .should('have.value', '00.00.999') + .should('have.prop', 'selectionStart', '00.00.'.length) + .should('have.prop', 'selectionEnd', '00.00.'.length) .type('00') - .should('have.value', '00:00.009') - .should('have.prop', 'selectionStart', '00:00.00'.length) - .should('have.prop', 'selectionEnd', '00:00.00'.length); + .should('have.value', '00.00.009') + .should('have.prop', 'selectionStart', '00.00.00'.length) + .should('have.prop', 'selectionEnd', '00.00.00'.length); }); it('moves cursor behind next character if new character is the same with the next one', () => { cy.get('@input') - .type('59:59.999') + .type('59.59.999') .type('{moveToStart}') - .type('{rightArrow}'.repeat('59:59.'.length)) + .type('{rightArrow}'.repeat('59.59.'.length)) .type('9') - .should('have.value', '59:59.999') - .should('have.prop', 'selectionStart', '59:59.9'.length) - .should('have.prop', 'selectionEnd', '59:59.9'.length); + .should('have.value', '59.59.999') + .should('have.prop', 'selectionStart', '59.59.9'.length) + .should('have.prop', 'selectionEnd', '59.59.9'.length); }); }); @@ -462,83 +462,83 @@ describe('Time', () => { .should('have.prop', 'selectionStart', '06'.length) .should('have.prop', 'selectionEnd', '06'.length) .type('6') - .should('have.value', '06:06') - .should('have.prop', 'selectionStart', '06:06'.length) - .should('have.prop', 'selectionEnd', '06:06'.length) + .should('have.value', '06.06') + .should('have.prop', 'selectionStart', '06.06'.length) + .should('have.prop', 'selectionEnd', '06.06'.length) .type('999') - .should('have.value', '06:06.999') - .should('have.prop', 'selectionStart', '06:06.999'.length) - .should('have.prop', 'selectionEnd', '06:06:999'.length); + .should('have.value', '06.06.999') + .should('have.prop', 'selectionStart', '06.06.999'.length) + .should('have.prop', 'selectionEnd', '06.06.999'.length); }); describe('Select range and press new digit', () => { it( - '|59|:59.999 => Type 2 => 2|0:59.999', + '|59|.59.999 => Type 2 => 2|0.59.999', BROWSER_SUPPORTS_REAL_EVENTS, () => { cy.get('@input') .type('5959999') - .should('have.value', '59:59.999') + .should('have.value', '59.59.999') .realPress([ - ...new Array(':59.999'.length).fill('ArrowLeft'), + ...new Array('.59.999'.length).fill('ArrowLeft'), 'Shift', ...new Array('59'.length).fill('ArrowLeft'), ]); cy.get('@input') .type('2') - .should('have.value', '20:59.999') + .should('have.value', '20.59.999') .should('have.prop', 'selectionStart', '2'.length) .should('have.prop', 'selectionEnd', '2'.length); }, ); it( - '|59|:59.999 => Type 6 => 06:|59.999', + '|59|.59.999 => Type 6 => 06.|59.999', BROWSER_SUPPORTS_REAL_EVENTS, () => { cy.get('@input') .type('5959999') - .should('have.value', '59:59.999') + .should('have.value', '59.59.999') .realPress([ - ...new Array(':59.999'.length).fill('ArrowLeft'), + ...new Array('.59.999'.length).fill('ArrowLeft'), 'Shift', ...new Array('59'.length).fill('ArrowLeft'), ]); cy.get('@input') .type('6') - .should('have.value', '06:59.999') - .should('have.prop', 'selectionStart', '06:'.length) - .should('have.prop', 'selectionEnd', '06:'.length); + .should('have.value', '06.59.999') + .should('have.prop', 'selectionStart', '06.'.length) + .should('have.prop', 'selectionEnd', '06.'.length); }, ); }); describe('accepts time segment separators typed by user', () => { - it('59 => Type : => 59:', () => { + it('59 => Type . => 59.', () => { cy.get('@input') .type('59') .should('have.value', '59') - .type(':') - .should('have.value', '59:') - .should('have.prop', 'selectionStart', '59:'.length) - .should('have.prop', 'selectionEnd', '59:'.length); + .type('.') + .should('have.value', '59.') + .should('have.prop', 'selectionStart', '59.'.length) + .should('have.prop', 'selectionEnd', '59.'.length); }); - it('59:59 => Type . => 59:59.', () => { + it('59.59 => Type . => 59:59.', () => { cy.get('@input') .type('5959') - .should('have.value', '59:59') + .should('have.value', '59.59') .type('.') - .should('have.value', '59:59.') - .should('have.prop', 'selectionStart', '59:59.'.length) - .should('have.prop', 'selectionEnd', '59:59.'.length); + .should('have.value', '59.59.') + .should('have.prop', 'selectionStart', '59.59.'.length) + .should('have.prop', 'selectionEnd', '59.59.'.length); }); }); - it('type 5959999 => 59:59.999', () => { - cy.get('@input').type('5959999').should('have.value', '59:59.999'); + it('type 5959999 => 59.59.999', () => { + cy.get('@input').type('5959999').should('have.value', '59.59.999'); }); }); From 0bf6b0c5937332bf04eaafd21f84adfe12c080d1 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 18 May 2024 09:08:07 +0300 Subject: [PATCH 07/11] refactor(kit): timeMode is MaskitoTimeMode not a string, added tests for parse time --- .../valid-date-time-preprocessor.ts | 3 +- .../processors/max-validation-preprocessor.ts | 4 +- .../src/lib/utils/time/parse-time-string.ts | 4 +- .../time/tests/parse-time-string.spec.ts | 50 +++++++++++++++++++ .../lib/utils/time/validate-time-string.ts | 4 +- 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 projects/kit/src/lib/utils/time/tests/parse-time-string.spec.ts diff --git a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts index 834cdc95f..23d76f77b 100644 --- a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts @@ -1,6 +1,7 @@ import type {MaskitoPreprocessor} from '@maskito/core'; import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../../constants'; +import {MaskitoTimeMode} from '../../../types'; import {escapeRegExp, validateDateString} from '../../../utils'; import {padTimeSegments, validateTimeString} from '../../../utils/time'; import {parseDateTimeString} from '../utils'; @@ -14,7 +15,7 @@ export function createValidDateTimePreprocessor({ dateModeTemplate: string; dateSegmentsSeparator: string; dateTimeSeparator: string; - timeMode: string; + timeMode: MaskitoTimeMode; }): MaskitoPreprocessor { const invalidCharsRegExp = new RegExp( `[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp( diff --git a/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts b/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts index 1f6353c94..35fa2a8dd 100644 --- a/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts +++ b/projects/kit/src/lib/masks/time/processors/max-validation-preprocessor.ts @@ -1,13 +1,13 @@ import type {MaskitoPreprocessor} from '@maskito/core'; import {TIME_FIXED_CHARACTERS} from '../../../constants'; -import type {MaskitoTimeSegments} from '../../../types'; +import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../../types'; import {escapeRegExp} from '../../../utils'; import {padTimeSegments, validateTimeString} from '../../../utils/time'; export function createMaxValidationPreprocessor( timeSegmentMaxValues: MaskitoTimeSegments, - timeMode: string, + timeMode: MaskitoTimeMode, ): MaskitoPreprocessor { const paddedMaxValues = padTimeSegments(timeSegmentMaxValues); const invalidCharsRegExp = new RegExp( diff --git a/projects/kit/src/lib/utils/time/parse-time-string.ts b/projects/kit/src/lib/utils/time/parse-time-string.ts index c361bfcb2..5b1a42843 100644 --- a/projects/kit/src/lib/utils/time/parse-time-string.ts +++ b/projects/kit/src/lib/utils/time/parse-time-string.ts @@ -1,11 +1,11 @@ -import type {MaskitoTimeSegments} from '../../types'; +import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../types'; /** * @param timeString can be with/without fixed characters */ export function parseTimeString( timeString: string, - timeMode: string, + timeMode: MaskitoTimeMode, ): Partial { const onlyDigits = timeString.replaceAll(/\D+/g, ''); diff --git a/projects/kit/src/lib/utils/time/tests/parse-time-string.spec.ts b/projects/kit/src/lib/utils/time/tests/parse-time-string.spec.ts new file mode 100644 index 000000000..80b9c99b7 --- /dev/null +++ b/projects/kit/src/lib/utils/time/tests/parse-time-string.spec.ts @@ -0,0 +1,50 @@ +import {describe, expect, it} from '@jest/globals'; + +import {parseTimeString} from '../parse-time-string'; + +describe('parseTimeString', () => { + it('HH', () => { + expect(parseTimeString('19', 'HH')).toEqual({ + hours: '19', + }); + }); + + it('HH:MM', () => { + expect(parseTimeString('23:59', 'HH:MM')).toEqual({ + hours: '23', + minutes: '59', + }); + }); + + it('HH:MM:SS', () => { + expect(parseTimeString('12:24:55', 'HH:MM:SS')).toEqual({ + hours: '12', + minutes: '24', + seconds: '55', + }); + }); + + it('HH:MM:SS.MSS', () => { + expect(parseTimeString('10:05:42.783', 'HH:MM:SS.MSS')).toEqual({ + hours: '10', + minutes: '05', + seconds: '42', + milliseconds: '783', + }); + }); + + it('MM.SS.MSS', () => { + expect(parseTimeString('12.30.001', 'MM.SS.MSS')).toEqual({ + minutes: '12', + seconds: '30', + milliseconds: '001', + }); + }); + + it('SS.MSS', () => { + expect(parseTimeString('59.999', 'SS.MSS')).toEqual({ + seconds: '59', + milliseconds: '999', + }); + }); +}); diff --git a/projects/kit/src/lib/utils/time/validate-time-string.ts b/projects/kit/src/lib/utils/time/validate-time-string.ts index b6a34e5e7..b1d2de973 100644 --- a/projects/kit/src/lib/utils/time/validate-time-string.ts +++ b/projects/kit/src/lib/utils/time/validate-time-string.ts @@ -1,5 +1,5 @@ import {TIME_FIXED_CHARACTERS, TIME_SEGMENT_VALUE_LENGTHS} from '../../constants'; -import type {MaskitoTimeSegments} from '../../types'; +import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../types'; import {escapeRegExp} from '../escape-reg-exp'; import {padWithZeroesUntilValid} from '../pad-with-zeroes-until-valid'; import {parseTimeString} from './parse-time-string'; @@ -20,7 +20,7 @@ export function validateTimeString({ paddedMaxValues: MaskitoTimeSegments; offset: number; selection: readonly [number, number]; - timeMode: string; + timeMode: MaskitoTimeMode; }): {validatedTimeString: string; updatedTimeSelection: [number, number]} { const parsedTime = parseTimeString(timeString, timeMode); From f3914afacffa9a54cf340e3e2f04ff241b9ec71f Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 18 May 2024 09:11:14 +0300 Subject: [PATCH 08/11] refactor(kit): parseTimeString --- .../src/lib/utils/time/parse-time-string.ts | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/projects/kit/src/lib/utils/time/parse-time-string.ts b/projects/kit/src/lib/utils/time/parse-time-string.ts index 5b1a42843..310c47be8 100644 --- a/projects/kit/src/lib/utils/time/parse-time-string.ts +++ b/projects/kit/src/lib/utils/time/parse-time-string.ts @@ -1,5 +1,12 @@ import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../types'; +const SEGMENT_FULL_NAME: Record = { + HH: 'hours', + MM: 'minutes', + SS: 'seconds', + MSS: 'milliseconds', +}; + /** * @param timeString can be with/without fixed characters */ @@ -9,32 +16,15 @@ export function parseTimeString( ): Partial { const onlyDigits = timeString.replaceAll(/\D+/g, ''); - const sliceIndexes = createSliceIndexes(timeMode); - - const timeSegments = { - hours: onlyDigits.slice(...sliceIndexes.hours), - minutes: onlyDigits.slice(...sliceIndexes.minutes), - seconds: onlyDigits.slice(...sliceIndexes.seconds), - milliseconds: onlyDigits.slice(...sliceIndexes.milliseconds), - }; + let offset = 0; return Object.fromEntries( - Object.entries(timeSegments).filter(([_, value]) => Boolean(value)), - ); -} - -function createSliceIndexes(timeMode: string): MaskitoTimeSegments<[number, number]> { - const offset = - Number(timeMode.startsWith('MM')) * 2 + - Number(timeMode.startsWith('SS')) * 4 + - Number(timeMode.startsWith('MSS')) * 6; + timeMode.split(/\W/).map(segmentAbbr => { + const segmentValue = onlyDigits.slice(offset, offset + segmentAbbr.length); - const changeSelection = (index: number): number => Math.max(index - offset, 0); + offset += segmentAbbr.length; - return { - hours: [changeSelection(0), changeSelection(2)], - minutes: [changeSelection(2), changeSelection(4)], - seconds: [changeSelection(4), changeSelection(6)], - milliseconds: [changeSelection(6), changeSelection(9)], - }; + return [SEGMENT_FULL_NAME[segmentAbbr], segmentValue]; + }), + ); } From e83b7399fe82b04356df696615fdf70568b19c2e Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 18 May 2024 09:17:44 +0300 Subject: [PATCH 09/11] refactor(kit): toTimeString --- projects/kit/src/lib/utils/time/to-time-string.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/kit/src/lib/utils/time/to-time-string.ts b/projects/kit/src/lib/utils/time/to-time-string.ts index 4b5536086..c41eae849 100644 --- a/projects/kit/src/lib/utils/time/to-time-string.ts +++ b/projects/kit/src/lib/utils/time/to-time-string.ts @@ -1,15 +1,15 @@ import type {MaskitoTimeSegments} from '../../types'; +const LEADING_NON_DIGITS = /^\D*/; +const TRAILING_NON_DIGITS = /\D*$/; + export function toTimeString({ hours = '', minutes = '', seconds = '', milliseconds = '', }: Partial): string { - const mm = hours ? minutes && `:${minutes}` : minutes; - const ss = hours || minutes ? seconds && `:${seconds}` : seconds; - const ms = - hours || minutes || seconds ? milliseconds && `.${milliseconds}` : milliseconds; - - return `${hours}${mm}${ss}${ms}`; + return `${hours}:${minutes}:${seconds}.${milliseconds}` + .replace(LEADING_NON_DIGITS, '') + .replace(TRAILING_NON_DIGITS, ''); } From 84b28187c50b9219284386d4499997d4f4062cf1 Mon Sep 17 00:00:00 2001 From: Stanslav Zaytsev Date: Sat, 18 May 2024 09:41:40 +0300 Subject: [PATCH 10/11] test(kit): toTimeString --- .../utils/time/tests/to-time-string.spec.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 projects/kit/src/lib/utils/time/tests/to-time-string.spec.ts diff --git a/projects/kit/src/lib/utils/time/tests/to-time-string.spec.ts b/projects/kit/src/lib/utils/time/tests/to-time-string.spec.ts new file mode 100644 index 000000000..61156eaa7 --- /dev/null +++ b/projects/kit/src/lib/utils/time/tests/to-time-string.spec.ts @@ -0,0 +1,133 @@ +import {toTimeString} from '../to-time-string'; + +describe('toTimeString', () => { + describe('HH', () => { + it('21', () => { + expect(toTimeString({hours: '21'})).toBe('21'); + }); + + it('1', () => { + expect(toTimeString({hours: '1'})).toBe('1'); + }); + }); + + describe('HH:MM', () => { + it('21:59', () => { + expect(toTimeString({hours: '21', minutes: '59'})).toBe('21:59'); + }); + + it('12:4', () => { + expect(toTimeString({hours: '12', minutes: '4'})).toBe('12:4'); + }); + }); + + describe('HH:MM:SS', () => { + it('21:59:23', () => { + expect(toTimeString({hours: '21', minutes: '59', seconds: '23'})).toBe( + '21:59:23', + ); + }); + + it('01:23:5', () => { + expect(toTimeString({hours: '01', minutes: '23', seconds: '5'})).toBe( + '01:23:5', + ); + }); + }); + + describe('HH:MM:SS.MSS', () => { + it('21:59:23.111', () => { + expect( + toTimeString({ + hours: '21', + minutes: '59', + seconds: '23', + milliseconds: '111', + }), + ).toBe('21:59:23.111'); + }); + + it('01:23:52.1', () => { + expect( + toTimeString({ + hours: '01', + minutes: '23', + seconds: '52', + milliseconds: '1', + }), + ).toBe('01:23:52.1'); + }); + + it('13:13:13.15', () => { + expect( + toTimeString({ + hours: '13', + minutes: '13', + seconds: '13', + milliseconds: '15', + }), + ).toBe('13:13:13.15'); + }); + }); + + describe('MM:SS.MSS', () => { + it('12:12.111', () => { + expect( + toTimeString({ + minutes: '12', + seconds: '12', + milliseconds: '111', + }), + ).toBe('12:12.111'); + }); + + it('23:01.9', () => { + expect( + toTimeString({ + minutes: '23', + seconds: '01', + milliseconds: '9', + }), + ).toBe('23:01.9'); + }); + + it('00:02.91', () => { + expect( + toTimeString({ + minutes: '00', + seconds: '02', + milliseconds: '91', + }), + ).toBe('00:02.91'); + }); + }); + + describe('SS.MSS', () => { + it('12.111', () => { + expect( + toTimeString({ + seconds: '12', + milliseconds: '111', + }), + ).toBe('12.111'); + }); + + it('01.9', () => { + expect( + toTimeString({ + seconds: '01', + milliseconds: '9', + }), + ).toBe('01.9'); + }); + + it('02.91', () => { + expect( + toTimeString({ + seconds: '02', + milliseconds: '91', + }), + ).toBe('02.91'); + }); + }); +}); From 5aade8c2fdd81005b80cc30daa61ae457fe881a5 Mon Sep 17 00:00:00 2001 From: taiga-family-bot Date: Sat, 18 May 2024 06:45:12 +0000 Subject: [PATCH 11/11] chore: apply changes after linting [bot] --- .../date-time/preprocessors/valid-date-time-preprocessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts index 23d76f77b..f48750bf2 100644 --- a/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts +++ b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts @@ -1,7 +1,7 @@ import type {MaskitoPreprocessor} from '@maskito/core'; import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../../constants'; -import {MaskitoTimeMode} from '../../../types'; +import type {MaskitoTimeMode} from '../../../types'; import {escapeRegExp, validateDateString} from '../../../utils'; import {padTimeSegments, validateTimeString} from '../../../utils/time'; import {parseDateTimeString} from '../utils';