diff --git a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts index 1021ca5ff..dd7ec1f58 100644 --- a/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts +++ b/projects/demo-integrations/cypress/tests/kit/number/number-prefix-postfix.cy.ts @@ -270,4 +270,43 @@ describe('Number | Prefix & Postfix', () => { ); }); }); + + describe('runtime changes of postfix', () => { + beforeEach(() => { + cy.visit(DemoPath.Cypress); + cy.get('#runtime-postfix-changes input') + .focus() + .should('have.value', '1 year') + .as('input'); + }); + + it('1| year => Type 0 => 10| years', () => { + cy.get('@input') + .type('{moveToStart}{rightArrow}') + .type('0') + .should('have.value', '10 years') + .should('have.prop', 'selectionStart', '10'.length) + .should('have.prop', 'selectionEnd', '10'.length); + }); + + it('10| years => Backspace => 1| year', () => { + cy.get('@input') + .type('{moveToStart}{rightArrow}') + .type('0') + .should('have.value', '10 years') + .type('{backspace}') + .should('have.value', '1 year') + .should('have.prop', 'selectionStart', '1'.length) + .should('have.prop', 'selectionEnd', '1'.length); + }); + + it('select all + delete', () => { + cy.get('@input') + .should('have.value', '1 year') + .type('{selectAll}{del}') + .should('have.value', '') + .should('have.prop', 'selectionStart', 0) + .should('have.prop', 'selectionEnd', 0); + }); + }); }); diff --git a/projects/demo/src/pages/cypress/cypress.module.ts b/projects/demo/src/pages/cypress/cypress.module.ts index 45ae709ad..8ece9432e 100644 --- a/projects/demo/src/pages/cypress/cypress.module.ts +++ b/projects/demo/src/pages/cypress/cypress.module.ts @@ -11,6 +11,7 @@ import {CypressDocPageComponent} from './cypress.component'; import {TestDocExample1} from './examples/1-predicate/component'; import {TestDocExample2} from './examples/2-native-max-length/component'; import {TestDocExample3} from './examples/3-mirrored-prefix-postfix/component'; +import {TestDocExample4, TestPipe4} from './examples/4-runtime-postfix-changes/component'; @NgModule({ imports: [ @@ -27,6 +28,8 @@ import {TestDocExample3} from './examples/3-mirrored-prefix-postfix/component'; TestDocExample1, TestDocExample2, TestDocExample3, + TestDocExample4, + TestPipe4, ], exports: [CypressDocPageComponent], }) diff --git a/projects/demo/src/pages/cypress/cypress.template.html b/projects/demo/src/pages/cypress/cypress.template.html index 635ff1621..541889ab2 100644 --- a/projects/demo/src/pages/cypress/cypress.template.html +++ b/projects/demo/src/pages/cypress/cypress.template.html @@ -8,6 +8,10 @@ + + diff --git a/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts b/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts new file mode 100644 index 000000000..0214af6d7 --- /dev/null +++ b/projects/demo/src/pages/cypress/examples/4-runtime-postfix-changes/component.ts @@ -0,0 +1,51 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Pipe, + PipeTransform, + ViewChild, +} from '@angular/core'; +import {MaskitoOptions} from '@maskito/core'; +import {maskitoNumberOptionsGenerator, maskitoParseNumber} from '@maskito/kit'; + +@Pipe({ + name: 'calculateMask', +}) +export class TestPipe4 implements PipeTransform { + transform(postfix: string): MaskitoOptions { + return maskitoNumberOptionsGenerator({ + postfix, + precision: 2, + thousandSeparator: ' ', + }); + } +} + +@Component({ + selector: 'test-doc-example-4', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TestDocExample4 { + @ViewChild('inputRef', {read: ElementRef, static: true}) + readonly inputRef!: ElementRef; + + get parsedValue(): number { + return maskitoParseNumber(this.inputRef.nativeElement.value); + } + + readonly pluralize = { + one: ` year`, + few: ` years`, + many: ` years`, + other: ` years`, + }; +} diff --git a/projects/kit/src/lib/masks/number/number-mask.ts b/projects/kit/src/lib/masks/number/number-mask.ts index 432a6272c..db254e52f 100644 --- a/projects/kit/src/lib/masks/number/number-mask.ts +++ b/projects/kit/src/lib/masks/number/number-mask.ts @@ -18,6 +18,7 @@ import { } from './plugins'; import { createDecimalZeroPaddingPostprocessor, + createInitializationOnlyPreprocessor, createMinMaxPostprocessor, createNonRemovableCharsDeletionPreprocessor, createNotEmptyIntegerPartPreprocessor, @@ -67,6 +68,11 @@ export function maskitoNumberOptionsGenerator({ isNegativeAllowed: min < 0, }), preprocessors: [ + createInitializationOnlyPreprocessor({ + decimalSeparator, + decimalPseudoSeparators, + pseudoMinuses, + }), createPseudoCharactersPreprocessor(CHAR_MINUS, pseudoMinuses), createPseudoCharactersPreprocessor(decimalSeparator, decimalPseudoSeparators), createNotEmptyIntegerPartPreprocessor({decimalSeparator, precision}), diff --git a/projects/kit/src/lib/masks/number/processors/index.ts b/projects/kit/src/lib/masks/number/processors/index.ts index d0b72579f..46d6c9f67 100644 --- a/projects/kit/src/lib/masks/number/processors/index.ts +++ b/projects/kit/src/lib/masks/number/processors/index.ts @@ -1,4 +1,5 @@ export * from './decimal-zero-padding-postprocessor'; +export * from './initialization-only-preprocessor'; export * from './leading-zeroes-validation-postprocessor'; export * from './min-max-postprocessor'; export * from './non-removable-chars-deletion-preprocessor'; diff --git a/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts b/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts new file mode 100644 index 000000000..955a818e9 --- /dev/null +++ b/projects/kit/src/lib/masks/number/processors/initialization-only-preprocessor.ts @@ -0,0 +1,50 @@ +import {MaskitoPreprocessor, maskitoTransform} from '@maskito/core'; + +import {generateMaskExpression} from '../utils'; + +/** + * This preprocessor works only once at initialization phase (when `new Maskito(...)` is executed). + * This preprocessor helps to avoid conflicts during transition from one mask to another (for the same input). + * For example, the developer changes postfix (or other mask's props) during run-time. + * ``` + * let maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' year'}); + * // [3 seconds later] + * maskitoOptions = maskitoNumberOptionsGenerator({postfix: ' years'}); + * ``` + */ +export function createInitializationOnlyPreprocessor({ + decimalSeparator, + decimalPseudoSeparators, + pseudoMinuses, +}: { + decimalSeparator: string; + decimalPseudoSeparators: readonly string[]; + pseudoMinuses: readonly string[]; +}): MaskitoPreprocessor { + let isInitializationPhase = true; + const cleanNumberMask = generateMaskExpression({ + decimalSeparator, + decimalPseudoSeparators, + pseudoMinuses, + prefix: '', + postfix: '', + thousandSeparator: '', + precision: Infinity, + isNegativeAllowed: true, + }); + + return ({elementState, data}) => { + if (!isInitializationPhase) { + return {elementState, data}; + } + + isInitializationPhase = false; + + return { + elementState: maskitoTransform(elementState, { + mask: cleanNumberMask, + }), + data, + }; + }; +} diff --git a/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts b/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts index 4f1b49493..a43096744 100644 --- a/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts +++ b/projects/kit/src/lib/masks/number/tests/number-mask.spec.ts @@ -1,13 +1,17 @@ -import {maskitoTransform} from '@maskito/core'; +import {MASKITO_DEFAULT_OPTIONS, MaskitoOptions, maskitoTransform} from '@maskito/core'; import {maskitoNumberOptionsGenerator} from '../number-mask'; -describe('Number', () => { - describe('`precision` is `0` and it is paste from clipboard', () => { - const options = maskitoNumberOptionsGenerator({ - decimalSeparator: ',', - decimalPseudoSeparators: ['.'], - precision: 0, +describe('Number (maskitoTransform)', () => { + describe('`precision` is `0`', () => { + let options: MaskitoOptions = MASKITO_DEFAULT_OPTIONS; + + beforeEach(() => { + options = maskitoNumberOptionsGenerator({ + decimalSeparator: ',', + decimalPseudoSeparators: ['.'], + precision: 0, + }); }); it('drops decimal part (123,45)', () => { @@ -17,5 +21,9 @@ describe('Number', () => { it('drops decimal part (123.45)', () => { expect(maskitoTransform('123.45', options)).toBe('123'); }); + + it('keeps minus sign (-123)', () => { + expect(maskitoTransform('-123', options)).toBe('−123'); + }); }); }); diff --git a/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts b/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts index f0d1c014f..e75e3a543 100644 --- a/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts +++ b/projects/kit/src/lib/masks/number/utils/generate-mask-expression.ts @@ -10,6 +10,8 @@ export function generateMaskExpression({ thousandSeparator, prefix, postfix, + decimalPseudoSeparators = [], + pseudoMinuses = [], }: { decimalSeparator: string; isNegativeAllowed: boolean; @@ -17,18 +19,22 @@ export function generateMaskExpression({ thousandSeparator: string; prefix: string; postfix: string; + decimalPseudoSeparators?: readonly string[]; + pseudoMinuses?: readonly string[]; }): MaskitoMask { const computedPrefix = computeAllOptionalCharsRegExp(prefix); const digit = '\\d'; - const optionalMinus = isNegativeAllowed ? `${CHAR_MINUS}?` : ''; + const optionalMinus = isNegativeAllowed + ? `[${CHAR_MINUS}${pseudoMinuses.map(x => `\\${x}`).join('')}]?` + : ''; const integerPart = thousandSeparator ? `[${digit}${escapeRegExp(thousandSeparator)}]*` : `[${digit}]*`; const decimalPart = precision > 0 - ? `(${escapeRegExp(decimalSeparator)}${digit}{0,${ - Number.isFinite(precision) ? precision : '' - }})?` + ? `([${escapeRegExp(decimalSeparator)}${decimalPseudoSeparators + .map(escapeRegExp) + .join('')}]${digit}{0,${Number.isFinite(precision) ? precision : ''}})?` : ''; const computedPostfix = computeAllOptionalCharsRegExp(postfix);