diff --git a/CHANGELOG.md b/CHANGELOG.md index 360cc8cb..23168c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,15 @@ +# 19.0.5(2024-12-17) + +### Fix + +- Fix ([#1481](https://github.com/JsDaddy/ngx-mask/issues/1481)) +- Fix ([#1411](https://github.com/JsDaddy/ngx-mask/issues/1411)) + # 19.0.4(2024-12-13) ### Feature -- add input property instantPrefix +- add input property instantPrefix ### Fix diff --git a/USAGE.md b/USAGE.md index 9e0932f8..cfa80741 100644 --- a/USAGE.md +++ b/USAGE.md @@ -132,7 +132,6 @@ pattern = { You can add prefix to you masked value - #### Usage ```html @@ -150,10 +149,9 @@ When set to false, the prefix only becomes visible when a value is present in th ```html - + ``` - ### suffix (string) You can add suffix to you masked value diff --git a/bun.lockb b/bun.lockb index 1f5f9289..7da869d5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 435286ae..947bed2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ngx-mask", - "version": "19.0.4", + "version": "19.0.5", "description": "Awesome ngx mask", "license": "MIT", "engines": { @@ -60,67 +60,67 @@ "url": "https://github.com/JsDaddy/ngx-mask.git" }, "dependencies": { - "@angular/animations": "19.0.3", - "@angular/common": "19.0.3", - "@angular/compiler": "19.0.3", - "@angular/core": "19.0.3", - "@angular/forms": "19.0.3", - "@angular/platform-browser": "19.0.3", - "@angular/platform-browser-dynamic": "19.0.3", - "@angular/router": "19.0.3", + "@angular/animations": "19.0.4", + "@angular/common": "19.0.4", + "@angular/compiler": "19.0.4", + "@angular/core": "19.0.4", + "@angular/forms": "19.0.4", + "@angular/platform-browser": "19.0.4", + "@angular/platform-browser-dynamic": "19.0.4", + "@angular/router": "19.0.4", "@types/jest": "^29.5.14", "@types/mocha": "^10.0.10", "ajv": "^8.17.1", "cypress": "^13.16.1", - "highlight.js": "11.10.0", + "highlight.js": "11.11.0", "ngx-highlightjs": "12.0.0", - "ngxtension": "^4.1.0", + "ngxtension": "^4.2.0", "rxjs": "7.8.1", "semantic-release": "24.2.0", "semantic-release-export-data": "^1.1.0", - "snyk": "^1.1294.2" + "snyk": "^1.1294.3" }, "devDependencies": { - "@angular-devkit/build-angular": "19.0.3", - "@angular-eslint/builder": "19.0.0", - "@angular-eslint/eslint-plugin": "19.0.0", - "@angular-eslint/eslint-plugin-template": "19.0.0", - "@angular-eslint/schematics": "19.0.0", - "@angular-eslint/template-parser": "19.0.0", - "@angular/cli": "19.0.3", - "@angular/compiler-cli": "19.0.3", - "@angular/language-service": "19.0.3", - "@commitlint/cli": "19.6.0", + "@angular-devkit/build-angular": "19.0.5", + "@angular-eslint/builder": "19.0.2", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "@angular-eslint/schematics": "19.0.2", + "@angular-eslint/template-parser": "19.0.2", + "@angular/cli": "19.0.5", + "@angular/compiler-cli": "19.0.4", + "@angular/language-service": "19.0.4", + "@commitlint/cli": "19.6.1", "@commitlint/config-conventional": "19.6.0", "@jscutlery/cypress-angular": "^0.9.22", "@types/highlight.js": "9.12.4", "@types/jasmine": "5.1.5", - "@types/node": "22.10.1", - "@typescript-eslint/eslint-plugin": "8.17.0", - "@typescript-eslint/parser": "8.17.0", + "@types/node": "22.10.2", + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", "@web/test-runner": "^0.19.0", "angular-cli-ghpages": "2.0.3", "angular-http-server": "1.12.0", - "eslint": "9.16.0", + "eslint": "9.17.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-json": "4.0.1", "eslint-plugin-prettier": "5.2.1", "jasmine-core": "5.5.0", "jasmine-spec-reporter": "7.0.0", - "lint-staged": "15.2.10", + "lint-staged": "15.2.11", "markdownlint-cli": "0.43.0", "ng-packagr": "19.0.1", "npm-check-updates": "^17.1.11", "prettier": "3.4.2", - "puppeteer": "23.10.1", - "stylelint": "16.11.0", + "puppeteer": "23.10.4", + "stylelint": "16.12.0", "stylelint-config-prettier": "9.0.5", "stylelint-config-recommended-scss": "14.1.0", "stylelint-prettier": "5.0.2", "type-coverage": "^2.29.7", "typescript": "5.6.3", - "angular-eslint": "^19.0.0", - "typescript-eslint": "^8.17.0", + "angular-eslint": "^19.0.2", + "typescript-eslint": "^8.18.1", "tailwindcss": "^3.4.16", "bun-types": "^1.1.38", "postcss": "8.4.49", diff --git a/projects/ngx-mask-lib/package.json b/projects/ngx-mask-lib/package.json index c78fd945..7ce1935b 100644 --- a/projects/ngx-mask-lib/package.json +++ b/projects/ngx-mask-lib/package.json @@ -1,6 +1,6 @@ { "name": "ngx-mask", - "version": "19.0.4", + "version": "19.0.5", "description": "awesome ngx mask", "keywords": [ "ng2-mask", diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts index 529f920d..b5ab3521 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask-applier.service.ts @@ -197,11 +197,62 @@ export class NgxMaskApplierService { } const precision: number = this.getPrecision(maskExpression); - const decimalMarker = Array.isArray(this.decimalMarker) - ? this.thousandSeparator === MaskExpression.DOT - ? MaskExpression.COMMA - : MaskExpression.DOT - : this.decimalMarker; + let decimalMarker = this.decimalMarker; + + if (Array.isArray(this.decimalMarker)) { + const marker = this.decimalMarker.find((dm) => dm !== this.thousandSeparator); + + decimalMarker = marker + ? marker + : this.actualValue.includes(this.decimalMarker[0]) + ? this.decimalMarker[0] + : this.decimalMarker[1]; + } + + if (backspaced) { + const { decimalMarkerIndex, nonZeroIndex } = this._findFirstNonZeroAndDecimalIndex( + processedValue, + decimalMarker as '.' | ',' + ); + const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS; + const zeroIndexNumberZero = processedValue[0] === MaskExpression.NUMBER_ZERO; + const zeroIndexDecimalMarker = processedValue[0] === decimalMarker; + const firstIndexDecimalMarker = processedValue[1] === decimalMarker; + + if ( + (zeroIndexDecimalMarker && !nonZeroIndex) || + (zeroIndexMinus && firstIndexDecimalMarker && !nonZeroIndex) || + (zeroIndexNumberZero && !decimalMarkerIndex && !nonZeroIndex) + ) { + processedValue = MaskExpression.NUMBER_ZERO; + } + + if ( + decimalMarkerIndex && + nonZeroIndex && + zeroIndexMinus && + processedPosition === 1 + ) { + if (decimalMarkerIndex < nonZeroIndex || decimalMarkerIndex > nonZeroIndex) { + processedValue = MaskExpression.MINUS + processedValue.slice(nonZeroIndex); + } + } + + if (!decimalMarkerIndex && nonZeroIndex && processedValue.length > nonZeroIndex) { + processedValue = zeroIndexMinus + ? MaskExpression.MINUS + processedValue.slice(nonZeroIndex) + : processedValue.slice(nonZeroIndex); + } + + if (decimalMarkerIndex && nonZeroIndex && processedPosition === 0) { + if (decimalMarkerIndex < nonZeroIndex) { + processedValue = processedValue.slice(decimalMarkerIndex - 1); + } + if (decimalMarkerIndex > nonZeroIndex) { + processedValue = processedValue.slice(nonZeroIndex); + } + } + } if (precision === 0) { processedValue = this.allowNegativeNumbers @@ -240,7 +291,8 @@ export class NgxMaskApplierService { if ( processedValue[0] === MaskExpression.NUMBER_ZERO && processedValue[1] !== decimalMarker && - processedValue[1] !== this.thousandSeparator + processedValue[1] !== this.thousandSeparator && + !backspaced ) { processedValue = processedValue.length > 1 @@ -252,6 +304,7 @@ export class NgxMaskApplierService { } if ( this.allowNegativeNumbers && + !backspaced && processedValue[0] === MaskExpression.MINUS && (processedValue[1] === decimalMarker || processedValue[1] === MaskExpression.NUMBER_ZERO) @@ -272,61 +325,6 @@ export class NgxMaskApplierService { } } - if (backspaced) { - const inputValueAfterZero = processedValue.slice( - this._findFirstNonZeroDigitIndex(processedValue), - processedValue.length - ); - const positionOfZeroOrDecimalMarker = - processedValue[processedPosition] === MaskExpression.NUMBER_ZERO || - processedValue[processedPosition] === decimalMarker; - const zeroIndexNumberZero = processedValue[0] === MaskExpression.NUMBER_ZERO; - const firstIndexNumberZero = processedValue[1] === MaskExpression.NUMBER_ZERO; - const zeroIndexMinus = processedValue[0] === MaskExpression.MINUS; - const zeroIndexThousand = processedValue[0] === this.thousandSeparator; - const firstIndexDecimalMarker = processedValue[1] === decimalMarker; - const zeroIndexDecimalMarker = processedValue[0] === decimalMarker; - const secondIndexDecimalMarker = processedValue[2] === decimalMarker; - - if (zeroIndexNumberZero && firstIndexDecimalMarker && processedPosition === 0) { - return processedValue; - } - - if (zeroIndexDecimalMarker && processedPosition === 0) { - processedValue = inputValueAfterZero; - } - - if ( - zeroIndexNumberZero && - firstIndexDecimalMarker && - positionOfZeroOrDecimalMarker && - processedPosition < 2 - ) { - processedValue = inputValueAfterZero; - } - if ( - zeroIndexMinus && - firstIndexNumberZero && - secondIndexDecimalMarker && - positionOfZeroOrDecimalMarker && - processedPosition < 3 - ) { - processedValue = MaskExpression.MINUS + inputValueAfterZero; - } - - if ( - inputValueAfterZero !== MaskExpression.MINUS && - ((processedPosition === 0 && (zeroIndexNumberZero || zeroIndexThousand)) || - (this.allowNegativeNumbers && - processedPosition === 1 && - zeroIndexMinus && - !firstIndexNumberZero)) - ) { - processedValue = zeroIndexMinus - ? MaskExpression.MINUS + inputValueAfterZero - : inputValueAfterZero; - } - } // TODO: we had different rexexps here for the different cases... but tests dont seam to bother - check this // separator: no COMMA, dot-sep: no SPACE, COMMA OK, comma-sep: no SPACE, COMMA OK @@ -1036,13 +1034,29 @@ export class NgxMaskApplierService { } } - private _findFirstNonZeroDigitIndex(inputString: string): number { + private _findFirstNonZeroAndDecimalIndex(inputString: string, decimalMarker: '.' | ',') { + let decimalMarkerIndex: number | null = null; + let nonZeroIndex: number | null = null; + for (let i = 0; i < inputString.length; i++) { const char = inputString[i]; - if (char && char >= '1' && char <= '9') { - return i; + + if (char === decimalMarker && decimalMarkerIndex === null) { + decimalMarkerIndex = i; + } + + if (char && char >= '1' && char <= '9' && nonZeroIndex === null) { + nonZeroIndex = i; + } + + if (decimalMarkerIndex !== null && nonZeroIndex !== null) { + break; } } - return -1; + + return { + decimalMarkerIndex, + nonZeroIndex, + }; } } diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts index 6cb07c9f..0a8591ac 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.directive.ts @@ -161,6 +161,32 @@ export class NgxMaskDirective implements ControlValueAccessor, OnChanges, Valida } if (thousandSeparator) { this._maskService.thousandSeparator = thousandSeparator.currentValue; + if (thousandSeparator.previousValue && thousandSeparator.currentValue) { + const previousDecimalMarker = this._maskService.decimalMarker; + + if (thousandSeparator.currentValue === this._maskService.decimalMarker) { + this._maskService.decimalMarker = + thousandSeparator.currentValue === MaskExpression.COMMA + ? MaskExpression.DOT + : MaskExpression.COMMA; + } + if (this._maskService.dropSpecialCharacters === true) { + this._maskService.specialCharacters = this._config.specialCharacters; + } + if ( + typeof previousDecimalMarker === 'string' && + typeof this._maskService.decimalMarker === 'string' + ) { + this._inputValue.set( + this._inputValue() + .split(thousandSeparator.previousValue) + .join('') + .replace(previousDecimalMarker, this._maskService.decimalMarker) + ); + this._maskService.actualValue = this._inputValue(); + } + this._maskService.writingValue = true; + } } if (decimalMarker) { this._maskService.decimalMarker = decimalMarker.currentValue; diff --git a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts index 064e88a1..bd95795c 100644 --- a/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts +++ b/projects/ngx-mask-lib/src/lib/ngx-mask.service.ts @@ -611,6 +611,7 @@ export class NgxMaskService extends NgxMaskApplierService { ) ) : ''; + this.writingValue = false; this.maskChanged = false; return; } diff --git a/projects/ngx-mask-lib/src/test/allow-negative-numbers.spec.ts b/projects/ngx-mask-lib/src/test/allow-negative-numbers.spec.ts index 5eb8bf00..9a89808b 100644 --- a/projects/ngx-mask-lib/src/test/allow-negative-numbers.spec.ts +++ b/projects/ngx-mask-lib/src/test/allow-negative-numbers.spec.ts @@ -41,8 +41,8 @@ describe('Directive: Mask (Allow negative numbers)', () => { component.dropSpecialCharacters.set(true); component.form.setValue(-123456); - equal('-123456.00', '-123,456.00', fixture); expect(component.form.value).toBe(-123456); + equal('-123456.00', '-123,456.00', fixture); }); it('allowNegativeNumber to mask', () => { diff --git a/projects/ngx-mask-lib/src/test/separator.cy-spec.ts b/projects/ngx-mask-lib/src/test/separator.cy-spec.ts index 4ae25843..a373041d 100644 --- a/projects/ngx-mask-lib/src/test/separator.cy-spec.ts +++ b/projects/ngx-mask-lib/src/test/separator.cy-spec.ts @@ -297,7 +297,7 @@ describe('Test Date Hh:m0', () => { .type('{leftArrow}'.repeat(2)) .type('{backspace}') .should('have.value', '-0.14') - .type('{leftArrow}'.repeat(2)) + .type('{leftArrow}') .type('{backspace}') .should('have.value', '-14'); }); @@ -317,6 +317,7 @@ describe('Test Date Hh:m0', () => { .type('{leftArrow}'.repeat(2)) .type('{backspace}') .should('have.value', '-1') + .type('{rightArrow}') .type('{backspace}') .should('have.value', '-'); }); @@ -462,4 +463,47 @@ describe('Test Date Hh:m0', () => { .type('{backspace}') .should('have.value', '5'); }); + + it('should correct work after backspace separator.2 when after first digit 0', () => { + cy.mount(CypressTestMaskComponent, { + componentProperties: { + mask: signal('separator.2'), + decimalMarker: signal(','), + thousandSeparator: signal(' '), + }, + }); + + cy.get('#masked') + .type('1 000 000,05') + .should('have.value', '1 000 000,05') + .type('{leftArrow}'.repeat(11)) + .type('{backspace}') + .should('have.value', '0,05'); + + cy.get('#masked').clear(); + + cy.get('#masked') + .type('60,05') + .should('have.value', '60,05') + .type('{leftArrow}'.repeat(4)) + .type('{backspace}') + .should('have.value', '0,05'); + }); + + it('should correct work after backspace separator.2 when after first digit 0', () => { + cy.mount(CypressTestMaskComponent, { + componentProperties: { + mask: signal('separator.2'), + decimalMarker: signal('.'), + thousandSeparator: signal(' '), + }, + }); + + cy.get('#masked') + .type('200.05') + .should('have.value', '200.05') + .type('{leftArrow}'.repeat(5)) + .type('{backspace}') + .should('have.value', '0.05'); + }); }); diff --git a/projects/ngx-mask-lib/src/test/separator.spec.ts b/projects/ngx-mask-lib/src/test/separator.spec.ts index cc629b5f..7707786a 100644 --- a/projects/ngx-mask-lib/src/test/separator.spec.ts +++ b/projects/ngx-mask-lib/src/test/separator.spec.ts @@ -4,7 +4,7 @@ import { By } from '@angular/platform-browser'; import type { DebugElement } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { TestMaskComponent } from './utils/test-component.component'; -import { equal, Paste, typeTest } from './utils/test-functions.component'; +import { equal, Paste, Type, typeTest } from './utils/test-functions.component'; import { initialConfig, NgxMaskDirective, provideNgxMask } from 'ngx-mask'; describe('Separator: Mask', () => { @@ -1902,4 +1902,67 @@ describe('Separator: Mask', () => { expect(component.form.dirty).toBe(false); expect(component.form.pristine).toBe(true); }); + + it('should show correct value in model after changing thousandSeparator', () => { + component.mask.set('separator.2'); + component.thousandSeparator.set(' '); + component.decimalMarker.set(','); + + const inputElement = fixture.nativeElement.querySelector('input'); + inputElement.value = '100000.00'; + inputElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + + expect(component.form.value).toBe('100000.00'); + + component.thousandSeparator.set('.'); + fixture.detectChanges(); + + expect(component.form.value).toBe('100000.00'); + + component.thousandSeparator.set('-'); + fixture.detectChanges(); + + expect(component.form.value).toBe('100000.00'); + + component.thousandSeparator.set(','); + fixture.detectChanges(); + + expect(component.form.value).toBe('100000.00'); + }); + + it('should show correct value in input after changing thousandSeparator', () => { + const debugElement: DebugElement = fixture.debugElement.query(By.css('input')); + const inputTarget: HTMLInputElement = debugElement.nativeElement as HTMLInputElement; + spyOnProperty(document, 'activeElement').and.returnValue(inputTarget); + fixture.detectChanges(); + component.mask.set('separator.2'); + component.thousandSeparator.set(' '); + component.decimalMarker.set(','); + + equal('123456,10', '123 456,10', fixture, false, Type); + expect(inputTarget.value).toBe('123 456,10'); + expect(component.form.value).toBe('123456.10'); + + component.thousandSeparator.set('.'); + fixture.detectChanges(); + + equal('123456,10', '123.456,10', fixture, false, Type); + expect(inputTarget.value).toBe('123.456,10'); + expect(component.form.value).toBe('123456.10'); + + component.thousandSeparator.set('-'); + fixture.detectChanges(); + + equal('123456,10', '123-456,10', fixture, false, Type); + expect(inputTarget.value).toBe('123-456,10'); + expect(component.form.value).toBe('123456.10'); + + component.thousandSeparator.set(','); + fixture.detectChanges(); + + equal('123456.10', '123,456.10', fixture, false, Type); + expect(inputTarget.value).toBe('123,456.10'); + expect(component.form.value).toBe('123456.10'); + }); });