pyc-website

main website for pyc inc.

git clone https://9o.is/git/pyc-website.git

ui-mask.js

(19718B)


      1 'use strict';
      2 
      3 /*
      4  Attaches input mask onto input element
      5  */
      6 angular.module('ui.mask', [])
      7   .value('uiMaskConfig', {
      8     'maskDefinitions': {
      9       '9': /\d/,
     10       'A': /[a-zA-Z]/,
     11       '*': /[a-zA-Z0-9]/
     12     }
     13   })
     14   .directive('uiMask', ['uiMaskConfig', '$parse', function (maskConfig, $parse) {
     15     return {
     16       priority: 100,
     17       require: 'ngModel',
     18       restrict: 'A',
     19       compile: function uiMaskCompilingFunction(){
     20         var options = maskConfig;
     21 
     22         return function uiMaskLinkingFunction(scope, iElement, iAttrs, controller){
     23           var maskProcessed = false, eventsBound = false,
     24             maskCaretMap, maskPatterns, maskPlaceholder, maskComponents,
     25           // Minimum required length of the value to be considered valid
     26             minRequiredLength,
     27             value, valueMasked, isValid,
     28           // Vars for initializing/uninitializing
     29             originalPlaceholder = iAttrs.placeholder,
     30             originalMaxlength = iAttrs.maxlength,
     31           // Vars used exclusively in eventHandler()
     32             oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength;
     33 
     34           function initialize(maskAttr){
     35             if (!angular.isDefined(maskAttr)) {
     36               return uninitialize();
     37             }
     38             processRawMask(maskAttr);
     39             if (!maskProcessed) {
     40               return uninitialize();
     41             }
     42             initializeElement();
     43             bindEventListeners();
     44             return true;
     45           }
     46 
     47           function initPlaceholder(placeholderAttr) {
     48             if(! angular.isDefined(placeholderAttr)) {
     49               return;
     50             }
     51 
     52             maskPlaceholder = placeholderAttr;
     53 
     54             // If the mask is processed, then we need to update the value
     55             if (maskProcessed) {
     56               eventHandler();
     57             }
     58           }
     59 
     60           function formatter(fromModelValue){
     61             if (!maskProcessed) {
     62               return fromModelValue;
     63             }
     64             value = unmaskValue(fromModelValue || '');
     65             isValid = validateValue(value);
     66             controller.$setValidity('mask', isValid);
     67             return isValid && value.length ? maskValue(value) : undefined;
     68           }
     69 
     70           function parser(fromViewValue){
     71             if (!maskProcessed) {
     72               return fromViewValue;
     73             }
     74             value = unmaskValue(fromViewValue || '');
     75             isValid = validateValue(value);
     76             // We have to set viewValue manually as the reformatting of the input
     77             // value performed by eventHandler() doesn't happen until after
     78             // this parser is called, which causes what the user sees in the input
     79             // to be out-of-sync with what the controller's $viewValue is set to.
     80             controller.$viewValue = value.length ? maskValue(value) : '';
     81             controller.$setValidity('mask', isValid);
     82             if (value === '' && controller.$error.required !== undefined) {
     83               controller.$setValidity('required', false);
     84             }
     85             return isValid ? value : undefined;
     86           }
     87 
     88           var linkOptions = {};
     89 
     90           if (iAttrs.uiOptions) {
     91             linkOptions = scope.$eval('[' + iAttrs.uiOptions + ']');
     92             if (angular.isObject(linkOptions[0])) {
     93               // we can't use angular.copy nor angular.extend, they lack the power to do a deep merge
     94               linkOptions = (function(original, current){
     95                 for(var i in original) {
     96                   if (Object.prototype.hasOwnProperty.call(original, i)) {
     97                     if (!current[i]) {
     98                       current[i] = angular.copy(original[i]);
     99                     } else {
    100                       angular.extend(current[i], original[i]);
    101                     }
    102                   }
    103                 }
    104                 return current;
    105               })(options, linkOptions[0]);
    106             }
    107           } else {
    108             linkOptions = options;
    109           }
    110 
    111           iAttrs.$observe('uiMask', initialize);
    112           iAttrs.$observe('placeholder', initPlaceholder);
    113           var modelViewValue = false;
    114           iAttrs.$observe('modelViewValue', function(val) {
    115             if(val === 'true') {
    116               modelViewValue = true;
    117             }
    118           });
    119           scope.$watch(iAttrs.ngModel, function(val) {
    120             if(modelViewValue && val) {
    121               var model = $parse(iAttrs.ngModel);
    122               model.assign(scope, controller.$viewValue);
    123             }
    124           });
    125           controller.$formatters.push(formatter);
    126           controller.$parsers.push(parser);
    127 
    128           function uninitialize(){
    129             maskProcessed = false;
    130             unbindEventListeners();
    131 
    132             if (angular.isDefined(originalPlaceholder)) {
    133               iElement.attr('placeholder', originalPlaceholder);
    134             } else {
    135               iElement.removeAttr('placeholder');
    136             }
    137 
    138             if (angular.isDefined(originalMaxlength)) {
    139               iElement.attr('maxlength', originalMaxlength);
    140             } else {
    141               iElement.removeAttr('maxlength');
    142             }
    143 
    144             iElement.val(controller.$modelValue);
    145             controller.$viewValue = controller.$modelValue;
    146             return false;
    147           }
    148 
    149           function initializeElement(){
    150             value = oldValueUnmasked = unmaskValue(controller.$modelValue || '');
    151             valueMasked = oldValue = maskValue(value);
    152             isValid = validateValue(value);
    153             var viewValue = isValid && value.length ? valueMasked : '';
    154             if (iAttrs.maxlength) { // Double maxlength to allow pasting new val at end of mask
    155               iElement.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2);
    156             }
    157             iElement.attr('placeholder', maskPlaceholder);
    158             iElement.val(viewValue);
    159             controller.$viewValue = viewValue;
    160             // Not using $setViewValue so we don't clobber the model value and dirty the form
    161             // without any kind of user interaction.
    162           }
    163 
    164           function bindEventListeners(){
    165             if (eventsBound) {
    166               return;
    167             }
    168             iElement.bind('blur', blurHandler);
    169             iElement.bind('mousedown mouseup', mouseDownUpHandler);
    170             iElement.bind('input keyup click focus', eventHandler);
    171             eventsBound = true;
    172           }
    173 
    174           function unbindEventListeners(){
    175             if (!eventsBound) {
    176               return;
    177             }
    178             iElement.unbind('blur', blurHandler);
    179             iElement.unbind('mousedown', mouseDownUpHandler);
    180             iElement.unbind('mouseup', mouseDownUpHandler);
    181             iElement.unbind('input', eventHandler);
    182             iElement.unbind('keyup', eventHandler);
    183             iElement.unbind('click', eventHandler);
    184             iElement.unbind('focus', eventHandler);
    185             eventsBound = false;
    186           }
    187 
    188           function validateValue(value){
    189             // Zero-length value validity is ngRequired's determination
    190             return value.length ? value.length >= minRequiredLength : true;
    191           }
    192 
    193           function unmaskValue(value){
    194             var valueUnmasked = '',
    195               maskPatternsCopy = maskPatterns.slice();
    196             // Preprocess by stripping mask components from value
    197             value = value.toString();
    198             angular.forEach(maskComponents, function (component){
    199               value = value.replace(component, '');
    200             });
    201             angular.forEach(value.split(''), function (chr){
    202               if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) {
    203                 valueUnmasked += chr;
    204                 maskPatternsCopy.shift();
    205               }
    206             });
    207             return valueUnmasked;
    208           }
    209 
    210           function maskValue(unmaskedValue){
    211             var valueMasked = '',
    212                 maskCaretMapCopy = maskCaretMap.slice();
    213 
    214             angular.forEach(maskPlaceholder.split(''), function (chr, i){
    215               if (unmaskedValue.length && i === maskCaretMapCopy[0]) {
    216                 valueMasked  += unmaskedValue.charAt(0) || '_';
    217                 unmaskedValue = unmaskedValue.substr(1);
    218                 maskCaretMapCopy.shift();
    219               }
    220               else {
    221                 valueMasked += chr;
    222               }
    223             });
    224             return valueMasked;
    225           }
    226 
    227           function getPlaceholderChar(i) {
    228             var placeholder = iAttrs.placeholder;
    229 
    230             if (typeof placeholder !== 'undefined' && placeholder[i]) {
    231               return placeholder[i];
    232             } else {
    233               return '_';
    234             }
    235           }
    236 
    237           // Generate array of mask components that will be stripped from a masked value
    238           // before processing to prevent mask components from being added to the unmasked value.
    239           // E.g., a mask pattern of '+7 9999' won't have the 7 bleed into the unmasked value.
    240           // If a maskable char is followed by a mask char and has a mask
    241           // char behind it, we'll split it into it's own component so if
    242           // a user is aggressively deleting in the input and a char ahead
    243           // of the maskable char gets deleted, we'll still be able to strip
    244           // it in the unmaskValue() preprocessing.
    245           function getMaskComponents() {
    246             return maskPlaceholder.replace(/[_]+/g, '_').replace(/([^_]+)([a-zA-Z0-9])([^_])/g, '$1$2_$3').split('_');
    247           }
    248 
    249           function processRawMask(mask){
    250             var characterCount = 0;
    251 
    252             maskCaretMap    = [];
    253             maskPatterns    = [];
    254             maskPlaceholder = '';
    255 
    256             if (typeof mask === 'string') {
    257               minRequiredLength = 0;
    258 
    259               var isOptional = false,
    260                   splitMask  = mask.split('');
    261 
    262               angular.forEach(splitMask, function (chr, i){
    263                 if (linkOptions.maskDefinitions[chr]) {
    264 
    265                   maskCaretMap.push(characterCount);
    266 
    267                   maskPlaceholder += getPlaceholderChar(i);
    268                   maskPatterns.push(linkOptions.maskDefinitions[chr]);
    269 
    270                   characterCount++;
    271                   if (!isOptional) {
    272                     minRequiredLength++;
    273                   }
    274                 }
    275                 else if (chr === '?') {
    276                   isOptional = true;
    277                 }
    278                 else {
    279                   maskPlaceholder += chr;
    280                   characterCount++;
    281                 }
    282               });
    283             }
    284             // Caret position immediately following last position is valid.
    285             maskCaretMap.push(maskCaretMap.slice().pop() + 1);
    286 
    287             maskComponents = getMaskComponents();
    288             maskProcessed  = maskCaretMap.length > 1 ? true : false;
    289           }
    290 
    291           function blurHandler(){
    292             oldCaretPosition = 0;
    293             oldSelectionLength = 0;
    294             if (!isValid || value.length === 0) {
    295               valueMasked = '';
    296               iElement.val('');
    297               scope.$apply(function (){
    298                 controller.$setViewValue('');
    299               });
    300             }
    301           }
    302 
    303           function mouseDownUpHandler(e){
    304             if (e.type === 'mousedown') {
    305               iElement.bind('mouseout', mouseoutHandler);
    306             } else {
    307               iElement.unbind('mouseout', mouseoutHandler);
    308             }
    309           }
    310 
    311           iElement.bind('mousedown mouseup', mouseDownUpHandler);
    312 
    313           function mouseoutHandler(){
    314             /*jshint validthis: true */
    315             oldSelectionLength = getSelectionLength(this);
    316             iElement.unbind('mouseout', mouseoutHandler);
    317           }
    318 
    319           function eventHandler(e){
    320             /*jshint validthis: true */
    321             e = e || {};
    322             // Allows more efficient minification
    323             var eventWhich = e.which,
    324               eventType = e.type;
    325 
    326             // Prevent shift and ctrl from mucking with old values
    327             if (eventWhich === 16 || eventWhich === 91) { return;}
    328 
    329             var val = iElement.val(),
    330               valOld = oldValue,
    331               valMasked,
    332               valUnmasked = unmaskValue(val),
    333               valUnmaskedOld = oldValueUnmasked,
    334               valAltered = false,
    335 
    336               caretPos = getCaretPosition(this) || 0,
    337               caretPosOld = oldCaretPosition || 0,
    338               caretPosDelta = caretPos - caretPosOld,
    339               caretPosMin = maskCaretMap[0],
    340               caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(),
    341 
    342               selectionLenOld = oldSelectionLength || 0,
    343               isSelected = getSelectionLength(this) > 0,
    344               wasSelected = selectionLenOld > 0,
    345 
    346             // Case: Typing a character to overwrite a selection
    347               isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld),
    348             // Case: Delete and backspace behave identically on a selection
    349               isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld),
    350               isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes
    351 
    352               isKeyLeftArrow = eventWhich === 37,
    353             // Necessary due to "input" event not providing a key code
    354               isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)),
    355               isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0 ) && !wasSelected),
    356 
    357             // Handles cases where caret is moved and placed in front of invalid maskCaretMap position. Logic below
    358             // ensures that, on click or leftward caret placement, caret is moved leftward until directly right of
    359             // non-mask character. Also applied to click since users are (arguably) more likely to backspace
    360             // a character when clicking within a filled input.
    361               caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin;
    362 
    363             oldSelectionLength = getSelectionLength(this);
    364 
    365             // These events don't require any action
    366             if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup'))) {
    367               return;
    368             }
    369 
    370             // Value Handling
    371             // ==============
    372 
    373             // User attempted to delete but raw value was unaffected--correct this grievous offense
    374             if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) {
    375               while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) {
    376                 caretPos--;
    377               }
    378               while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) {
    379                 caretPos++;
    380               }
    381               var charIndex = maskCaretMap.indexOf(caretPos);
    382               // Strip out non-mask character that user would have deleted if mask hadn't been in the way.
    383               valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1);
    384               valAltered = true;
    385             }
    386 
    387             // Update values
    388             valMasked = maskValue(valUnmasked);
    389 
    390             oldValue = valMasked;
    391             oldValueUnmasked = valUnmasked;
    392             iElement.val(valMasked);
    393             if (valAltered) {
    394               // We've altered the raw value after it's been $digest'ed, we need to $apply the new value.
    395               scope.$apply(function (){
    396                 controller.$setViewValue(valUnmasked);
    397               });
    398             }
    399 
    400             // Caret Repositioning
    401             // ===================
    402 
    403             // Ensure that typing always places caret ahead of typed character in cases where the first char of
    404             // the input is a mask char and the caret is placed at the 0 position.
    405             if (isAddition && (caretPos <= caretPosMin)) {
    406               caretPos = caretPosMin + 1;
    407             }
    408 
    409             if (caretBumpBack) {
    410               caretPos--;
    411             }
    412 
    413             // Make sure caret is within min and max position limits
    414             caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos;
    415 
    416             // Scoot the caret back or forth until it's in a non-mask position and within min/max position limits
    417             while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) {
    418               caretPos += caretBumpBack ? -1 : 1;
    419             }
    420 
    421             if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) {
    422               caretPos++;
    423             }
    424             oldCaretPosition = caretPos;
    425             setCaretPosition(this, caretPos);
    426           }
    427 
    428           function isValidCaretPosition(pos){ return maskCaretMap.indexOf(pos) > -1; }
    429 
    430           function getCaretPosition(input){
    431             if (!input) return 0;
    432             if (input.selectionStart !== undefined) {
    433               return input.selectionStart;
    434             } else if (document.selection) {
    435               // Curse you IE
    436               input.focus();
    437               var selection = document.selection.createRange();
    438               selection.moveStart('character', -input.value.length);
    439               return selection.text.length;
    440             }
    441             return 0;
    442           }
    443 
    444           function setCaretPosition(input, pos){
    445             if (!input) return 0;
    446             if (input.offsetWidth === 0 || input.offsetHeight === 0) {
    447               return; // Input's hidden
    448             }
    449             if (input.setSelectionRange) {
    450               input.focus();
    451               input.setSelectionRange(pos, pos);
    452             }
    453             else if (input.createTextRange) {
    454               // Curse you IE
    455               var range = input.createTextRange();
    456               range.collapse(true);
    457               range.moveEnd('character', pos);
    458               range.moveStart('character', pos);
    459               range.select();
    460             }
    461           }
    462 
    463           function getSelectionLength(input){
    464             if (!input) return 0;
    465             if (input.selectionStart !== undefined) {
    466               return (input.selectionEnd - input.selectionStart);
    467             }
    468             if (document.selection) {
    469               return (document.selection.createRange().text.length);
    470             }
    471             return 0;
    472           }
    473 
    474           // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
    475           if (!Array.prototype.indexOf) {
    476             Array.prototype.indexOf = function (searchElement /*, fromIndex */){
    477               if (this === null) {
    478                 throw new TypeError();
    479               }
    480               var t = Object(this);
    481               var len = t.length >>> 0;
    482               if (len === 0) {
    483                 return -1;
    484               }
    485               var n = 0;
    486               if (arguments.length > 1) {
    487                 n = Number(arguments[1]);
    488                 if (n !== n) { // shortcut for verifying if it's NaN
    489                   n = 0;
    490                 } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
    491                   n = (n > 0 || -1) * Math.floor(Math.abs(n));
    492                 }
    493               }
    494               if (n >= len) {
    495                 return -1;
    496               }
    497               var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
    498               for (; k < len; k++) {
    499                 if (k in t && t[k] === searchElement) {
    500                   return k;
    501                 }
    502               }
    503               return -1;
    504             };
    505           }
    506 
    507         };
    508       }
    509     };
    510   }
    511 ]);