From 434c9c5f349ab3c19e11722e95313c5763203b08 Mon Sep 17 00:00:00 2001 From: Soshi Homma Date: Mon, 19 Feb 2024 19:14:30 +0900 Subject: [PATCH] feat(kit): add full-width numbers support for `Time`, `Date`, `DateTime`, `DateRange` (#1043) --- .../date-range-fullwidth-to-halfwidth.cy.ts | 35 ++++++++++++++++ .../date-time-fullwidth-to-halfwidth.cy.ts | 36 +++++++++++++++++ .../date/date-fullwidth-to-halfwidth.cy.ts | 40 +++++++++++++++++++ .../time/time-fullwidth-to-halfwidth.cy.ts | 35 ++++++++++++++++ .../src/lib/constants/unicode-characters.ts | 14 +++++++ .../lib/masks/date-range/date-range-mask.ts | 2 + .../src/lib/masks/date-time/date-time-mask.ts | 4 ++ projects/kit/src/lib/masks/date/date-mask.ts | 2 + .../lib/masks/date/tests/date-mask.spec.ts | 3 +- .../kit/src/lib/masks/number/number-mask.ts | 2 +- .../src/lib/masks/number/processors/index.ts | 1 - projects/kit/src/lib/masks/time/time-mask.ts | 8 +++- .../processors/colon-convert-preprocessor.ts | 20 ++++++++++ .../fullwidth-to-halfwidth-preprocessor.ts | 2 +- projects/kit/src/lib/processors/index.ts | 2 + projects/kit/src/lib/utils/index.ts | 2 + .../utils/tests/to-half-width-colon.spec.ts | 7 ++++ .../utils/tests/to-half-width-number.spec.ts | 0 .../kit/src/lib/utils/to-half-width-colon.ts | 10 +++++ .../number => }/utils/to-half-width-number.ts | 0 20 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 projects/demo-integrations/src/tests/kit/date-range/date-range-fullwidth-to-halfwidth.cy.ts create mode 100644 projects/demo-integrations/src/tests/kit/date-time/date-time-fullwidth-to-halfwidth.cy.ts create mode 100644 projects/demo-integrations/src/tests/kit/date/date-fullwidth-to-halfwidth.cy.ts create mode 100644 projects/demo-integrations/src/tests/kit/time/time-fullwidth-to-halfwidth.cy.ts create mode 100644 projects/kit/src/lib/processors/colon-convert-preprocessor.ts rename projects/kit/src/lib/{masks/number => }/processors/fullwidth-to-halfwidth-preprocessor.ts (88%) create mode 100644 projects/kit/src/lib/utils/tests/to-half-width-colon.spec.ts rename projects/kit/src/lib/{masks/number => }/utils/tests/to-half-width-number.spec.ts (100%) create mode 100644 projects/kit/src/lib/utils/to-half-width-colon.ts rename projects/kit/src/lib/{masks/number => }/utils/to-half-width-number.ts (100%) diff --git a/projects/demo-integrations/src/tests/kit/date-range/date-range-fullwidth-to-halfwidth.cy.ts b/projects/demo-integrations/src/tests/kit/date-range/date-range-fullwidth-to-halfwidth.cy.ts new file mode 100644 index 000000000..3e36b8258 --- /dev/null +++ b/projects/demo-integrations/src/tests/kit/date-range/date-range-fullwidth-to-halfwidth.cy.ts @@ -0,0 +1,35 @@ +import {DemoPath} from '@demo/constants'; + +describe('DateRange | Full width character parsing', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.DateRange}/API?mode=yyyy%2Fmm%2Fdd&dateSeparator=%2F`); + cy.get('#demo-content input').should('be.visible').first().focus().as('input'); + }); + + describe('basic typing', () => { + const tests = [ + // [Typed value, Masked value] + ['2', '2'], + ['20', '20'], + ['201', '201'], + ['2016', '2016'], + ['20162', '2016/02'], + ['2016228', '2016/02/28'], + ['20162282', '2016/02/28 – 2'], + ['201622820', '2016/02/28 – 20'], + ['20162282020', '2016/02/28 – 2020'], + ['201622820204', '2016/02/28 – 2020/04'], + ['2016228202044', '2016/02/28 – 2020/04/04'], + ] 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); + }); + }); + }); +}); diff --git a/projects/demo-integrations/src/tests/kit/date-time/date-time-fullwidth-to-halfwidth.cy.ts b/projects/demo-integrations/src/tests/kit/date-time/date-time-fullwidth-to-halfwidth.cy.ts new file mode 100644 index 000000000..43ff9fb2e --- /dev/null +++ b/projects/demo-integrations/src/tests/kit/date-time/date-time-fullwidth-to-halfwidth.cy.ts @@ -0,0 +1,36 @@ +import {DemoPath} from '@demo/constants'; + +describe('DateTime | Full width character parsing', () => { + beforeEach(() => { + cy.visit( + `/${DemoPath.DateTime}/API?dateMode=yyyy%2Fmm%2Fdd&timeMode=HH:MM:SS&dateSeparator=%2F`, + ); + cy.get('#demo-content input').should('be.visible').first().focus().as('input'); + }); + + describe('basic typing', () => { + const tests = [ + // [Typed value, Masked value] + ['2', '2'], + ['20', '20'], + ['201', '201'], + ['2016', '2016'], + ['20162', '2016/02'], + ['2016228', '2016/02/28'], + ['20162283', '2016/02/28, 03'], + ['2016228330', '2016/02/28, 03:30'], + ['20162283304', '2016/02/28, 03:30:4'], + ['201622833045', '2016/02/28, 03:30:45'], + ] 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); + }); + }); + }); +}); diff --git a/projects/demo-integrations/src/tests/kit/date/date-fullwidth-to-halfwidth.cy.ts b/projects/demo-integrations/src/tests/kit/date/date-fullwidth-to-halfwidth.cy.ts new file mode 100644 index 000000000..1c154ea01 --- /dev/null +++ b/projects/demo-integrations/src/tests/kit/date/date-fullwidth-to-halfwidth.cy.ts @@ -0,0 +1,40 @@ +import {DemoPath} from '@demo/constants'; + +/* NOTE: yyyy/mm/dd is very common in Japan */ +describe('Date', () => { + describe('Full width character parsing', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.Date}/API?mode=yyyy%2Fmm%2Fdd&separator=%2F`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + describe('Accepts all of full width characters', () => { + it('20191231 => 2019/12/31', () => { + cy.get('@input') + .type('20191231') + .should('have.value', '2019/12/31') + .should('have.prop', 'selectionStart', '2019/12/31'.length) + .should('have.prop', 'selectionEnd', '2019/12/31'.length); + }); + }); + + describe('pads digits with zero if date segment exceeds its max possible value', () => { + // NOTE: months can be > 12 => pads the first 2 with zero + it('20102| => type 9 => 2010/02|', () => { + cy.get('@input') + .type('20102') + .should('have.value', '2010/02') + .should('have.prop', 'selectionStart', '2010/02'.length) + .should('have.prop', 'selectionEnd', '2010/02'.length) + .type('9') + .should('have.value', '2010/02/09') + .should('have.prop', 'selectionStart', '2010/02/09'.length) + .should('have.prop', 'selectionEnd', '2010/02/09'.length); + }); + }); + }); +}); diff --git a/projects/demo-integrations/src/tests/kit/time/time-fullwidth-to-halfwidth.cy.ts b/projects/demo-integrations/src/tests/kit/time/time-fullwidth-to-halfwidth.cy.ts new file mode 100644 index 000000000..ccbfaf102 --- /dev/null +++ b/projects/demo-integrations/src/tests/kit/time/time-fullwidth-to-halfwidth.cy.ts @@ -0,0 +1,35 @@ +import {DemoPath} from '@demo/constants'; + +describe('Time', () => { + describe('Full width character parsing', () => { + beforeEach(() => { + cy.visit(`/${DemoPath.Time}/API?mode=HH:MM`); + cy.get('#demo-content input') + .should('be.visible') + .first() + .focus() + .as('input'); + }); + + describe('basic typing (1 character per keydown)', () => { + const tests = [ + // [Typed value, Masked value, caretIndex] + ['1', '1', 1], + ['12', '12', '12'.length], + ['12:', '12:', '12:'.length], + ['123', '12:3', '12:3'.length], + ['1234', '12:34', '12:34'.length], + ] as const; + + tests.forEach(([typedValue, maskedValue, caretIndex]) => { + it(`Type "${typedValue}" => "${maskedValue}"`, () => { + cy.get('@input') + .type(typedValue) + .should('have.value', maskedValue) + .should('have.prop', 'selectionStart', caretIndex) + .should('have.prop', 'selectionEnd', caretIndex); + }); + }); + }); + }); +}); diff --git a/projects/kit/src/lib/constants/unicode-characters.ts b/projects/kit/src/lib/constants/unicode-characters.ts index c208ccf7c..a78854f14 100644 --- a/projects/kit/src/lib/constants/unicode-characters.ts +++ b/projects/kit/src/lib/constants/unicode-characters.ts @@ -46,3 +46,17 @@ export const CHAR_MINUS = '\u2212'; * is used as prolonged sounds in Japanese. */ export const CHAR_JP_HYPHEN = '\u30FC'; + +/** + * {@link https://symbl.cc/en/003A/ Colon} + * is a punctuation mark that connects parts of a text logically. + * --- + * is also used as separator in time. + */ +export const CHAR_COLON = '\u003A'; + +/** + * {@link https://symbl.cc/en/FF1A/ Full-width colon} + * is a full-width punctuation mark used to separate parts of a text commonly in Japanese. + */ +export const CHAR_JP_COLON = '\uFF1A'; diff --git a/projects/kit/src/lib/masks/date-range/date-range-mask.ts b/projects/kit/src/lib/masks/date-range/date-range-mask.ts index d60dc7626..3c1daa362 100644 --- a/projects/kit/src/lib/masks/date-range/date-range-mask.ts +++ b/projects/kit/src/lib/masks/date-range/date-range-mask.ts @@ -4,6 +4,7 @@ import {CHAR_EN_DASH, CHAR_NO_BREAK_SPACE} from '../../constants'; import { createDateSegmentsZeroPaddingPostprocessor, createFirstDateEndSeparatorPreprocessor, + createFullWidthToHalfWidthPreprocessor, createMinMaxDatePostprocessor, createValidDatePreprocessor, createZeroPlaceholdersPreprocessor, @@ -42,6 +43,7 @@ export function maskitoDateRangeOptionsGenerator({ mask: [...dateMask, ...Array.from(rangeSeparator), ...dateMask], overwriteMode: 'replace', preprocessors: [ + createFullWidthToHalfWidthPreprocessor(), createFirstDateEndSeparatorPreprocessor({ dateModeTemplate, dateSegmentSeparator: dateSeparator, 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 0b92aa458..6e3cc7dfd 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 @@ -2,8 +2,10 @@ import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions} from '@maskito/core'; import {TIME_FIXED_CHARACTERS} from '../../constants'; import { + createColonConvertPreprocessor, createDateSegmentsZeroPaddingPostprocessor, createFirstDateEndSeparatorPreprocessor, + createFullWidthToHalfWidthPreprocessor, createZeroPlaceholdersPreprocessor, normalizeDatePreprocessor, } from '../../processors'; @@ -41,6 +43,8 @@ export function maskitoDateTimeOptionsGenerator({ ], overwriteMode: 'replace', preprocessors: [ + createFullWidthToHalfWidthPreprocessor(), + createColonConvertPreprocessor(), createFirstDateEndSeparatorPreprocessor({ dateModeTemplate, dateSegmentSeparator: dateSeparator, diff --git a/projects/kit/src/lib/masks/date/date-mask.ts b/projects/kit/src/lib/masks/date/date-mask.ts index 2b197b3b6..f675e4db6 100644 --- a/projects/kit/src/lib/masks/date/date-mask.ts +++ b/projects/kit/src/lib/masks/date/date-mask.ts @@ -2,6 +2,7 @@ import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions} from '@maskito/core'; import { createDateSegmentsZeroPaddingPostprocessor, + createFullWidthToHalfWidthPreprocessor, createMinMaxDatePostprocessor, createValidDatePreprocessor, createZeroPlaceholdersPreprocessor, @@ -29,6 +30,7 @@ export function maskitoDateOptionsGenerator({ ), overwriteMode: 'replace', preprocessors: [ + createFullWidthToHalfWidthPreprocessor(), createZeroPlaceholdersPreprocessor(), normalizeDatePreprocessor({ dateModeTemplate, diff --git a/projects/kit/src/lib/masks/date/tests/date-mask.spec.ts b/projects/kit/src/lib/masks/date/tests/date-mask.spec.ts index 583468df4..06eeb488c 100644 --- a/projects/kit/src/lib/masks/date/tests/date-mask.spec.ts +++ b/projects/kit/src/lib/masks/date/tests/date-mask.spec.ts @@ -67,8 +67,7 @@ describe('Date (maskitoTransform)', () => { }); }); - // TODO: https://github.com/taiga-family/maskito/pull/907 - xit('accepts full width characters', () => { + it('accepts full width characters', () => { expect(maskitoTransform('12345', options)).toBe('1234/05'); expect(maskitoTransform('12341226', options)).toBe('1234/12/26'); }); diff --git a/projects/kit/src/lib/masks/number/number-mask.ts b/projects/kit/src/lib/masks/number/number-mask.ts index 5b3861dbf..c05f1a2e3 100644 --- a/projects/kit/src/lib/masks/number/number-mask.ts +++ b/projects/kit/src/lib/masks/number/number-mask.ts @@ -10,6 +10,7 @@ import { CHAR_ZERO_WIDTH_SPACE, } from '../../constants'; import { + createFullWidthToHalfWidthPreprocessor, maskitoPostfixPostprocessorGenerator, maskitoPrefixPostprocessorGenerator, } from '../../processors'; @@ -21,7 +22,6 @@ import { import { createAffixesFilterPreprocessor, createDecimalZeroPaddingPostprocessor, - createFullWidthToHalfWidthPreprocessor, createInitializationOnlyPreprocessor, createMinMaxPostprocessor, createNonRemovableCharsDeletionPreprocessor, diff --git a/projects/kit/src/lib/masks/number/processors/index.ts b/projects/kit/src/lib/masks/number/processors/index.ts index 8412f47fb..cd319c49f 100644 --- a/projects/kit/src/lib/masks/number/processors/index.ts +++ b/projects/kit/src/lib/masks/number/processors/index.ts @@ -1,6 +1,5 @@ export * from './affixes-filter-preprocessor'; export * from './decimal-zero-padding-postprocessor'; -export * from './fullwidth-to-halfwidth-preprocessor'; export * from './initialization-only-preprocessor'; export * from './leading-zeroes-validation-postprocessor'; export * from './min-max-postprocessor'; diff --git a/projects/kit/src/lib/masks/time/time-mask.ts b/projects/kit/src/lib/masks/time/time-mask.ts index 096ef8f6a..15dd3dd7e 100644 --- a/projects/kit/src/lib/masks/time/time-mask.ts +++ b/projects/kit/src/lib/masks/time/time-mask.ts @@ -1,7 +1,11 @@ import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions} from '@maskito/core'; import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../constants'; -import {createZeroPlaceholdersPreprocessor} from '../../processors'; +import { + createColonConvertPreprocessor, + createFullWidthToHalfWidthPreprocessor, + createZeroPlaceholdersPreprocessor, +} from '../../processors'; import {MaskitoTimeMode, MaskitoTimeSegments} from '../../types'; import {createMaxValidationPreprocessor} from './processors'; @@ -23,6 +27,8 @@ export function maskitoTimeOptionsGenerator({ TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/, ), preprocessors: [ + createFullWidthToHalfWidthPreprocessor(), + createColonConvertPreprocessor(), createZeroPlaceholdersPreprocessor(), createMaxValidationPreprocessor(enrichedTimeSegmentMaxValues), ], diff --git a/projects/kit/src/lib/processors/colon-convert-preprocessor.ts b/projects/kit/src/lib/processors/colon-convert-preprocessor.ts new file mode 100644 index 000000000..94fbd557f --- /dev/null +++ b/projects/kit/src/lib/processors/colon-convert-preprocessor.ts @@ -0,0 +1,20 @@ +import {MaskitoPreprocessor} from '@maskito/core'; + +import {toHalfWidthColon} from '../utils'; + +/** + * Convert full width colon (:) to half width one (:) + */ +export function createColonConvertPreprocessor(): MaskitoPreprocessor { + return ({elementState, data}) => { + const {value, selection} = elementState; + + return { + elementState: { + selection, + value: toHalfWidthColon(value), + }, + data: toHalfWidthColon(data), + }; + }; +} diff --git a/projects/kit/src/lib/masks/number/processors/fullwidth-to-halfwidth-preprocessor.ts b/projects/kit/src/lib/processors/fullwidth-to-halfwidth-preprocessor.ts similarity index 88% rename from projects/kit/src/lib/masks/number/processors/fullwidth-to-halfwidth-preprocessor.ts rename to projects/kit/src/lib/processors/fullwidth-to-halfwidth-preprocessor.ts index ad651916d..4278ccda6 100644 --- a/projects/kit/src/lib/masks/number/processors/fullwidth-to-halfwidth-preprocessor.ts +++ b/projects/kit/src/lib/processors/fullwidth-to-halfwidth-preprocessor.ts @@ -1,6 +1,6 @@ import {MaskitoPreprocessor} from '@maskito/core'; -import {toHalfWidthNumber} from '../utils/to-half-width-number'; +import {toHalfWidthNumber} from '../utils'; /** * Convert full width numbers like 1, 2 to half width numbers 1, 2 diff --git a/projects/kit/src/lib/processors/index.ts b/projects/kit/src/lib/processors/index.ts index 5dfab21df..1c88274e6 100644 --- a/projects/kit/src/lib/processors/index.ts +++ b/projects/kit/src/lib/processors/index.ts @@ -1,5 +1,7 @@ +export {createColonConvertPreprocessor} from './colon-convert-preprocessor'; export {createDateSegmentsZeroPaddingPostprocessor} from './date-segments-zero-padding-postprocessor'; export {createFirstDateEndSeparatorPreprocessor} from './first-date-end-separator-preprocessor'; +export {createFullWidthToHalfWidthPreprocessor} from './fullwidth-to-halfwidth-preprocessor'; export {createMinMaxDatePostprocessor} from './min-max-date-postprocessor'; export {normalizeDatePreprocessor} from './normalize-date-preprocessor'; export {maskitoPostfixPostprocessorGenerator} from './postfix-postprocessor'; diff --git a/projects/kit/src/lib/utils/index.ts b/projects/kit/src/lib/utils/index.ts index 55f1801f8..a3ceb0e74 100644 --- a/projects/kit/src/lib/utils/index.ts +++ b/projects/kit/src/lib/utils/index.ts @@ -16,3 +16,5 @@ export * from './find-common-beginning-substr'; export * from './identity'; export * from './is-empty'; export * from './pad-with-zeroes-until-valid'; +export * from './to-half-width-colon'; +export * from './to-half-width-number'; diff --git a/projects/kit/src/lib/utils/tests/to-half-width-colon.spec.ts b/projects/kit/src/lib/utils/tests/to-half-width-colon.spec.ts new file mode 100644 index 000000000..42ae21c0f --- /dev/null +++ b/projects/kit/src/lib/utils/tests/to-half-width-colon.spec.ts @@ -0,0 +1,7 @@ +import {toHalfWidthColon} from '../to-half-width-colon'; + +describe('`toHalfWidthColon` utility converts full width colon to half width colon', () => { + it(': => :', () => { + expect(toHalfWidthColon(':')).toBe(':'); + }); +}); diff --git a/projects/kit/src/lib/masks/number/utils/tests/to-half-width-number.spec.ts b/projects/kit/src/lib/utils/tests/to-half-width-number.spec.ts similarity index 100% rename from projects/kit/src/lib/masks/number/utils/tests/to-half-width-number.spec.ts rename to projects/kit/src/lib/utils/tests/to-half-width-number.spec.ts diff --git a/projects/kit/src/lib/utils/to-half-width-colon.ts b/projects/kit/src/lib/utils/to-half-width-colon.ts new file mode 100644 index 000000000..d53e1077f --- /dev/null +++ b/projects/kit/src/lib/utils/to-half-width-colon.ts @@ -0,0 +1,10 @@ +import {CHAR_COLON, CHAR_JP_COLON} from '../constants'; + +/** + * Replace fullwidth colon with half width colon + * @param fullWidthColon full width colon + * @returns processed half width colon + */ +export function toHalfWidthColon(fullWidthColon: string): string { + return fullWidthColon.replace(new RegExp(CHAR_JP_COLON, 'g'), CHAR_COLON); +} diff --git a/projects/kit/src/lib/masks/number/utils/to-half-width-number.ts b/projects/kit/src/lib/utils/to-half-width-number.ts similarity index 100% rename from projects/kit/src/lib/masks/number/utils/to-half-width-number.ts rename to projects/kit/src/lib/utils/to-half-width-number.ts