gumga-mask-ng
Version:
Gumga Mask (Angular 1.x)
580 lines (512 loc) • 20.6 kB
JavaScript
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // identity function for calling harmony imports with the correct context
/******/ __webpack_require__.i = function(value) { return value; };
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "/dist/";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
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 gumgaMaskCompilingFunction() {
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 gumgaMaskLinkingFunction(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,
numberOfOptionalCharacters;
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.gumgaMaskOptions) {
linkOptions = scope.$eval(attrs.gumgaMaskOptions);
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('gumgaMask', initialize);
if (angular.isDefined(attrs.gumgaMaskPlaceholder)) {
attrs.$observe('gumgaMaskPlaceholder', 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 - numberOfOptionalCharacters : 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 gumgaMaskPlaceholder 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.gumgaMaskPlaceholder) ? attrs.gumgaMaskPlaceholder : 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;
numberOfOptionalCharacters = 0;
var 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++;
minRequiredLength++;
} else if (chr === '?') {
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;
}
};
}
};
}
var _module = angular.module('gumga.mask', []).directive('gumgaMask', Mask);
exports.default = _module.name;
/***/ })
/******/ ]);