├── .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 [](https://travis-ci.org/angular-ui/ui-mask) [](http://badge.fury.io/js/angular-ui-mask) [](http://badge.fury.io/bo/angular-ui-mask) [](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 | [ ](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 |
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 |
--------------------------------------------------------------------------------