From 6d6b2c17b5c0f62bc804451524cd4b2ce3e50660 Mon Sep 17 00:00:00 2001 From: Vladimir Potekhin <46284632+vladimirpotekhin@users.noreply.github.com> Date: Tue, 28 Feb 2023 13:51:16 +0300 Subject: [PATCH] feat(kit): new `DateTime` mask (#146) --- .../tests/kit/date-time/date-time-basic.cy.ts | 301 ++++++++++++++++++ .../kit/date-time/date-time-min-max.cy.ts | 85 +++++ .../tests/kit/date-time/date-time-mode.cy.ts | 158 +++++++++ .../kit/date-time/date-time-separator.cy.ts | 49 +++ .../cypress/tests/kit/date/date-basic.cy.ts | 8 + projects/demo/src/app/app.routes.ts | 10 + projects/demo/src/app/demo-path.ts | 1 + .../date-time/date-time-mask-doc.component.ts | 76 +++++ .../date-time/date-time-mask-doc.module.ts | 33 ++ .../date-time-mask-doc.template.html | 134 ++++++++ .../1-date-time-localization/component.ts | 28 ++ .../examples/1-date-time-localization/mask.ts | 7 + .../date-time/examples/2-min-max/component.ts | 28 ++ .../kit/date-time/examples/2-min-max/mask.ts | 9 + projects/demo/src/pages/pages.ts | 6 + projects/kit/src/index.ts | 8 +- .../default-time-segment-max-values.ts | 0 projects/kit/src/lib/constants/index.ts | 3 + .../lib/constants/possible-dates-separator.ts | 2 + .../constants/time-fixed-characters.ts | 0 .../constants/time-segment-value-lengths.ts | 0 .../min-max-range-length-postprocessor.ts | 4 +- .../constants/date-time-separator.ts | 1 + .../lib/masks/date-time/constants/index.ts | 1 + .../src/lib/masks/date-time/date-time-mask.ts | 50 +++ projects/kit/src/lib/masks/date-time/index.ts | 1 + .../masks/date-time/postprocessors/index.ts | 1 + .../min-max-date-time-postprocessor.ts | 52 +++ .../masks/date-time/preprocessors/index.ts | 1 + .../valid-date-time-preprocessor.ts | 103 ++++++ .../src/lib/masks/date-time/utils/index.ts | 2 + .../utils/is-date-time-string-complete.ts | 16 + .../date-time/utils/parse-date-time-string.ts | 17 + .../kit/src/lib/masks/time/constants/index.ts | 3 - projects/kit/src/lib/masks/time/index.ts | 1 - .../processors/max-validation-preprocessor.ts | 57 +--- projects/kit/src/lib/masks/time/time-mask.ts | 4 +- .../kit/src/lib/masks/time/types/index.ts | 2 - .../processors/min-max-date-postprocessor.ts | 4 +- .../lib/processors/valid-date-preprocessor.ts | 4 +- projects/kit/src/lib/types/index.ts | 2 + .../lib/{masks/time => }/types/time-mode.ts | 0 .../{masks/time => }/types/time-segments.ts | 0 .../src/lib/utils/date/date-to-segments.ts | 8 +- ...ompleted.ts => is-date-string-complete.ts} | 2 +- .../src/lib/utils/date/segments-to-date.ts | 18 +- .../kit/src/lib/utils/date/to-date-string.ts | 25 +- projects/kit/src/lib/utils/index.ts | 2 +- .../{masks/time/utils => utils/time}/index.ts | 1 + .../utils => utils/time}/pad-time-segments.ts | 6 +- .../utils => utils/time}/parse-time-string.ts | 4 +- .../utils => utils/time}/to-time-string.ts | 2 +- .../lib/utils/time/validate-time-string.ts | 67 ++++ 53 files changed, 1331 insertions(+), 76 deletions(-) create mode 100644 projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts create mode 100644 projects/demo-integrations/cypress/tests/kit/date-time/date-time-min-max.cy.ts create mode 100644 projects/demo-integrations/cypress/tests/kit/date-time/date-time-mode.cy.ts create mode 100644 projects/demo-integrations/cypress/tests/kit/date-time/date-time-separator.cy.ts create mode 100644 projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts create mode 100644 projects/demo/src/pages/kit/date-time/date-time-mask-doc.module.ts create mode 100644 projects/demo/src/pages/kit/date-time/date-time-mask-doc.template.html create mode 100644 projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/component.ts create mode 100644 projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/mask.ts create mode 100644 projects/demo/src/pages/kit/date-time/examples/2-min-max/component.ts create mode 100644 projects/demo/src/pages/kit/date-time/examples/2-min-max/mask.ts rename projects/kit/src/lib/{masks/time => }/constants/default-time-segment-max-values.ts (100%) rename projects/kit/src/lib/{masks/time => }/constants/time-fixed-characters.ts (100%) rename projects/kit/src/lib/{masks/time => }/constants/time-segment-value-lengths.ts (100%) create mode 100644 projects/kit/src/lib/masks/date-time/constants/date-time-separator.ts create mode 100644 projects/kit/src/lib/masks/date-time/constants/index.ts create mode 100644 projects/kit/src/lib/masks/date-time/date-time-mask.ts create mode 100644 projects/kit/src/lib/masks/date-time/index.ts create mode 100644 projects/kit/src/lib/masks/date-time/postprocessors/index.ts create mode 100644 projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts create mode 100644 projects/kit/src/lib/masks/date-time/preprocessors/index.ts create mode 100644 projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts create mode 100644 projects/kit/src/lib/masks/date-time/utils/index.ts create mode 100644 projects/kit/src/lib/masks/date-time/utils/is-date-time-string-complete.ts create mode 100644 projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts delete mode 100644 projects/kit/src/lib/masks/time/constants/index.ts delete mode 100644 projects/kit/src/lib/masks/time/types/index.ts rename projects/kit/src/lib/{masks/time => }/types/time-mode.ts (100%) rename projects/kit/src/lib/{masks/time => }/types/time-segments.ts (100%) rename projects/kit/src/lib/utils/date/{is-date-string-completed.ts => is-date-string-complete.ts} (85%) rename projects/kit/src/lib/{masks/time/utils => utils/time}/index.ts (72%) rename projects/kit/src/lib/{masks/time/utils => utils/time}/pad-time-segments.ts (79%) rename projects/kit/src/lib/{masks/time/utils => utils/time}/parse-time-string.ts (82%) rename projects/kit/src/lib/{masks/time/utils => utils/time}/to-time-string.ts (86%) create mode 100644 projects/kit/src/lib/utils/time/validate-time-string.ts 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 new file mode 100644 index 000000000..2691c66fa --- /dev/null +++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-basic.cy.ts @@ -0,0 +1,301 @@ +import {DemoPath} from '@demo/path'; + +describe('DateTime | Basic', () => { + beforeEach(() => { + cy.visit( + `/${DemoPath.DateTime}/API?dateMode=dd%2Fmm%2Fyyyy&timeMode=HH:MM:SS.MSS`, + ); + cy.get('#demo-content input').should('be.visible').first().focus().as('input'); + }); + + describe('basic typing', () => { + const tests = [ + // [Typed value, Masked value] + ['1', '1'], + ['18', '18'], + ['181', '18.1'], + ['1811', '18.11'], + ['18112', '18.11.2'], + ['18112016', '18.11.2016'], + ['181120162', '18.11.2016, 2'], + ['1811201623', '18.11.2016, 23'], + ['18112016231', '18.11.2016, 23:1'], + ['181120162315', '18.11.2016, 23:15'], + ['1811201623152', '18.11.2016, 23:15:2'], + ['18112016231522', '18.11.2016, 23:15:22'], + ['18112016231522123', '18.11.2016, 23:15:22.123'], + ] as const; + + tests.forEach(([typedValue, maskedValue]) => { + it(`Type "${typedValue}" => "${maskedValue}"`, () => { + cy.get('@input') + .type(typedValue) + .should('have.value', maskedValue) + .should('have.prop', 'selectionStart', maskedValue.length) + .should('have.prop', 'selectionEnd', maskedValue.length); + }); + }); + }); + + describe('invalid date cases', () => { + it('Empty input => Type "9" => "09|"', () => { + cy.get('@input') + .type('9') + .should('have.value', '09') + .should('have.prop', 'selectionStart', '09'.length) + .should('have.prop', 'selectionEnd', '09'.length); + }); + + it('27| => type 2 => 27.02|', () => { + cy.get('@input') + .type('27') + .should('have.value', '27') + .should('have.prop', 'selectionStart', '27'.length) + .should('have.prop', 'selectionEnd', '27'.length) + .type('2') + .should('have.value', '27.02') + .should('have.prop', 'selectionStart', '27.02'.length) + .should('have.prop', 'selectionEnd', '27.02'.length); + }); + + it('3| => Type 7 => no value changes', () => { + cy.get('@input') + .type('3') + .should('have.value', '3') + .should('have.prop', 'selectionStart', '3'.length) + .should('have.prop', 'selectionEnd', '3'.length) + .type('7') + .should('have.value', '3') + .should('have.prop', 'selectionStart', '3'.length) + .should('have.prop', 'selectionEnd', '3'.length); + }); + }); + + describe('invalid time cases', () => { + beforeEach(() => { + cy.get('@input').type('10102020'); + }); + it('"10.10.2020" => Type "9" => "10.10.2020, 09|"', () => { + cy.get('@input') + .type('9') + .should('have.value', '10.10.2020, 09') + .should('have.prop', 'selectionStart', '10.10.2020, 09'.length) + .should('have.prop', 'selectionEnd', '10.10.2020, 09'.length); + }); + + it('"10.10.2020, 10" => Type "9" => "10.10.2020, 10:09|"', () => { + cy.get('@input') + .type('10') + .should('have.value', '10.10.2020, 10') + .should('have.prop', 'selectionStart', '10.10.2020, 10'.length) + .should('have.prop', 'selectionEnd', '10.10.2020, 10'.length) + .type('9') + .should('have.value', '10.10.2020, 10:09') + .should('have.prop', 'selectionStart', '10.10.2020, 10:09'.length) + .should('have.prop', 'selectionEnd', '10.10.2020, 10:09'.length); + }); + + it('"10.10.2020, 2" => Type "7" => no value changes', () => { + cy.get('@input') + .type('2') + .should('have.value', '10.10.2020, 2') + .should('have.prop', 'selectionStart', '10.10.2020, 2'.length) + .should('have.prop', 'selectionEnd', '10.10.2020, 2'.length) + .type('7') + .should('have.value', '10.10.2020, 2') + .should('have.prop', 'selectionStart', '10.10.2020, 2'.length) + .should('have.prop', 'selectionEnd', '10.10.2020, 2'.length); + }); + }); + + describe('basic erasing (value = "20.01.1990, 15:40:20" & caret is placed after the last value)', () => { + beforeEach(() => { + cy.get('@input').type('20011990154020'); + }); + + const tests = [ + // [How many times "Backspace"-key was pressed, caretPosition, Masked value] + [1, '20.01.1990, 15:40:2'.length, '20.01.1990, 15:40:2'], + [4, '20.01.1990, 15'.length, '20.01.1990, 15'], + [5, '20.01.1990, 1'.length, '20.01.1990, 1'], + [6, '20.01.1990'.length, '20.01.1990'], + [8, '20.01.19'.length, '20.01.19'], + [12, '20'.length, '20'], + [13, '2'.length, '2'], + ] as const; + + tests.forEach(([n, caretIndex, maskedValue]) => { + it(`Backspace x${n} => "${maskedValue}"`, () => { + cy.get('@input') + .type('{backspace}'.repeat(n)) + .should('have.value', maskedValue) + .should('have.prop', 'selectionStart', caretIndex) + .should('have.prop', 'selectionEnd', caretIndex); + }); + }); + + it('Delete => no value change && no caret index change', () => { + cy.get('@input') + .type('{del}') + .should('have.value', '20.01.1990, 15:40:20') + .should('have.prop', 'selectionStart', '20.01.1990, 15:40:20'.length) + .should('have.prop', 'selectionEnd', '20.01.1990, 15:40:20'.length); + }); + }); + + describe('Editing somewhere in the middle of a value (NOT the last character)', () => { + it('25.02.19|99, 15:35 => Backspace => 25.02.1|099, 15:35 => Type "8" => 25.02.18|99, 15:35', () => { + cy.get('@input') + .type('250219991535') + .should('have.value', '25.02.1999, 15:35') + .type('{leftArrow}'.repeat('99, 15:35'.length)) + .should('have.prop', 'selectionStart', '25.02.19'.length) + .should('have.prop', 'selectionEnd', '25.02.19'.length) + .type('{backspace}') + .should('have.value', '25.02.1099, 15:35') + .should('have.prop', 'selectionStart', '25.02.1'.length) + .should('have.prop', 'selectionEnd', '25.02.1'.length) + .type('8') + .should('have.value', '25.02.1899, 15:35') + .should('have.prop', 'selectionStart', '25.02.18'.length) + .should('have.prop', 'selectionEnd', '25.02.18'.length); + }); + + it('13.06.1736, 15:05|:20 => Backspace => 13.06.1736, 15:0|0:20 => Type "3" => 13.06.1736, 15:03:20', () => { + cy.get('@input') + .type('13061736150520') + .should('have.value', '13.06.1736, 15:05:20') + .type('{leftArrow}'.repeat(':20'.length)) + .should('have.prop', 'selectionStart', '13.06.1736, 15:05'.length) + .should('have.prop', 'selectionEnd', '13.06.1736, 15:05'.length) + .type('{backspace}') + .should('have.value', '13.06.1736, 15:00:20') + .should('have.prop', 'selectionStart', '13.06.1736, 15:0'.length) + .should('have.prop', 'selectionEnd', '13.06.1736, 15:0'.length) + .type('3') + .should('have.value', '13.06.1736, 15:03:20') + .should('have.prop', 'selectionStart', '13.06.1736, 15:03:'.length) + .should('have.prop', 'selectionEnd', '13.06.1736, 15:03:'.length); + }); + + it('12.|12.2010, 12:30 => Type "9" => 12.09.|2010, 12:30', () => { + cy.get('@input') + .type('121220101230') + .should('have.value', '12.12.2010, 12:30') + .type('{leftArrow}'.repeat('12.2010, 12:30'.length)) + .should('have.prop', 'selectionStart', '12.'.length) + .should('have.prop', 'selectionEnd', '12.'.length) + .type('9') + .should('have.value', '12.09.2010, 12:30') + .should('have.prop', 'selectionStart', '12.09.'.length) + .should('have.prop', 'selectionEnd', '12.09.'.length); + }); + + it('12.12.2010, |12:30 => Type "9" => 12.12.2010, 09|:30', () => { + cy.get('@input') + .type('121220101230') + .should('have.value', '12.12.2010, 12:30') + .type('{leftArrow}'.repeat('12:30'.length)) + .should('have.prop', 'selectionStart', '12.12.2010, '.length) + .should('have.prop', 'selectionEnd', '12.12.2010, '.length) + .type('9') + .should('have.value', '12.12.2010, 09:30') + .should('have.prop', 'selectionStart', '12.12.2010, 09:'.length) + .should('have.prop', 'selectionEnd', '12.12.2010, 09:'.length); + }); + }); + + describe('Text selection', () => { + describe('Select range and press Backspace / Delete', () => { + it('10.|12|.2005, 12:30 => Backspace => 10.|00.2005, 12:30', () => { + cy.get('@input') + .type('101220051230') + .should('have.value', '10.12.2005, 12:30') + .type('{leftArrow}'.repeat('.2005, 12:30'.length)) + .realPress([ + 'Shift', + ...Array('12'.length).fill('ArrowLeft'), + 'Backspace', + ]); + + cy.get('@input') + .should('have.value', '10.00.2005, 12:30') + .should('have.prop', 'selectionStart', '10.'.length) + .should('have.prop', 'selectionEnd', '10.'.length); + }); + + it('10.12.2005, |12|:30 => Backspace => 10.12.2005, |00:30', () => { + cy.get('@input') + .type('101220051230') + .should('have.value', '10.12.2005, 12:30') + .type('{leftArrow}'.repeat(':30'.length)) + .realPress([ + 'Shift', + ...Array('12'.length).fill('ArrowLeft'), + 'Backspace', + ]); + + cy.get('@input') + .should('have.value', '10.12.2005, 00:30') + .should('have.prop', 'selectionStart', '10.12.2005, '.length) + .should('have.prop', 'selectionEnd', '10.12.2005, '.length); + }); + + it('1|1.1|1.2011, 12:30 => Delete => 10.0|1.2011, 12:30', () => { + cy.get('@input') + .type('111120111230') + .should('have.value', '11.11.2011, 12:30') + .type('{leftArrow}'.repeat('1.2011, 12:30'.length)) + .realPress(['Shift', ...Array('1.1'.length).fill('ArrowLeft')]); + + cy.get('@input') + .type('{del}') + .should('have.value', '10.01.2011, 12:30') + .should('have.prop', 'selectionStart', '10.0'.length) + .should('have.prop', 'selectionEnd', '10.0'.length); + }); + + it('11.11.2011, 1|2:3|0 => Delete => 11.11.2011, 10:0|0', () => { + cy.get('@input') + .type('111120111230') + .should('have.value', '11.11.2011, 12:30') + .type('{leftArrow}'.repeat('0'.length)) + .realPress(['Shift', ...Array('2.3'.length).fill('ArrowLeft')]); + + cy.get('@input') + .type('{del}') + .should('have.value', '11.11.2011, 10:00') + .should('have.prop', 'selectionStart', '11.11.2011, 10:0'.length) + .should('have.prop', 'selectionEnd', '11.11.2011, 10:0'.length); + }); + }); + + describe('Select range and press new digit', () => { + it('|12|.11.2022 (specifically do not completes value) => Press 3 => 3|0.11.2022', () => { + cy.get('@input') + .type('12112022') + .type('{leftArrow}'.repeat('.11.2022'.length)) + .realPress(['Shift', ...Array('12'.length).fill('ArrowLeft')]); + + cy.get('@input') + .type('3') + .should('have.value', '30.11.2022') + .should('have.prop', 'selectionStart', '3'.length) + .should('have.prop', 'selectionEnd', '3'.length); + }); + + it('01.01.2000, |12|:30 => Press 2 => 01.01.2000, 2|0:30', () => { + cy.get('@input') + .type('010120001230') + .type('{leftArrow}'.repeat(':30'.length)) + .realPress(['Shift', ...Array('12'.length).fill('ArrowLeft')]); + + cy.get('@input') + .type('2') + .should('have.value', '01.01.2000, 20:30') + .should('have.prop', 'selectionStart', '01.01.2000, 2'.length) + .should('have.prop', 'selectionEnd', '01.01.2000, 2'.length); + }); + }); + }); +}); diff --git a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-min-max.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-min-max.cy.ts new file mode 100644 index 000000000..cb2410b8a --- /dev/null +++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-min-max.cy.ts @@ -0,0 +1,85 @@ +import {DemoPath} from '@demo/path'; + +describe('DateTime | Min & Max dates', () => { + describe('Max', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?max=2020-05-05T12:20`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('accepts date less than max value', () => { + cy.get('@input') + .type('18.12.2019,23:50') + .should('have.value', '18.12.2019, 23:50') + .should('have.prop', 'selectionStart', '18.12.2019, 23:50'.length) + .should('have.prop', 'selectionEnd', '18.12.2019, 23:50'.length); + }); + + it('05.05.2020, 12:2| => Type 5 => 05.05.2020, 12:20 (max value)', () => { + cy.get('@input') + .type('05.05.2020,12:2') + .should('have.value', '05.05.2020, 12:2') + .should('have.prop', 'selectionStart', '05.05.2020, 12:2'.length) + .should('have.prop', 'selectionEnd', '05.05.2020, 12:2'.length) + .type('5') + .should('have.value', '05.05.2020, 12:20') + .should('have.prop', 'selectionStart', '05.05.2020, 12:20'.length) + .should('have.prop', 'selectionEnd', '05.05.2020, 12:20'.length); + }); + + it('18.12.20|19, 12:20 => Type 2 => 05.05.202|0, 12:20 (max value)', () => { + cy.get('@input') + .type('18.12.2019,12:20') + .type('{leftArrow}'.repeat('19, 12:20'.length)) + .type('2') + .should('have.value', '05.05.2020, 12:20') + .should('have.prop', 'selectionStart', '05.05.202'.length) + .should('have.prop', 'selectionEnd', '05.05.202'.length); + }); + }); + + describe('Min', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?min=1995-10-14T15:32`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('accepts date more than min value', () => { + cy.get('@input') + .type('13.04.2001,11:23') + .should('have.value', '13.04.2001, 11:23') + .should('have.prop', 'selectionStart', '13.04.2001, 11:23'.length) + .should('have.prop', 'selectionEnd', '13.04.2001, 11:23'.length); + }); + + it('14.10.1995, 15:3| => Type 1 => 14.10.1995, 15:32 (min)', () => { + cy.get('@input') + .type('14.10.1995,15:3') + .should('have.value', '14.10.1995, 15:3') + .should('have.prop', 'selectionStart', '14.10.1995, 15:3'.length) + .should('have.prop', 'selectionEnd', '14.10.1995, 15:3'.length) + .type('1') + .should('have.value', '14.10.1995, 15:32') + .should('have.prop', 'selectionStart', '14.10.1995, 15:32'.length) + .should('have.prop', 'selectionEnd', '14.10.1995, 15:32'.length); + }); + + it('14.|10.1995, 10:20 => Type 9 => 14.10.|1995, 15:32 (min)', () => { + cy.get('@input') + .type('14.10.1995,10:20') + .type('{leftArrow}'.repeat('10.1995, 10:20'.length)) + .type('9') + .should('have.value', '14.10.1995, 15:32') + .should('have.prop', 'selectionStart', '14.10.'.length) + .should('have.prop', 'selectionEnd', '14.10.'.length); + }); + }); +}); diff --git a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-mode.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-mode.cy.ts new file mode 100644 index 000000000..954b14187 --- /dev/null +++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-mode.cy.ts @@ -0,0 +1,158 @@ +import {DemoPath} from '@demo/path'; + +describe('DateTime | mode', () => { + describe('Date mode', () => { + describe('mm.dd.yyyy', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?dateMode=mm%2Fdd%2Fyyyy`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('12.31.2000, 13:20', () => { + cy.get('@input') + .type('123120001320') + .should('have.value', '12.31.2000, 13:20') + .should('have.prop', 'selectionStart', '12.31.2000, 13:20'.length) + .should('have.prop', 'selectionEnd', '12.31.2000, 13:20'.length); + }); + + it('Empty input => Type 3 => 03|', () => { + cy.get('@input') + .type('3') + .should('have.value', '03') + .should('have.prop', 'selectionStart', '03'.length) + .should('have.prop', 'selectionEnd', '03'.length); + }); + + it('12| => Type 3 => 12.3|', () => { + cy.get('@input') + .type('123') + .should('have.value', '12.3') + .should('have.prop', 'selectionStart', '12.3'.length) + .should('have.prop', 'selectionEnd', '12.3'.length); + }); + + it('12| => Type 4 => 12.04|', () => { + cy.get('@input') + .type('124') + .should('have.value', '12.04') + .should('have.prop', 'selectionStart', '12.04'.length) + .should('have.prop', 'selectionEnd', '12.04'.length); + }); + }); + + describe('yyyy.mm.dd', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?dateMode=yyyy%2Fmm%2Fdd`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('2000.12.31, 12:20', () => { + cy.get('@input') + .type('20001231,1220') + .should('have.value', '2000.12.31, 12:20') + .should('have.prop', 'selectionStart', '2000.12.31, 12:20'.length) + .should('have.prop', 'selectionEnd', '2000.12.31, 12:20'.length); + }); + + it('2000| => Type 3 => 2000.03|', () => { + cy.get('@input') + .type('20003') + .should('have.value', '2000.03') + .should('have.prop', 'selectionStart', '2000.03'.length) + .should('have.prop', 'selectionEnd', '2000.03'.length); + }); + + it('2000.03| => Type 5 => 2000.03.05|', () => { + cy.get('@input') + .type('200035') + .should('have.value', '2000.03.05') + .should('have.prop', 'selectionStart', '2000.03.05'.length) + .should('have.prop', 'selectionEnd', '2000.03.05'.length); + }); + }); + }); + + describe('Time', () => { + describe('HH:MM', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?timeMode=HH:MM`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('12.01.2000, 13:20 => type 12 => no value changes', () => { + cy.get('@input') + .type('120120001320') + .should('have.value', '12.01.2000, 13:20') + .should('have.prop', 'selectionStart', '12.01.2000, 13:20'.length) + .should('have.prop', 'selectionEnd', '12.01.2000, 13:20'.length) + .type('12') + .should('have.value', '12.01.2000, 13:20') + .should('have.prop', 'selectionStart', '12.01.2000, 13:20'.length) + .should('have.prop', 'selectionEnd', '12.01.2000, 13:20'.length); + }); + }); + + describe('HH:MM:SS', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?timeMode=HH:MM:SS`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('12.01.2000, 13:20:30 => type 12 => no value changes', () => { + cy.get('@input') + .type('12012000132030') + .should('have.value', '12.01.2000, 13:20:30') + .should('have.prop', 'selectionStart', '12.01.2000, 13:20:30'.length) + .should('have.prop', 'selectionEnd', '12.01.2000, 13:20:30'.length) + .type('12') + .should('have.value', '12.01.2000, 13:20:30') + .should('have.prop', 'selectionStart', '12.01.2000, 13:20:30'.length) + .should('have.prop', 'selectionEnd', '12.01.2000, 13:20:30'.length); + }); + }); + + describe('HH:MM:SS.MSS', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?timeMode=HH:MM:SS.MSS`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('12.01.2000, 13:20:30.123', () => { + cy.get('@input') + .type('12012000132030123') + .should('have.value', '12.01.2000, 13:20:30.123') + .should( + 'have.prop', + 'selectionStart', + '12.01.2000, 13:20:30.123'.length, + ) + .should( + 'have.prop', + 'selectionEnd', + '12.01.2000, 13:20:30.123'.length, + ); + }); + }); + }); +}); diff --git a/projects/demo-integrations/cypress/tests/kit/date-time/date-time-separator.cy.ts b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-separator.cy.ts new file mode 100644 index 000000000..221205e60 --- /dev/null +++ b/projects/demo-integrations/cypress/tests/kit/date-time/date-time-separator.cy.ts @@ -0,0 +1,49 @@ +import {DemoPath} from '@demo/path'; + +describe('DateTime | Separator', () => { + describe('/', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?dateSeparator=/`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('14/12/1997', () => { + cy.get('@input').type('14121997').should('have.value', '14/12/1997'); + }); + + it('rejects dot as separator', () => { + cy.get('@input') + .type('1412.') + .should('have.value', '14/12') + .type('2000') + .should('have.value', '14/12/2000'); + }); + }); + + describe('-', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateTime}/API?dateSeparator=-`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + it('14-12-1997', () => { + cy.get('@input').type('14121997').should('have.value', '14-12-1997'); + }); + + it('rejects dot as separator', () => { + cy.get('@input') + .type('14') + .should('have.value', '14') + .type('12.') + .should('have.value', '14-12'); + }); + }); +}); 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 95431e6bc..ae8bdeeae 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 @@ -61,6 +61,14 @@ describe('Date', () => { .should('have.prop', 'selectionStart', '3'.length) .should('have.prop', 'selectionEnd', '3'.length); }); + + it('year less than 100', () => { + cy.get('@input') + .type('10100012') + .should('have.value', '10.10.0012') + .should('have.prop', 'selectionStart', '10.10.0012'.length) + .should('have.prop', 'selectionEnd', '10.10.0012'.length); + }); }); describe('basic erasing (value = "10.11.2002" & caret is placed after the last value)', () => { diff --git a/projects/demo/src/app/app.routes.ts b/projects/demo/src/app/app.routes.ts index d8f01835c..b1ee862cb 100644 --- a/projects/demo/src/app/app.routes.ts +++ b/projects/demo/src/app/app.routes.ts @@ -108,6 +108,16 @@ export const appRoutes: Routes = [ }, }, // Recipes + { + path: DemoPath.DateTime, + loadChildren: async () => + import(`../pages/kit/date-time/date-time-mask-doc.module`).then( + m => m.DateTimeMaskDocModule, + ), + data: { + title: `DateTime`, + }, + }, { path: DemoPath.Card, loadChildren: async () => diff --git a/projects/demo/src/app/demo-path.ts b/projects/demo/src/app/demo-path.ts index 81ec073c6..48ce12687 100644 --- a/projects/demo/src/app/demo-path.ts +++ b/projects/demo/src/app/demo-path.ts @@ -9,6 +9,7 @@ export const enum DemoPath { Time = 'kit/time', Date = 'kit/date', DateRange = 'kit/date-range', + DateTime = 'kit/date-time', Card = 'recipes/card', Phone = 'recipes/phone', Textarea = 'recipes/textarea', diff --git a/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts new file mode 100644 index 000000000..a2ebe5b6a --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.component.ts @@ -0,0 +1,76 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormControl} from '@angular/forms'; +import {MaskitoOptions} from '@maskito/core'; +import { + MaskitoDateMode, + maskitoDateTimeOptionsGenerator, + MaskitoTimeMode, +} from '@maskito/kit'; +import {TuiDocExample} from '@taiga-ui/addon-doc'; +import {CHAR_NO_BREAK_SPACE, tuiPure} from '@taiga-ui/cdk'; + +type GeneratorOptions = Required< + NonNullable[0]> +>; + +@Component({ + selector: 'date-time-mask-doc', + templateUrl: './date-time-mask-doc.template.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DateTimeMaskDocComponent implements GeneratorOptions { + readonly dateTimeLocalization: TuiDocExample = { + MaskitoOptions: import('./examples/1-date-time-localization/mask.ts?raw'), + }; + + readonly dateTimeMinMax: TuiDocExample = { + MaskitoOptions: import('./examples/2-min-max/mask.ts?raw'), + }; + + apiPageControl = new FormControl(''); + + readonly dateModeOptions: MaskitoDateMode[] = [ + `dd/mm/yyyy`, + `mm/dd/yyyy`, + `yyyy/mm/dd`, + ]; + + readonly timeModeOptions: MaskitoTimeMode[] = [`HH:MM`, `HH:MM:SS`, `HH:MM:SS.MSS`]; + readonly minMaxOptions = [ + '0001-01-01T00:00:00', + '9999-12-31T23:59:59', + '2000-01-01T12:30', + '2025-05-10T18:30', + ]; + + dateMode: MaskitoDateMode = this.dateModeOptions[0]; + timeMode: MaskitoTimeMode = this.timeModeOptions[0]; + dateSeparator = '.'; + minStr = this.minMaxOptions[0]; + maxStr = this.minMaxOptions[1]; + min = new Date(this.minStr); + max = new Date(this.maxStr); + + maskitoOptions: MaskitoOptions = maskitoDateTimeOptionsGenerator(this); + + @tuiPure + getPlaceholder( + dateMode: MaskitoDateMode, + timeMode: MaskitoTimeMode, + separator: string, + ): string { + const dateTimeSep = `,${CHAR_NO_BREAK_SPACE}`; + + return `${dateMode.replace(/\//g, separator)}${dateTimeSep}${timeMode}`; + } + + updateOptions(): void { + this.maskitoOptions = maskitoDateTimeOptionsGenerator(this); + } + + updateDate(): void { + this.min = new Date(this.minStr); + this.max = new Date(this.maxStr); + this.updateOptions(); + } +} diff --git a/projects/demo/src/pages/kit/date-time/date-time-mask-doc.module.ts b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.module.ts new file mode 100644 index 000000000..e8eb5d013 --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.module.ts @@ -0,0 +1,33 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; +import {MaskitoModule} from '@maskito/angular'; +import {TuiAddonDocModule, tuiGenerateRoutes} from '@taiga-ui/addon-doc'; +import {TuiLinkModule, TuiTextfieldControllerModule} from '@taiga-ui/core'; +import {TuiInputModule} from '@taiga-ui/kit'; + +import {DateTimeMaskDocComponent} from './date-time-mask-doc.component'; +import {DateTimeMaskDocExample1} from './examples/1-date-time-localization/component'; +import {DateTimeMaskDocExample2} from './examples/2-min-max/component'; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MaskitoModule, + TuiAddonDocModule, + TuiInputModule, + TuiLinkModule, + TuiTextfieldControllerModule, + RouterModule.forChild(tuiGenerateRoutes(DateTimeMaskDocComponent)), + ], + declarations: [ + DateTimeMaskDocComponent, + DateTimeMaskDocExample1, + DateTimeMaskDocExample2, + ], + exports: [DateTimeMaskDocComponent], +}) +export class DateTimeMaskDocModule {} diff --git a/projects/demo/src/pages/kit/date-time/date-time-mask-doc.template.html b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.template.html new file mode 100644 index 000000000..a9887b33c --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/date-time-mask-doc.template.html @@ -0,0 +1,134 @@ + + + Use + maskitoDateTimeOptionsGenerator + to create mask to input a date and time. + + + + Use + dateMode + , + timeMode + and + dateSeparator + parameters to get mask with a language‑sensitive representation + of dates. + + + + + + + Parameters + min + and + max + allow to set the least and the greatest available dates. They + accept native + + Date + + . + + + + + + + + + + Enter date and time + + + + + + + Date format mode + + + + Time format mode + + + + Date separator + +

+ Default: + . + (dot). +

+
+ + + Minimum date + + + Maximum date + +
+
+
diff --git a/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/component.ts b/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/component.ts new file mode 100644 index 000000000..98f3e0883 --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/component.ts @@ -0,0 +1,28 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +import mask from './mask'; + +@Component({ + selector: 'date-time-mask-doc-example-1', + template: ` + + Localization + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DateTimeMaskDocExample1 { + value = '09/20/2020, 15:30'; + readonly filler = 'mm/dd/yyyy, hh:mm'; + readonly mask = mask; +} diff --git a/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/mask.ts b/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/mask.ts new file mode 100644 index 000000000..93b2ecc1e --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/examples/1-date-time-localization/mask.ts @@ -0,0 +1,7 @@ +import {maskitoDateTimeOptionsGenerator} from '@maskito/kit'; + +export default maskitoDateTimeOptionsGenerator({ + dateMode: 'mm/dd/yyyy', + timeMode: 'HH:MM', + dateSeparator: '/', +}); diff --git a/projects/demo/src/pages/kit/date-time/examples/2-min-max/component.ts b/projects/demo/src/pages/kit/date-time/examples/2-min-max/component.ts new file mode 100644 index 000000000..f1e517009 --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/examples/2-min-max/component.ts @@ -0,0 +1,28 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +import mask from './mask'; + +@Component({ + selector: 'date-time-mask-doc-example-2', + template: ` + + Min-max + + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DateTimeMaskDocExample2 { + value = '09-01-2018, 15:30'; + readonly filler = 'dd-mm-yyyy, hh:mm'; + readonly mask = mask; +} diff --git a/projects/demo/src/pages/kit/date-time/examples/2-min-max/mask.ts b/projects/demo/src/pages/kit/date-time/examples/2-min-max/mask.ts new file mode 100644 index 000000000..862ea8518 --- /dev/null +++ b/projects/demo/src/pages/kit/date-time/examples/2-min-max/mask.ts @@ -0,0 +1,9 @@ +import {maskitoDateTimeOptionsGenerator} from '@maskito/kit'; + +export default maskitoDateTimeOptionsGenerator({ + dateMode: 'dd/mm/yyyy', + timeMode: 'HH:MM', + dateSeparator: '-', + min: new Date(2010, 1, 15, 12, 30, 0), + max: new Date(2020, 8, 15, 18, 30, 0), +}); diff --git a/projects/demo/src/pages/pages.ts b/projects/demo/src/pages/pages.ts index c409dc01b..dcd8a5a5d 100644 --- a/projects/demo/src/pages/pages.ts +++ b/projects/demo/src/pages/pages.ts @@ -63,6 +63,12 @@ export const DEMO_PAGES: TuiDocPages = [ route: DemoPath.DateRange, keywords: `date, day, month, year, mask, range, kit, generator`, }, + { + section: 'Kit', + title: 'DateTime', + route: DemoPath.DateTime, + keywords: `date, day, month, year, mask, time, date-time, hour, minute, second, kit, generator`, + }, { section: 'Recipes', title: 'Card', diff --git a/projects/kit/src/index.ts b/projects/kit/src/index.ts index ca6f496c7..dccdff119 100644 --- a/projects/kit/src/index.ts +++ b/projects/kit/src/index.ts @@ -1,5 +1,11 @@ export * from './lib/masks/date'; export * from './lib/masks/date-range'; +export * from './lib/masks/date-time'; export * from './lib/masks/number'; export * from './lib/masks/time'; -export {MaskitoDateMode, MaskitoDateSegments} from './lib/types'; +export { + MaskitoDateMode, + MaskitoDateSegments, + MaskitoTimeMode, + MaskitoTimeSegments, +} from './lib/types'; diff --git a/projects/kit/src/lib/masks/time/constants/default-time-segment-max-values.ts b/projects/kit/src/lib/constants/default-time-segment-max-values.ts similarity index 100% rename from projects/kit/src/lib/masks/time/constants/default-time-segment-max-values.ts rename to projects/kit/src/lib/constants/default-time-segment-max-values.ts diff --git a/projects/kit/src/lib/constants/index.ts b/projects/kit/src/lib/constants/index.ts index 6d142b9b0..f5373907e 100644 --- a/projects/kit/src/lib/constants/index.ts +++ b/projects/kit/src/lib/constants/index.ts @@ -1,3 +1,6 @@ export * from './default-min-max-dates'; +export * from './default-time-segment-max-values'; export * from './possible-dates-separator'; +export * from './time-fixed-characters'; +export * from './time-segment-value-lengths'; export * from './unicode-characters'; diff --git a/projects/kit/src/lib/constants/possible-dates-separator.ts b/projects/kit/src/lib/constants/possible-dates-separator.ts index 7f455747e..cf58d881e 100644 --- a/projects/kit/src/lib/constants/possible-dates-separator.ts +++ b/projects/kit/src/lib/constants/possible-dates-separator.ts @@ -6,3 +6,5 @@ export const POSSIBLE_DATES_SEPARATOR = [ CHAR_EM_DASH, CHAR_MINUS, ]; + +export const POSSIBLE_DATE_TIME_SEPARATOR = [',', ' ']; diff --git a/projects/kit/src/lib/masks/time/constants/time-fixed-characters.ts b/projects/kit/src/lib/constants/time-fixed-characters.ts similarity index 100% rename from projects/kit/src/lib/masks/time/constants/time-fixed-characters.ts rename to projects/kit/src/lib/constants/time-fixed-characters.ts diff --git a/projects/kit/src/lib/masks/time/constants/time-segment-value-lengths.ts b/projects/kit/src/lib/constants/time-segment-value-lengths.ts similarity index 100% rename from projects/kit/src/lib/masks/time/constants/time-segment-value-lengths.ts rename to projects/kit/src/lib/constants/time-segment-value-lengths.ts diff --git a/projects/kit/src/lib/masks/date-range/min-max-range-length-postprocessor.ts b/projects/kit/src/lib/masks/date-range/min-max-range-length-postprocessor.ts index d8ce42a87..d36c02c5c 100644 --- a/projects/kit/src/lib/masks/date-range/min-max-range-length-postprocessor.ts +++ b/projects/kit/src/lib/masks/date-range/min-max-range-length-postprocessor.ts @@ -6,7 +6,7 @@ import { appendDate, clamp, dateToSegments, - isDateStringCompleted, + isDateStringComplete, isEmpty, parseDateRangeString, parseDateString, @@ -36,7 +36,7 @@ export function createMinMaxRangeLengthPostprocessor({ if ( dateStrings.length !== 2 || - dateStrings.some(date => !isDateStringCompleted(date, dateModeTemplate)) + dateStrings.some(date => !isDateStringComplete(date, dateModeTemplate)) ) { return {value, selection}; } diff --git a/projects/kit/src/lib/masks/date-time/constants/date-time-separator.ts b/projects/kit/src/lib/masks/date-time/constants/date-time-separator.ts new file mode 100644 index 000000000..882f20a11 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/constants/date-time-separator.ts @@ -0,0 +1 @@ +export const DATE_TIME_SEPARATOR = `, `; diff --git a/projects/kit/src/lib/masks/date-time/constants/index.ts b/projects/kit/src/lib/masks/date-time/constants/index.ts new file mode 100644 index 000000000..412f97d80 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/constants/index.ts @@ -0,0 +1 @@ +export * from './date-time-separator'; 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 new file mode 100644 index 000000000..d5d58d1c2 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/date-time-mask.ts @@ -0,0 +1,50 @@ +import {MaskitoOptions, maskitoPipe} from '@maskito/core'; + +import {TIME_FIXED_CHARACTERS} from '../../constants'; +import {createZeroPlaceholdersPreprocessor} from '../../processors'; +import {MaskitoDateMode, MaskitoTimeMode} from '../../types'; +import {DATE_TIME_SEPARATOR} from './constants'; +import {createMinMaxDateTimePostprocessor} from './postprocessors'; +import {createValidDateTimePreprocessor} from './preprocessors'; + +export function maskitoDateTimeOptionsGenerator({ + dateMode, + timeMode, + dateSeparator = '.', + min, + max, +}: { + dateMode: MaskitoDateMode; + timeMode: MaskitoTimeMode; + dateSeparator?: string; + max?: Date; + min?: Date; +}): MaskitoOptions { + const dateModeTemplate = dateMode.split('/').join(dateSeparator); + + return { + mask: [ + ...Array.from(dateModeTemplate).map(char => + char === dateSeparator ? char : /\d/, + ), + ...DATE_TIME_SEPARATOR.split(''), + ...Array.from(timeMode).map(char => + TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/, + ), + ], + overwriteMode: 'replace', + preprocessor: maskitoPipe( + createZeroPlaceholdersPreprocessor(), + createValidDateTimePreprocessor({ + dateModeTemplate, + dateSegmentsSeparator: dateSeparator, + }), + ), + postprocessor: createMinMaxDateTimePostprocessor({ + min, + max, + dateModeTemplate, + timeMode, + }), + }; +} diff --git a/projects/kit/src/lib/masks/date-time/index.ts b/projects/kit/src/lib/masks/date-time/index.ts new file mode 100644 index 000000000..2f0802649 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/index.ts @@ -0,0 +1 @@ +export {maskitoDateTimeOptionsGenerator} from './date-time-mask'; diff --git a/projects/kit/src/lib/masks/date-time/postprocessors/index.ts b/projects/kit/src/lib/masks/date-time/postprocessors/index.ts new file mode 100644 index 000000000..4b57683a9 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/postprocessors/index.ts @@ -0,0 +1 @@ +export * from './min-max-date-time-postprocessor'; 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 new file mode 100644 index 000000000..b2ca28e05 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/postprocessors/min-max-date-time-postprocessor.ts @@ -0,0 +1,52 @@ +import {MaskitoOptions} from '@maskito/core'; + +import {DEFAULT_MAX_DATE, DEFAULT_MIN_DATE} from '../../../constants'; +import {MaskitoTimeMode} from '../../../types'; +import { + clamp, + dateToSegments, + parseDateString, + segmentsToDate, + toDateString, +} from '../../../utils'; +import {parseTimeString} from '../../../utils/time'; +import {isDateTimeStringComplete, parseDateTimeString} from '../utils'; + +export function createMinMaxDateTimePostprocessor({ + dateModeTemplate, + timeMode, + min = DEFAULT_MIN_DATE, + max = DEFAULT_MAX_DATE, +}: { + dateModeTemplate: string; + timeMode: MaskitoTimeMode; + min?: Date; + max?: Date; +}): NonNullable { + return ({value, selection}) => { + const [dateString, timeString] = parseDateTimeString(value, dateModeTemplate); + + if (!isDateTimeStringComplete(value, dateModeTemplate, timeMode)) { + return { + selection, + value: value, + }; + } + + const parsedDate = parseDateString(dateString, dateModeTemplate); + const parsedTime = parseTimeString(timeString); + const date = segmentsToDate(parsedDate, parsedTime); + const clampedDate = clamp(date, min, max); + + const validatedValue = toDateString( + dateToSegments(clampedDate), + dateModeTemplate, + timeMode, + ); + + return { + selection, + value: validatedValue, + }; + }; +} diff --git a/projects/kit/src/lib/masks/date-time/preprocessors/index.ts b/projects/kit/src/lib/masks/date-time/preprocessors/index.ts new file mode 100644 index 000000000..fb37bff51 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/preprocessors/index.ts @@ -0,0 +1 @@ +export * from './valid-date-time-preprocessor'; 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 new file mode 100644 index 000000000..0b2a2541a --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/preprocessors/valid-date-time-preprocessor.ts @@ -0,0 +1,103 @@ +import {MaskitoOptions} from '@maskito/core'; + +import { + DEFAULT_TIME_SEGMENT_MAX_VALUES, + POSSIBLE_DATE_TIME_SEPARATOR, +} from '../../../constants'; +import {validateDateString} from '../../../utils'; +import {padTimeSegments, validateTimeString} from '../../../utils/time'; +import {DATE_TIME_SEPARATOR} from '../constants'; +import {parseDateTimeString} from '../utils'; + +export function createValidDateTimePreprocessor({ + dateModeTemplate, + dateSegmentsSeparator, +}: { + dateModeTemplate: string; + dateSegmentsSeparator: string; +}): NonNullable { + return ({elementState, data}) => { + const {value, selection} = elementState; + + if (data === dateSegmentsSeparator) { + return { + elementState, + data: selection[0] === value.length ? data : '', + }; + } + + if (POSSIBLE_DATE_TIME_SEPARATOR.includes(data)) { + return {elementState, data: DATE_TIME_SEPARATOR}; + } + + const newCharacters = data.replace( + new RegExp(`[^\\d\\${dateSegmentsSeparator}]`, 'g'), + '', + ); + + if (!newCharacters) { + return {elementState, data: ''}; + } + + const [from, rawTo] = selection; + let to = rawTo + data.length; + const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); + + const [dateString, timeString] = parseDateTimeString( + newPossibleValue, + dateModeTemplate, + ); + let validatedValue = ''; + const hasDateTimeSeparator = newPossibleValue.includes(DATE_TIME_SEPARATOR); + + const {validatedDateString, updatedSelection} = validateDateString({ + dateString, + dateModeTemplate, + offset: 0, + selection: [from, to], + }); + + if (dateString && !validatedDateString) { + return {elementState, data: ''}; // prevent changes + } + + to = updatedSelection[1]; + + validatedValue += validatedDateString; + + const paddedMaxValues = padTimeSegments(DEFAULT_TIME_SEGMENT_MAX_VALUES); + + const {validatedTimeString, updatedTimeSelection} = validateTimeString({ + timeString, + paddedMaxValues, + offset: validatedValue.length + DATE_TIME_SEPARATOR.length, + selection: [from, to], + }); + + if (timeString && !validatedTimeString) { + return {elementState, data: ''}; // prevent changes + } + + to = updatedTimeSelection[1]; + + validatedValue += hasDateTimeSeparator + ? DATE_TIME_SEPARATOR + validatedTimeString + : validatedTimeString; + + const newData = validatedValue.slice(from, to); + + return { + elementState: { + selection, + value: + validatedValue.slice(0, from) + + newData + .split(dateSegmentsSeparator) + .map(segment => '0'.repeat(segment.length)) + .join(dateSegmentsSeparator) + + validatedValue.slice(to), + }, + data: newData, + }; + }; +} diff --git a/projects/kit/src/lib/masks/date-time/utils/index.ts b/projects/kit/src/lib/masks/date-time/utils/index.ts new file mode 100644 index 000000000..496b5d002 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/utils/index.ts @@ -0,0 +1,2 @@ +export * from './is-date-time-string-complete'; +export * from './parse-date-time-string'; diff --git a/projects/kit/src/lib/masks/date-time/utils/is-date-time-string-complete.ts b/projects/kit/src/lib/masks/date-time/utils/is-date-time-string-complete.ts new file mode 100644 index 000000000..b3f57ec10 --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/utils/is-date-time-string-complete.ts @@ -0,0 +1,16 @@ +import {DATE_TIME_SEPARATOR} from '../constants'; + +export function isDateTimeStringComplete( + dateTimeString: string, + dateMode: string, + timeMode: string, +): boolean { + return ( + dateTimeString.length >= + dateMode.length + timeMode.length + DATE_TIME_SEPARATOR.length && + dateTimeString + .split(DATE_TIME_SEPARATOR)[0] + .split(/\D/) + .every(segment => !segment.match(/^0+$/)) + ); +} diff --git a/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts b/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts new file mode 100644 index 000000000..2a9ef60eb --- /dev/null +++ b/projects/kit/src/lib/masks/date-time/utils/parse-date-time-string.ts @@ -0,0 +1,17 @@ +import {DATE_TIME_SEPARATOR} from '../constants'; + +export function parseDateTimeString( + dateTime: string, + dateModeTemplate: string, +): string[] { + const hasSeparator = dateTime.includes(DATE_TIME_SEPARATOR); + + return [ + dateTime.slice(0, dateModeTemplate.length), + dateTime.slice( + hasSeparator + ? dateModeTemplate.length + DATE_TIME_SEPARATOR.length + : dateModeTemplate.length, + ), + ]; +} diff --git a/projects/kit/src/lib/masks/time/constants/index.ts b/projects/kit/src/lib/masks/time/constants/index.ts deleted file mode 100644 index bd430b180..000000000 --- a/projects/kit/src/lib/masks/time/constants/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './default-time-segment-max-values'; -export * from './time-fixed-characters'; -export * from './time-segment-value-lengths'; diff --git a/projects/kit/src/lib/masks/time/index.ts b/projects/kit/src/lib/masks/time/index.ts index ba59b4f09..1229598ad 100644 --- a/projects/kit/src/lib/masks/time/index.ts +++ b/projects/kit/src/lib/masks/time/index.ts @@ -1,2 +1 @@ export {maskitoTimeOptionsGenerator} from './time-mask'; -export {MaskitoTimeMode, MaskitoTimeSegments} from './types'; 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 bedfe9778..88a53bd37 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,9 +1,7 @@ import {MaskitoOptions} from '@maskito/core'; -import {padWithZeroesUntilValid} from '../../../utils'; -import {TIME_SEGMENT_VALUE_LENGTHS} from '../constants'; -import {MaskitoTimeSegments} from '../types'; -import {padTimeSegments, parseTimeString, toTimeString} from '../utils'; +import {MaskitoTimeSegments} from '../../../types'; +import {padTimeSegments, validateTimeString} from '../../../utils/time'; export function createMaxValidationPreprocessor( timeSegmentMaxValues: MaskitoTimeSegments, @@ -21,56 +19,29 @@ export function createMaxValidationPreprocessor( const [from, rawTo] = selection; let to = rawTo + newCharacters.length; // to be conformed with `overwriteMode: replace` const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to); - const possibleTimeSegments = Object.entries( - parseTimeString(newPossibleValue), - ) as Array<[keyof MaskitoTimeSegments, string]>; - const validatedTimeSegments: Partial = {}; + const {validatedTimeString, updatedTimeSelection} = validateTimeString({ + timeString: newPossibleValue, + paddedMaxValues, + offset: 0, + selection: [from, to], + }); - for (const [segmentName, segmentValue] of possibleTimeSegments) { - const validatedTime = toTimeString(validatedTimeSegments); - const maxSegmentValue = paddedMaxValues[segmentName]; - - const fantomSeparator = validatedTime.length && 1; - - const lastSegmentDigitIndex = - validatedTime.length + - fantomSeparator + - TIME_SEGMENT_VALUE_LENGTHS[segmentName]; - const isLastSegmentDigitAdded = - lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to; - - if ( - isLastSegmentDigitAdded && - Number(segmentValue) > Number(maxSegmentValue) - ) { - // 2|0:00 => Type 9 => 2|0:00 - return {elementState, data: ''}; // prevent changes - } - - const {validatedSegmentValue, prefixedZeroesCount} = padWithZeroesUntilValid( - segmentValue, - maxSegmentValue, - ); - - to += prefixedZeroesCount; - - validatedTimeSegments[segmentName] = validatedSegmentValue; + if (newPossibleValue && !validatedTimeString) { + return {elementState, data: ''}; // prevent changes } - const finalTimeString = toTimeString(validatedTimeSegments); - - to = finalTimeString.length - value.slice(to).length; + to = updatedTimeSelection[1]; - const newData = finalTimeString.slice(from, to); + const newData = validatedTimeString.slice(from, to); return { elementState: { selection, value: - finalTimeString.slice(0, from) + + validatedTimeString.slice(0, from) + '0'.repeat(newData.length) + - finalTimeString.slice(to), + validatedTimeString.slice(to), }, data: newData, }; diff --git a/projects/kit/src/lib/masks/time/time-mask.ts b/projects/kit/src/lib/masks/time/time-mask.ts index 89239e60e..a8c3c32e5 100644 --- a/projects/kit/src/lib/masks/time/time-mask.ts +++ b/projects/kit/src/lib/masks/time/time-mask.ts @@ -1,9 +1,9 @@ import {MaskitoOptions, maskitoPipe} from '@maskito/core'; +import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../constants'; import {createZeroPlaceholdersPreprocessor} from '../../processors'; -import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from './constants'; +import {MaskitoTimeMode, MaskitoTimeSegments} from '../../types'; import {createMaxValidationPreprocessor} from './processors'; -import {MaskitoTimeMode, MaskitoTimeSegments} from './types'; export function maskitoTimeOptionsGenerator({ mode, diff --git a/projects/kit/src/lib/masks/time/types/index.ts b/projects/kit/src/lib/masks/time/types/index.ts deleted file mode 100644 index a23f2a4ef..000000000 --- a/projects/kit/src/lib/masks/time/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './time-mode'; -export * from './time-segments'; 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 dadaa6557..c7ded16b3 100644 --- a/projects/kit/src/lib/processors/min-max-date-postprocessor.ts +++ b/projects/kit/src/lib/processors/min-max-date-postprocessor.ts @@ -4,7 +4,7 @@ import {DEFAULT_MAX_DATE, DEFAULT_MIN_DATE} from '../constants'; import { clamp, dateToSegments, - isDateStringCompleted, + isDateStringComplete, parseDateRangeString, parseDateString, segmentsToDate, @@ -31,7 +31,7 @@ export function createMinMaxDatePostprocessor({ for (const dateString of dateStrings) { validatedValue += validatedValue ? datesSeparator : ''; - if (!isDateStringCompleted(dateString, dateModeTemplate)) { + if (!isDateStringComplete(dateString, dateModeTemplate)) { validatedValue += dateString; continue; } diff --git a/projects/kit/src/lib/processors/valid-date-preprocessor.ts b/projects/kit/src/lib/processors/valid-date-preprocessor.ts index 781a397dd..7038f78fe 100644 --- a/projects/kit/src/lib/processors/valid-date-preprocessor.ts +++ b/projects/kit/src/lib/processors/valid-date-preprocessor.ts @@ -15,10 +15,10 @@ export function createValidDatePreprocessor({ return ({elementState, data}) => { const {value, selection} = elementState; - if (data === dateSegmentsSeparator && value.length === selection[0]) { + if (data === dateSegmentsSeparator) { return { elementState, - data, + data: selection[0] === value.length ? data : '', }; } diff --git a/projects/kit/src/lib/types/index.ts b/projects/kit/src/lib/types/index.ts index cdaa544bf..3185ea1f0 100644 --- a/projects/kit/src/lib/types/index.ts +++ b/projects/kit/src/lib/types/index.ts @@ -1,2 +1,4 @@ export * from './date-mode'; export * from './date-segments'; +export * from './time-mode'; +export * from './time-segments'; diff --git a/projects/kit/src/lib/masks/time/types/time-mode.ts b/projects/kit/src/lib/types/time-mode.ts similarity index 100% rename from projects/kit/src/lib/masks/time/types/time-mode.ts rename to projects/kit/src/lib/types/time-mode.ts diff --git a/projects/kit/src/lib/masks/time/types/time-segments.ts b/projects/kit/src/lib/types/time-segments.ts similarity index 100% rename from projects/kit/src/lib/masks/time/types/time-segments.ts rename to projects/kit/src/lib/types/time-segments.ts diff --git a/projects/kit/src/lib/utils/date/date-to-segments.ts b/projects/kit/src/lib/utils/date/date-to-segments.ts index 134fd9a58..d3fccddd0 100644 --- a/projects/kit/src/lib/utils/date/date-to-segments.ts +++ b/projects/kit/src/lib/utils/date/date-to-segments.ts @@ -1,9 +1,13 @@ -import {MaskitoDateSegments} from '../../types'; +import {MaskitoDateSegments, MaskitoTimeSegments} from '../../types'; -export function dateToSegments(date: Date): MaskitoDateSegments { +export function dateToSegments(date: Date): MaskitoDateSegments & MaskitoTimeSegments { return { day: String(date.getDate()).padStart(2, '0'), month: String(date.getMonth() + 1).padStart(2, '0'), year: String(date.getFullYear()).padStart(4, '0'), + hours: String(date.getHours()).padStart(2, '0'), + minutes: String(date.getMinutes()).padStart(2, '0'), + seconds: String(date.getSeconds()).padStart(2, '0'), + milliseconds: String(date.getMilliseconds()).padStart(3, '0'), }; } diff --git a/projects/kit/src/lib/utils/date/is-date-string-completed.ts b/projects/kit/src/lib/utils/date/is-date-string-complete.ts similarity index 85% rename from projects/kit/src/lib/utils/date/is-date-string-completed.ts rename to projects/kit/src/lib/utils/date/is-date-string-complete.ts index 561170435..f6a5de7df 100644 --- a/projects/kit/src/lib/utils/date/is-date-string-completed.ts +++ b/projects/kit/src/lib/utils/date/is-date-string-complete.ts @@ -1,4 +1,4 @@ -export function isDateStringCompleted( +export function isDateStringComplete( dateString: string, dateModeTemplate: string, ): boolean { diff --git a/projects/kit/src/lib/utils/date/segments-to-date.ts b/projects/kit/src/lib/utils/date/segments-to-date.ts index 6b42e46a3..a89a23498 100644 --- a/projects/kit/src/lib/utils/date/segments-to-date.ts +++ b/projects/kit/src/lib/utils/date/segments-to-date.ts @@ -1,11 +1,23 @@ -import {MaskitoDateSegments} from '../../types'; +import {MaskitoDateSegments, MaskitoTimeSegments} from '../../types'; -export function segmentsToDate(parsedDate: Partial): Date { +export function segmentsToDate( + parsedDate: Partial, + parsedTime?: Partial, +): Date { const year = parsedDate.year?.length === 2 ? `20${parsedDate.year}` : parsedDate.year; - return new Date( + const date = new Date( Number(year ?? '0'), Number(parsedDate.month ?? '1') - 1, Number(parsedDate.day ?? '1'), + Number(parsedTime?.hours ?? '0'), + Number(parsedTime?.minutes ?? '0'), + Number(parsedTime?.seconds ?? '0'), + Number(parsedTime?.milliseconds ?? '0'), ); + + // needed for years less than 1900 + date.setUTCFullYear(Number(year ?? '0')); + + return date; } diff --git a/projects/kit/src/lib/utils/date/to-date-string.ts b/projects/kit/src/lib/utils/date/to-date-string.ts index 7b46a7bd4..34bc7dc47 100644 --- a/projects/kit/src/lib/utils/date/to-date-string.ts +++ b/projects/kit/src/lib/utils/date/to-date-string.ts @@ -1,15 +1,30 @@ -import {MaskitoDateSegments} from '../../types'; +import {DATE_TIME_SEPARATOR} from '../../masks/date-time/constants'; +import {MaskitoDateSegments, MaskitoTimeSegments} from '../../types'; export function toDateString( - {day, month, year}: Partial>, - mode: string, + { + day, + month, + year, + hours, + minutes, + seconds, + milliseconds, + }: Partial & Partial>, + dateMode: string, + timeMode?: string, ): string { - const safeYear = mode.match(/y/g)?.length === 2 ? year?.slice(-2) : year; + const safeYear = dateMode.match(/y/g)?.length === 2 ? year?.slice(-2) : year; + const fullMode = dateMode + (timeMode ? DATE_TIME_SEPARATOR + timeMode : ''); - return mode + return fullMode .replace(/d+/g, day ?? '') .replace(/m+/g, month ?? '') .replace(/y+/g, safeYear ?? '') + .replace(/H+/g, hours ?? '') + .replace(/MSS/g, milliseconds ?? '') + .replace(/M+/g, minutes ?? '') + .replace(/S+/g, seconds ?? '') .replace(/^\D+/g, '') .replace(/\D+$/g, ''); } diff --git a/projects/kit/src/lib/utils/index.ts b/projects/kit/src/lib/utils/index.ts index 8a27877a7..4ff896cbc 100644 --- a/projects/kit/src/lib/utils/index.ts +++ b/projects/kit/src/lib/utils/index.ts @@ -2,7 +2,7 @@ export * from './clamp'; export * from './date/append-date'; export * from './date/date-segment-value-length'; export * from './date/date-to-segments'; -export * from './date/is-date-string-completed'; +export * from './date/is-date-string-complete'; export * from './date/parse-date-range-string'; export * from './date/parse-date-string'; export * from './date/segments-to-date'; diff --git a/projects/kit/src/lib/masks/time/utils/index.ts b/projects/kit/src/lib/utils/time/index.ts similarity index 72% rename from projects/kit/src/lib/masks/time/utils/index.ts rename to projects/kit/src/lib/utils/time/index.ts index 9ab5eeb4f..dccc93c20 100644 --- a/projects/kit/src/lib/masks/time/utils/index.ts +++ b/projects/kit/src/lib/utils/time/index.ts @@ -1,3 +1,4 @@ export * from './pad-time-segments'; export * from './parse-time-string'; export * from './to-time-string'; +export * from './validate-time-string'; diff --git a/projects/kit/src/lib/masks/time/utils/pad-time-segments.ts b/projects/kit/src/lib/utils/time/pad-time-segments.ts similarity index 79% rename from projects/kit/src/lib/masks/time/utils/pad-time-segments.ts rename to projects/kit/src/lib/utils/time/pad-time-segments.ts index a18d9be43..aa25b1e5d 100644 --- a/projects/kit/src/lib/masks/time/utils/pad-time-segments.ts +++ b/projects/kit/src/lib/utils/time/pad-time-segments.ts @@ -1,6 +1,6 @@ -import {getObjectFromEntries} from '../../../utils'; -import {TIME_SEGMENT_VALUE_LENGTHS} from '../constants'; -import {MaskitoTimeSegments} from '../types'; +import {TIME_SEGMENT_VALUE_LENGTHS} from '../../constants'; +import {MaskitoTimeSegments} from '.././../types'; +import {getObjectFromEntries} from '../get-object-from-entries'; export function padTimeSegments( timeSegments: MaskitoTimeSegments, diff --git a/projects/kit/src/lib/masks/time/utils/parse-time-string.ts b/projects/kit/src/lib/utils/time/parse-time-string.ts similarity index 82% rename from projects/kit/src/lib/masks/time/utils/parse-time-string.ts rename to projects/kit/src/lib/utils/time/parse-time-string.ts index 67a233f0f..f6e3364db 100644 --- a/projects/kit/src/lib/masks/time/utils/parse-time-string.ts +++ b/projects/kit/src/lib/utils/time/parse-time-string.ts @@ -1,5 +1,5 @@ -import {getObjectFromEntries} from '../../../utils'; -import {MaskitoTimeSegments} from '../types'; +import {MaskitoTimeSegments} from '../../types'; +import {getObjectFromEntries} from '../get-object-from-entries'; /** * @param timeString can be with/without fixed characters diff --git a/projects/kit/src/lib/masks/time/utils/to-time-string.ts b/projects/kit/src/lib/utils/time/to-time-string.ts similarity index 86% rename from projects/kit/src/lib/masks/time/utils/to-time-string.ts rename to projects/kit/src/lib/utils/time/to-time-string.ts index 55b8e5c28..5af10f4de 100644 --- a/projects/kit/src/lib/masks/time/utils/to-time-string.ts +++ b/projects/kit/src/lib/utils/time/to-time-string.ts @@ -1,4 +1,4 @@ -import {MaskitoTimeSegments} from '../types'; +import {MaskitoTimeSegments} from '../../types'; export function toTimeString({ hours = '', diff --git a/projects/kit/src/lib/utils/time/validate-time-string.ts b/projects/kit/src/lib/utils/time/validate-time-string.ts new file mode 100644 index 000000000..b2ade714b --- /dev/null +++ b/projects/kit/src/lib/utils/time/validate-time-string.ts @@ -0,0 +1,67 @@ +import {TIME_SEGMENT_VALUE_LENGTHS} from '../../constants'; +import {MaskitoTimeSegments} from '../../types'; +import {padWithZeroesUntilValid} from '../pad-with-zeroes-until-valid'; +import {parseTimeString} from './parse-time-string'; +import {toTimeString} from './to-time-string'; + +export function validateTimeString({ + timeString, + paddedMaxValues, + offset, + selection: [from, to], +}: { + timeString: string; + paddedMaxValues: MaskitoTimeSegments; + offset: number; + selection: [number, number]; +}): {validatedTimeString: string; updatedTimeSelection: [number, number]} { + const parsedTime = parseTimeString(timeString); + + const possibleTimeSegments = Object.entries(parsedTime) as Array< + [keyof MaskitoTimeSegments, string] + >; + + const validatedTimeSegments: Partial = {}; + + let paddedZeroes = 0; + + for (const [segmentName, segmentValue] of possibleTimeSegments) { + const validatedTime = toTimeString(validatedTimeSegments); + const maxSegmentValue = paddedMaxValues[segmentName]; + + const fantomSeparator = validatedTime.length && 1; + + const lastSegmentDigitIndex = + offset + + validatedTime.length + + fantomSeparator + + TIME_SEGMENT_VALUE_LENGTHS[segmentName]; + const isLastSegmentDigitAdded = + lastSegmentDigitIndex >= from && lastSegmentDigitIndex <= to; + + if (isLastSegmentDigitAdded && Number(segmentValue) > Number(maxSegmentValue)) { + // 2|0:00 => Type 9 => 2|0:00 + return {validatedTimeString: '', updatedTimeSelection: [from, to]}; // prevent changes + } + + const {validatedSegmentValue, prefixedZeroesCount} = padWithZeroesUntilValid( + segmentValue, + `${maxSegmentValue}`, + ); + + paddedZeroes += prefixedZeroesCount; + + validatedTimeSegments[segmentName] = validatedSegmentValue; + } + + const validatedTimeString = toTimeString(validatedTimeSegments); + const addedDateSegmentSeparators = validatedTimeString.length - timeString.length; + + return { + validatedTimeString, + updatedTimeSelection: [ + from + paddedZeroes + addedDateSegmentSeparators, + to + paddedZeroes + addedDateSegmentSeparators, + ], + }; +}