mask.js 19 KB

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