diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b7c7476..9f6b19f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,31 @@
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.6](https://github.com/assisrafael/angular-input-masks/compare/v4.4.5...v4.4.6) (2026-10-28)
+
+
+
+### [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..d15937dd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-input-masks",
- "version": "4.4.1",
+ "version": "4.4.6",
"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-time/date-time-pt-br.html b/src/global/date-time/date-time-pt-br.html
new file mode 100644
index 00000000..dccb84b7
--- /dev/null
+++ b/src/global/date-time/date-time-pt-br.html
@@ -0,0 +1,42 @@
+
+
+
+
+ Date Time Spec
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/global/date-time/date-time.html b/src/global/date-time/date-time.html
new file mode 100644
index 00000000..7160452b
--- /dev/null
+++ b/src/global/date-time/date-time.html
@@ -0,0 +1,41 @@
+
+
+
+
+ Date Time Spec
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/global/date-time/date-time.js b/src/global/date-time/date-time.js
new file mode 100644
index 00000000..2d3c89cf
--- /dev/null
+++ b/src/global/date-time/date-time.js
@@ -0,0 +1,135 @@
+'use strict';
+
+var formatDate = require('date-fns/format');
+var parseDate = require('date-fns/parse');
+var isValidDate = require('date-fns/isValid');
+var StringMask = require('string-mask');
+
+var dateFormatMapByLocale = {
+ 'pt-br': 'dd/MM/yyyy',
+ 'es-ar': 'dd/MM/yyyy',
+ 'en-us': 'MM/dd/yyyy',
+ 'fr-fr': 'dd/MM/yyyy',
+ 'ru' : 'dd.MM.yyyy'
+};
+
+function normalizarParaObjetoDate(value) {
+ if (!value) return null;
+
+ if (typeof value === 'object' && value instanceof Date) {
+ return isNaN(value.getTime()) ? null : value;
+ }
+
+ var str = value.toString().trim();
+ if (str.length < 8) {
+ return null;
+ }
+
+ var partesDataHora = str.split('T');
+ var apenasData = partesDataHora[0].split(' ')[0].replace(/\//g, '-');
+ var partes = apenasData.split('-');
+
+ if (partes.length !== 3) {
+ var dataNativa = new Date(value);
+ return isNaN(dataNativa.getTime()) ? null : dataNativa;
+ }
+
+ var ano, mes, dia;
+ if (partes[0].length === 4) {
+ ano = parseInt(partes[0], 10);
+ mes = parseInt(partes[1], 10) - 1;
+ dia = parseInt(partes[2], 10);
+ } else if (partes[2].length === 4) {
+ dia = parseInt(partes[0], 10);
+ mes = parseInt(partes[1], 10) - 1;
+ ano = parseInt(partes[2], 10);
+ } else {
+ return null;
+ }
+
+ var hora = 0, minuto = 0, segundo = 0;
+ var parteHorario = partesDataHora[1] || (str.includes(' ') ? str.split(' ')[1] : null);
+
+ if (parteHorario) {
+ 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);
+ }
+
+ var dataFinal = new Date(ano, mes, dia, hora, minuto, segundo);
+ return isNaN(dataFinal.getTime()) ? null : dataFinal;
+}
+
+function DateTimeMaskDirective($locale) {
+ var currentLocale = ($locale.id || 'pt-br').toLowerCase();
+ var dateFormat = dateFormatMapByLocale[currentLocale] || 'dd/MM/yyyy';
+
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function (scope, element, attrs, ctrl) {
+ attrs.parse = attrs.parse || 'true';
+
+ // Define se o formato de hora será completo (HH:mm:ss) ou curto (HH:mm)
+ var timeFormat = (attrs.uiDateTimeMask === 'short') ? 'HH:mm' : 'HH:mm:ss';
+ var dynamicFormat = dateFormat + ' ' + timeFormat;
+
+ // Substitui as letras por zeros para a máscara de caracteres do StringMask
+ var maskPattern = dynamicFormat.replace(/[DMYHms]/g, '0');
+ var dateTimeMask = new StringMask(maskPattern);
+ var unformattedLength = maskPattern.replace(/[^0-9]/g, '').length;
+
+ function formatter(value) {
+ if (ctrl.$isEmpty(value)) {
+ return null;
+ }
+
+ var dataValida = normalizarParaObjetoDate(value);
+ var cleanValue = value;
+
+ if (dataValida) {
+ cleanValue = formatDate(dataValida, dynamicFormat);
+ }
+
+ cleanValue = cleanValue.replace(/[^0-9]/g, '').slice(0, unformattedLength);
+ var formattedValue = dateTimeMask.apply(cleanValue) || '';
+
+ return formattedValue.trim().replace(/[^0-9]$/, '');
+ }
+
+ ctrl.$formatters.push(formatter);
+
+ ctrl.$parsers.push(function parser(value) {
+ if (ctrl.$isEmpty(value)) {
+ return value;
+ }
+
+ var formattedValue = formatter(value);
+
+ if (ctrl.$viewValue !== formattedValue) {
+ ctrl.$setViewValue(formattedValue);
+ ctrl.$render();
+ }
+
+ return attrs.parse === 'false'
+ ? formattedValue
+ : parseDate(formattedValue, dynamicFormat, new Date());
+ });
+
+ ctrl.$validators.dateTime = function validator(modelValue, viewValue) {
+ if (ctrl.$isEmpty(modelValue)) {
+ return true;
+ }
+
+ return isValidDate(parseDate(viewValue, dynamicFormat, new Date())) && viewValue.length === dynamicFormat.length;
+ };
+ }
+ };
+}
+
+DateTimeMaskDirective.$inject = ['$locale'];
+
+module.exports = DateTimeMaskDirective;
\ No newline at end of file
diff --git a/src/global/date-time/date-time.spec.js b/src/global/date-time/date-time.spec.js
new file mode 100644
index 00000000..4556b0a2
--- /dev/null
+++ b/src/global/date-time/date-time.spec.js
@@ -0,0 +1,165 @@
+'use strict';
+
+var StringMask = require('string-mask');
+var parseDate = require('date-fns/parse');
+var formatDate = require('date-fns/format');
+
+describe('uiDateTimeMask', function() {
+ describe('default ("YYYY-MM-DD HH:mm:ss") mask', function() {
+ it('should load the demo page', function() {
+ browser.get('/src/global/date-time/date-time.html');
+ expect(browser.getTitle()).toEqual('Date Time Spec');
+ });
+
+ it('should format a date and time', function() {
+ var dateTimeFormatter = new StringMask('0000-00-00 00:00:00'),
+ formattedDateTimeAsString, numberToFormat = '', inputKeysToSend = '19991231153045';
+
+ var input = element(by.model('dateTimeMask')),
+ value = element(by.exactBinding('dateTimeMask'));
+
+ var i;
+ for (i = 0; i < 14; i++) {
+ var key = inputKeysToSend.charAt(i);
+ input.sendKeys(key);
+ numberToFormat += key;
+ formattedDateTimeAsString = dateTimeFormatter.apply(numberToFormat).replace(/[\-\ :\s]+$/, '');
+ expect(input.getAttribute('value')).toEqual(formattedDateTimeAsString);
+ }
+
+ expect(value.evaluate('dateTimeMask.toISOString()')).toEqual(parseDate(formattedDateTimeAsString, 'YYYY-MM-DD HH:mm:ss', new Date()).toISOString());
+
+ for (i = 13; i >= 0; i--) {
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ numberToFormat = numberToFormat.slice(0, -1);
+ if (numberToFormat) {
+ formattedDateTimeAsString = dateTimeFormatter.apply(numberToFormat).replace(/[\-\ :\s]+$/, '');
+ expect(input.getAttribute('value')).toEqual(formattedDateTimeAsString);
+ }
+ }
+ });
+
+ it('should format a model initialized with a date object', function() {
+ var input = element(by.model('initializedDateTimeMask')),
+ value = element(by.exactBinding('initializedDateTimeMask'));
+
+ value.evaluate('initializedDateTimeMask.toString()').then((initialValue) => {
+ var dateTimeValue = formatDate(new Date(initialValue), 'YYYY-MM-DD HH:mm:ss');
+ expect(input.getAttribute('value')).toEqual(dateTimeValue);
+ });
+ });
+
+ it('should format a model initialized with a ISO string', function() {
+ var input = element(by.model('initializedWithISOStringDateTimeMask')),
+ value = element(by.exactBinding('initializedWithISOStringDateTimeMask'));
+
+ value.getText().then((textValue) => {
+ var dateTimeValue = formatDate(parseDate(textValue, 'YYYY-MM-DD HH:mm:ss', new Date()), 'YYYY-MM-DD HH:mm:ss');
+ expect(input.getAttribute('value')).toEqual(dateTimeValue);
+ });
+ });
+
+ it('should be valid if the model is a valid date and time', function() {
+ var inputKeysToSend = '12311999153045';
+
+ var input = element(by.model('dateTimeMask')),
+ valid = element(by.binding('form.dateTimeMaskInput.$error'));
+
+ var i;
+ for (i = 0; i < 13; i++) {
+ input.sendKeys(inputKeysToSend.charAt(i));
+ expect(valid.getText()).toEqual('{ "dateTime": true }');
+ }
+
+ input.sendKeys(inputKeysToSend.charAt(13));
+ expect(valid.getText()).toEqual('{}');
+
+ for (i = 13; i > 0; i--) {
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ expect(valid.getText()).toEqual('{ "dateTime": true }');
+ }
+
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ expect(valid.getText()).toEqual('{}');
+ });
+ });
+
+ describe('pt-br ("DD/MM/YYYY HH:mm:ss") mask', function() {
+ it('should load the demo page', function() {
+ browser.get('/src/global/date-time/date-time-pt-br.html');
+ expect(browser.getTitle()).toEqual('Date Time Spec');
+ });
+
+ it('should format a date and time', function() {
+ var dateTimeFormatter = new StringMask('00/00/0000 00:00:00'),
+ formattedDateTimeAsString, numberToFormat = '', inputKeysToSend = '31121999153045';
+
+ var input = element(by.model('dateTimeMask')),
+ value = element(by.exactBinding('dateTimeMask'));
+
+ var i;
+ for (i = 0; i < 14; i++) {
+ var key = inputKeysToSend.charAt(i);
+ input.sendKeys(key);
+ numberToFormat += key;
+ formattedDateTimeAsString = dateTimeFormatter.apply(numberToFormat).replace(/[\/\ :\s]+$/, '');
+ expect(input.getAttribute('value')).toEqual(formattedDateTimeAsString);
+ }
+
+ expect(value.evaluate('dateTimeMask.toISOString()')).toEqual(parseDate(formattedDateTimeAsString, 'DD/MM/YYYY HH:mm:ss', new Date()).toISOString());
+
+ for (i = 13; i >= 0; i--) {
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ numberToFormat = numberToFormat.slice(0, -1);
+ if (numberToFormat) {
+ formattedDateTimeAsString = dateTimeFormatter.apply(numberToFormat).replace(/[\/\ :\s]+$/, '');
+ expect(input.getAttribute('value')).toEqual(formattedDateTimeAsString);
+ }
+ }
+ });
+
+ it('should format a model initialized with a date object', function() {
+ var input = element(by.model('initializedDateTimeMask')),
+ value = element(by.exactBinding('initializedDateTimeMask'));
+
+ value.evaluate('initializedDateTimeMask.toString()').then((initialValue) => {
+ var dateTimeValue = formatDate(new Date(initialValue), 'DD/MM/YYYY HH:mm:ss');
+ expect(input.getAttribute('value')).toEqual(dateTimeValue);
+ });
+ });
+
+ it('should format a model initialized with a ISO string', function() {
+ var input = element(by.model('initializedWithISOStringDateTimeMask')),
+ value = element(by.exactBinding('initializedWithISOStringDateTimeMask'));
+
+ value.evaluate('initializedDateTimeMask.toString()').then((initialValue) => {
+ var dateTimeValue = formatDate(new Date(initialValue), 'DD/MM/YYYY HH:mm:ss');
+ expect(input.getAttribute('value')).toEqual(dateTimeValue);
+ });
+ });
+
+ it('should be valid if the model is a valid date and time', function() {
+ var inputKeysToSend = '31121999153045';
+
+ var input = element(by.model('dateTimeMask')),
+ valid = element(by.binding('form.dateTimeMaskInput.$error'));
+
+ var i;
+ for (i = 0; i < 13; i++) {
+ input.sendKeys(inputKeysToSend.charAt(i));
+ expect(valid.getText()).toEqual('{ "dateTime": true }');
+ }
+
+ input.sendKeys(inputKeysToSend.charAt(13));
+ expect(valid.getText()).toEqual('{}');
+
+ for (i = 13; i > 0; i--) {
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ expect(valid.getText()).toEqual('{ "dateTime": true }');
+ }
+
+ input.sendKeys(protractor.Key.BACK_SPACE);
+ expect(valid.getText()).toEqual('{}');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/global/date-time/date-time.test.js b/src/global/date-time/date-time.test.js
new file mode 100644
index 00000000..ea4bc875
--- /dev/null
+++ b/src/global/date-time/date-time.test.js
@@ -0,0 +1,106 @@
+'use strict';
+
+require('../global-masks');
+
+describe('ui-date-time-mask', function() {
+ beforeEach(angular.mock.module('ui.utils.masks.global'));
+
+ it('should throw an error if used without ng-model', function() {
+ expect(function() {
+ TestUtil.compile('');
+ }).toThrow();
+ });
+
+ it('should register a $parser and a $formatter', function() {
+ var input = TestUtil.compile('');
+ var model = input.controller('ngModel');
+
+ var maskedInput = TestUtil.compile('');
+ var maskedModel = maskedInput.controller('ngModel');
+
+ expect(maskedModel.$parsers.length).toBe(model.$parsers.length + 1);
+ expect(maskedModel.$formatters.length).toBe(model.$formatters.length + 1);
+ });
+
+ it('should format initial model values', function() {
+ var date = new Date('1999-12-31 15:30:45');
+ var input = TestUtil.compile('', {
+ model: date
+ });
+
+ var model = input.controller('ngModel');
+ expect(model.$viewValue).toBe('12/31/1999 15:30:45');
+ });
+
+ it('should use specified mask', function() {
+ var input = TestUtil.compile('', {
+ model: new Date('1999-12-31 15:30:45')
+ });
+
+ var model = input.controller('ngModel');
+ expect(model.$viewValue).toBe('31.12.1999 15:30:45');
+ });
+
+ it('should ignore non digits', function() {
+ var input = TestUtil.compile('');
+ var model = input.controller('ngModel');
+
+ var tests = [
+ {value:'@', viewValue:''},
+ {value:'1-', viewValue:'1'},
+ {value:'1999a', viewValue:'1999'},
+ {value:'1999_12', viewValue:'1999-12'},
+ {value:'1999123!', viewValue:'1999-12-3'},
+ {value:'19991231153045!', viewValue:'1999-12-31 15:30:45'},
+ ];
+
+ tests.forEach(function(test) {
+ input.val(test.value).triggerHandler('input');
+ expect(model.$viewValue).toBe(test.viewValue);
+ });
+ });
+
+ it('should parse input', angular.mock.inject(function($rootScope) {
+ var input = TestUtil.compile('');
+
+ input.val('1999-12-31 15:30:45').triggerHandler('input');
+ expect($rootScope.model instanceof Date).toBe(true);
+ }));
+
+ it('should not parse input when parse disabled', angular.mock.inject(function($rootScope) {
+ var input = TestUtil.compile('');
+
+ input.val('1999-12-31 15:30:45').triggerHandler('input');
+ expect(typeof ($rootScope.model) === 'string').toBe(true);
+ }));
+
+ it('should handle corner cases', angular.mock.inject(function($rootScope) {
+ var input = TestUtil.compile('');
+ var model = input.controller('ngModel');
+
+ var tests = [
+ {modelValue: '', viewValue: ''},
+ {modelValue: null, viewValue: null},
+ {}, //tests undefined values
+ ];
+
+ tests.forEach(function(test) {
+ $rootScope.model = test.modelValue;
+ $rootScope.$digest();
+ expect(model.$viewValue).toBe(null);
+ });
+ }));
+
+ it('should correctly format database strings with single-digit fields without affecting the raw model structure', function() {
+ var input = TestUtil.compile('', {
+ model: '1999-12-3 5:4:9'
+ });
+
+ var model = input.controller('ngModel');
+
+ expect(model.$viewValue).toBe('03/12/1999 05:04:09');
+
+ input.val('03/12/1999 05:04:09').triggerHandler('input');
+ expect(model.$modelValue).toBe('1999-12-3 5:4:9');
+ });
+});
\ No newline at end of file
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');
+});
});
diff --git a/src/global/global-masks.js b/src/global/global-masks.js
index faf1c001..d1acaf78 100644
--- a/src/global/global-masks.js
+++ b/src/global/global-masks.js
@@ -3,10 +3,11 @@
var m = angular.module('ui.utils.masks.global', [])
.directive('uiCreditCardMask', require('./credit-card/credit-card'))
.directive('uiDateMask', require('./date/date'))
+ .directive('uiDateTimeMask', require('./date-time/date-time'))
.directive('uiMoneyMask', require('./money/money'))
.directive('uiNumberMask', require('./number/number'))
.directive('uiPercentageMask', require('./percentage/percentage'))
.directive('uiScientificNotationMask', require('./scientific-notation/scientific-notation'))
- .directive('uiTimeMask', require('./time/time'));
+ .directive('uiTimeMask', require('./time/time'))
module.exports = m.name;