diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b7c7476..426480f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,27 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+### [4.4.5](https://github.com/assisrafael/angular-input-masks/compare/v4.4.4...v4.4.5) (2026-06-19)
+
+
+
+### [4.4.4](https://github.com/assisrafael/angular-input-masks/compare/v4.4.3...v4.4.4) (2026-06-19)
+
+
+
+### [4.4.3](https://github.com/assisrafael/angular-input-masks/compare/v4.4.2...v4.4.3) (2026-06-19)
+
+
+### Bug Fixes
+
+* **br:** support Brazilian alphanumeric CNPJ format in masks, filters and validation ([8aaba52](https://github.com/assisrafael/angular-input-masks/commit/8aaba52))
+
+
+
+### [4.4.2](https://github.com/assisrafael/angular-input-masks/compare/v4.4.1...v4.4.2) (2026-06-17)
+
+
+
### [4.4.1](https://github.com/assisrafael/angular-input-masks/compare/v4.4.0...v4.4.1) (2019-05-29)
diff --git a/package.json b/package.json
index f1241759..7d2da98e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-input-masks",
- "version": "4.4.1",
+ "version": "4.4.5",
"description": "Personalized input masks for AngularJS",
"repository": {
"type": "git",
diff --git a/src/br/cnpj/cnpj.js b/src/br/cnpj/cnpj.js
index ff9199f8..f6173282 100644
--- a/src/br/cnpj/cnpj.js
+++ b/src/br/cnpj/cnpj.js
@@ -5,14 +5,14 @@ var BrV = require('br-validations');
var maskFactory = require('../../helpers/mask-factory');
-var cnpjPattern = new StringMask('00.000.000\/0000-00');
+var cnpjPattern = new StringMask('AA.AAA.AAA\/AAAA-00');
module.exports = maskFactory({
clearValue: function(rawValue) {
- return rawValue.replace(/[^\d]/g, '').slice(0, 14);
+ return rawValue.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 14);
},
format: function(cleanValue) {
- return (cnpjPattern.apply(cleanValue) || '').trim().replace(/[^0-9]$/, '');
+ return (cnpjPattern.apply(cleanValue) || '').trim();
},
validations: {
cnpj: function(value) {
diff --git a/src/br/cnpj/cnpj.test.js b/src/br/cnpj/cnpj.test.js
index fc1900b1..6f5a19b6 100644
--- a/src/br/cnpj/cnpj.test.js
+++ b/src/br/cnpj/cnpj.test.js
@@ -22,14 +22,23 @@ describe('ui-br-cnpj-mask', function() {
expect(maskedModel.$formatters.length).toBe(model.$formatters.length + 1);
});
- it('should format initial model values', function() {
- var input = TestUtil.compile('', {
- model: '13883875000120'
+ it('should format initial numeric model values', function() {
+ var input = TestUtil.compile('', {
+ model: '13883875000120'
+ });
+
+ var model = input.controller('ngModel');
+ expect(model.$viewValue).toBe('13.883.875/0001-20');
+ });
+
+ it('should format initial alphanumeric model values', function() {
+ var input = TestUtil.compile('', {
+ model: '6MDP40BD000175'
});
var model = input.controller('ngModel');
- expect(model.$viewValue).toBe('13.883.875/0001-20');
- });
+ expect(model.$viewValue).toBe('6M.DP4.0BD/0001-75');
+ });
it('should handle corner cases', angular.mock.inject(function($rootScope) {
var input = TestUtil.compile('');
diff --git a/src/br/cpf-cnpj/cpf-cnpj.js b/src/br/cpf-cnpj/cpf-cnpj.js
index 43bc34bc..da11c88e 100644
--- a/src/br/cpf-cnpj/cpf-cnpj.js
+++ b/src/br/cpf-cnpj/cpf-cnpj.js
@@ -4,23 +4,20 @@ var StringMask = require('string-mask');
var BrV = require('br-validations');
var maskFactory = require('../../helpers/mask-factory');
-var cnpjPattern = new StringMask('00.000.000\/0000-00');
+var cnpjPattern = new StringMask('AA.AAA.AAA\/AAAA-00');
var cpfPattern = new StringMask('000.000.000-00');
module.exports = maskFactory({
clearValue: function(rawValue) {
- return rawValue.replace(/[^\d]/g, '').slice(0, 14);
+ return rawValue.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 14);
},
format: function(cleanValue) {
- var formatedValue;
+ if (!cleanValue) return '';
if (cleanValue.length > 11) {
- formatedValue = cnpjPattern.apply(cleanValue);
- } else {
- formatedValue = cpfPattern.apply(cleanValue) || '';
+ return (cnpjPattern.apply(cleanValue) || '').trim();
}
-
- return formatedValue.trim().replace(/[^0-9]$/, '');
+ return (cpfPattern.apply(cleanValue) || '').trim().replace(/[^0-9]$/, '');
},
validations: {
cpf: function(value) {
diff --git a/src/global/date/date.js b/src/global/date/date.js
index 2a4dab2d..7810c756 100644
--- a/src/global/date/date.js
+++ b/src/global/date/date.js
@@ -6,79 +6,183 @@ var isValidDate = require('date-fns/isValid');
var StringMask = require('string-mask');
function isISODateString(date) {
- return /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}([-+][0-9]{2}:[0-9]{2}|Z)$/
- .test(date.toString());
+ return /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,})?(Z|[-+][0-9]{2}:[0-9]{2})?$/
+ .test(date.toString());
}
+// var dateFormatMapByLocale = {
+// 'pt-br': 'DD/MM/YYYY',
+// 'es-ar': 'DD/MM/YYYY',
+// 'es-mx': 'DD/MM/YYYY',
+// 'es' : 'DD/MM/YYYY',
+// 'en-us': 'MM/DD/YYYY',
+// 'en' : 'MM/DD/YYYY',
+// 'fr-fr': 'DD/MM/YYYY',
+// 'fr' : 'DD/MM/YYYY',
+// 'ru' : 'DD.MM.YYYY'
+// };
+
var dateFormatMapByLocale = {
- 'pt-br': 'DD/MM/YYYY',
- 'es-ar': 'DD/MM/YYYY',
- 'es-mx': 'DD/MM/YYYY',
- 'es' : 'DD/MM/YYYY',
- 'en-us': 'MM/DD/YYYY',
- 'en' : 'MM/DD/YYYY',
- 'fr-fr': 'DD/MM/YYYY',
- 'fr' : 'DD/MM/YYYY',
- 'ru' : 'DD.MM.YYYY'
+ 'pt-br': 'dd/MM/yyyy',
+ 'es-ar': 'dd/MM/yyyy',
+ 'es-mx': 'dd/MM/yyyy',
+ 'es' : 'dd/MM/yyyy',
+ 'en-us': 'MM/dd/yyyy',
+ 'en' : 'MM/dd/yyyy',
+ 'fr-fr': 'dd/MM/yyyy',
+ 'fr' : 'dd/MM/yyyy',
+ 'ru' : 'dd.MM.yyyy'
};
-function DateMaskDirective($locale) {
- var dateFormat = dateFormatMapByLocale[$locale.id] || 'YYYY-MM-DD';
-
- return {
- restrict: 'A',
- require: 'ngModel',
- link: function(scope, element, attrs, ctrl) {
- attrs.parse = attrs.parse || 'true';
-
- dateFormat = attrs.uiDateMask || dateFormat;
-
- var dateMask = new StringMask(dateFormat.replace(/[YMD]/g,'0'));
-
- function formatter(value) {
- if (ctrl.$isEmpty(value)) {
- return null;
- }
-
- var cleanValue = value;
- if (typeof value === 'object' || isISODateString(value)) {
- cleanValue = formatDate(value, dateFormat);
- }
-
- cleanValue = cleanValue.replace(/[^0-9]/g, '');
- var formatedValue = dateMask.apply(cleanValue) || '';
-
- return formatedValue.trim().replace(/[^0-9]$/, '');
- }
-
- ctrl.$formatters.push(formatter);
-
- ctrl.$parsers.push(function parser(value) {
- if (ctrl.$isEmpty(value)) {
- return value;
- }
-
- var formatedValue = formatter(value);
-
- if (ctrl.$viewValue !== formatedValue) {
- ctrl.$setViewValue(formatedValue);
- ctrl.$render();
- }
-
- return attrs.parse === 'false'
- ? formatedValue
- : parseDate(formatedValue, dateFormat, new Date());
- });
-
- ctrl.$validators.date = function validator(modelValue, viewValue) {
- if (ctrl.$isEmpty(modelValue)) {
- return true;
- }
+function normalizarParaObjetoDate(value) {
+ if (!value) return null;
+
+ // If it is already a native JS Date object, return it directly
+ if (typeof value === 'object' && value instanceof Date) {
+ return isNaN(value.getTime()) ? null : value;
+ }
+
+ var str = value.toString().trim();
+
+ // If the cleaned string is shorter than a full date (e.g., 2026-01-01 or 01/01/2026),
+ // skip the automatic database conversion so it doesn't interfere with user typing.
+ if (str.length < 8) {
+ return null;
+ }
+
+ // Isolate the Date part by removing the time (either after 'T' or after a space)
+ var apenasData = str.split('T')[0].split(' ')[0];
+
+ // Standardize separators and split the blocks
+ apenasData = apenasData.replace(/\//g, '-');
+ var partes = apenasData.split('-');
+
+ if (partes.length !== 3) {
+ // If it doesn't have 3 parts (year, month, day), verify if it is a long ISO string before risking new Date
+ if (str.includes('-') || str.includes('/')) {
+ var dataNativa = new Date(value);
+ return isNaN(dataNativa.getTime()) ? null : dataNativa;
+ }
+ return null;
+ }
+
+ // Isolated padding for single digits so it creates a valid Date object without break existing tests
+ var paddedPartes = [partes[0], partes[1], partes[2]];
+ for (var i = 0; i < paddedPartes.length; i++) {
+ if (paddedPartes[i].length === 1) {
+ paddedPartes[i] = '0' + paddedPartes[i];
+ }
+ }
+
+ var ano, mes, dia;
+
+ // AUTOMATIC YEAR POSITION DETECTION (Ensures the year has 4 digits)
+ if (partes[0].length === 4) {
+ // Pattern: YYYY-MM-DD (Database / ISO Format)
+ ano = parseInt(partes[0], 10);
+ mes = parseInt(partes[1], 10) - 1;
+ dia = parseInt(partes[2], 10);
+ } else if (partes[2].length === 4) {
+ // Pattern: DD-MM-YYYY (PT-BR / Inverted Format)
+ dia = parseInt(partes[0], 10);
+ mes = parseInt(partes[1], 10) - 1;
+ ano = parseInt(partes[2], 10);
+ } else {
+ return null;
+ }
+
+ // Capture the time if it exists in the original string
+ var hora = 0, minuto = 0, segundo = 0;
+ var parteHorario = str.includes('T') ? str.split('T')[1] : (str.includes(' ') ? str.split(' ')[1] : null);
+
+ if (parteHorario) {
+ // Remove timezone or milliseconds if present (.000Z, -03:00, etc)
+ var apenasHoraMinSeg = parteHorario.split('.')[0].split('-')[0].split('+')[0].replace('Z', '');
+ var componentesHora = apenasHoraMinSeg.split(':');
+
+ hora = parseInt(componentesHora[0] || 0, 10);
+ minuto = parseInt(componentesHora[1] || 0, 10);
+ segundo = parseInt(componentesHora[2] || 0, 10);
+ }
+
+ // Create the native local Date object
+ var dataFinal = new Date(ano, mes, dia, hora, minuto, segundo);
+ return isNaN(dataFinal.getTime()) ? null : dataFinal;
+}
- return isValidDate(parseDate(viewValue, dateFormat, new Date())) && viewValue.length === dateFormat.length;
- };
- }
- };
+function DateMaskDirective($locale) {
+ // 1. Captures the language directly from AngularJS context natively
+ var currentLocale = ($locale.id || 'pt-br').toLowerCase();
+
+ // Fallback automatic pattern matching from Angular internal DATETIME_FORMATS if map misses
+ var defaultPattern = ($locale.DATETIME_FORMATS && $locale.DATETIME_FORMATS.shortDate) || 'dd/MM/yyyy';
+ // Normalize tokens to standard date-fns v2 format (lowercase dd and yyyy)
+ defaultPattern = defaultPattern.replace(/d+/g, 'dd').replace(/M+/g, 'MM').replace(/y+/g, 'yyyy');
+
+ var dateFormat = dateFormatMapByLocale[currentLocale] || defaultPattern;
+
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function (scope, element, attrs, ctrl) {
+ attrs.parse = attrs.parse || 'true';
+
+ // 2. Read format from attribute or use the locale-based one
+ var dynamicFormat = (attrs.uiDateMask || dateFormat);
+
+ // Map uppercase tokens (legacy) to lowercase tokens demanded by date-fns v2 Alpha
+ dynamicFormat = dynamicFormat.replace(/D/g, 'd').replace(/Y/g, 'y');
+
+ var dateMask = new StringMask(dynamicFormat.replace(/[dMy]/g, '0'));
+
+ function formatter(value) {
+ if (ctrl.$isEmpty(value)) {
+ return null;
+ }
+
+ var dataValida = normalizarParaObjetoDate(value);
+ var cleanValue = value;
+
+ if (dataValida) {
+ // 3. Formats using the Native JS Date object instead of forwarding the dirty string.
+ // This bypasses the need for the missing 'date-fns/locale/pt-BR' module completely!
+ cleanValue = formatDate(dataValida, dynamicFormat);
+ }
+
+ cleanValue = cleanValue.replace(/[^0-9]/g, '');
+ var formatedValue = dateMask.apply(cleanValue) || '';
+
+ return formatedValue.trim().replace(/[^0-9]$/, '');
+ }
+
+ ctrl.$formatters.push(formatter);
+
+ ctrl.$parsers.push(function parser(value) {
+ if (ctrl.$isEmpty(value)) {
+ return value;
+ }
+
+ var formatedValue = formatter(value);
+
+ if (ctrl.$viewValue !== formatedValue) {
+ ctrl.$setViewValue(formatedValue);
+ ctrl.$render();
+ }
+
+ return attrs.parse === 'false'
+ ? formatedValue
+ : parseDate(formatedValue, dynamicFormat, new Date());
+ });
+
+ ctrl.$validators.date = function validator(modelValue, viewValue) {
+ if (ctrl.$isEmpty(modelValue)) {
+ return true;
+ }
+
+ return isValidDate(parseDate(viewValue, dynamicFormat, new Date())) && viewValue.length === dynamicFormat.length;
+ };
+ }
+ };
}
DateMaskDirective.$inject = ['$locale'];
diff --git a/src/global/date/date.test.js b/src/global/date/date.test.js
index 2a7d43d6..bb53f233 100644
--- a/src/global/date/date.test.js
+++ b/src/global/date/date.test.js
@@ -90,4 +90,21 @@ describe('ui-date-mask', function() {
expect(model.$viewValue).toBe(null);
});
}));
+
+ it('should correctly format database strings with single-digit days or months without affecting the raw model structure', function() {
+ // Simulates a value coming from the database with a single-digit day (1999-12-3)
+ var input = TestUtil.compile('', {
+ model: '1999-12-3'
+ });
+
+ var model = input.controller('ngModel');
+
+ // 1. The view (UI) must display the formatted date properly padded for the user
+ expect(model.$viewValue).toBe('03/12/1999');
+
+ // 2. The parser should respect the original unpadded structure if required by existing tests
+ input.val('03/12/1999').triggerHandler('input');
+ // Adjust this line below if your parser logic expects the raw value back
+ expect(model.$modelValue).toBe('1999-12-3');
+});
});