gumga-date-ng
Version:
Gumga Date
497 lines (438 loc) • 18.7 kB
JavaScript
(function(){
'use strict';
Mask.$inject = ['$parse'];
function Mask($parse) {
function isFocused (elem) {
return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
}
return {
priority: 100,
require: 'ngModel',
restrict: 'A',
scope: false,
compile: function gumgaDateMaskCompilingFunction() {
var options = {
maskDefinitions: {
// Numéricos
'9': /\d/,
// Alfa
'A': /[a-zA-Z]/,
// Alfanuméricos
'*': /[a-zA-Z0-9]/
},
// Se true, limpa o campo caso inválido no evento onBlur
clearOnBlur: true,
// Eventos para processamento
eventsToHandle: ['input', 'keyup', 'click', 'focus']
};
return function gumgaDateMaskLinkingFunction(scope, elm, attrs, ctrl) {
var maskProcessed = false, eventsBound = false,
maskCaretMap, maskPatterns, maskPlaceholder, maskComponents,
minRequiredLength,
value, valueMasked, isValid,
originalPlaceholder = attrs.placeholder,
originalMaxlength = attrs.maxlength,
// // Variáveis usadas exclusivamente para eventos
oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength;
function initialize(maskAttr) {
if (!angular.isDefined(maskAttr)) {
return uninitialize();
}
processRawMask(maskAttr);
if (!maskProcessed) {
return uninitialize();
}
initializeElement();
bindEventListeners();
return true;
}
function initPlaceholder(placeholderAttr) {
if (!placeholderAttr) {
return;
}
maskPlaceholder = placeholderAttr;
// Atualizamos o valor do input
if (maskProcessed) {
elm.val(maskValue(unmaskValue(elm.val())));
}
}
function formatter(fromModelValue) {
if (!maskProcessed) {
return fromModelValue;
}
value = unmaskValue(fromModelValue || '');
isValid = validateValue(value);
ctrl.$setValidity('mask', isValid);
return isValid && value.length ? maskValue(value) : undefined;
}
function parser(fromViewValue) {
if (!maskProcessed) {
return fromViewValue;
}
value = unmaskValue(fromViewValue || '');
isValid = validateValue(value);
ctrl.$viewValue = value.length ? maskValue(value) : '';
ctrl.$setValidity('mask', isValid);
if (value === '' && attrs.required) {
ctrl.$setValidity('required', !ctrl.$error.required);
}
return isValid ? value : undefined;
}
var linkOptions = options;
if (attrs.gumgaDateMaskOptions) {
linkOptions = scope.$eval(attrs.gumgaDateMaskOptions);
if (angular.isObject(linkOptions)) {
linkOptions = (function(original, current) {
for (var i in original) {
if (Object.prototype.hasOwnProperty.call(original, i)) {
if (current[i] === undefined) {
current[i] = angular.copy(original[i]);
} else {
angular.extend(current[i], original[i]);
}
}
}
return current;
})(options, linkOptions);
}
} else {
linkOptions = options;
}
attrs.$observe('gumgaDateMask', initialize);
if (angular.isDefined(attrs.gumgaDateMaskPlaceholder)) {
attrs.$observe('gumgaDateMaskPlaceholder', initPlaceholder);
}
else {
attrs.$observe('placeholder', initPlaceholder);
}
var modelViewValue = false;
attrs.$observe('modelViewValue', function(val) {
if (val === 'true') {
modelViewValue = true;
}
});
scope.$watch(attrs.ngModel, function(val) {
if (modelViewValue && val) {
var model = $parse(attrs.ngModel);
model.assign(scope, ctrl.$viewValue);
}
});
ctrl.$formatters.push(formatter);
ctrl.$parsers.push(parser);
function uninitialize() {
maskProcessed = false;
unbindEventListeners();
if (angular.isDefined(originalPlaceholder)) {
elm.attr('placeholder', originalPlaceholder);
} else {
elm.removeAttr('placeholder');
}
if (angular.isDefined(originalMaxlength)) {
elm.attr('maxlength', originalMaxlength);
} else {
elm.removeAttr('maxlength');
}
elm.val(ctrl.$modelValue);
ctrl.$viewValue = ctrl.$modelValue;
return false;
}
function initializeElement() {
value = oldValueUnmasked = unmaskValue(ctrl.$modelValue || '');
valueMasked = oldValue = maskValue(value);
isValid = validateValue(value);
var viewValue = isValid && value.length ? valueMasked : '';
if (attrs.maxlength) { // Double maxlength to allow pasting new val at end of mask
elm.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2);
}
if ( ! originalPlaceholder) {
elm.attr('placeholder', maskPlaceholder);
}
elm.val(viewValue);
ctrl.$viewValue = viewValue;
ctrl.$setValidity('mask', isValid);
// Não usando $setViewValue, então não sobreescreve
// o valor do model sem interação do usuário.
}
function bindEventListeners() {
if (eventsBound) {
return;
}
elm.bind('blur', blurHandler);
elm.bind('mousedown mouseup', mouseDownUpHandler);
elm.bind(linkOptions.eventsToHandle.join(' '), eventHandler);
elm.bind('paste', onPasteHandler);
eventsBound = true;
}
function unbindEventListeners() {
if (!eventsBound) {
return;
}
elm.unbind('blur', blurHandler);
elm.unbind('mousedown', mouseDownUpHandler);
elm.unbind('mouseup', mouseDownUpHandler);
elm.unbind('input', eventHandler);
elm.unbind('keyup', eventHandler);
elm.unbind('click', eventHandler);
elm.unbind('focus', eventHandler);
elm.unbind('paste', onPasteHandler);
eventsBound = false;
}
function validateValue(value) {
// Valida o tamanho mínimo requerido da máscara
return value.length ? value.length >= minRequiredLength : true;
}
// Remove máscara
function unmaskValue(value) {
var valueUnmasked = '',
maskPatternsCopy = maskPatterns.slice();
// Processo para retirar componentes do valor
value = value.toString();
angular.forEach(maskComponents, function(component) {
value = value.replace(component, '');
});
angular.forEach(value.split(''), function(chr) {
if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) {
valueUnmasked += chr;
maskPatternsCopy.shift();
}
});
return valueUnmasked;
}
// Adiciona máscara
function maskValue(unmaskedValue) {
var valueMasked = '',
maskCaretMapCopy = maskCaretMap.slice();
angular.forEach(maskPlaceholder.split(''), function(chr, i) {
if (unmaskedValue.length && i === maskCaretMapCopy[0]) {
valueMasked += unmaskedValue.charAt(0) || '_';
unmaskedValue = unmaskedValue.substr(1);
maskCaretMapCopy.shift();
}
else {
valueMasked += chr;
}
});
return valueMasked;
}
// O atributo padrão placeholder funciona normalmente,
// o atributo gumgaDateMaskPlaceholder define a máscara com o placeholder
// e deve atender a quantidade de caracteres da máscara.
function getPlaceholderChar(i) {
var placeholder = angular.isDefined(attrs.gumgaDateMaskPlaceholder) ? attrs.gumgaDateMaskPlaceholder : attrs.placeholder;
if (typeof placeholder !== 'undefined' && placeholder[i]) {
return placeholder[i];
} else {
return '_';
}
}
function getMaskComponents() {
return maskPlaceholder.replace(/[_]+/g, '_').replace(/([^_]+)([a-zA-Z0-9])([^_])/g, '$1$2_$3').split('_');
}
function processRawMask(mask) {
var characterCount = 0;
maskCaretMap = [];
maskPatterns = [];
maskPlaceholder = '';
if (typeof mask === 'string') {
minRequiredLength = 0;
var isOptional = false,
numberOfOptionalCharacters = 0,
splitMask = mask.split('');
angular.forEach(splitMask, function(chr, i) {
if (linkOptions.maskDefinitions[chr]) {
maskCaretMap.push(characterCount);
maskPlaceholder += getPlaceholderChar(i - numberOfOptionalCharacters);
maskPatterns.push(linkOptions.maskDefinitions[chr]);
characterCount++;
if (!isOptional) {
minRequiredLength++;
}
}
else if (chr === '?') {
isOptional = true;
numberOfOptionalCharacters++;
}
else {
maskPlaceholder += chr;
characterCount++;
}
});
}
// Posição do cursor imediatamente após última posição válida
maskCaretMap.push(maskCaretMap.slice().pop() + 1);
maskComponents = getMaskComponents();
maskProcessed = maskCaretMap.length > 1 ? true : false;
}
function blurHandler() {
// Se clearOnBlur for true em options,
// limpa o campo caso esteja inválido.
if (linkOptions.clearOnBlur) {
oldCaretPosition = 0;
oldSelectionLength = 0;
if (!isValid || value.length === 0) {
valueMasked = '';
elm.val('');
scope.$apply(function() {
ctrl.$setViewValue('');
});
}
}
}
function mouseDownUpHandler(e) {
if (e.type === 'mousedown') {
elm.bind('mouseout', mouseoutHandler);
} else {
elm.unbind('mouseout', mouseoutHandler);
}
}
elm.bind('mousedown mouseup', mouseDownUpHandler);
function mouseoutHandler() {
/*jshint validthis: true */
oldSelectionLength = getSelectionLength(this);
elm.unbind('mouseout', mouseoutHandler);
}
function onPasteHandler() {
/*jshint validthis: true */
setCaretPosition(this, elm.val().length);
}
function eventHandler(e) {
/*jshint validthis: true */
e = e || {};
// Permite uma minificação mais eficiente
var eventWhich = e.which,
eventType = e.type;
if (eventWhich === 16 || eventWhich === 91) {
return;
}
var val = elm.val(),
valOld = oldValue,
valMasked,
valUnmasked = unmaskValue(val),
valUnmaskedOld = oldValueUnmasked,
caretPos = getCaretPosition(this) || 0,
caretPosOld = oldCaretPosition || 0,
caretPosDelta = caretPos - caretPosOld,
caretPosMin = maskCaretMap[0],
caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(),
selectionLenOld = oldSelectionLength || 0,
isSelected = getSelectionLength(this) > 0,
wasSelected = selectionLenOld > 0,
// Case: Digitando um caracter para substituir uma seleção
isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld),
// Case: Delete e backspace se comportam de forma idêntica em uma seleção
isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld),
isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes
isKeyLeftArrow = eventWhich === 37,
// Necessária devido ao evento não fornecer um keycode
isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)),
isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0) && !wasSelected),
// Lida com casos onde acento circunflexo é movido e colocado na frente da posição maskCaretMap inválido.
// Logic abaixo assegura que, ao clicar ou posicionamento acento circunflexo para a esquerda, acento
// circunflexo é movido para a esquerda até à direita directamente de caráter não-máscara.
// Também aplicado para clicar uma vez que os usuários são (discutivelmente) mais propensos a voltar
// atrás com um personagem ao clicar dentro de uma entrada cheia.
caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin;
oldSelectionLength = getSelectionLength(this);
// Eventos que não requerem nenhuma ação
if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup'))) {
return;
}
// Controle de valores
// ==============
// User attempted to delete but raw value was unaffected--correct this grievous offense
// O usuário tentou apagar, mas valor bruto não foi afetado - corrigir este grave ofensa
if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) {
while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) {
caretPos--;
}
while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) {
caretPos++;
}
var charIndex = maskCaretMap.indexOf(caretPos);
// Strip out non-mask character that user would have deleted if mask hadn't been in the way.
valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1);
}
// Atualiza valor
valMasked = maskValue(valUnmasked);
oldValue = valMasked;
oldValueUnmasked = valUnmasked;
elm.val(valMasked);
ctrl.$setViewValue(valUnmasked);
// Posição do cursor
// ===================
// Caractere digitado a frente nos casos em que o primeiro caractere de entrada é um char máscara e o cursor
// for colocado na posição 0.
if (isAddition && (caretPos <= caretPosMin)) {
caretPos = caretPosMin + 1;
}
if (caretBumpBack) {
caretPos--;
}
caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos;
while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) {
caretPos += caretBumpBack ? -1 : 1;
}
if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) {
caretPos++;
}
oldCaretPosition = caretPos;
setCaretPosition(this, caretPos);
}
function isValidCaretPosition(pos) {
return maskCaretMap.indexOf(pos) > -1;
}
function getCaretPosition(input) {
if (!input)
return 0;
if (input.selectionStart !== undefined) {
return input.selectionStart;
} else if (document.selection) {
if (isFocused(elm[0])) {
// Maldito seja o IE
input.focus();
var selection = document.selection.createRange();
selection.moveStart('character', input.value ? -input.value.length : 0);
return selection.text.length;
}
}
return 0;
}
function setCaretPosition(input, pos) {
if (!input)
return 0;
if (input.offsetWidth === 0 || input.offsetHeight === 0) {
return; // Inputs escondidos
}
if (input.setSelectionRange) {
if (isFocused(elm[0])) {
input.focus();
input.setSelectionRange(pos, pos);
}
}
else if (input.createTextRange) {
// Maldito seja o IE
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}
function getSelectionLength(input) {
if (!input)
return 0;
if (input.selectionStart !== undefined) {
return (input.selectionEnd - input.selectionStart);
}
if (document.selection) {
return (document.selection.createRange().text.length);
}
return 0;
}
}
}
}
}
angular.module('gumga.date.mask', [])
.directive('gumgaDateMask', Mask);
})();