├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── demo └── index.html ├── dist ├── mask.js └── mask.min.js ├── gulpfile.js ├── index.js ├── karma.conf.js ├── logos └── browser-stack.png ├── package.json ├── protractor.config.js ├── protractor.travis.config.js ├── src └── mask.js └── test ├── maskSpec.js └── maskSpec.protractor.js /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | node_modules/ 3 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "browser": true, 4 | "eqnull": true, 5 | "expr": true, 6 | "immed": true, 7 | "laxbreak": true, 8 | "loopfunc": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "noempty": true, 12 | "nonew": true, 13 | "quotmark": true, 14 | "smarttabs": true, 15 | "strict": true, 16 | "sub": true, 17 | "trailing": true, 18 | "undef": true, 19 | "unused": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | git: 5 | depth: 10 6 | before_install: 7 | - npm install -qg bower gulp-cli 8 | - npm install -q 9 | - bower install --force 10 | - bower install --force 11 | script: gulp ci 12 | sudo: false 13 | env: 14 | global: 15 | - SAUCE_USERNAME=angular-ui-mask 16 | - SAUCE_ACCESS_KEY=b4b4d64e-4188-4cae-a91c-29214a389c8a 17 | deploy: 18 | provider: npm 19 | email: adrien.crivelli@gmail.com 20 | api_key: 21 | secure: BO798oqVC/vfFllHuEfFR+A1j0VEWbsLUkEkHq4bZMUO626VCd1NwEzboDcVI6xROcNJ9RiAZ6BhG9oQTBdCshENxCbESL3qjEj3q+9UJwKnp2QttdTxXojajKxrAG5hkSpnICSiZpQIbudRphx9gc8ul/ETc7MGnptAhkF7ecIk4C9jF9E7czW4mu68+N4Nx/NWI4cjAHSE88KRHuF5O/tbbOn5txhuQCwex34DBLEevGlT3GF7V7NFlrDW7OzYDh2MwqRreITLoZJ9NuMfH0uA1bIfQgn4/Uh+5J27WNVcJr6PYWLcA0Fo48EK1OuxsJX1AmNSEy2QBmCE5hcifFdpigrusKemBLZbhO5TCcmdWXyIYz/8pH6XnHs48DFTUYosjdfhtAQ65y6Zv4wGxpYNtviagApefGOOQHfRsig3l2adXQriTctVtoUDJ9TfdG/gaIrhztKKJJ8RkBBEAs/acAW3iPgn5w4aT8pGj4cKcubEFYrJjmU0vQfUCsE+brr/iE6JGZ/Ga5d3A2n512n+yicQULFtNqZRXwjqMfA6+eHo0ImzJP9bP1E39Z5u1DT2BfegBH9UrLaIPc7xK8saoxed7mrczlnzbvO0+ibFsHP5uQZJ0jyVZ/V25UDsIXp1IlL4hNZ6dwfAu/7qir3UdcsM6+iqMUR4WqMiaRg= 22 | on: 23 | tags: true 24 | repo: angular-ui/ui-mask 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 AngularUI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ui-mask [![Build Status](https://travis-ci.org/angular-ui/ui-mask.svg?branch=master)](https://travis-ci.org/angular-ui/ui-mask) [![npm version](https://badge.fury.io/js/angular-ui-mask.svg)](http://badge.fury.io/js/angular-ui-mask) [![Bower version](https://badge.fury.io/bo/angular-ui-mask.svg)](http://badge.fury.io/bo/angular-ui-mask) [![Join the chat at https://gitter.im/angular-ui/ui-mask](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular-ui/ui-mask?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | Apply a mask on an input field so the user can only type pre-determined pattern. 4 | 5 | # PROJECT NEEDS NEW MAINTAINER! 6 | **Please reach out to [Dean Sofer / ProLoser](https://github.com/proloser) if you're interested in taking over!!!** 7 | 8 | ## Requirements 9 | 10 | - AngularJS 11 | 12 | ## Usage 13 | 14 | 15 | ### Bower 16 | 17 | You can get it from [Bower](http://bower.io/) 18 | 19 | ```sh 20 | bower install angular-ui-mask 21 | ``` 22 | 23 | Load the script files in your application: 24 | 25 | ```html 26 | 27 | 28 | ``` 29 | 30 | Add the specific module to your dependencies: 31 | 32 | ```javascript 33 | angular.module('myApp', ['ui.mask', ...]) 34 | ``` 35 | 36 | ### NPM (CommonJS, ES6 module) 37 | 38 | Also you can use it as CommonJS or ES6 module with any build system that supports those type of modules (Webpack, SystemJS, JSPM etc): 39 | 40 | ```sh 41 | npm install angular-ui-mask 42 | ``` 43 | 44 | And then include it with 45 | 46 | ```javascript 47 | // CommonJS 48 | var uiMask = require('angular-ui-mask'); 49 | angular.module('myApp', [uiMask, ...]); 50 | ``` 51 | 52 | ```javascript 53 | // ES6 module 54 | import uiMask from 'angular-ui-mask'; 55 | angular.module('myApp', [uiMask, ...]); 56 | ``` 57 | 58 | ### Customizing 59 | You can customize several behaviors of ui-mask by taking advantage of the `ui-options` object. Declare `ui-options` as an additional attribute on the same element where you declare `ui-mask`. 60 | 61 | Inside of `ui-options`, you can customize these five properties: 62 | 63 | * `maskDefinitions` - default: `{ 64 | '9': /\d/, 65 | 'A': /[a-zA-Z]/, 66 | '*': /[a-zA-Z0-9]/ 67 | }`, 68 | * `clearOnBlur` - default: `true`, 69 | * `clearOnBlurPlaceholder` - default: `false`, 70 | * `eventsToHandle` - default: `['input', 'keyup', 'click', 'focus']` 71 | * `addDefaultPlaceholder` - default: `true` 72 | * `escChar` - default: `'\\'` 73 | * `allowInvalidValue` - default: `false` 74 | 75 | When customizing `eventsToHandle`, `clearOnBlur`, or `addDefaultPlaceholder`, the value you supply will replace the default. To customize `eventsToHandle`, be sure to replace the entire array. 76 | 77 | Whereas, `maskDefinitions` is an object, so any custom object you supply will be merged together with the defaults using `angular.extend()`. This allows you to override the defaults selectively, if you wish. 78 | 79 | When setting `clearOnBlurPlaceholder` to `true`, it will show the placeholder text instead of the empty mask. It requires the `ui-mask-placeholder` attribute to be set on the input to display properly. 80 | 81 | If the `escChar` (\\ by default) is encountered in a mask, the next character will be treated as a literal and not a mask definition key. To disable the `escChar` feature completely, set `escChar` to `null`. 82 | 83 | When `allowInvalidValue` is set to true, apply value to `$modelValue` even if it isn't valid. By default, if you write an invalid value, the model will stay `undefined`. 84 | 85 | #### Global customization 86 | In addition to customizing behaviors for a specific element, you can also customize the behaviors globally. To do this, simply use the `uiMaskConfig` provider in your app configuration. Example: 87 | 88 | ```javascript 89 | app.config(['uiMask.ConfigProvider', function(uiMaskConfigProvider) { 90 | uiMaskConfigProvider.maskDefinitions({'A': /[a-z]/, '*': /[a-zA-Z0-9]/}); 91 | uiMaskConfigProvider.clearOnBlur(false); 92 | uiMaskConfigProvider.eventsToHandle(['input', 'keyup', 'click']); 93 | } 94 | ``` 95 | 96 | #### maskDefinitions 97 | The keys in `maskDefinitions` represent the special tokens/characters used in your mask declaration to delimit acceptable ranges of inputs. For example, we use '9' here to accept any numeric values for a phone number: `ui-mask="(999) 999-9999"`. The values associated with each token are regexen. Each regex defines the ranges of values that will be acceptable as inputs in the position of that token. 98 | 99 | #### modelViewValue 100 | If this is set to true, then the model value bound with `ng-model` will be the same as the `$viewValue` meaning it will contain any static mask characters present in the mask definition. This will not set the model value to a `$viewValue` that is considered invalid. 101 | 102 | #### uiMaskPlaceholder 103 | Allows customizing the mask placeholder when a user has focused the input element and while typing in their value 104 | 105 | #### uiMaskPlaceholderChar 106 | Allows customizing the mask placeholder character. The default mask placeholder is `_`. 107 | 108 | Set this attribute to the word `space` if you want the placeholder character to be whitespace. 109 | 110 | #### addDefaultPlaceholder 111 | The default placeholder is constructed from the `ui-mask` definition so a mask of `999-9999` would have a default placeholder of `___-____`; unless you have overridden the default placeholder character. 112 | 113 | ## Testing 114 | 115 | Most of the testing is done using Karma to run the tests and SauceLabs to provide the different browser environments to test against. 116 | 117 | Mobile testing and debugging uses BrowserStack for its abilities to remotely debug mobile devices from a browser. 118 | 119 | [BrowserStack](https://www.browserstack.com) 120 | 121 | ## Development 122 | 123 | We use Karma and jshint to ensure the quality of the code. The easiest way to run these checks is to use gulp: 124 | 125 | ```sh 126 | npm install -g gulp-cli 127 | npm install && bower install 128 | gulp 129 | ``` 130 | 131 | The karma task will try to open Firefox and Chrome as browser in which to run the tests. Make sure this is available or change the configuration in `karma.conf.js` 132 | 133 | 134 | ### Gulp watch 135 | 136 | `gulp watch` will automatically test your code and build a release whenever source files change. 137 | 138 | ### How to release 139 | 140 | Use gulp to bump version, build and create a tag. Then push to GitHub: 141 | 142 | ````sh 143 | gulp release [--patch|--minor|--major] 144 | git push --tags origin master # push everything to GitHub 145 | ```` 146 | 147 | Travis will take care of testing and publishing to npm's registry (bower will pick up the change automatically). Finally [create a release on GitHub](https://github.com/angular-ui/ui-mask/releases/new) from the tag created by Travis. 148 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "AngularUI Team", 3 | "name": "angular-ui-mask", 4 | "homepage": "https://github.com/angular-ui/ui-mask", 5 | "main": "./dist/mask.js", 6 | "license": "MIT", 7 | "dependencies": { 8 | "angular": ">= 1.3.6" 9 | }, 10 | "devDependencies": { 11 | "angular-mocks": ">=1.3.6" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularJS ui-mask 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 |
20 | 25 |
26 |
27 |

What?

28 | 29 |

Apply a mask on an input field so the user can only type pre-determined pattern.

30 |
31 |
32 | 33 | 34 |
35 | 36 | 37 |
Model value: {{vm.x}}
38 |
Model value: undefined
39 |
NgModelController.$viewValue: {{demo.masked.$viewValue}}
40 |
NgModelController.$viewValue: undefined
41 |
42 |
43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | A Any letter.
52 | 9 Any number.
53 | * Any letter or number.
54 | ? Make any part of the mask optional. 55 |
56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 | 64 | 65 | You can use any single char, or exactly space to use space symbol. 66 | The default value if nothing is specified is: _ 67 | 68 |
69 |
70 | 71 |
72 | 73 | 74 |
75 |

76 | 77 |

78 |

79 | 80 |

81 |

82 | 83 |

84 |

85 | 86 |

87 |

88 | 89 |

90 |

91 | 92 |

93 |

94 | 95 |

96 |

97 | 98 |

99 |

100 | 101 |

102 |
103 |
104 | 105 |
106 | 107 | 108 |
109 | 110 |
111 |
112 | 113 |
114 | 115 | 116 |
117 |
Use the following checkbox to reinitialize the ui-mask by removing and adding the target element from the DOM. Helps with testing different ui-options
118 | 119 |
120 |
121 | 122 |
123 | 124 | 125 |
126 | input
127 | keyup
128 | click
129 | focus
130 | ui-options{{options | json}} 131 |
132 |
133 |
134 | 135 |
136 |
137 |

How?

138 |
<input type="text" ng-model="phonenumber" ui-mask="(999) 999-9999"  ui-mask-placeholder ui-mask-placeholder-char="_"/>
139 |

Replace "(999) 999-9999" with your desired mask.

140 |
141 |
142 |
143 | 144 | 145 | -------------------------------------------------------------------------------- /dist/mask.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-ui-mask 3 | * https://github.com/angular-ui/ui-mask 4 | * Version: 1.8.7 - 2016-07-26T15:59:07.992Z 5 | * License: MIT 6 | */ 7 | 8 | 9 | (function () { 10 | 'use strict'; 11 | /* 12 | Attaches input mask onto input element 13 | */ 14 | angular.module('ui.mask', []) 15 | .value('uiMaskConfig', { 16 | maskDefinitions: { 17 | '9': /\d/, 18 | 'A': /[a-zA-Z]/, 19 | '*': /[a-zA-Z0-9]/ 20 | }, 21 | clearOnBlur: true, 22 | clearOnBlurPlaceholder: false, 23 | escChar: '\\', 24 | eventsToHandle: ['input', 'keyup', 'click', 'focus'], 25 | addDefaultPlaceholder: true, 26 | allowInvalidValue: false 27 | }) 28 | .provider('uiMask.Config', function() { 29 | var options = {}; 30 | 31 | this.maskDefinitions = function(maskDefinitions) { 32 | return options.maskDefinitions = maskDefinitions; 33 | }; 34 | this.clearOnBlur = function(clearOnBlur) { 35 | return options.clearOnBlur = clearOnBlur; 36 | }; 37 | this.clearOnBlurPlaceholder = function(clearOnBlurPlaceholder) { 38 | return options.clearOnBlurPlaceholder = clearOnBlurPlaceholder; 39 | }; 40 | this.eventsToHandle = function(eventsToHandle) { 41 | return options.eventsToHandle = eventsToHandle; 42 | }; 43 | this.addDefaultPlaceholder = function(addDefaultPlaceholder) { 44 | return options.addDefaultPlaceholder = addDefaultPlaceholder; 45 | }; 46 | this.allowInvalidValue = function(allowInvalidValue) { 47 | return options.allowInvalidValue = allowInvalidValue; 48 | }; 49 | this.$get = ['uiMaskConfig', function(uiMaskConfig) { 50 | var tempOptions = uiMaskConfig; 51 | for(var prop in options) { 52 | if (angular.isObject(options[prop]) && !angular.isArray(options[prop])) { 53 | angular.extend(tempOptions[prop], options[prop]); 54 | } else { 55 | tempOptions[prop] = options[prop]; 56 | } 57 | } 58 | 59 | return tempOptions; 60 | }]; 61 | }) 62 | .directive('uiMask', ['uiMask.Config', function(maskConfig) { 63 | function isFocused (elem) { 64 | return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); 65 | } 66 | 67 | return { 68 | priority: 100, 69 | require: 'ngModel', 70 | restrict: 'A', 71 | compile: function uiMaskCompilingFunction() { 72 | var options = angular.copy(maskConfig); 73 | 74 | return function uiMaskLinkingFunction(scope, iElement, iAttrs, controller) { 75 | var maskProcessed = false, eventsBound = false, 76 | maskCaretMap, maskPatterns, maskPlaceholder, maskComponents, 77 | // Minimum required length of the value to be considered valid 78 | minRequiredLength, 79 | value, valueMasked, isValid, 80 | // Vars for initializing/uninitializing 81 | originalPlaceholder = iAttrs.placeholder, 82 | originalMaxlength = iAttrs.maxlength, 83 | // Vars used exclusively in eventHandler() 84 | oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength, 85 | // Used for communicating if a backspace operation should be allowed between 86 | // keydownHandler and eventHandler 87 | preventBackspace; 88 | 89 | var originalIsEmpty = controller.$isEmpty; 90 | controller.$isEmpty = function(value) { 91 | if (maskProcessed) { 92 | return originalIsEmpty(unmaskValue(value || '')); 93 | } else { 94 | return originalIsEmpty(value); 95 | } 96 | }; 97 | 98 | function initialize(maskAttr) { 99 | if (!angular.isDefined(maskAttr)) { 100 | return uninitialize(); 101 | } 102 | processRawMask(maskAttr); 103 | if (!maskProcessed) { 104 | return uninitialize(); 105 | } 106 | initializeElement(); 107 | bindEventListeners(); 108 | return true; 109 | } 110 | 111 | function initPlaceholder(placeholderAttr) { 112 | if ( ! placeholderAttr) { 113 | return; 114 | } 115 | 116 | maskPlaceholder = placeholderAttr; 117 | 118 | // If the mask is processed, then we need to update the value 119 | // but don't set the value if there is nothing entered into the element 120 | // and there is a placeholder attribute on the element because that 121 | // will only set the value as the blank maskPlaceholder 122 | // and override the placeholder on the element 123 | if (maskProcessed && !(iElement.val().length === 0 && angular.isDefined(iAttrs.placeholder))) { 124 | iElement.val(maskValue(unmaskValue(iElement.val()))); 125 | } 126 | } 127 | 128 | function initPlaceholderChar() { 129 | return initialize(iAttrs.uiMask); 130 | } 131 | 132 | var modelViewValue = false; 133 | iAttrs.$observe('modelViewValue', function(val) { 134 | if (val === 'true') { 135 | modelViewValue = true; 136 | } 137 | }); 138 | 139 | iAttrs.$observe('allowInvalidValue', function(val) { 140 | linkOptions.allowInvalidValue = val === '' 141 | ? true 142 | : !!val; 143 | formatter(controller.$modelValue); 144 | }); 145 | 146 | function formatter(fromModelValue) { 147 | if (!maskProcessed) { 148 | return fromModelValue; 149 | } 150 | value = unmaskValue(fromModelValue || ''); 151 | isValid = validateValue(value); 152 | controller.$setValidity('mask', isValid); 153 | 154 | if (!value.length) return undefined; 155 | if (isValid || linkOptions.allowInvalidValue) { 156 | return maskValue(value); 157 | } else { 158 | return undefined; 159 | } 160 | } 161 | 162 | function parser(fromViewValue) { 163 | if (!maskProcessed) { 164 | return fromViewValue; 165 | } 166 | value = unmaskValue(fromViewValue || ''); 167 | isValid = validateValue(value); 168 | // We have to set viewValue manually as the reformatting of the input 169 | // value performed by eventHandler() doesn't happen until after 170 | // this parser is called, which causes what the user sees in the input 171 | // to be out-of-sync with what the controller's $viewValue is set to. 172 | controller.$viewValue = value.length ? maskValue(value) : ''; 173 | controller.$setValidity('mask', isValid); 174 | 175 | if (isValid || linkOptions.allowInvalidValue) { 176 | return modelViewValue ? controller.$viewValue : value; 177 | } 178 | } 179 | 180 | var linkOptions = {}; 181 | 182 | if (iAttrs.uiOptions) { 183 | linkOptions = scope.$eval('[' + iAttrs.uiOptions + ']'); 184 | if (angular.isObject(linkOptions[0])) { 185 | // we can't use angular.copy nor angular.extend, they lack the power to do a deep merge 186 | linkOptions = (function(original, current) { 187 | for (var i in original) { 188 | if (Object.prototype.hasOwnProperty.call(original, i)) { 189 | if (current[i] === undefined) { 190 | current[i] = angular.copy(original[i]); 191 | } else { 192 | if (angular.isObject(current[i]) && !angular.isArray(current[i])) { 193 | current[i] = angular.extend({}, original[i], current[i]); 194 | } 195 | } 196 | } 197 | } 198 | return current; 199 | })(options, linkOptions[0]); 200 | } else { 201 | linkOptions = options; //gotta be a better way to do this.. 202 | } 203 | } else { 204 | linkOptions = options; 205 | } 206 | 207 | iAttrs.$observe('uiMask', initialize); 208 | if (angular.isDefined(iAttrs.uiMaskPlaceholder)) { 209 | iAttrs.$observe('uiMaskPlaceholder', initPlaceholder); 210 | } 211 | else { 212 | iAttrs.$observe('placeholder', initPlaceholder); 213 | } 214 | if (angular.isDefined(iAttrs.uiMaskPlaceholderChar)) { 215 | iAttrs.$observe('uiMaskPlaceholderChar', initPlaceholderChar); 216 | } 217 | 218 | controller.$formatters.unshift(formatter); 219 | controller.$parsers.unshift(parser); 220 | 221 | function uninitialize() { 222 | maskProcessed = false; 223 | unbindEventListeners(); 224 | 225 | if (angular.isDefined(originalPlaceholder)) { 226 | iElement.attr('placeholder', originalPlaceholder); 227 | } else { 228 | iElement.removeAttr('placeholder'); 229 | } 230 | 231 | if (angular.isDefined(originalMaxlength)) { 232 | iElement.attr('maxlength', originalMaxlength); 233 | } else { 234 | iElement.removeAttr('maxlength'); 235 | } 236 | 237 | iElement.val(controller.$modelValue); 238 | controller.$viewValue = controller.$modelValue; 239 | return false; 240 | } 241 | 242 | function initializeElement() { 243 | value = oldValueUnmasked = unmaskValue(controller.$modelValue || ''); 244 | valueMasked = oldValue = maskValue(value); 245 | isValid = validateValue(value); 246 | if (iAttrs.maxlength) { // Double maxlength to allow pasting new val at end of mask 247 | iElement.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2); 248 | } 249 | if ( ! originalPlaceholder && linkOptions.addDefaultPlaceholder) { 250 | iElement.attr('placeholder', maskPlaceholder); 251 | } 252 | var viewValue = controller.$modelValue; 253 | var idx = controller.$formatters.length; 254 | while(idx--) { 255 | viewValue = controller.$formatters[idx](viewValue); 256 | } 257 | controller.$viewValue = viewValue || ''; 258 | controller.$render(); 259 | // Not using $setViewValue so we don't clobber the model value and dirty the form 260 | // without any kind of user interaction. 261 | } 262 | 263 | function bindEventListeners() { 264 | if (eventsBound) { 265 | return; 266 | } 267 | iElement.bind('blur', blurHandler); 268 | iElement.bind('mousedown mouseup', mouseDownUpHandler); 269 | iElement.bind('keydown', keydownHandler); 270 | iElement.bind(linkOptions.eventsToHandle.join(' '), eventHandler); 271 | eventsBound = true; 272 | } 273 | 274 | function unbindEventListeners() { 275 | if (!eventsBound) { 276 | return; 277 | } 278 | iElement.unbind('blur', blurHandler); 279 | iElement.unbind('mousedown', mouseDownUpHandler); 280 | iElement.unbind('mouseup', mouseDownUpHandler); 281 | iElement.unbind('keydown', keydownHandler); 282 | iElement.unbind('input', eventHandler); 283 | iElement.unbind('keyup', eventHandler); 284 | iElement.unbind('click', eventHandler); 285 | iElement.unbind('focus', eventHandler); 286 | eventsBound = false; 287 | } 288 | 289 | function validateValue(value) { 290 | // Zero-length value validity is ngRequired's determination 291 | return value.length ? value.length >= minRequiredLength : true; 292 | } 293 | 294 | function unmaskValue(value) { 295 | var valueUnmasked = '', 296 | input = iElement[0], 297 | maskPatternsCopy = maskPatterns.slice(), 298 | selectionStart = oldCaretPosition, 299 | selectionEnd = selectionStart + getSelectionLength(input), 300 | valueOffset, valueDelta, tempValue = ''; 301 | // Preprocess by stripping mask components from value 302 | value = value.toString(); 303 | valueOffset = 0; 304 | valueDelta = value.length - maskPlaceholder.length; 305 | angular.forEach(maskComponents, function(component) { 306 | var position = component.position; 307 | //Only try and replace the component if the component position is not within the selected range 308 | //If component was in selected range then it was removed with the user input so no need to try and remove that component 309 | if (!(position >= selectionStart && position < selectionEnd)) { 310 | if (position >= selectionStart) { 311 | position += valueDelta; 312 | } 313 | if (value.substring(position, position + component.value.length) === component.value) { 314 | tempValue += value.slice(valueOffset, position);// + value.slice(position + component.value.length); 315 | valueOffset = position + component.value.length; 316 | } 317 | } 318 | }); 319 | value = tempValue + value.slice(valueOffset); 320 | angular.forEach(value.split(''), function(chr) { 321 | if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) { 322 | valueUnmasked += chr; 323 | maskPatternsCopy.shift(); 324 | } 325 | }); 326 | 327 | return valueUnmasked; 328 | } 329 | 330 | function maskValue(unmaskedValue) { 331 | var valueMasked = '', 332 | maskCaretMapCopy = maskCaretMap.slice(); 333 | 334 | angular.forEach(maskPlaceholder.split(''), function(chr, i) { 335 | if (unmaskedValue.length && i === maskCaretMapCopy[0]) { 336 | valueMasked += unmaskedValue.charAt(0) || '_'; 337 | unmaskedValue = unmaskedValue.substr(1); 338 | maskCaretMapCopy.shift(); 339 | } 340 | else { 341 | valueMasked += chr; 342 | } 343 | }); 344 | return valueMasked; 345 | } 346 | 347 | function getPlaceholderChar(i) { 348 | var placeholder = angular.isDefined(iAttrs.uiMaskPlaceholder) ? iAttrs.uiMaskPlaceholder : iAttrs.placeholder, 349 | defaultPlaceholderChar; 350 | 351 | if (angular.isDefined(placeholder) && placeholder[i]) { 352 | return placeholder[i]; 353 | } else { 354 | defaultPlaceholderChar = angular.isDefined(iAttrs.uiMaskPlaceholderChar) && iAttrs.uiMaskPlaceholderChar ? iAttrs.uiMaskPlaceholderChar : '_'; 355 | return (defaultPlaceholderChar.toLowerCase() === 'space') ? ' ' : defaultPlaceholderChar[0]; 356 | } 357 | } 358 | 359 | // Generate array of mask components that will be stripped from a masked value 360 | // before processing to prevent mask components from being added to the unmasked value. 361 | // E.g., a mask pattern of '+7 9999' won't have the 7 bleed into the unmasked value. 362 | function getMaskComponents() { 363 | var maskPlaceholderChars = maskPlaceholder.split(''), 364 | maskPlaceholderCopy, components; 365 | 366 | //maskCaretMap can have bad values if the input has the ui-mask attribute implemented as an obversable property, e.g. the demo page 367 | if (maskCaretMap && !isNaN(maskCaretMap[0])) { 368 | //Instead of trying to manipulate the RegEx based on the placeholder characters 369 | //we can simply replace the placeholder characters based on the already built 370 | //maskCaretMap to underscores and leave the original working RegEx to get the proper 371 | //mask components 372 | angular.forEach(maskCaretMap, function(value) { 373 | maskPlaceholderChars[value] = '_'; 374 | }); 375 | } 376 | maskPlaceholderCopy = maskPlaceholderChars.join(''); 377 | components = maskPlaceholderCopy.replace(/[_]+/g, '_').split('_'); 378 | components = components.filter(function(s) { 379 | return s !== ''; 380 | }); 381 | 382 | // need a string search offset in cases where the mask contains multiple identical components 383 | // E.g., a mask of 99.99.99-999.99 384 | var offset = 0; 385 | return components.map(function(c) { 386 | var componentPosition = maskPlaceholderCopy.indexOf(c, offset); 387 | offset = componentPosition + 1; 388 | return { 389 | value: c, 390 | position: componentPosition 391 | }; 392 | }); 393 | } 394 | 395 | function processRawMask(mask) { 396 | var characterCount = 0; 397 | 398 | maskCaretMap = []; 399 | maskPatterns = []; 400 | maskPlaceholder = ''; 401 | 402 | if (angular.isString(mask)) { 403 | minRequiredLength = 0; 404 | 405 | var isOptional = false, 406 | numberOfOptionalCharacters = 0, 407 | splitMask = mask.split(''); 408 | 409 | var inEscape = false; 410 | angular.forEach(splitMask, function(chr, i) { 411 | if (inEscape) { 412 | inEscape = false; 413 | maskPlaceholder += chr; 414 | characterCount++; 415 | } 416 | else if (linkOptions.escChar === chr) { 417 | inEscape = true; 418 | } 419 | else if (linkOptions.maskDefinitions[chr]) { 420 | maskCaretMap.push(characterCount); 421 | 422 | maskPlaceholder += getPlaceholderChar(i - numberOfOptionalCharacters); 423 | maskPatterns.push(linkOptions.maskDefinitions[chr]); 424 | 425 | characterCount++; 426 | if (!isOptional) { 427 | minRequiredLength++; 428 | } 429 | 430 | isOptional = false; 431 | } 432 | else if (chr === '?') { 433 | isOptional = true; 434 | numberOfOptionalCharacters++; 435 | } 436 | else { 437 | maskPlaceholder += chr; 438 | characterCount++; 439 | } 440 | }); 441 | } 442 | // Caret position immediately following last position is valid. 443 | maskCaretMap.push(maskCaretMap.slice().pop() + 1); 444 | 445 | maskComponents = getMaskComponents(); 446 | maskProcessed = maskCaretMap.length > 1 ? true : false; 447 | } 448 | 449 | var prevValue = iElement.val(); 450 | function blurHandler() { 451 | if (linkOptions.clearOnBlur || ((linkOptions.clearOnBlurPlaceholder) && (value.length === 0) && iAttrs.placeholder)) { 452 | oldCaretPosition = 0; 453 | oldSelectionLength = 0; 454 | if (!isValid || value.length === 0) { 455 | valueMasked = ''; 456 | iElement.val(''); 457 | scope.$apply(function() { 458 | //only $setViewValue when not $pristine to avoid changing $pristine state. 459 | if (!controller.$pristine) { 460 | controller.$setViewValue(''); 461 | } 462 | }); 463 | } 464 | } 465 | //Check for different value and trigger change. 466 | //Check for different value and trigger change. 467 | if (value !== prevValue) { 468 | // #157 Fix the bug from the trigger when backspacing exactly on the first letter (emptying the field) 469 | // and then blurring out. 470 | // Angular uses html element and calls setViewValue(element.value.trim()), setting it to the trimmed mask 471 | // when it should be empty 472 | var currentVal = iElement.val(); 473 | var isTemporarilyEmpty = value === '' && currentVal && angular.isDefined(iAttrs.uiMaskPlaceholderChar) && iAttrs.uiMaskPlaceholderChar === 'space'; 474 | if(isTemporarilyEmpty) { 475 | iElement.val(''); 476 | } 477 | triggerChangeEvent(iElement[0]); 478 | if(isTemporarilyEmpty) { 479 | iElement.val(currentVal); 480 | } 481 | } 482 | prevValue = value; 483 | } 484 | 485 | function triggerChangeEvent(element) { 486 | var change; 487 | if (angular.isFunction(window.Event) && !element.fireEvent) { 488 | // modern browsers and Edge 489 | try { 490 | change = new Event('change', { 491 | view: window, 492 | bubbles: true, 493 | cancelable: false 494 | }); 495 | } catch (ex) { 496 | //this is for certain mobile browsers that have the Event object 497 | //but don't support the Event constructor #168 498 | change = document.createEvent('HTMLEvents'); 499 | change.initEvent('change', false, true); 500 | } finally { 501 | element.dispatchEvent(change); 502 | } 503 | } else if ('createEvent' in document) { 504 | // older browsers 505 | change = document.createEvent('HTMLEvents'); 506 | change.initEvent('change', false, true); 507 | element.dispatchEvent(change); 508 | } 509 | else if (element.fireEvent) { 510 | // IE <= 11 511 | element.fireEvent('onchange'); 512 | } 513 | } 514 | 515 | function mouseDownUpHandler(e) { 516 | if (e.type === 'mousedown') { 517 | iElement.bind('mouseout', mouseoutHandler); 518 | } else { 519 | iElement.unbind('mouseout', mouseoutHandler); 520 | } 521 | } 522 | 523 | iElement.bind('mousedown mouseup', mouseDownUpHandler); 524 | 525 | function mouseoutHandler() { 526 | /*jshint validthis: true */ 527 | oldSelectionLength = getSelectionLength(this); 528 | iElement.unbind('mouseout', mouseoutHandler); 529 | } 530 | 531 | function keydownHandler(e) { 532 | /*jshint validthis: true */ 533 | var isKeyBackspace = e.which === 8, 534 | caretPos = getCaretPosition(this) - 1 || 0, //value in keydown is pre change so bump caret position back to simulate post change 535 | isCtrlZ = e.which === 90 && e.ctrlKey; //ctrl+z pressed 536 | 537 | if (isKeyBackspace) { 538 | while(caretPos >= 0) { 539 | if (isValidCaretPosition(caretPos)) { 540 | //re-adjust the caret position. 541 | //Increment to account for the initial decrement to simulate post change caret position 542 | setCaretPosition(this, caretPos + 1); 543 | break; 544 | } 545 | caretPos--; 546 | } 547 | preventBackspace = caretPos === -1; 548 | } 549 | 550 | if (isCtrlZ) { 551 | // prevent IE bug - value should be returned to initial state 552 | iElement.val(''); 553 | e.preventDefault(); 554 | } 555 | } 556 | 557 | function eventHandler(e) { 558 | /*jshint validthis: true */ 559 | e = e || {}; 560 | // Allows more efficient minification 561 | var eventWhich = e.which, 562 | eventType = e.type; 563 | 564 | // Prevent shift and ctrl from mucking with old values 565 | if (eventWhich === 16 || eventWhich === 91) { 566 | return; 567 | } 568 | 569 | var val = iElement.val(), 570 | valOld = oldValue, 571 | valMasked, 572 | valAltered = false, 573 | valUnmasked = unmaskValue(val), 574 | valUnmaskedOld = oldValueUnmasked, 575 | caretPos = getCaretPosition(this) || 0, 576 | caretPosOld = oldCaretPosition || 0, 577 | caretPosDelta = caretPos - caretPosOld, 578 | caretPosMin = maskCaretMap[0], 579 | caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(), 580 | selectionLenOld = oldSelectionLength || 0, 581 | isSelected = getSelectionLength(this) > 0, 582 | wasSelected = selectionLenOld > 0, 583 | // Case: Typing a character to overwrite a selection 584 | isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld), 585 | // Case: Delete and backspace behave identically on a selection 586 | isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld), 587 | isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes 588 | 589 | isKeyLeftArrow = eventWhich === 37, 590 | // Necessary due to "input" event not providing a key code 591 | isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)), 592 | isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0) && !wasSelected), 593 | // Handles cases where caret is moved and placed in front of invalid maskCaretMap position. Logic below 594 | // ensures that, on click or leftward caret placement, caret is moved leftward until directly right of 595 | // non-mask character. Also applied to click since users are (arguably) more likely to backspace 596 | // a character when clicking within a filled input. 597 | caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin; 598 | 599 | oldSelectionLength = getSelectionLength(this); 600 | 601 | // These events don't require any action 602 | if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup' || eventType === 'focus'))) { 603 | return; 604 | } 605 | 606 | if (isKeyBackspace && preventBackspace) { 607 | iElement.val(maskPlaceholder); 608 | // This shouldn't be needed but for some reason after aggressive backspacing the controller $viewValue is incorrect. 609 | // This keeps the $viewValue updated and correct. 610 | scope.$apply(function () { 611 | controller.$setViewValue(''); // $setViewValue should be run in angular context, otherwise the changes will be invisible to angular and user code. 612 | }); 613 | setCaretPosition(this, caretPosOld); 614 | return; 615 | } 616 | 617 | // Value Handling 618 | // ============== 619 | 620 | // User attempted to delete but raw value was unaffected--correct this grievous offense 621 | if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) { 622 | while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) { 623 | caretPos--; 624 | } 625 | while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) { 626 | caretPos++; 627 | } 628 | var charIndex = maskCaretMap.indexOf(caretPos); 629 | // Strip out non-mask character that user would have deleted if mask hadn't been in the way. 630 | valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1); 631 | 632 | // If value has not changed, don't want to call $setViewValue, may be caused by IE raising input event due to placeholder 633 | if (valUnmasked !== valUnmaskedOld) 634 | valAltered = true; 635 | } 636 | 637 | // Update values 638 | valMasked = maskValue(valUnmasked); 639 | 640 | oldValue = valMasked; 641 | oldValueUnmasked = valUnmasked; 642 | 643 | //additional check to fix the problem where the viewValue is out of sync with the value of the element. 644 | //better fix for commit 2a83b5fb8312e71d220a497545f999fc82503bd9 (I think) 645 | if (!valAltered && val.length > valMasked.length) 646 | valAltered = true; 647 | 648 | iElement.val(valMasked); 649 | 650 | //we need this check. What could happen if you don't have it is that you'll set the model value without the user 651 | //actually doing anything. Meaning, things like pristine and touched will be set. 652 | if (valAltered) { 653 | scope.$apply(function () { 654 | controller.$setViewValue(valMasked); // $setViewValue should be run in angular context, otherwise the changes will be invisible to angular and user code. 655 | }); 656 | } 657 | 658 | // Caret Repositioning 659 | // =================== 660 | 661 | // Ensure that typing always places caret ahead of typed character in cases where the first char of 662 | // the input is a mask char and the caret is placed at the 0 position. 663 | if (isAddition && (caretPos <= caretPosMin)) { 664 | caretPos = caretPosMin + 1; 665 | } 666 | 667 | if (caretBumpBack) { 668 | caretPos--; 669 | } 670 | 671 | // Make sure caret is within min and max position limits 672 | caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos; 673 | 674 | // Scoot the caret back or forth until it's in a non-mask position and within min/max position limits 675 | while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) { 676 | caretPos += caretBumpBack ? -1 : 1; 677 | } 678 | 679 | if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) { 680 | caretPos++; 681 | } 682 | oldCaretPosition = caretPos; 683 | setCaretPosition(this, caretPos); 684 | } 685 | 686 | function isValidCaretPosition(pos) { 687 | return maskCaretMap.indexOf(pos) > -1; 688 | } 689 | 690 | function getCaretPosition(input) { 691 | if (!input) 692 | return 0; 693 | if (input.selectionStart !== undefined) { 694 | return input.selectionStart; 695 | } else if (document.selection) { 696 | if (isFocused(iElement[0])) { 697 | // Curse you IE 698 | input.focus(); 699 | var selection = document.selection.createRange(); 700 | selection.moveStart('character', input.value ? -input.value.length : 0); 701 | return selection.text.length; 702 | } 703 | } 704 | return 0; 705 | } 706 | 707 | function setCaretPosition(input, pos) { 708 | if (!input) 709 | return 0; 710 | if (input.offsetWidth === 0 || input.offsetHeight === 0) { 711 | return; // Input's hidden 712 | } 713 | if (input.setSelectionRange) { 714 | if (isFocused(iElement[0])) { 715 | input.focus(); 716 | input.setSelectionRange(pos, pos); 717 | } 718 | } 719 | else if (input.createTextRange) { 720 | // Curse you IE 721 | var range = input.createTextRange(); 722 | range.collapse(true); 723 | range.moveEnd('character', pos); 724 | range.moveStart('character', pos); 725 | range.select(); 726 | } 727 | } 728 | 729 | function getSelectionLength(input) { 730 | if (!input) 731 | return 0; 732 | if (input.selectionStart !== undefined) { 733 | return (input.selectionEnd - input.selectionStart); 734 | } 735 | if (window.getSelection) { 736 | return (window.getSelection().toString().length); 737 | } 738 | if (document.selection) { 739 | return (document.selection.createRange().text.length); 740 | } 741 | return 0; 742 | } 743 | 744 | // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf 745 | if (!Array.prototype.indexOf) { 746 | Array.prototype.indexOf = function(searchElement /*, fromIndex */) { 747 | if (this === null) { 748 | throw new TypeError(); 749 | } 750 | var t = Object(this); 751 | var len = t.length >>> 0; 752 | if (len === 0) { 753 | return -1; 754 | } 755 | var n = 0; 756 | if (arguments.length > 1) { 757 | n = Number(arguments[1]); 758 | if (n !== n) { // shortcut for verifying if it's NaN 759 | n = 0; 760 | } else if (n !== 0 && n !== Infinity && n !== -Infinity) { 761 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 762 | } 763 | } 764 | if (n >= len) { 765 | return -1; 766 | } 767 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 768 | for (; k < len; k++) { 769 | if (k in t && t[k] === searchElement) { 770 | return k; 771 | } 772 | } 773 | return -1; 774 | }; 775 | } 776 | 777 | }; 778 | } 779 | }; 780 | } 781 | ]); 782 | 783 | }()); -------------------------------------------------------------------------------- /dist/mask.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * angular-ui-mask 3 | * https://github.com/angular-ui/ui-mask 4 | * Version: 1.8.7 - 2016-07-26T15:59:07.992Z 5 | * License: MIT 6 | */ 7 | !function(){"use strict";angular.module("ui.mask",[]).value("uiMaskConfig",{maskDefinitions:{9:/\d/,A:/[a-zA-Z]/,"*":/[a-zA-Z0-9]/},clearOnBlur:!0,clearOnBlurPlaceholder:!1,escChar:"\\",eventsToHandle:["input","keyup","click","focus"],addDefaultPlaceholder:!0,allowInvalidValue:!1}).provider("uiMask.Config",function(){var e={};this.maskDefinitions=function(n){return e.maskDefinitions=n},this.clearOnBlur=function(n){return e.clearOnBlur=n},this.clearOnBlurPlaceholder=function(n){return e.clearOnBlurPlaceholder=n},this.eventsToHandle=function(n){return e.eventsToHandle=n},this.addDefaultPlaceholder=function(n){return e.addDefaultPlaceholder=n},this.allowInvalidValue=function(n){return e.allowInvalidValue=n},this.$get=["uiMaskConfig",function(n){var t=n;for(var a in e)angular.isObject(e[a])&&!angular.isArray(e[a])?angular.extend(t[a],e[a]):t[a]=e[a];return t}]}).directive("uiMask",["uiMask.Config",function(e){function n(e){return e===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(e.type||e.href||~e.tabIndex)}return{priority:100,require:"ngModel",restrict:"A",compile:function(){var t=angular.copy(e);return function(e,a,i,r){function l(e){return angular.isDefined(e)?(w(e),K?(h(),d(),!0):f()):f()}function u(e){e&&(T=e,!K||0===a.val().length&&angular.isDefined(i.placeholder)||a.val(m(p(a.val()))))}function o(){return l(i.uiMask)}function c(e){return K?(j=p(e||""),R=g(j),r.$setValidity("mask",R),j.length&&(R||Q.allowInvalidValue)?m(j):void 0):e}function s(e){return K?(j=p(e||""),R=g(j),r.$viewValue=j.length?m(j):"",r.$setValidity("mask",R),R||Q.allowInvalidValue?J?r.$viewValue:j:void 0):e}function f(){return K=!1,v(),angular.isDefined(q)?a.attr("placeholder",q):a.removeAttr("placeholder"),angular.isDefined(W)?a.attr("maxlength",W):a.removeAttr("maxlength"),a.val(r.$modelValue),r.$viewValue=r.$modelValue,!1}function h(){j=F=p(r.$modelValue||""),H=_=m(j),R=g(j),i.maxlength&&a.attr("maxlength",2*S[S.length-1]),!q&&Q.addDefaultPlaceholder&&a.attr("placeholder",T);for(var e=r.$modelValue,n=r.$formatters.length;n--;)e=r.$formatters[n](e);r.$viewValue=e||"",r.$render()}function d(){Z||(a.bind("blur",y),a.bind("mousedown mouseup",V),a.bind("keydown",M),a.bind(Q.eventsToHandle.join(" "),O),Z=!0)}function v(){Z&&(a.unbind("blur",y),a.unbind("mousedown",V),a.unbind("mouseup",V),a.unbind("keydown",M),a.unbind("input",O),a.unbind("keyup",O),a.unbind("click",O),a.unbind("focus",O),Z=!1)}function g(e){return e.length?e.length>=I:!0}function p(e){var n,t,i="",r=a[0],l=A.slice(),u=L,o=u+C(r),c="";return e=e.toString(),n=0,t=e.length-T.length,angular.forEach(B,function(a){var i=a.position;i>=u&&o>i||(i>=u&&(i+=t),e.substring(i,i+a.value.length)===a.value&&(c+=e.slice(n,i),n=i+a.value.length))}),e=c+e.slice(n),angular.forEach(e.split(""),function(e){l.length&&l[0].test(e)&&(i+=e,l.shift())}),i}function m(e){var n="",t=S.slice();return angular.forEach(T.split(""),function(a,i){e.length&&i===t[0]?(n+=e.charAt(0)||"_",e=e.substr(1),t.shift()):n+=a}),n}function b(e){var n,t=angular.isDefined(i.uiMaskPlaceholder)?i.uiMaskPlaceholder:i.placeholder;return angular.isDefined(t)&&t[e]?t[e]:(n=angular.isDefined(i.uiMaskPlaceholderChar)&&i.uiMaskPlaceholderChar?i.uiMaskPlaceholderChar:"_","space"===n.toLowerCase()?" ":n[0])}function k(){var e,n,t=T.split("");S&&!isNaN(S[0])&&angular.forEach(S,function(e){t[e]="_"}),e=t.join(""),n=e.replace(/[_]+/g,"_").split("_"),n=n.filter(function(e){return""!==e});var a=0;return n.map(function(n){var t=e.indexOf(n,a);return a=t+1,{value:n,position:t}})}function w(e){var n=0;if(S=[],A=[],T="",angular.isString(e)){I=0;var t=!1,a=0,i=e.split(""),r=!1;angular.forEach(i,function(e,i){r?(r=!1,T+=e,n++):Q.escChar===e?r=!0:Q.maskDefinitions[e]?(S.push(n),T+=b(i-a),A.push(Q.maskDefinitions[e]),n++,t||I++,t=!1):"?"===e?(t=!0,a++):(T+=e,n++)})}S.push(S.slice().pop()+1),B=k(),K=S.length>1?!0:!1}function y(){if((Q.clearOnBlur||Q.clearOnBlurPlaceholder&&0===j.length&&i.placeholder)&&(L=0,N=0,R&&0!==j.length||(H="",a.val(""),e.$apply(function(){r.$pristine||r.$setViewValue("")}))),j!==U){var n=a.val(),t=""===j&&n&&angular.isDefined(i.uiMaskPlaceholderChar)&&"space"===i.uiMaskPlaceholderChar;t&&a.val(""),$(a[0]),t&&a.val(n)}U=j}function $(e){var n;if(angular.isFunction(window.Event)&&!e.fireEvent)try{n=new Event("change",{view:window,bubbles:!0,cancelable:!1})}catch(t){n=document.createEvent("HTMLEvents"),n.initEvent("change",!1,!0)}finally{e.dispatchEvent(n)}else"createEvent"in document?(n=document.createEvent("HTMLEvents"),n.initEvent("change",!1,!0),e.dispatchEvent(n)):e.fireEvent&&e.fireEvent("onchange")}function V(e){"mousedown"===e.type?a.bind("mouseout",E):a.unbind("mouseout",E)}function E(){N=C(this),a.unbind("mouseout",E)}function M(e){var n=8===e.which,t=P(this)-1||0,i=90===e.which&&e.ctrlKey;if(n){for(;t>=0;){if(D(t)){x(this,t+1);break}t--}z=-1===t}i&&(a.val(""),e.preventDefault())}function O(n){n=n||{};var t=n.which,i=n.type;if(16!==t&&91!==t){var l,u=a.val(),o=_,c=!1,s=p(u),f=F,h=P(this)||0,d=L||0,v=h-d,g=S[0],b=S[s.length]||S.slice().shift(),k=N||0,w=C(this)>0,y=k>0,$=u.length>o.length||k&&u.length>o.length-k,V=u.length=37&&40>=t&&n.shiftKey,M=37===t,O=8===t||"keyup"!==i&&V&&-1===v,A=46===t||"keyup"!==i&&V&&0===v&&!y,B=(M||O||"click"===i)&&h>g;if(N=C(this),!E&&(!w||"click"!==i&&"keyup"!==i&&"focus"!==i)){if(O&&z)return a.val(T),e.$apply(function(){r.$setViewValue("")}),void x(this,d);if("input"===i&&V&&!y&&s===f){for(;O&&h>g&&!D(h);)h--;for(;A&&b>h&&-1===S.indexOf(h);)h++;var I=S.indexOf(h);s=s.substring(0,I)+s.substring(I+1),s!==f&&(c=!0)}for(l=m(s),_=l,F=s,!c&&u.length>l.length&&(c=!0),a.val(l),c&&e.$apply(function(){r.$setViewValue(l)}),$&&g>=h&&(h=g+1),B&&h--,h=h>b?b:g>h?g:h;!D(h)&&h>g&&b>h;)h+=B?-1:1;(B&&b>h||$&&!D(d))&&h++,L=h,x(this,h)}}}function D(e){return S.indexOf(e)>-1}function P(e){if(!e)return 0;if(void 0!==e.selectionStart)return e.selectionStart;if(document.selection&&n(a[0])){e.focus();var t=document.selection.createRange();return t.moveStart("character",e.value?-e.value.length:0),t.text.length}return 0}function x(e,t){if(!e)return 0;if(0!==e.offsetWidth&&0!==e.offsetHeight)if(e.setSelectionRange)n(a[0])&&(e.focus(),e.setSelectionRange(t,t));else if(e.createTextRange){var i=e.createTextRange();i.collapse(!0),i.moveEnd("character",t),i.moveStart("character",t),i.select()}}function C(e){return e?void 0!==e.selectionStart?e.selectionEnd-e.selectionStart:window.getSelection?window.getSelection().toString().length:document.selection?document.selection.createRange().text.length:0:0}var S,A,T,B,I,j,H,R,_,F,L,N,z,K=!1,Z=!1,q=i.placeholder,W=i.maxlength,G=r.$isEmpty;r.$isEmpty=function(e){return G(K?p(e||""):e)};var J=!1;i.$observe("modelViewValue",function(e){"true"===e&&(J=!0)}),i.$observe("allowInvalidValue",function(e){Q.allowInvalidValue=""===e?!0:!!e,c(r.$modelValue)});var Q={};i.uiOptions?(Q=e.$eval("["+i.uiOptions+"]"),Q=angular.isObject(Q[0])?function(e,n){for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(void 0===n[t]?n[t]=angular.copy(e[t]):angular.isObject(n[t])&&!angular.isArray(n[t])&&(n[t]=angular.extend({},e[t],n[t])));return n}(t,Q[0]):t):Q=t,i.$observe("uiMask",l),angular.isDefined(i.uiMaskPlaceholder)?i.$observe("uiMaskPlaceholder",u):i.$observe("placeholder",u),angular.isDefined(i.uiMaskPlaceholderChar)&&i.$observe("uiMaskPlaceholderChar",o),r.$formatters.unshift(c),r.$parsers.unshift(s);var U=a.val();a.bind("mousedown mouseup",V),Array.prototype.indexOf||(Array.prototype.indexOf=function(e){if(null===this)throw new TypeError;var n=Object(this),t=n.length>>>0;if(0===t)return-1;var a=0;if(arguments.length>1&&(a=Number(arguments[1]),a!==a?a=0:0!==a&&a!==1/0&&a!==-(1/0)&&(a=(a>0||-1)*Math.floor(Math.abs(a)))),a>=t)return-1;for(var i=a>=0?a:Math.max(t-Math.abs(a),0);t>i;i++)if(i in n&&n[i]===e)return i;return-1})}}}}])}(); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gulp = require('gulp'); 3 | var Server = require('karma').Server; 4 | var concat = require('gulp-concat'); 5 | var jshint = require('gulp-jshint'); 6 | var header = require('gulp-header'); 7 | var footer = require('gulp-footer'); 8 | var rename = require('gulp-rename'); 9 | var es = require('event-stream'); 10 | var del = require('del'); 11 | var uglify = require('gulp-uglify'); 12 | var plumber = require('gulp-plumber');//To prevent pipe breaking caused by errors at 'watch' 13 | var git = require('gulp-git'); 14 | var bump = require('gulp-bump'); 15 | var runSequence = require('run-sequence'); 16 | var geSaLaKaCuLa = require('gesalakacula'); 17 | var reKaLa = geSaLaKaCuLa.recursiveKarmaLauncher; 18 | var connect = require('gulp-connect'); 19 | var angularProtractor = require('gulp-angular-protractor'); 20 | var sauceConnectLauncher = require('sauce-connect-launcher'); 21 | var versionAfterBump, sauceConnectProcess; 22 | 23 | gulp.task('default', ['build', 'test']); 24 | gulp.task('build', ['scripts']); 25 | gulp.task('test', ['build', 'protractor', 'karma']); 26 | gulp.task('ci', ['protractor-sauce','karma-sauce'], function() { 27 | closeSauceConnect(); 28 | }); 29 | 30 | gulp.task('watch', ['build', 'karma-watch'], function() { 31 | gulp.watch(['src/**/*.{js,html}'], ['build']); 32 | }); 33 | 34 | gulp.task('clean', function(cb) { 35 | del(['dist'], cb); 36 | }); 37 | 38 | gulp.task('scripts', ['clean'], function() { 39 | 40 | var buildLib = function() { 41 | return gulp.src(['src/*.js']) 42 | .pipe(plumber({ 43 | errorHandler: handleError 44 | })) 45 | .pipe(header('(function () { \n\'use strict\';\n')) 46 | .pipe(footer('\n}());')) 47 | .pipe(jshint()) 48 | .pipe(jshint.reporter('jshint-stylish')) 49 | .pipe(jshint.reporter('fail')); 50 | }; 51 | var config = { 52 | pkg: JSON.parse(fs.readFileSync('./package.json')), 53 | banner: 54 | '/*!\n' + 55 | ' * <%= pkg.name %>\n' + 56 | ' * <%= pkg.homepage %>\n' + 57 | ' * Version: <%= pkg.version %> - <%= timestamp %>\n' + 58 | ' * License: <%= pkg.license %>\n' + 59 | ' */\n\n\n' 60 | }; 61 | 62 | return es.merge(buildLib()) 63 | .pipe(plumber({ 64 | errorHandler: handleError 65 | })) 66 | .pipe(concat('mask.js')) 67 | .pipe(header(config.banner, { 68 | timestamp: (new Date()).toISOString(), pkg: config.pkg 69 | })) 70 | .pipe(gulp.dest('dist')) 71 | .pipe(uglify({preserveComments: 'some'})) 72 | .pipe(rename({extname: '.min.js'})) 73 | .pipe(gulp.dest('dist')); 74 | 75 | }); 76 | 77 | gulp.task('karma', ['build'], function(callback) { 78 | runKarma(true, callback); 79 | }); 80 | gulp.task('karma-watch', ['build'], function(callback) { 81 | runKarma(false, callback); 82 | }); 83 | 84 | function runKarma(singleRun, callback) { 85 | var server = new Server({configFile: __dirname + '/karma.conf.js', singleRun: singleRun}, function(exitCode) { 86 | callback(); 87 | }); 88 | server.start(); 89 | } 90 | 91 | gulp.task('start-sauce-connect', function(callback) { 92 | sauceConnectLauncher({ 93 | username: process.env.SAUCE_USERNAME, 94 | accessKey: process.env.SAUCE_ACCESS_KEY, 95 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER 96 | }, function(err, sauceProcess) { 97 | if (err) { 98 | callback(err); 99 | } 100 | 101 | sauceConnectProcess = sauceProcess; 102 | callback(); 103 | }); 104 | }); 105 | 106 | function closeSauceConnect() { 107 | sauceConnectProcess.close(function() { 108 | console.log("Closed Sauce Connect process"); 109 | }); 110 | } 111 | 112 | gulp.task('karma-sauce', ['build', 'start-sauce-connect'], function(callback) { 113 | var customLaunchers = geSaLaKaCuLa({ 114 | 'Windows 7': { 115 | 'internet explorer': '10..11' 116 | }, 117 | 'Windows 10': { 118 | 'MicrosoftEdge': '13' 119 | }, 120 | 'OS X 10.10': { 121 | 'chrome': '43..44', 122 | 'firefox': '39..40', 123 | 'safari': '8' 124 | } 125 | }); 126 | 127 | reKaLa({ 128 | karma: Server, 129 | customLaunchers: customLaunchers 130 | }, function(code) { 131 | if (code) { 132 | closeSauceConnect(); 133 | } 134 | callback(code); 135 | }); 136 | }); 137 | 138 | gulp.task('protractor', ['build'], function(callback) { 139 | runProtractor('protractor.config.js', callback); 140 | }); 141 | 142 | gulp.task('protractor-sauce', ['build', 'start-sauce-connect'], function(callback) { 143 | runProtractor('protractor.travis.config.js', callback); 144 | }); 145 | 146 | var runProtractor = function(configFile, callback) { 147 | connect.server({ 148 | port: 8000 149 | }); 150 | 151 | gulp.src(['test/maskSpec.protractor.js']) 152 | .pipe(angularProtractor({ 153 | 'configFile': configFile, 154 | 'debug': false, 155 | 'autoStartStopServer': true 156 | })) 157 | .on('error', function(e) { 158 | closeSauceConnect(); 159 | callback(e); 160 | }) 161 | .on('end', function() { 162 | connect.serverClose(); 163 | callback(); 164 | }); 165 | }; 166 | 167 | var handleError = function(err) { 168 | console.log(err.toString()); 169 | this.emit('end'); 170 | }; 171 | 172 | gulp.task('release:bump', function() { 173 | var type = process.argv[3] ? process.argv[3].substr(2) : 'patch'; 174 | return gulp.src(['./package.json']) 175 | .pipe(bump({type: type})) 176 | .pipe(gulp.dest('./')) 177 | .on('end', function() { 178 | versionAfterBump = require('./package.json').version; 179 | }); 180 | }); 181 | 182 | gulp.task('release:rebuild', function(cb) { 183 | runSequence('release:bump', 'build', cb); // bump will here be executed before build 184 | }); 185 | 186 | gulp.task('release:commit', ['release:rebuild'], function() { 187 | return gulp.src(['./package.json', 'dist/**/*']) 188 | .pipe(git.add()) 189 | .pipe(git.commit(versionAfterBump)); 190 | }); 191 | 192 | gulp.task('release:tag', ['release:commit'], function() { 193 | git.tag(versionAfterBump, versionAfterBump); 194 | }); 195 | 196 | gulp.task('release', ['release:tag']); 197 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | //https://github.com/angular/angular.js/pull/10732 2 | 3 | var angular = require('angular'); 4 | var mask = require('./dist/mask'); 5 | 6 | module.exports = 'ui.mask'; 7 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Sep 27 2013 23:41:22 GMT+0200 (W. Europe Daylight Time) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // frameworks to use 8 | frameworks: ['jasmine'], 9 | 10 | 11 | // list of files / patterns to load in the browser 12 | files: [ 13 | 'bower_components/angular/angular.js', 14 | 'bower_components/angular-mocks/angular-mocks.js', 15 | 'dist/*.min.js', 16 | 'test/*Spec.js' 17 | ], 18 | 19 | 20 | // list of files to exclude 21 | exclude: [ 22 | 23 | ], 24 | 25 | 26 | // test results reporter to use 27 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 28 | reporters: ['progress','saucelabs'], 29 | 30 | 31 | // web server port 32 | port: 9876, 33 | 34 | 35 | // enable / disable colors in the output (reporters and logs) 36 | colors: true, 37 | 38 | 39 | // level of logging 40 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 41 | logLevel: config.LOG_INFO, 42 | 43 | 44 | // enable / disable watching file and executing tests whenever any file changes 45 | autoWatch: true, 46 | 47 | 48 | // Start these browsers, currently available: 49 | // - Chrome 50 | // - ChromeCanary 51 | // - Firefox 52 | // - Opera 53 | // - Safari (only Mac) 54 | // - PhantomJS 55 | // - IE (only Windows) 56 | browsers: ['Firefox'], 57 | 58 | 59 | // If browser does not capture in given timeout [ms], kill it 60 | captureTimeout: 60000, 61 | 62 | 63 | // Continuous Integration mode 64 | // if true, it capture browsers, run tests and exit 65 | singleRun: false 66 | }); 67 | 68 | // Sauce Specific configuration for CI 69 | if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) { 70 | config.reporters.push('saucelabs'); 71 | 72 | config.set({ 73 | sauceLabs: { 74 | testName: 'UI Mask CI', 75 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, 76 | startConnect: false 77 | }, 78 | captureTimeout: 120000, 79 | singleRun: true 80 | }); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /logos/browser-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angular-ui/ui-mask/8a79a15f24838e8463ee795c11bd7d74f2e21c13/logos/browser-stack.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-mask", 3 | "version": "1.8.7", 4 | "author": "https://github.com/angular-ui/ui-mask/graphs/contributors", 5 | "license": "MIT", 6 | "homepage": "https://github.com/angular-ui/ui-mask", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "del": "~1.2.0", 10 | "event-stream": "~3.3.1", 11 | "gesalakacula": "^1.4.0", 12 | "gulp": "~3.9.0", 13 | "gulp-angular-protractor": "0.0.6", 14 | "gulp-bump": "^0.3.1", 15 | "gulp-concat": "~2.6.0", 16 | "gulp-connect": "^2.3.1", 17 | "gulp-footer": "~1.0.5", 18 | "gulp-git": "^1.4.0", 19 | "gulp-header": "~1.2.2", 20 | "gulp-jshint": "1.11.2", 21 | "gulp-plumber": "^1.0.1", 22 | "gulp-rename": "~1.2.2", 23 | "gulp-uglify": "~1.2.0", 24 | "jasmine-core": "^2.3.4", 25 | "jshint-stylish": "~2.0.1", 26 | "karma": "^0.13.9", 27 | "karma-chrome-launcher": "^0.2.0", 28 | "karma-coverage": "~0.5", 29 | "karma-firefox-launcher": "~0.1", 30 | "karma-jasmine": "~0.3", 31 | "karma-ng-html2js-preprocessor": "^0.1.0", 32 | "karma-sauce-launcher": "^0.2.14", 33 | "phantomjs": "^1.9.18", 34 | "protractor": "^3.0.0", 35 | "run-sequence": "^1.1.2" 36 | }, 37 | "scripts": {}, 38 | "main": "./index.js", 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/angular-ui/ui-mask.git" 42 | }, 43 | "keywords": [ 44 | "angular", 45 | "angular-ui", 46 | "mask" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /protractor.config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | seleniumAddress: 'http://localhost:4444/wd/hub', 3 | specs: ['test/maskSpec.protractor.js'], 4 | baseUrl: 'http://localhost:8000' 5 | }; 6 | -------------------------------------------------------------------------------- /protractor.travis.config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | sauceUser: process.env.SAUCE_USERNAME, 3 | sauceKey: process.env.SAUCE_ACCESS_KEY, 4 | 5 | capabilities: { 6 | 'browserName': 'chrome', 7 | 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 8 | 'build': process.env.TRAVIS_BUILD_NUMBER, 9 | 'name': 'ui-mask Protractor Tests' 10 | }, 11 | 12 | specs: ['test/maskSpec.protractor.js'], 13 | baseUrl: 'http://localhost:8000' 14 | }; 15 | -------------------------------------------------------------------------------- /src/mask.js: -------------------------------------------------------------------------------- 1 | /* 2 | Attaches input mask onto input element 3 | */ 4 | angular.module('ui.mask', []) 5 | .value('uiMaskConfig', { 6 | maskDefinitions: { 7 | '9': /\d/, 8 | 'A': /[a-zA-Z]/, 9 | '*': /[a-zA-Z0-9]/ 10 | }, 11 | clearOnBlur: true, 12 | clearOnBlurPlaceholder: false, 13 | escChar: '\\', 14 | eventsToHandle: ['input', 'keyup', 'click', 'focus'], 15 | addDefaultPlaceholder: true, 16 | allowInvalidValue: false 17 | }) 18 | .provider('uiMask.Config', function() { 19 | var options = {}; 20 | 21 | this.maskDefinitions = function(maskDefinitions) { 22 | return options.maskDefinitions = maskDefinitions; 23 | }; 24 | this.clearOnBlur = function(clearOnBlur) { 25 | return options.clearOnBlur = clearOnBlur; 26 | }; 27 | this.clearOnBlurPlaceholder = function(clearOnBlurPlaceholder) { 28 | return options.clearOnBlurPlaceholder = clearOnBlurPlaceholder; 29 | }; 30 | this.eventsToHandle = function(eventsToHandle) { 31 | return options.eventsToHandle = eventsToHandle; 32 | }; 33 | this.addDefaultPlaceholder = function(addDefaultPlaceholder) { 34 | return options.addDefaultPlaceholder = addDefaultPlaceholder; 35 | }; 36 | this.allowInvalidValue = function(allowInvalidValue) { 37 | return options.allowInvalidValue = allowInvalidValue; 38 | }; 39 | this.$get = ['uiMaskConfig', function(uiMaskConfig) { 40 | var tempOptions = uiMaskConfig; 41 | for(var prop in options) { 42 | if (angular.isObject(options[prop]) && !angular.isArray(options[prop])) { 43 | angular.extend(tempOptions[prop], options[prop]); 44 | } else { 45 | tempOptions[prop] = options[prop]; 46 | } 47 | } 48 | 49 | return tempOptions; 50 | }]; 51 | }) 52 | .directive('uiMask', ['uiMask.Config', function(maskConfig) { 53 | function isFocused (elem) { 54 | return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); 55 | } 56 | 57 | return { 58 | priority: 100, 59 | require: 'ngModel', 60 | restrict: 'A', 61 | compile: function uiMaskCompilingFunction() { 62 | var options = angular.copy(maskConfig); 63 | 64 | return function uiMaskLinkingFunction(scope, iElement, iAttrs, controller) { 65 | var maskProcessed = false, eventsBound = false, 66 | maskCaretMap, maskPatterns, maskPlaceholder, maskComponents, 67 | // Minimum required length of the value to be considered valid 68 | minRequiredLength, 69 | value, valueMasked, isValid, 70 | // Vars for initializing/uninitializing 71 | originalPlaceholder = iAttrs.placeholder, 72 | originalMaxlength = iAttrs.maxlength, 73 | // Vars used exclusively in eventHandler() 74 | oldValue, oldValueUnmasked, oldCaretPosition, oldSelectionLength, 75 | // Used for communicating if a backspace operation should be allowed between 76 | // keydownHandler and eventHandler 77 | preventBackspace; 78 | 79 | var originalIsEmpty = controller.$isEmpty; 80 | controller.$isEmpty = function(value) { 81 | if (maskProcessed) { 82 | return originalIsEmpty(unmaskValue(value || '')); 83 | } else { 84 | return originalIsEmpty(value); 85 | } 86 | }; 87 | 88 | function initialize(maskAttr) { 89 | if (!angular.isDefined(maskAttr)) { 90 | return uninitialize(); 91 | } 92 | processRawMask(maskAttr); 93 | if (!maskProcessed) { 94 | return uninitialize(); 95 | } 96 | initializeElement(); 97 | bindEventListeners(); 98 | return true; 99 | } 100 | 101 | function initPlaceholder(placeholderAttr) { 102 | if ( ! placeholderAttr) { 103 | return; 104 | } 105 | 106 | maskPlaceholder = placeholderAttr; 107 | 108 | // If the mask is processed, then we need to update the value 109 | // but don't set the value if there is nothing entered into the element 110 | // and there is a placeholder attribute on the element because that 111 | // will only set the value as the blank maskPlaceholder 112 | // and override the placeholder on the element 113 | if (maskProcessed && !(iElement.val().length === 0 && angular.isDefined(iAttrs.placeholder))) { 114 | iElement.val(maskValue(unmaskValue(iElement.val()))); 115 | } 116 | } 117 | 118 | function initPlaceholderChar() { 119 | return initialize(iAttrs.uiMask); 120 | } 121 | 122 | var modelViewValue = false; 123 | iAttrs.$observe('modelViewValue', function(val) { 124 | if (val === 'true') { 125 | modelViewValue = true; 126 | } 127 | }); 128 | 129 | iAttrs.$observe('allowInvalidValue', function(val) { 130 | linkOptions.allowInvalidValue = val === '' 131 | ? true 132 | : !!val; 133 | formatter(controller.$modelValue); 134 | }); 135 | 136 | function formatter(fromModelValue) { 137 | if (!maskProcessed) { 138 | return fromModelValue; 139 | } 140 | value = unmaskValue(fromModelValue || ''); 141 | isValid = validateValue(value); 142 | controller.$setValidity('mask', isValid); 143 | 144 | if (!value.length) return undefined; 145 | if (isValid || linkOptions.allowInvalidValue) { 146 | return maskValue(value); 147 | } else { 148 | return undefined; 149 | } 150 | } 151 | 152 | function parser(fromViewValue) { 153 | if (!maskProcessed) { 154 | return fromViewValue; 155 | } 156 | value = unmaskValue(fromViewValue || ''); 157 | isValid = validateValue(value); 158 | // We have to set viewValue manually as the reformatting of the input 159 | // value performed by eventHandler() doesn't happen until after 160 | // this parser is called, which causes what the user sees in the input 161 | // to be out-of-sync with what the controller's $viewValue is set to. 162 | controller.$viewValue = value.length ? maskValue(value) : ''; 163 | controller.$setValidity('mask', isValid); 164 | 165 | if (isValid || linkOptions.allowInvalidValue) { 166 | return modelViewValue ? controller.$viewValue : value; 167 | } 168 | } 169 | 170 | var linkOptions = {}; 171 | 172 | if (iAttrs.uiOptions) { 173 | linkOptions = scope.$eval('[' + iAttrs.uiOptions + ']'); 174 | if (angular.isObject(linkOptions[0])) { 175 | // we can't use angular.copy nor angular.extend, they lack the power to do a deep merge 176 | linkOptions = (function(original, current) { 177 | for (var i in original) { 178 | if (Object.prototype.hasOwnProperty.call(original, i)) { 179 | if (current[i] === undefined) { 180 | current[i] = angular.copy(original[i]); 181 | } else { 182 | if (angular.isObject(current[i]) && !angular.isArray(current[i])) { 183 | current[i] = angular.extend({}, original[i], current[i]); 184 | } 185 | } 186 | } 187 | } 188 | return current; 189 | })(options, linkOptions[0]); 190 | } else { 191 | linkOptions = options; //gotta be a better way to do this.. 192 | } 193 | } else { 194 | linkOptions = options; 195 | } 196 | 197 | iAttrs.$observe('uiMask', initialize); 198 | if (angular.isDefined(iAttrs.uiMaskPlaceholder)) { 199 | iAttrs.$observe('uiMaskPlaceholder', initPlaceholder); 200 | } 201 | else { 202 | iAttrs.$observe('placeholder', initPlaceholder); 203 | } 204 | if (angular.isDefined(iAttrs.uiMaskPlaceholderChar)) { 205 | iAttrs.$observe('uiMaskPlaceholderChar', initPlaceholderChar); 206 | } 207 | 208 | controller.$formatters.unshift(formatter); 209 | controller.$parsers.unshift(parser); 210 | 211 | function uninitialize() { 212 | maskProcessed = false; 213 | unbindEventListeners(); 214 | 215 | if (angular.isDefined(originalPlaceholder)) { 216 | iElement.attr('placeholder', originalPlaceholder); 217 | } else { 218 | iElement.removeAttr('placeholder'); 219 | } 220 | 221 | if (angular.isDefined(originalMaxlength)) { 222 | iElement.attr('maxlength', originalMaxlength); 223 | } else { 224 | iElement.removeAttr('maxlength'); 225 | } 226 | 227 | iElement.val(controller.$modelValue); 228 | controller.$viewValue = controller.$modelValue; 229 | return false; 230 | } 231 | 232 | function initializeElement() { 233 | value = oldValueUnmasked = unmaskValue(controller.$modelValue || ''); 234 | valueMasked = oldValue = maskValue(value); 235 | isValid = validateValue(value); 236 | if (iAttrs.maxlength) { // Double maxlength to allow pasting new val at end of mask 237 | iElement.attr('maxlength', maskCaretMap[maskCaretMap.length - 1] * 2); 238 | } 239 | if ( ! originalPlaceholder && linkOptions.addDefaultPlaceholder) { 240 | iElement.attr('placeholder', maskPlaceholder); 241 | } 242 | var viewValue = controller.$modelValue; 243 | var idx = controller.$formatters.length; 244 | while(idx--) { 245 | viewValue = controller.$formatters[idx](viewValue); 246 | } 247 | controller.$viewValue = viewValue || ''; 248 | controller.$render(); 249 | // Not using $setViewValue so we don't clobber the model value and dirty the form 250 | // without any kind of user interaction. 251 | } 252 | 253 | function bindEventListeners() { 254 | if (eventsBound) { 255 | return; 256 | } 257 | iElement.bind('blur', blurHandler); 258 | iElement.bind('mousedown mouseup', mouseDownUpHandler); 259 | iElement.bind('keydown', keydownHandler); 260 | iElement.bind(linkOptions.eventsToHandle.join(' '), eventHandler); 261 | eventsBound = true; 262 | } 263 | 264 | function unbindEventListeners() { 265 | if (!eventsBound) { 266 | return; 267 | } 268 | iElement.unbind('blur', blurHandler); 269 | iElement.unbind('mousedown', mouseDownUpHandler); 270 | iElement.unbind('mouseup', mouseDownUpHandler); 271 | iElement.unbind('keydown', keydownHandler); 272 | iElement.unbind('input', eventHandler); 273 | iElement.unbind('keyup', eventHandler); 274 | iElement.unbind('click', eventHandler); 275 | iElement.unbind('focus', eventHandler); 276 | eventsBound = false; 277 | } 278 | 279 | function validateValue(value) { 280 | // Zero-length value validity is ngRequired's determination 281 | return value.length ? value.length >= minRequiredLength : true; 282 | } 283 | 284 | function unmaskValue(value) { 285 | var valueUnmasked = '', 286 | input = iElement[0], 287 | maskPatternsCopy = maskPatterns.slice(), 288 | selectionStart = oldCaretPosition, 289 | selectionEnd = selectionStart + getSelectionLength(input), 290 | valueOffset, valueDelta, tempValue = ''; 291 | // Preprocess by stripping mask components from value 292 | value = value.toString(); 293 | valueOffset = 0; 294 | valueDelta = value.length - maskPlaceholder.length; 295 | angular.forEach(maskComponents, function(component) { 296 | var position = component.position; 297 | //Only try and replace the component if the component position is not within the selected range 298 | //If component was in selected range then it was removed with the user input so no need to try and remove that component 299 | if (!(position >= selectionStart && position < selectionEnd)) { 300 | if (position >= selectionStart) { 301 | position += valueDelta; 302 | } 303 | if (value.substring(position, position + component.value.length) === component.value) { 304 | tempValue += value.slice(valueOffset, position);// + value.slice(position + component.value.length); 305 | valueOffset = position + component.value.length; 306 | } 307 | } 308 | }); 309 | value = tempValue + value.slice(valueOffset); 310 | angular.forEach(value.split(''), function(chr) { 311 | if (maskPatternsCopy.length && maskPatternsCopy[0].test(chr)) { 312 | valueUnmasked += chr; 313 | maskPatternsCopy.shift(); 314 | } 315 | }); 316 | 317 | return valueUnmasked; 318 | } 319 | 320 | function maskValue(unmaskedValue) { 321 | var valueMasked = '', 322 | maskCaretMapCopy = maskCaretMap.slice(); 323 | 324 | angular.forEach(maskPlaceholder.split(''), function(chr, i) { 325 | if (unmaskedValue.length && i === maskCaretMapCopy[0]) { 326 | valueMasked += unmaskedValue.charAt(0) || '_'; 327 | unmaskedValue = unmaskedValue.substr(1); 328 | maskCaretMapCopy.shift(); 329 | } 330 | else { 331 | valueMasked += chr; 332 | } 333 | }); 334 | return valueMasked; 335 | } 336 | 337 | function getPlaceholderChar(i) { 338 | var placeholder = angular.isDefined(iAttrs.uiMaskPlaceholder) ? iAttrs.uiMaskPlaceholder : iAttrs.placeholder, 339 | defaultPlaceholderChar; 340 | 341 | if (angular.isDefined(placeholder) && placeholder[i]) { 342 | return placeholder[i]; 343 | } else { 344 | defaultPlaceholderChar = angular.isDefined(iAttrs.uiMaskPlaceholderChar) && iAttrs.uiMaskPlaceholderChar ? iAttrs.uiMaskPlaceholderChar : '_'; 345 | return (defaultPlaceholderChar.toLowerCase() === 'space') ? ' ' : defaultPlaceholderChar[0]; 346 | } 347 | } 348 | 349 | // Generate array of mask components that will be stripped from a masked value 350 | // before processing to prevent mask components from being added to the unmasked value. 351 | // E.g., a mask pattern of '+7 9999' won't have the 7 bleed into the unmasked value. 352 | function getMaskComponents() { 353 | var maskPlaceholderChars = maskPlaceholder.split(''), 354 | maskPlaceholderCopy, components; 355 | 356 | //maskCaretMap can have bad values if the input has the ui-mask attribute implemented as an obversable property, e.g. the demo page 357 | if (maskCaretMap && !isNaN(maskCaretMap[0])) { 358 | //Instead of trying to manipulate the RegEx based on the placeholder characters 359 | //we can simply replace the placeholder characters based on the already built 360 | //maskCaretMap to underscores and leave the original working RegEx to get the proper 361 | //mask components 362 | angular.forEach(maskCaretMap, function(value) { 363 | maskPlaceholderChars[value] = '_'; 364 | }); 365 | } 366 | maskPlaceholderCopy = maskPlaceholderChars.join(''); 367 | components = maskPlaceholderCopy.replace(/[_]+/g, '_').split('_'); 368 | components = components.filter(function(s) { 369 | return s !== ''; 370 | }); 371 | 372 | // need a string search offset in cases where the mask contains multiple identical components 373 | // E.g., a mask of 99.99.99-999.99 374 | var offset = 0; 375 | return components.map(function(c) { 376 | var componentPosition = maskPlaceholderCopy.indexOf(c, offset); 377 | offset = componentPosition + 1; 378 | return { 379 | value: c, 380 | position: componentPosition 381 | }; 382 | }); 383 | } 384 | 385 | function processRawMask(mask) { 386 | var characterCount = 0; 387 | 388 | maskCaretMap = []; 389 | maskPatterns = []; 390 | maskPlaceholder = ''; 391 | 392 | if (angular.isString(mask)) { 393 | minRequiredLength = 0; 394 | 395 | var isOptional = false, 396 | numberOfOptionalCharacters = 0, 397 | splitMask = mask.split(''); 398 | 399 | var inEscape = false; 400 | angular.forEach(splitMask, function(chr, i) { 401 | if (inEscape) { 402 | inEscape = false; 403 | maskPlaceholder += chr; 404 | characterCount++; 405 | } 406 | else if (linkOptions.escChar === chr) { 407 | inEscape = true; 408 | } 409 | else if (linkOptions.maskDefinitions[chr]) { 410 | maskCaretMap.push(characterCount); 411 | 412 | maskPlaceholder += getPlaceholderChar(i - numberOfOptionalCharacters); 413 | maskPatterns.push(linkOptions.maskDefinitions[chr]); 414 | 415 | characterCount++; 416 | if (!isOptional) { 417 | minRequiredLength++; 418 | } 419 | 420 | isOptional = false; 421 | } 422 | else if (chr === '?') { 423 | isOptional = true; 424 | numberOfOptionalCharacters++; 425 | } 426 | else { 427 | maskPlaceholder += chr; 428 | characterCount++; 429 | } 430 | }); 431 | } 432 | // Caret position immediately following last position is valid. 433 | maskCaretMap.push(maskCaretMap.slice().pop() + 1); 434 | 435 | maskComponents = getMaskComponents(); 436 | maskProcessed = maskCaretMap.length > 1 ? true : false; 437 | } 438 | 439 | var prevValue = iElement.val(); 440 | function blurHandler() { 441 | if (linkOptions.clearOnBlur || ((linkOptions.clearOnBlurPlaceholder) && (value.length === 0) && iAttrs.placeholder)) { 442 | oldCaretPosition = 0; 443 | oldSelectionLength = 0; 444 | if (!isValid || value.length === 0) { 445 | valueMasked = ''; 446 | iElement.val(''); 447 | scope.$apply(function() { 448 | //only $setViewValue when not $pristine to avoid changing $pristine state. 449 | if (!controller.$pristine) { 450 | controller.$setViewValue(''); 451 | } 452 | }); 453 | } 454 | } 455 | //Check for different value and trigger change. 456 | //Check for different value and trigger change. 457 | if (value !== prevValue) { 458 | // #157 Fix the bug from the trigger when backspacing exactly on the first letter (emptying the field) 459 | // and then blurring out. 460 | // Angular uses html element and calls setViewValue(element.value.trim()), setting it to the trimmed mask 461 | // when it should be empty 462 | var currentVal = iElement.val(); 463 | var isTemporarilyEmpty = value === '' && currentVal && angular.isDefined(iAttrs.uiMaskPlaceholderChar) && iAttrs.uiMaskPlaceholderChar === 'space'; 464 | if(isTemporarilyEmpty) { 465 | iElement.val(''); 466 | } 467 | triggerChangeEvent(iElement[0]); 468 | if(isTemporarilyEmpty) { 469 | iElement.val(currentVal); 470 | } 471 | } 472 | prevValue = value; 473 | } 474 | 475 | function triggerChangeEvent(element) { 476 | var change; 477 | if (angular.isFunction(window.Event) && !element.fireEvent) { 478 | // modern browsers and Edge 479 | try { 480 | change = new Event('change', { 481 | view: window, 482 | bubbles: true, 483 | cancelable: false 484 | }); 485 | } catch (ex) { 486 | //this is for certain mobile browsers that have the Event object 487 | //but don't support the Event constructor #168 488 | change = document.createEvent('HTMLEvents'); 489 | change.initEvent('change', false, true); 490 | } finally { 491 | element.dispatchEvent(change); 492 | } 493 | } else if ('createEvent' in document) { 494 | // older browsers 495 | change = document.createEvent('HTMLEvents'); 496 | change.initEvent('change', false, true); 497 | element.dispatchEvent(change); 498 | } 499 | else if (element.fireEvent) { 500 | // IE <= 11 501 | element.fireEvent('onchange'); 502 | } 503 | } 504 | 505 | function mouseDownUpHandler(e) { 506 | if (e.type === 'mousedown') { 507 | iElement.bind('mouseout', mouseoutHandler); 508 | } else { 509 | iElement.unbind('mouseout', mouseoutHandler); 510 | } 511 | } 512 | 513 | iElement.bind('mousedown mouseup', mouseDownUpHandler); 514 | 515 | function mouseoutHandler() { 516 | /*jshint validthis: true */ 517 | oldSelectionLength = getSelectionLength(this); 518 | iElement.unbind('mouseout', mouseoutHandler); 519 | } 520 | 521 | function keydownHandler(e) { 522 | /*jshint validthis: true */ 523 | var isKeyBackspace = e.which === 8, 524 | caretPos = getCaretPosition(this) - 1 || 0, //value in keydown is pre change so bump caret position back to simulate post change 525 | isCtrlZ = e.which === 90 && e.ctrlKey; //ctrl+z pressed 526 | 527 | if (isKeyBackspace) { 528 | while(caretPos >= 0) { 529 | if (isValidCaretPosition(caretPos)) { 530 | //re-adjust the caret position. 531 | //Increment to account for the initial decrement to simulate post change caret position 532 | setCaretPosition(this, caretPos + 1); 533 | break; 534 | } 535 | caretPos--; 536 | } 537 | preventBackspace = caretPos === -1; 538 | } 539 | 540 | if (isCtrlZ) { 541 | // prevent IE bug - value should be returned to initial state 542 | iElement.val(''); 543 | e.preventDefault(); 544 | } 545 | } 546 | 547 | function eventHandler(e) { 548 | /*jshint validthis: true */ 549 | e = e || {}; 550 | // Allows more efficient minification 551 | var eventWhich = e.which, 552 | eventType = e.type; 553 | 554 | // Prevent shift and ctrl from mucking with old values 555 | if (eventWhich === 16 || eventWhich === 91) { 556 | return; 557 | } 558 | 559 | var val = iElement.val(), 560 | valOld = oldValue, 561 | valMasked, 562 | valAltered = false, 563 | valUnmasked = unmaskValue(val), 564 | valUnmaskedOld = oldValueUnmasked, 565 | caretPos = getCaretPosition(this) || 0, 566 | caretPosOld = oldCaretPosition || 0, 567 | caretPosDelta = caretPos - caretPosOld, 568 | caretPosMin = maskCaretMap[0], 569 | caretPosMax = maskCaretMap[valUnmasked.length] || maskCaretMap.slice().shift(), 570 | selectionLenOld = oldSelectionLength || 0, 571 | isSelected = getSelectionLength(this) > 0, 572 | wasSelected = selectionLenOld > 0, 573 | // Case: Typing a character to overwrite a selection 574 | isAddition = (val.length > valOld.length) || (selectionLenOld && val.length > valOld.length - selectionLenOld), 575 | // Case: Delete and backspace behave identically on a selection 576 | isDeletion = (val.length < valOld.length) || (selectionLenOld && val.length === valOld.length - selectionLenOld), 577 | isSelection = (eventWhich >= 37 && eventWhich <= 40) && e.shiftKey, // Arrow key codes 578 | 579 | isKeyLeftArrow = eventWhich === 37, 580 | // Necessary due to "input" event not providing a key code 581 | isKeyBackspace = eventWhich === 8 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === -1)), 582 | isKeyDelete = eventWhich === 46 || (eventType !== 'keyup' && isDeletion && (caretPosDelta === 0) && !wasSelected), 583 | // Handles cases where caret is moved and placed in front of invalid maskCaretMap position. Logic below 584 | // ensures that, on click or leftward caret placement, caret is moved leftward until directly right of 585 | // non-mask character. Also applied to click since users are (arguably) more likely to backspace 586 | // a character when clicking within a filled input. 587 | caretBumpBack = (isKeyLeftArrow || isKeyBackspace || eventType === 'click') && caretPos > caretPosMin; 588 | 589 | oldSelectionLength = getSelectionLength(this); 590 | 591 | // These events don't require any action 592 | if (isSelection || (isSelected && (eventType === 'click' || eventType === 'keyup' || eventType === 'focus'))) { 593 | return; 594 | } 595 | 596 | if (isKeyBackspace && preventBackspace) { 597 | iElement.val(maskPlaceholder); 598 | // This shouldn't be needed but for some reason after aggressive backspacing the controller $viewValue is incorrect. 599 | // This keeps the $viewValue updated and correct. 600 | scope.$apply(function () { 601 | controller.$setViewValue(''); // $setViewValue should be run in angular context, otherwise the changes will be invisible to angular and user code. 602 | }); 603 | setCaretPosition(this, caretPosOld); 604 | return; 605 | } 606 | 607 | // Value Handling 608 | // ============== 609 | 610 | // User attempted to delete but raw value was unaffected--correct this grievous offense 611 | if ((eventType === 'input') && isDeletion && !wasSelected && valUnmasked === valUnmaskedOld) { 612 | while (isKeyBackspace && caretPos > caretPosMin && !isValidCaretPosition(caretPos)) { 613 | caretPos--; 614 | } 615 | while (isKeyDelete && caretPos < caretPosMax && maskCaretMap.indexOf(caretPos) === -1) { 616 | caretPos++; 617 | } 618 | var charIndex = maskCaretMap.indexOf(caretPos); 619 | // Strip out non-mask character that user would have deleted if mask hadn't been in the way. 620 | valUnmasked = valUnmasked.substring(0, charIndex) + valUnmasked.substring(charIndex + 1); 621 | 622 | // If value has not changed, don't want to call $setViewValue, may be caused by IE raising input event due to placeholder 623 | if (valUnmasked !== valUnmaskedOld) 624 | valAltered = true; 625 | } 626 | 627 | // Update values 628 | valMasked = maskValue(valUnmasked); 629 | 630 | oldValue = valMasked; 631 | oldValueUnmasked = valUnmasked; 632 | 633 | //additional check to fix the problem where the viewValue is out of sync with the value of the element. 634 | //better fix for commit 2a83b5fb8312e71d220a497545f999fc82503bd9 (I think) 635 | if (!valAltered && val.length > valMasked.length) 636 | valAltered = true; 637 | 638 | iElement.val(valMasked); 639 | 640 | //we need this check. What could happen if you don't have it is that you'll set the model value without the user 641 | //actually doing anything. Meaning, things like pristine and touched will be set. 642 | if (valAltered) { 643 | scope.$apply(function () { 644 | controller.$setViewValue(valMasked); // $setViewValue should be run in angular context, otherwise the changes will be invisible to angular and user code. 645 | }); 646 | } 647 | 648 | // Caret Repositioning 649 | // =================== 650 | 651 | // Ensure that typing always places caret ahead of typed character in cases where the first char of 652 | // the input is a mask char and the caret is placed at the 0 position. 653 | if (isAddition && (caretPos <= caretPosMin)) { 654 | caretPos = caretPosMin + 1; 655 | } 656 | 657 | if (caretBumpBack) { 658 | caretPos--; 659 | } 660 | 661 | // Make sure caret is within min and max position limits 662 | caretPos = caretPos > caretPosMax ? caretPosMax : caretPos < caretPosMin ? caretPosMin : caretPos; 663 | 664 | // Scoot the caret back or forth until it's in a non-mask position and within min/max position limits 665 | while (!isValidCaretPosition(caretPos) && caretPos > caretPosMin && caretPos < caretPosMax) { 666 | caretPos += caretBumpBack ? -1 : 1; 667 | } 668 | 669 | if ((caretBumpBack && caretPos < caretPosMax) || (isAddition && !isValidCaretPosition(caretPosOld))) { 670 | caretPos++; 671 | } 672 | oldCaretPosition = caretPos; 673 | setCaretPosition(this, caretPos); 674 | } 675 | 676 | function isValidCaretPosition(pos) { 677 | return maskCaretMap.indexOf(pos) > -1; 678 | } 679 | 680 | function getCaretPosition(input) { 681 | if (!input) 682 | return 0; 683 | if (input.selectionStart !== undefined) { 684 | return input.selectionStart; 685 | } else if (document.selection) { 686 | if (isFocused(iElement[0])) { 687 | // Curse you IE 688 | input.focus(); 689 | var selection = document.selection.createRange(); 690 | selection.moveStart('character', input.value ? -input.value.length : 0); 691 | return selection.text.length; 692 | } 693 | } 694 | return 0; 695 | } 696 | 697 | function setCaretPosition(input, pos) { 698 | if (!input) 699 | return 0; 700 | if (input.offsetWidth === 0 || input.offsetHeight === 0) { 701 | return; // Input's hidden 702 | } 703 | if (input.setSelectionRange) { 704 | if (isFocused(iElement[0])) { 705 | input.focus(); 706 | input.setSelectionRange(pos, pos); 707 | } 708 | } 709 | else if (input.createTextRange) { 710 | // Curse you IE 711 | var range = input.createTextRange(); 712 | range.collapse(true); 713 | range.moveEnd('character', pos); 714 | range.moveStart('character', pos); 715 | range.select(); 716 | } 717 | } 718 | 719 | function getSelectionLength(input) { 720 | if (!input) 721 | return 0; 722 | if (input.selectionStart !== undefined) { 723 | return (input.selectionEnd - input.selectionStart); 724 | } 725 | if (window.getSelection) { 726 | return (window.getSelection().toString().length); 727 | } 728 | if (document.selection) { 729 | return (document.selection.createRange().text.length); 730 | } 731 | return 0; 732 | } 733 | 734 | // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf 735 | if (!Array.prototype.indexOf) { 736 | Array.prototype.indexOf = function(searchElement /*, fromIndex */) { 737 | if (this === null) { 738 | throw new TypeError(); 739 | } 740 | var t = Object(this); 741 | var len = t.length >>> 0; 742 | if (len === 0) { 743 | return -1; 744 | } 745 | var n = 0; 746 | if (arguments.length > 1) { 747 | n = Number(arguments[1]); 748 | if (n !== n) { // shortcut for verifying if it's NaN 749 | n = 0; 750 | } else if (n !== 0 && n !== Infinity && n !== -Infinity) { 751 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 752 | } 753 | } 754 | if (n >= len) { 755 | return -1; 756 | } 757 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 758 | for (; k < len; k++) { 759 | if (k in t && t[k] === searchElement) { 760 | return k; 761 | } 762 | } 763 | return -1; 764 | }; 765 | } 766 | 767 | }; 768 | } 769 | }; 770 | } 771 | ]); 772 | -------------------------------------------------------------------------------- /test/maskSpec.js: -------------------------------------------------------------------------------- 1 | describe("uiMask", function () { 2 | "use strict"; 3 | 4 | var formHtml = "
"; 5 | var inputHtml = ""; 6 | var compileElement, scope, config, timeout, uiMaskConfigProvider; 7 | 8 | beforeEach(module("ui.mask")); 9 | beforeEach(function() { 10 | angular.module("test",[]).directive("toUpper", function() { 11 | return { 12 | priority: 200, 13 | require: 'ngModel', 14 | restrict: 'A', 15 | link: function (scope, iElement, iAttrs, controller) { 16 | controller.$formatters.push(function(fromModelValue) { 17 | return angular.uppercase(fromModelValue); 18 | }); 19 | controller.$parsers.push(function(fromViewValue) { 20 | return angular.lowercase(fromViewValue); 21 | }); 22 | } 23 | } 24 | }) 25 | .config(['uiMask.ConfigProvider', function(configProvider) { 26 | uiMaskConfigProvider = configProvider; 27 | }]); 28 | module("test"); 29 | }); 30 | beforeEach(inject(function ($rootScope, $compile, uiMaskConfig, $timeout) { 31 | scope = $rootScope; 32 | config = uiMaskConfig; 33 | compileElement = function(html) { 34 | return $compile(html)(scope); 35 | }; 36 | timeout = $timeout; 37 | })); 38 | 39 | describe("initialization", function () { 40 | 41 | it("should not not happen if the mask is undefined or invalid", function() { 42 | var input = compileElement(inputHtml); 43 | scope.$apply("x = 'abc123'"); 44 | expect(input.val()).toBe("abc123"); 45 | scope.$apply("mask = '()_abc123'"); 46 | expect(input.val()).toBe("abc123"); 47 | }); 48 | 49 | it("should mask the value only if it's valid", function() { 50 | var input = compileElement(inputHtml); 51 | scope.$apply("x = 'abc123'"); 52 | scope.$apply("mask = '(A) * 9'"); 53 | expect(input.val()).toBe("(a) b 1"); 54 | scope.$apply("mask = '(A) * 9 A'"); 55 | expect(input.val()).toBe(""); 56 | }); 57 | 58 | it("should not dirty or invalidate the input", function() { 59 | var input = compileElement(inputHtml); 60 | scope.$apply("x = 'abc123'"); 61 | scope.$apply("mask = '(9) * A'"); 62 | expect(input.hasClass("ng-pristine")).toBeTruthy(); 63 | scope.$apply("mask = '(9) * A 9'"); 64 | expect(input.hasClass("ng-pristine")).toBeTruthy(); 65 | }); 66 | 67 | it("should not change the model value", function() { 68 | scope.$apply("x = 'abc123'"); 69 | scope.$apply("mask = '(A) * 9'"); 70 | expect(scope.x).toBe("abc123"); 71 | scope.$apply("mask = '(A) * 9 A'"); 72 | expect(scope.x).toBe("abc123"); 73 | }); 74 | 75 | it("should not dirty or invalidate the input", function() { 76 | var input = compileElement(inputHtml); 77 | scope.$apply("x = 'abc123'"); 78 | scope.$apply("mask = '(9) * A'"); 79 | 80 | //Test blur 81 | input.triggerHandler("blur"); 82 | expect(input.hasClass("ng-pristine")).toBeTruthy(); 83 | }); 84 | 85 | it("should set ngModelController.$viewValue to match input value", function() { 86 | compileElement(formHtml); 87 | scope.$apply("x = 'abc123'"); 88 | scope.$apply("mask = '(A) * 9'"); 89 | expect(scope.test.input.$viewValue).toBe("(a) b 1"); 90 | scope.$apply("mask = '(A) * 9 A'"); 91 | expect(scope.test.input.$viewValue).toBe(""); 92 | }); 93 | 94 | }); 95 | describe("with other directives", function() { 96 | beforeEach(function () { 97 | compileElement("
"); 98 | }); 99 | it("should play nicely", function() { 100 | scope.$apply("x = 'abc123'"); 101 | scope.$apply("mask = '(A) * 9'"); 102 | expect(scope.x).toBe("abc123"); 103 | expect(scope.test.input.$viewValue).toBe("(A) B 1"); 104 | scope.$apply("mask = '(A)AA'"); 105 | expect(scope.test.input.$viewValue).toBe("(A)BC"); 106 | }); 107 | describe("with model-view-value", function() { 108 | var input = undefined; 109 | beforeEach(function () { 110 | input = compileElement("
"); 111 | input = input.find('input') 112 | }); 113 | it("should set the model value to the masked view value parsed by other directive", function() { 114 | scope.$apply("x = '(a) b 1'"); 115 | scope.$apply("mask = '(A) * 9'"); 116 | expect(scope.test.input.$viewValue).toBe("(A) B 1"); 117 | input.val("(A) C 2").triggerHandler("input").triggerHandler("change"); 118 | scope.$apply(); 119 | expect(scope.x).toBe("(a) c 2"); 120 | }); 121 | }); 122 | }); 123 | 124 | describe("user input", function () { 125 | it("should mask-as-you-type", function() { 126 | var form = compileElement(formHtml); 127 | var input = form.find("input"); 128 | scope.$apply("x = ''"); 129 | scope.$apply("mask = '(A) * 9'"); 130 | input.val("a").triggerHandler("input"); 131 | expect(input.val()).toBe("(a) _ _"); 132 | input.val("ab").triggerHandler("input"); 133 | expect(input.val()).toBe("(a) b _"); 134 | input.val("ab1").triggerHandler("input"); 135 | expect(input.val()).toBe("(a) b 1"); 136 | }); 137 | 138 | it("should set ngModelController.$viewValue to match input value", function() { 139 | var form = compileElement(formHtml); 140 | var input = form.find("input"); 141 | scope.$apply("x = ''"); 142 | scope.$apply("mask = '(A) * 9'"); 143 | input.val("a").triggerHandler("input"); 144 | input.triggerHandler("change"); // Because IE8 and below are terrible 145 | expect(scope.test.input.$viewValue).toBe("(a) _ _"); 146 | }); 147 | 148 | it("should maintain $viewValue consistent with input value", function() { 149 | var form = compileElement(formHtml); 150 | var input = form.find("input"); 151 | scope.$apply("x = ''"); 152 | scope.$apply("mask = '99 9'"); 153 | input.val("3333").triggerHandler("input"); 154 | input.val("3333").triggerHandler("input"); // It used to has a bug when pressing a key repeatedly 155 | timeout(function() { 156 | expect(scope.test.input.$viewValue).toBe("33 3"); 157 | }, 0, false); 158 | }); 159 | 160 | it("should parse unmasked value to model", function() { 161 | var form = compileElement(formHtml); 162 | var input = form.find("input"); 163 | scope.$apply("x = ''"); 164 | scope.$apply("mask = '(A) * 9'"); 165 | input.val("abc123").triggerHandler("input"); 166 | input.triggerHandler("change"); // Because IE8 and below are terrible 167 | expect(scope.x).toBe("ab1"); 168 | }); 169 | 170 | it("should set model to undefined if masked value is invalid", function() { 171 | var form = compileElement(formHtml); 172 | var input = form.find("input"); 173 | scope.$apply("x = ''"); 174 | scope.$apply("mask = '(A) * 9'"); 175 | input.val("a").triggerHandler("input"); 176 | input.triggerHandler("change"); // Because IE8 and below are terrible 177 | expect(scope.x).toBeUndefined(); 178 | }); 179 | 180 | it("should not set model to an empty mask", function() { 181 | var form = compileElement(formHtml); 182 | var input = form.find("input"); 183 | scope.$apply("x = ''"); 184 | scope.$apply("mask = '(A) * 9'"); 185 | input.triggerHandler("input"); 186 | expect(scope.x).toBe(""); 187 | }); 188 | 189 | it("should not setValidity on required to false on a control that isn't required", function() { 190 | var input = compileElement(""); 191 | scope.$apply("x = ''"); 192 | scope.$apply("mask = '(A) * 9'"); 193 | scope.$apply("required = true"); 194 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 195 | input.triggerHandler("input"); 196 | expect(scope.x).toBe(""); 197 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 198 | 199 | input = compileElement(""); 200 | scope.$apply("required = false"); 201 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 202 | input.triggerHandler("input"); 203 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 204 | input.triggerHandler("focus"); 205 | input.triggerHandler("blur"); 206 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 207 | input.val("").triggerHandler("input"); 208 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 209 | }); 210 | 211 | it("should setValidity on required to true when control is required and value is empty", function() { 212 | var input = compileElement(""); 213 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 214 | scope.$apply("x = ''"); 215 | scope.$apply("mask = '(A) * 9'"); 216 | scope.$apply("required = true"); 217 | input.triggerHandler("input"); 218 | expect(input.data("$ngModelController").$error.required).toBe(true); 219 | 220 | input = compileElement(""); 221 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 222 | scope.$apply("mask = '(A) A 9'");//change the mask so the $digest cycle runs the initialization code 223 | input.triggerHandler("input"); 224 | expect(input.data("$ngModelController").$error.required).toBe(true); 225 | }); 226 | 227 | it("should not setValidity on required when control is required and value is non empty", function() { 228 | var input = compileElement(""); 229 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 230 | scope.$apply("x = ''"); 231 | scope.$apply("mask = '(A) * 9'"); 232 | scope.$apply("required = true"); 233 | input.triggerHandler("input"); 234 | expect(input.data("$ngModelController").$error.required).toBe(true); 235 | input.val("(abc123_) _ _").triggerHandler("input"); 236 | expect(scope.x).toBe("ab1"); 237 | expect(input.data("$ngModelController").$error.required).toBeUndefined(); 238 | }); 239 | 240 | it("should set the model value properly when control is required and the mask is undefined", function() { 241 | var input = compileElement(''); 242 | scope.$apply("x = ''"); 243 | expect(scope.mask).toBeUndefined(); 244 | input.val("12345").triggerHandler("change"); 245 | expect(scope.x).toBe("12345"); 246 | }); 247 | 248 | it("should not bleed static mask characters into the value when backspacing", function() { 249 | var input = compileElement(inputHtml); 250 | scope.$apply("x = ''"); 251 | scope.$apply("mask = 'QT****'"); 252 | input.triggerHandler('focus'); 253 | expect(input.val()).toBe("QT____"); 254 | //simulate a backspace event 255 | input.triggerHandler({ type: 'keydown', which: 8 }); 256 | input.triggerHandler({ type: 'keyup', which: 8 }); 257 | expect(input.val()).toBe("QT____"); 258 | expect(scope.x).toBe(''); 259 | }); 260 | 261 | it("should set model value properly when the value contains the same character as a static mask character", function() { 262 | var input = compileElement(inputHtml); 263 | scope.$apply("mask = '19'"); 264 | input.triggerHandler("input"); 265 | expect(input.val()).toBe("1_"); 266 | input.val("11").triggerHandler("change"); 267 | expect(scope.x).toBe("1"); 268 | 269 | scope.$apply("mask = '9991999'"); 270 | scope.$apply("x = ''"); 271 | input.triggerHandler("input"); 272 | expect(input.val()).toBe("___1___"); 273 | input.val("1231456").triggerHandler("change"); 274 | expect(scope.x).toBe("123456"); 275 | }); 276 | 277 | it("should mask the input properly with multiple identical mask components", function() { 278 | var input = compileElement(inputHtml); 279 | scope.$apply("mask = '99.99.99-999.99'"); 280 | input.val("811").triggerHandler("input"); 281 | expect(input.val()).toBe("81.1_.__-___.__"); 282 | }); 283 | 284 | it("should set the model value properly even if it's not full", function() { 285 | var input1 = compileElement(''); 286 | var input2 = compileElement(''); 287 | scope.$apply("mask = '9999'"); 288 | 289 | input1.val('11').triggerHandler("change"); 290 | expect(scope.x).toBe("11"); 291 | 292 | input2.val('22').triggerHandler("change"); 293 | expect(scope.x).toBe("22"); 294 | 295 | scope.$apply("x = '33'"); 296 | expect(input1.val()).toBe("33__"); 297 | expect(input2.val()).toBe("33__"); 298 | }); 299 | }); 300 | 301 | describe("verify change is called", function () { 302 | var input = undefined; 303 | var doneCount = 0; 304 | 305 | beforeEach(function (done) { 306 | input = compileElement(inputHtml); 307 | scope.$apply("x = ''"); 308 | scope.$apply("mask = '**?9'"); 309 | input.on("change", function () { 310 | doneCount++; 311 | done(); 312 | }); 313 | input.val("aa").triggerHandler("change"); 314 | input.triggerHandler("blur"); 315 | input.val("aa").triggerHandler("change"); 316 | input.triggerHandler("blur"); 317 | }); 318 | 319 | it("should have triggered change", function () { 320 | expect(doneCount).toBe(1); 321 | }); 322 | }); 323 | 324 | describe("with model-view-value", function() { 325 | var input = undefined; 326 | beforeEach(function () { 327 | input = compileElement("
"); 328 | input = input.find('input'); 329 | }); 330 | it("should set the mask in the model", function() { 331 | scope.$apply("x = '(a) b 1'"); 332 | scope.$apply("mask = '(A) * 9'"); 333 | expect(scope.test.input.$viewValue).toBe("(a) b 1"); 334 | input.val("(a) c 2").triggerHandler("input").triggerHandler("change"); 335 | scope.$apply(); 336 | expect(scope.x).toBe("(a) c 2"); 337 | }); 338 | }); 339 | describe("changes from the model", function () { 340 | it("should set the correct ngModelController.$viewValue", function() { 341 | compileElement(formHtml); 342 | scope.$apply("mask = '(A) * 9'"); 343 | scope.$apply("x = ''"); 344 | expect(scope.test.input.$viewValue).not.toBeDefined(); 345 | scope.$apply("x = 'abc'"); 346 | expect(scope.test.input.$viewValue).not.toBeDefined(); 347 | scope.$apply("x = 'abc123'"); 348 | expect(scope.test.input.$viewValue).toBe("(a) b 1"); 349 | }); 350 | 351 | it("should accept change model and mask on same $digest", function() { 352 | compileElement(formHtml); 353 | scope.$apply(" x='1234'; mask = '99-99';"); 354 | scope.$apply(" x='123'; mask = '99-9';"); 355 | expect(scope.test.input.$viewValue).toBe('12-3'); 356 | expect(scope.x).toBe('123'); 357 | }); 358 | 359 | it("should set validity when setting model and mask on same $digest", function() { 360 | compileElement(formHtml); 361 | scope.$apply(" x='1234'; mask = '99-99';"); 362 | scope.$apply(" x='123'; mask = '99-9';"); 363 | expect(scope.test.input.$valid).toBe(true); 364 | }); 365 | }); 366 | 367 | describe("default mask definitions", function () { 368 | it("should accept optional mask after '?'", function (){ 369 | var input = compileElement(inputHtml); 370 | 371 | scope.$apply("x = ''"); 372 | scope.$apply("mask = '**?9'"); 373 | 374 | input.val("aa___").triggerHandler("input"); 375 | input.triggerHandler("blur"); 376 | expect(input.val()).toBe("aa_"); 377 | 378 | input.val("99a___").triggerHandler("input"); 379 | input.triggerHandler("blur"); 380 | expect(input.val()).toBe("99_"); 381 | 382 | input.val("992___").triggerHandler("input"); 383 | input.triggerHandler("blur"); 384 | expect(input.val()).toBe("992"); 385 | }); 386 | 387 | it("should limit optional mask to a single character", function() { 388 | var form = compileElement(formHtml); 389 | var input = form.find("input"); 390 | scope.$apply("x = ''"); 391 | scope.$apply("mask = '9?99'"); 392 | input.val("1").triggerHandler("input"); 393 | input.triggerHandler("change"); // Because IE8 and below are terrible 394 | expect(scope.x).toBeUndefined(); 395 | }); 396 | }); 397 | 398 | describe("escChar", function () { 399 | it("should escape default mask definitions", function() { 400 | var escapeHtml = "", 401 | input = compileElement(escapeHtml); 402 | scope.$apply(function() { 403 | scope.x = ''; 404 | scope.mask = '\\A\\9\\*\\?*'; 405 | }); 406 | expect(input.attr("placeholder")).toBe("A9*?_"); 407 | input.val("a").triggerHandler("input"); 408 | expect(input.val()).toBe("A9*?a"); 409 | }); 410 | it("should not confuse entered values with escaped values", function() { 411 | var escapeHtml = "", 412 | input = compileElement(escapeHtml); 413 | scope.$apply(function() { 414 | scope.x = ''; 415 | scope.mask = '\\A\\9\\*\\?****'; 416 | }); 417 | expect(input.attr("placeholder")).toBe("A9*?____"); 418 | input.val("A9A9").triggerHandler("input"); 419 | expect(input.val()).toBe("A9*?A9A9"); 420 | }); 421 | it("should escape custom mask definitions", function() { 422 | scope.options = { 423 | maskDefinitions: { 424 | "Q": /[Qq]/ 425 | } 426 | }; 427 | var input = compileElement(inputHtml); 428 | scope.$apply(function() { 429 | scope.x = ''; 430 | scope.mask = '\\QQ'; 431 | }); 432 | expect(input.attr("placeholder")).toBe("Q_"); 433 | input.val("q").triggerHandler("input"); 434 | expect(input.val()).toBe("Qq"); 435 | }); 436 | it("should escape normal characters", function() { 437 | var input = compileElement(inputHtml); 438 | scope.$apply(function() { 439 | scope.x = ''; 440 | scope.mask = '\\W*'; 441 | }); 442 | expect(input.attr("placeholder")).toBe("W_"); 443 | input.val("q").triggerHandler("input"); 444 | expect(input.val()).toBe("Wq"); 445 | }); 446 | it("should escape itself", function() { 447 | var escapeHtml = "", 448 | input = compileElement(escapeHtml); 449 | scope.$apply(function() { 450 | scope.x = ''; 451 | scope.mask = '\\\\*'; 452 | }); 453 | scope.$apply("x = ''"); 454 | scope.$apply("mask = '\\\\\\\\*'"); 455 | expect(input.attr("placeholder")).toBe("\\_"); 456 | input.val("a").triggerHandler("input"); 457 | expect(input.val()).toBe("\\a"); 458 | }); 459 | it("should change the escape character", function() { 460 | scope.options = { 461 | escChar: '!', 462 | maskDefinitions: { 463 | "Q": /[Qq]/ 464 | } 465 | }; 466 | var input = compileElement(inputHtml); 467 | scope.$apply(function() { 468 | scope.x = ''; 469 | scope.mask = '\\!A!9!*!Q!!!W*'; 470 | }); 471 | expect(input.attr("placeholder")).toBe("\\A9*Q!W_"); 472 | input.val("a").triggerHandler("input"); 473 | expect(input.val()).toBe("\\A9*Q!Wa"); 474 | }); 475 | it("should use null to mean no escape character", function() { 476 | scope.options = { 477 | escChar: null, 478 | }; 479 | var input = compileElement(inputHtml); 480 | scope.$apply(function() { 481 | scope.x = ''; 482 | scope.mask = '\\!A!9!*!!*'; 483 | }); 484 | expect(input.attr("placeholder")).toBe("\\!_!_!_!!_"); 485 | input.val("a").triggerHandler("input"); 486 | expect(input.val()).toBe("\\!a!_!_!!_"); 487 | }); 488 | }); 489 | 490 | describe("placeholders", function () { 491 | it("should have default placeholder functionality", function() { 492 | var input = compileElement(inputHtml); 493 | 494 | scope.$apply("x = ''"); 495 | scope.$apply("mask = '99/99/9999'"); 496 | 497 | expect(input.attr("placeholder")).toBe("__/__/____"); 498 | }); 499 | 500 | it("should allow mask substitutions via the placeholder attribute", function() { 501 | 502 | var placeholderHtml = "", 503 | input = compileElement(placeholderHtml); 504 | 505 | scope.$apply("x = ''"); 506 | scope.$apply("mask = '99/99/9999'"); 507 | 508 | expect(input.attr("placeholder")).toBe("MM/DD/YYYY"); 509 | 510 | input.val("12").triggerHandler("input"); 511 | 512 | expect(input.val()).toBe("12/DD/YYYY"); 513 | }); 514 | 515 | it("should update mask substitutions via the placeholder attribute", function() { 516 | 517 | var placeholderHtml = "", 518 | input = compileElement(placeholderHtml); 519 | 520 | scope.$apply("x = ''"); 521 | scope.$apply("mask = '99/99/9999'"); 522 | scope.$apply("placeholder = 'DD/MM/YYYY'"); 523 | expect(input.attr("placeholder")).toBe("DD/MM/YYYY"); 524 | 525 | input.val("12").triggerHandler("input"); 526 | expect(input.val()).toBe("12/MM/YYYY"); 527 | 528 | scope.$apply("placeholder = 'MM/DD/YYYY'"); 529 | expect(input.val()).toBe("12/DD/YYYY"); 530 | 531 | input.triggerHandler("blur"); 532 | expect(input.attr("placeholder")).toBe("MM/DD/YYYY"); 533 | }); 534 | 535 | it("should ignore the '?' character", function() { 536 | var placeholderHtml = "", 537 | input = compileElement(placeholderHtml); 538 | 539 | scope.$apply("myDate = ''"); 540 | expect(input.attr("placeholder")).toBe("DD/MM/YYYY HH:mm"); 541 | }); 542 | 543 | it("should accept ui-mask-placeholder", function() { 544 | var placeholderHtml = "", 545 | input = compileElement(placeholderHtml); 546 | 547 | scope.$apply("x = ''"); 548 | scope.$apply("mask = '(999) 999-9999'"); 549 | input.triggerHandler("input"); 550 | expect(input.val()).toBe("(XXX) XXX-XXXX"); 551 | expect(input.attr("placeholder")).toBe("Phone Number"); 552 | }); 553 | 554 | it("should accept ui-mask-placeholder and not set val when first showing input", function() { 555 | var placeholderHtml = "", 556 | input = compileElement(placeholderHtml); 557 | 558 | scope.$apply("x = ''"); 559 | scope.$apply("mask = '(999) 999-9999'"); 560 | expect(input.val()).toBe(""); 561 | expect(input.attr("placeholder")).toBe("Phone Number"); 562 | }); 563 | 564 | it("should interpret empty ui-mask-placeholder", function() { 565 | var placeholderHtml = "", 566 | input = compileElement(placeholderHtml); 567 | 568 | scope.$apply("x = ''"); 569 | scope.$apply("mask = '(999) 999-9999'"); 570 | input.triggerHandler("input"); 571 | expect(input.val()).toBe("(___) ___-____"); 572 | expect(input.attr("placeholder")).toBe("Phone Number"); 573 | }); 574 | 575 | it("should accept ui-mask-placeholder-char", function() { 576 | var placeholderHtml = "", 577 | input = compileElement(placeholderHtml); 578 | 579 | scope.$apply("x = ''"); 580 | scope.$apply("mask = '(999) 999-9999'"); 581 | input.triggerHandler("input"); 582 | expect(input.val()).toBe("(XXX) XXX-XXXX"); 583 | expect(input.attr("placeholder")).toBe("Phone Number"); 584 | }); 585 | 586 | it("should accept ui-mask-placeholder-char with value `space`", function() { 587 | var placeholderHtml = "", 588 | input = compileElement(placeholderHtml); 589 | 590 | scope.$apply("x = ''"); 591 | scope.$apply("mask = '(999) 999-9999'"); 592 | input.triggerHandler("input"); 593 | expect(input.val()).toBe("( ) - "); 594 | expect(input.attr("placeholder")).toBe("Phone Number"); 595 | }); 596 | 597 | it("should not override placeholder value when ui-mask-placeholder is not set and ui-mask-placeholder-char is `space`", function() { 598 | var placeholderHtml = "", 599 | input = compileElement(placeholderHtml); 600 | 601 | scope.$apply("x = ''"); 602 | scope.$apply("mask = '99/99/9999'"); 603 | scope.$apply("placeholder = 'DD/MM/YYYY'"); 604 | expect(input.attr("placeholder")).toBe("DD/MM/YYYY"); 605 | 606 | input.val("12").triggerHandler("input"); 607 | expect(input.val()).toBe("12/MM/YYYY"); 608 | 609 | scope.$apply("placeholder = 'MM/DD/YYYY'"); 610 | expect(input.val()).toBe("12/DD/YYYY"); 611 | 612 | input.triggerHandler("blur"); 613 | expect(input.attr("placeholder")).toBe("MM/DD/YYYY"); 614 | }); 615 | 616 | it("should allow text input to be the same character as ui-mask-placeholder-char", function() { 617 | var placeholderHtml = "", 618 | input = compileElement(placeholderHtml); 619 | 620 | scope.$apply(); 621 | input.val("6505265486").triggerHandler("input"); 622 | expect(input.val()).toBe("(650) 526-5486"); 623 | }); 624 | 625 | it("should allow text input to be the same character as characters in ui-mask-placeholder", function() { 626 | var placeholderHtml = "", 627 | input = compileElement(placeholderHtml); 628 | 629 | scope.$apply(); 630 | input.val("6505265486").triggerHandler("input"); 631 | expect(input.val()).toBe("(650) 526-5486"); 632 | }); 633 | }); 634 | 635 | describe("configuration", function () { 636 | it("should accept the new mask definition set globally", function() { 637 | config.maskDefinitions["@"] = /[fz]/; 638 | 639 | var input = compileElement(inputHtml); 640 | 641 | scope.$apply("x = ''"); 642 | scope.$apply("mask = '@193'"); 643 | input.val("f123____").triggerHandler("input"); 644 | input.triggerHandler("blur"); 645 | expect(input.val()).toBe("f123"); 646 | }); 647 | 648 | it("should merge the mask definition set globally with the definition set per element", function() { 649 | scope.options = { 650 | maskDefinitions: { 651 | "A": /[A-Z]/, //make A caps only 652 | "b": /[a-z]/ //make b lowercase only 653 | } 654 | }; 655 | 656 | var input = compileElement(inputHtml); 657 | 658 | scope.$apply("x = ''"); 659 | scope.$apply("mask = '@193Ab'"); 660 | input.val("f123cCCc").triggerHandler("input"); 661 | input.triggerHandler("blur"); 662 | expect(input.val()).toBe("f123Cc"); 663 | }); 664 | 665 | it("should accept the new events to handle per element", function() { 666 | scope.options = { 667 | eventsToHandle: ['keyup'] 668 | }; 669 | 670 | var input = compileElement(inputHtml); 671 | 672 | scope.$apply("x = ''"); 673 | scope.$apply("mask = '@99-9'"); 674 | input.val("f111").triggerHandler("input focus click"); 675 | expect(input.val()).toBe("f111"); 676 | input.triggerHandler('keyup'); 677 | expect(input.val()).toBe("f11-1"); 678 | }); 679 | 680 | it("should accept the new mask definition set per element", function() { 681 | delete config.maskDefinitions["@"]; 682 | 683 | scope.options = { 684 | maskDefinitions: {"@": /[fz]/} 685 | }; 686 | 687 | var input = compileElement(inputHtml); 688 | 689 | scope.$apply("x = ''"); 690 | scope.$apply("mask = '@999'"); 691 | input.val("f111____").triggerHandler("input"); 692 | input.triggerHandler("blur"); 693 | expect(input.val()).toBe("f111"); 694 | }); 695 | 696 | it("should accept new addDefaultPlaceholder value set per element", function() { 697 | scope.options = { 698 | addDefaultPlaceholder: false 699 | }; 700 | 701 | var input = compileElement(inputHtml); 702 | scope.$apply("x = ''"); 703 | scope.$apply("mask = '@999'"); 704 | expect(input.attr('placeholder')).toBe(undefined); 705 | }); 706 | }); 707 | 708 | describe("blurring", function () { 709 | it("should clear an invalid value from the input", function() { 710 | var input = compileElement(inputHtml); 711 | scope.$apply("x = ''"); 712 | scope.$apply("mask = '(9) * A'"); 713 | input.val("a").triggerHandler("input"); 714 | input.triggerHandler("blur"); 715 | expect(input.val()).toBe(""); 716 | }); 717 | 718 | it("should clear an invalid value from the ngModelController.$viewValue", function() { 719 | var form = compileElement(formHtml); 720 | var input = form.find("input"); 721 | scope.$apply("x = ''"); 722 | scope.$apply("mask = '(A) * 9'"); 723 | input.val("a").triggerHandler("input"); 724 | input.triggerHandler("blur"); 725 | expect(scope.test.input.$viewValue).toBe(""); 726 | }); 727 | 728 | var inputHtmlClearOnBlur = ""; 729 | 730 | it("should not clear an invalid value if clearOnBlur is false", function() { 731 | scope.input = { 732 | options: {clearOnBlur: false} 733 | }; 734 | 735 | var input = compileElement(inputHtmlClearOnBlur); 736 | 737 | scope.$apply("x = ''"); 738 | scope.$apply("mask = '(9) * A'"); 739 | 740 | input.val("9a").triggerHandler("input"); 741 | input.triggerHandler("blur"); 742 | expect(input.val()).toBe("(9) a _"); 743 | }); 744 | 745 | it("should clear an invalid value if clearOnBlur is true", function() { 746 | scope.input = { 747 | options: {clearOnBlur: true} 748 | }; 749 | 750 | var input = compileElement(inputHtmlClearOnBlur); 751 | 752 | scope.$apply("x = ''"); 753 | scope.$apply("mask = '(9) * A'"); 754 | 755 | input.val("9a").triggerHandler("input"); 756 | input.triggerHandler("blur"); 757 | expect(input.val()).toBe(""); 758 | }); 759 | 760 | var inputHtmlClearOnBlurPlaceholder = ""; 761 | 762 | it("should not show placeholder when value is invalid if clearOnBlurPlaceholder is false", function() { 763 | scope.input = { 764 | options: { 765 | clearOnBlur: false, 766 | clearOnBlurPlaceholder: false 767 | } 768 | }; 769 | 770 | var input = compileElement(inputHtmlClearOnBlurPlaceholder); 771 | 772 | scope.$apply("x = ''"); 773 | scope.$apply("mask = '(9) * A'"); 774 | 775 | input.val("").triggerHandler("input"); 776 | input.triggerHandler("blur"); 777 | expect(input.val()).toBe("(_) _ _"); 778 | }); 779 | 780 | it("should show placeholder when value is invalid if clearOnBlurPlaceholder is true", function() { 781 | scope.input = { 782 | options: { 783 | clearOnBlur: false, 784 | clearOnBlurPlaceholder: true 785 | } 786 | }; 787 | 788 | var input = compileElement(inputHtmlClearOnBlurPlaceholder); 789 | 790 | scope.$apply("x = ''"); 791 | scope.$apply("mask = '(9) * A'"); 792 | 793 | input.val("").triggerHandler("input"); 794 | input.triggerHandler("blur"); 795 | expect(input.val()).toBe(""); 796 | expect(input.attr("placeholder")).toBe("PLACEHOLDER"); 797 | }); 798 | 799 | it("should not preserve $invalid on blur event", function() { 800 | var form = compileElement(formHtml); 801 | var input = form.find("input"); 802 | scope.$apply("x = ''"); 803 | scope.$apply("mask = '(A) * 9'"); 804 | input.val("a").triggerHandler("input"); 805 | input.triggerHandler("blur"); 806 | expect(scope.test.input.$invalid).toBe(false); 807 | }); 808 | 809 | it("should clear input on ctrl+z pressed", function() { 810 | var form = compileElement(formHtml); 811 | var input = form.find("input"); 812 | 813 | function triggerKeyboardEvent(el, type, keyCode, ctrlKey) { 814 | var eventObj = document.createEvent('Events'); 815 | 816 | if (eventObj.initEvent) { 817 | eventObj.initEvent('key' + type, true, true); 818 | } 819 | 820 | eventObj.keyCode = keyCode; 821 | eventObj.which = keyCode; 822 | eventObj.ctrlKey = ctrlKey; 823 | 824 | el.dispatchEvent(eventObj); 825 | } 826 | 827 | var triggerCtrlZ = function (element) { 828 | triggerKeyboardEvent(element[0], 'down', 90, true); 829 | triggerKeyboardEvent(element[0], 'up'); 830 | }; 831 | 832 | var triggerInput = function(element) { 833 | var evt = document.createEvent('HTMLEvents'); 834 | evt.initEvent('input', false, true); 835 | element[0].dispatchEvent(evt); 836 | }; 837 | 838 | scope.$apply("mask = '99.99.9999'"); 839 | input.val('11111111'); 840 | triggerInput(input); 841 | expect(input.clone().val()).toBe('11.11.1111'); 842 | triggerCtrlZ(input); 843 | scope.$digest(); 844 | expect(input.clone().val()).toBe('__.__.____'); 845 | }) 846 | }); 847 | 848 | describe("Configuration Provider", function() { 849 | it("should return default values", inject(function($injector) { 850 | var service = $injector.invoke(uiMaskConfigProvider.$get); 851 | expect(service.maskDefinitions).toEqual({'9': /\d/, 'A': /[a-zA-Z]/, '*': /[a-zA-Z0-9]/ }); 852 | expect(service.clearOnBlur).toEqual(true); 853 | expect(service.clearOnBlurPlaceholder).toEqual(false); 854 | expect(service.eventsToHandle).toEqual(['input', 'keyup', 'click', 'focus']); 855 | expect(service.addDefaultPlaceholder).toEqual(true); 856 | })); 857 | 858 | it("should merge default values with configured values", inject(function($injector) { 859 | uiMaskConfigProvider.maskDefinitions({'7': /\d/}); 860 | uiMaskConfigProvider.clearOnBlur(false); 861 | uiMaskConfigProvider.clearOnBlurPlaceholder(true); 862 | uiMaskConfigProvider.eventsToHandle(['input', 'keyup']); 863 | uiMaskConfigProvider.addDefaultPlaceholder(false); 864 | var service = $injector.invoke(uiMaskConfigProvider.$get); 865 | expect(service.maskDefinitions).toEqual({'7': /\d/, '9': /\d/, 'A': /[a-zA-Z]/, '*': /[a-zA-Z0-9]/ }); 866 | expect(service.clearOnBlur).toEqual(false); 867 | expect(service.clearOnBlurPlaceholder).toEqual(true); 868 | expect(service.eventsToHandle).toEqual(['input', 'keyup']); 869 | expect(service.addDefaultPlaceholder).toEqual(false); 870 | })); 871 | }); 872 | 873 | }); 874 | -------------------------------------------------------------------------------- /test/maskSpec.protractor.js: -------------------------------------------------------------------------------- 1 | describe('user input', function() { 2 | it('should remove characters properly when backspacing', function() { 3 | browser.get('demo/index.html'); 4 | 5 | var definitionElement = element(by.id('definition')); 6 | definitionElement.sendKeys('QT****'); 7 | 8 | var maskedElement = element(by.id('masked')); 9 | maskedElement.sendKeys('1234'); 10 | expect(maskedElement.getAttribute('value')).toBe('QT1234'); 11 | maskedElement.sendKeys(protractor.Key.BACK_SPACE); 12 | expect(maskedElement.getAttribute('value')).toBe('QT123_'); 13 | 14 | definitionElement.clear(); 15 | definitionElement.sendKeys('QT**BC**'); 16 | maskedElement.click(); 17 | maskedElement.sendKeys('1234'); 18 | expect(maskedElement.getAttribute('value')).toBe('QT12BC34'); 19 | maskedElement.sendKeys(protractor.Key.BACK_SPACE); 20 | expect(maskedElement.getAttribute('value')).toBe('QT12BC3_'); 21 | maskedElement.sendKeys(protractor.Key.BACK_SPACE); 22 | maskedElement.sendKeys(protractor.Key.BACK_SPACE); 23 | expect(maskedElement.getAttribute('value')).toBe('QT1_BC__'); 24 | }); 25 | }); 26 | --------------------------------------------------------------------------------