Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/br/cnpj/cnpj.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 14 additions & 5 deletions src/br/cnpj/cnpj.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input ng-model="model" ui-br-cnpj-mask>', {
model: '13883875000120'
it('should format initial numeric model values', function() {
var input = TestUtil.compile('<input ng-model="model" ui-br-cnpj-mask>', {
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('<input ng-model="model" ui-br-cnpj-mask>', {
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('<input ng-model="model" ui-br-cnpj-mask>');
Expand Down
13 changes: 5 additions & 8 deletions src/br/cpf-cnpj/cpf-cnpj.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
238 changes: 171 additions & 67 deletions src/global/date/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand Down
17 changes: 17 additions & 0 deletions src/global/date/date.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('<input ng-model="model" ui-date-mask>', {
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');
});
});