You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

860 lines
26 KiB

7 years ago
  1. /*!
  2. * v0.1.5
  3. * Copyright (c) 2014 First Opinion
  4. * formatter.js is open sourced under the MIT license.
  5. *
  6. * thanks to digitalBush/jquery.maskedinput for some of the trickier
  7. * keycode handling
  8. */
  9. //
  10. // Uses CommonJS, AMD or browser globals to create a jQuery plugin.
  11. //
  12. // Similar to jqueryPlugin.js but also tries to
  13. // work in a CommonJS environment.
  14. // It is unlikely jQuery will run in a CommonJS
  15. // environment. See jqueryPlugin.js if you do
  16. // not want to add the extra CommonJS detection.
  17. //
  18. (function (root, factory) {
  19. if (typeof define === 'function' && define.amd) {
  20. // AMD. Register as an anonymous module.
  21. define(['jQuery'], factory);
  22. } else if (typeof exports === 'object') {
  23. factory(require('jQuery'));
  24. } else {
  25. // Browser globals
  26. factory(root.jQuery);
  27. }
  28. }(this, function (jQuery) {
  29. /*
  30. * pattern.js
  31. *
  32. * Utilities to parse str pattern and return info
  33. *
  34. */
  35. var pattern = function () {
  36. // Define module
  37. var pattern = {};
  38. // Match information
  39. var DELIM_SIZE = 4;
  40. // Our regex used to parse
  41. var regexp = new RegExp('{{([^}]+)}}', 'g');
  42. //
  43. // Helper method to parse pattern str
  44. //
  45. var getMatches = function (pattern) {
  46. // Populate array of matches
  47. var matches = [], match;
  48. while (match = regexp.exec(pattern)) {
  49. matches.push(match);
  50. }
  51. return matches;
  52. };
  53. //
  54. // Create an object holding all formatted characters
  55. // with corresponding positions
  56. //
  57. pattern.parse = function (pattern) {
  58. // Our obj to populate
  59. var info = {
  60. inpts: {},
  61. chars: {}
  62. };
  63. // Pattern information
  64. var matches = getMatches(pattern), pLength = pattern.length;
  65. // Counters
  66. var mCount = 0, iCount = 0, i = 0;
  67. // Add inpts, move to end of match, and process
  68. var processMatch = function (val) {
  69. var valLength = val.length;
  70. for (var j = 0; j < valLength; j++) {
  71. info.inpts[iCount] = val.charAt(j);
  72. iCount++;
  73. }
  74. mCount++;
  75. i += val.length + DELIM_SIZE - 1;
  76. };
  77. // Process match or add chars
  78. for (i; i < pLength; i++) {
  79. if (mCount < matches.length && i === matches[mCount].index) {
  80. processMatch(matches[mCount][1]);
  81. } else {
  82. info.chars[i - mCount * DELIM_SIZE] = pattern.charAt(i);
  83. }
  84. }
  85. // Set mLength and return
  86. info.mLength = i - mCount * DELIM_SIZE;
  87. return info;
  88. };
  89. // Expose
  90. return pattern;
  91. }();
  92. /*
  93. * utils.js
  94. *
  95. * Independent helper methods (cross browser, etc..)
  96. *
  97. */
  98. var utils = function () {
  99. // Define module
  100. var utils = {};
  101. // Useragent info for keycode handling
  102. var uAgent = typeof navigator !== 'undefined' ? navigator.userAgent : null;
  103. //
  104. // Shallow copy properties from n objects to destObj
  105. //
  106. utils.extend = function (destObj) {
  107. for (var i = 1; i < arguments.length; i++) {
  108. for (var key in arguments[i]) {
  109. destObj[key] = arguments[i][key];
  110. }
  111. }
  112. return destObj;
  113. };
  114. //
  115. // Add a given character to a string at a defined pos
  116. //
  117. utils.addChars = function (str, chars, pos) {
  118. return str.substr(0, pos) + chars + str.substr(pos, str.length);
  119. };
  120. //
  121. // Remove a span of characters
  122. //
  123. utils.removeChars = function (str, start, end) {
  124. return str.substr(0, start) + str.substr(end, str.length);
  125. };
  126. //
  127. // Return true/false is num false between bounds
  128. //
  129. utils.isBetween = function (num, bounds) {
  130. bounds.sort(function (a, b) {
  131. return a - b;
  132. });
  133. return num > bounds[0] && num < bounds[1];
  134. };
  135. //
  136. // Helper method for cross browser event listeners
  137. //
  138. utils.addListener = function (el, evt, handler) {
  139. return typeof el.addEventListener !== 'undefined' ? el.addEventListener(evt, handler, false) : el.attachEvent('on' + evt, handler);
  140. };
  141. //
  142. // Helper method for cross browser implementation of preventDefault
  143. //
  144. utils.preventDefault = function (evt) {
  145. return evt.preventDefault ? evt.preventDefault() : evt.returnValue = false;
  146. };
  147. //
  148. // Helper method for cross browser implementation for grabbing
  149. // clipboard data
  150. //
  151. utils.getClip = function (evt) {
  152. if (evt.clipboardData) {
  153. return evt.clipboardData.getData('Text');
  154. }
  155. if (window.clipboardData) {
  156. return window.clipboardData.getData('Text');
  157. }
  158. };
  159. //
  160. // Loop over object and checking for matching properties
  161. //
  162. utils.getMatchingKey = function (which, keyCode, keys) {
  163. // Loop over and return if matched.
  164. for (var k in keys) {
  165. var key = keys[k];
  166. if (which === key.which && keyCode === key.keyCode) {
  167. return k;
  168. }
  169. }
  170. };
  171. //
  172. // Returns true/false if k is a del keyDown
  173. //
  174. utils.isDelKeyDown = function (which, keyCode) {
  175. var keys = {
  176. 'backspace': {
  177. 'which': 8,
  178. 'keyCode': 8
  179. },
  180. 'delete': {
  181. 'which': 46,
  182. 'keyCode': 46
  183. }
  184. };
  185. return utils.getMatchingKey(which, keyCode, keys);
  186. };
  187. //
  188. // Returns true/false if k is a del keyPress
  189. //
  190. utils.isDelKeyPress = function (which, keyCode) {
  191. var keys = {
  192. 'backspace': {
  193. 'which': 8,
  194. 'keyCode': 8,
  195. 'shiftKey': false
  196. },
  197. 'delete': {
  198. 'which': 0,
  199. 'keyCode': 46
  200. }
  201. };
  202. return utils.getMatchingKey(which, keyCode, keys);
  203. };
  204. // //
  205. // // Determine if keydown relates to specialKey
  206. // //
  207. // utils.isSpecialKeyDown = function (which, keyCode) {
  208. // var keys = {
  209. // 'tab': { 'which': 9, 'keyCode': 9 },
  210. // 'enter': { 'which': 13, 'keyCode': 13 },
  211. // 'end': { 'which': 35, 'keyCode': 35 },
  212. // 'home': { 'which': 36, 'keyCode': 36 },
  213. // 'leftarrow': { 'which': 37, 'keyCode': 37 },
  214. // 'uparrow': { 'which': 38, 'keyCode': 38 },
  215. // 'rightarrow': { 'which': 39, 'keyCode': 39 },
  216. // 'downarrow': { 'which': 40, 'keyCode': 40 },
  217. // 'F5': { 'which': 116, 'keyCode': 116 }
  218. // };
  219. // return utils.getMatchingKey(which, keyCode, keys);
  220. // };
  221. //
  222. // Determine if keypress relates to specialKey
  223. //
  224. utils.isSpecialKeyPress = function (which, keyCode) {
  225. var keys = {
  226. 'tab': {
  227. 'which': 0,
  228. 'keyCode': 9
  229. },
  230. 'enter': {
  231. 'which': 13,
  232. 'keyCode': 13
  233. },
  234. 'end': {
  235. 'which': 0,
  236. 'keyCode': 35
  237. },
  238. 'home': {
  239. 'which': 0,
  240. 'keyCode': 36
  241. },
  242. 'leftarrow': {
  243. 'which': 0,
  244. 'keyCode': 37
  245. },
  246. 'uparrow': {
  247. 'which': 0,
  248. 'keyCode': 38
  249. },
  250. 'rightarrow': {
  251. 'which': 0,
  252. 'keyCode': 39
  253. },
  254. 'downarrow': {
  255. 'which': 0,
  256. 'keyCode': 40
  257. },
  258. 'F5': {
  259. 'which': 116,
  260. 'keyCode': 116
  261. }
  262. };
  263. return utils.getMatchingKey(which, keyCode, keys);
  264. };
  265. //
  266. // Returns true/false if modifier key is held down
  267. //
  268. utils.isModifier = function (evt) {
  269. return evt.ctrlKey || evt.altKey || evt.metaKey;
  270. };
  271. //
  272. // Iterates over each property of object or array.
  273. //
  274. utils.forEach = function (collection, callback, thisArg) {
  275. if (collection.hasOwnProperty('length')) {
  276. for (var index = 0, len = collection.length; index < len; index++) {
  277. if (callback.call(thisArg, collection[index], index, collection) === false) {
  278. break;
  279. }
  280. }
  281. } else {
  282. for (var key in collection) {
  283. if (collection.hasOwnProperty(key)) {
  284. if (callback.call(thisArg, collection[key], key, collection) === false) {
  285. break;
  286. }
  287. }
  288. }
  289. }
  290. };
  291. // Expose
  292. return utils;
  293. }();
  294. /*
  295. * pattern-matcher.js
  296. *
  297. * Parses a pattern specification and determines appropriate pattern for an
  298. * input string
  299. *
  300. */
  301. var patternMatcher = function (pattern, utils) {
  302. //
  303. // Parse a matcher string into a RegExp. Accepts valid regular
  304. // expressions and the catchall '*'.
  305. // @private
  306. //
  307. var parseMatcher = function (matcher) {
  308. if (matcher === '*') {
  309. return /.*/;
  310. }
  311. return new RegExp(matcher);
  312. };
  313. //
  314. // Parse a pattern spec and return a function that returns a pattern
  315. // based on user input. The first matching pattern will be chosen.
  316. // Pattern spec format:
  317. // Array [
  318. // Object: { Matcher(RegExp String) : Pattern(Pattern String) },
  319. // ...
  320. // ]
  321. function patternMatcher(patternSpec) {
  322. var matchers = [], patterns = [];
  323. // Iterate over each pattern in order.
  324. utils.forEach(patternSpec, function (patternMatcher) {
  325. // Process single property object to obtain pattern and matcher.
  326. utils.forEach(patternMatcher, function (patternStr, matcherStr) {
  327. var parsedPattern = pattern.parse(patternStr), regExpMatcher = parseMatcher(matcherStr);
  328. matchers.push(regExpMatcher);
  329. patterns.push(parsedPattern);
  330. // Stop after one iteration.
  331. return false;
  332. });
  333. });
  334. var getPattern = function (input) {
  335. var matchedIndex;
  336. utils.forEach(matchers, function (matcher, index) {
  337. if (matcher.test(input)) {
  338. matchedIndex = index;
  339. return false;
  340. }
  341. });
  342. return matchedIndex === undefined ? null : patterns[matchedIndex];
  343. };
  344. return {
  345. getPattern: getPattern,
  346. patterns: patterns,
  347. matchers: matchers
  348. };
  349. }
  350. // Expose
  351. return patternMatcher;
  352. }(pattern, utils);
  353. /*
  354. * inpt-sel.js
  355. *
  356. * Cross browser implementation to get and set input selections
  357. *
  358. */
  359. var inptSel = function () {
  360. // Define module
  361. var inptSel = {};
  362. //
  363. // Get begin and end positions of selected input. Return 0's
  364. // if there is no selectiion data
  365. //
  366. inptSel.get = function (el) {
  367. // If normal browser return with result
  368. if (typeof el.selectionStart === 'number') {
  369. return {
  370. begin: el.selectionStart,
  371. end: el.selectionEnd
  372. };
  373. }
  374. // Uh-Oh. We must be IE. Fun with TextRange!!
  375. var range = document.selection.createRange();
  376. // Determine if there is a selection
  377. if (range && range.parentElement() === el) {
  378. var inputRange = el.createTextRange(), endRange = el.createTextRange(), length = el.value.length;
  379. // Create a working TextRange for the input selection
  380. inputRange.moveToBookmark(range.getBookmark());
  381. // Move endRange begin pos to end pos (hence endRange)
  382. endRange.collapse(false);
  383. // If we are at the very end of the input, begin and end
  384. // must both be the length of the el.value
  385. if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) {
  386. return {
  387. begin: length,
  388. end: length
  389. };
  390. }
  391. // Note: moveStart usually returns the units moved, which
  392. // one may think is -length, however, it will stop when it
  393. // gets to the begin of the range, thus giving us the
  394. // negative value of the pos.
  395. return {
  396. begin: -inputRange.moveStart('character', -length),
  397. end: -inputRange.moveEnd('character', -length)
  398. };
  399. }
  400. //Return 0's on no selection data
  401. return {
  402. begin: 0,
  403. end: 0
  404. };
  405. };
  406. //
  407. // Set the caret position at a specified location
  408. //
  409. inptSel.set = function (el, pos) {
  410. // Normalize pos
  411. if (typeof pos !== 'object') {
  412. pos = {
  413. begin: pos,
  414. end: pos
  415. };
  416. }
  417. // If normal browser
  418. if (el.setSelectionRange) {
  419. el.focus();
  420. el.setSelectionRange(pos.begin, pos.end);
  421. } else if (el.createTextRange) {
  422. var range = el.createTextRange();
  423. range.collapse(true);
  424. range.moveEnd('character', pos.end);
  425. range.moveStart('character', pos.begin);
  426. range.select();
  427. }
  428. };
  429. // Expose
  430. return inptSel;
  431. }();
  432. /*
  433. * formatter.js
  434. *
  435. * Class used to format input based on passed pattern
  436. *
  437. */
  438. var formatter = function (patternMatcher, inptSel, utils) {
  439. // Defaults
  440. var defaults = {
  441. persistent: false,
  442. repeat: false,
  443. placeholder: ' '
  444. };
  445. // Regexs for input validation
  446. var inptRegs = {
  447. '9': /[0-9]/,
  448. 'a': /[A-Za-z]/,
  449. '*': /[A-Za-z0-9]/
  450. };
  451. //
  452. // Class Constructor - Called with new Formatter(el, opts)
  453. // Responsible for setting up required instance variables, and
  454. // attaching the event listener to the element.
  455. //
  456. function Formatter(el, opts) {
  457. // Cache this
  458. var self = this;
  459. // Make sure we have an element. Make accesible to instance
  460. self.el = el;
  461. if (!self.el) {
  462. throw new TypeError('Must provide an existing element');
  463. }
  464. // Merge opts with defaults
  465. self.opts = utils.extend({}, defaults, opts);
  466. // 1 pattern is special case
  467. if (typeof self.opts.pattern !== 'undefined') {
  468. self.opts.patterns = self._specFromSinglePattern(self.opts.pattern);
  469. delete self.opts.pattern;
  470. }
  471. // Make sure we have valid opts
  472. if (typeof self.opts.patterns === 'undefined') {
  473. throw new TypeError('Must provide a pattern or array of patterns');
  474. }
  475. self.patternMatcher = patternMatcher(self.opts.patterns);
  476. // Upate pattern with initial value
  477. self._updatePattern();
  478. // Init values
  479. self.hldrs = {};
  480. self.focus = 0;
  481. // Add Listeners
  482. utils.addListener(self.el, 'keydown', function (evt) {
  483. self._keyDown(evt);
  484. });
  485. utils.addListener(self.el, 'keypress', function (evt) {
  486. self._keyPress(evt);
  487. });
  488. utils.addListener(self.el, 'paste', function (evt) {
  489. self._paste(evt);
  490. });
  491. // Persistence
  492. if (self.opts.persistent) {
  493. // Format on start
  494. self._processKey('', false);
  495. self.el.blur();
  496. // Add Listeners
  497. utils.addListener(self.el, 'focus', function (evt) {
  498. self._focus(evt);
  499. });
  500. utils.addListener(self.el, 'click', function (evt) {
  501. self._focus(evt);
  502. });
  503. utils.addListener(self.el, 'touchstart', function (evt) {
  504. self._focus(evt);
  505. });
  506. }
  507. }
  508. //
  509. // @public
  510. // Add new char
  511. //
  512. Formatter.addInptType = function (chr, reg) {
  513. inptRegs[chr] = reg;
  514. };
  515. //
  516. // @public
  517. // Apply the given pattern to the current input without moving caret.
  518. //
  519. Formatter.prototype.resetPattern = function (str) {
  520. // Update opts to hold new pattern
  521. this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns;
  522. // Get current state
  523. this.sel = inptSel.get(this.el);
  524. this.val = this.el.value;
  525. // Init values
  526. this.delta = 0;
  527. // Remove all formatted chars from val
  528. this._removeChars();
  529. this.patternMatcher = patternMatcher(this.opts.patterns);
  530. // Update pattern
  531. var newPattern = this.patternMatcher.getPattern(this.val);
  532. this.mLength = newPattern.mLength;
  533. this.chars = newPattern.chars;
  534. this.inpts = newPattern.inpts;
  535. // Format on start
  536. this._processKey('', false, true);
  537. };
  538. //
  539. // @private
  540. // Determine correct format pattern based on input val
  541. //
  542. Formatter.prototype._updatePattern = function () {
  543. // Determine appropriate pattern
  544. var newPattern = this.patternMatcher.getPattern(this.val);
  545. // Only update the pattern if there is an appropriate pattern for the value.
  546. // Otherwise, leave the current pattern (and likely delete the latest character.)
  547. if (newPattern) {
  548. // Get info about the given pattern
  549. this.mLength = newPattern.mLength;
  550. this.chars = newPattern.chars;
  551. this.inpts = newPattern.inpts;
  552. }
  553. };
  554. //
  555. // @private
  556. // Handler called on all keyDown strokes. All keys trigger
  557. // this handler. Only process delete keys.
  558. //
  559. Formatter.prototype._keyDown = function (evt) {
  560. // The first thing we need is the character code
  561. var k = evt.which || evt.keyCode;
  562. // If delete key
  563. if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) {
  564. // Process the keyCode and prevent default
  565. this._processKey(null, k);
  566. return utils.preventDefault(evt);
  567. }
  568. };
  569. //
  570. // @private
  571. // Handler called on all keyPress strokes. Only processes
  572. // character keys (as long as no modifier key is in use).
  573. //
  574. Formatter.prototype._keyPress = function (evt) {
  575. // The first thing we need is the character code
  576. var k, isSpecial;
  577. // Mozilla will trigger on special keys and assign the the value 0
  578. // We want to use that 0 rather than the keyCode it assigns.
  579. k = evt.which || evt.keyCode;
  580. isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode);
  581. // Process the keyCode and prevent default
  582. if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) {
  583. this._processKey(String.fromCharCode(k), false);
  584. return utils.preventDefault(evt);
  585. }
  586. };
  587. //
  588. // @private
  589. // Handler called on paste event.
  590. //
  591. Formatter.prototype._paste = function (evt) {
  592. // Process the clipboard paste and prevent default
  593. this._processKey(utils.getClip(evt), false);
  594. return utils.preventDefault(evt);
  595. };
  596. //
  597. // @private
  598. // Handle called on focus event.
  599. //
  600. Formatter.prototype._focus = function () {
  601. // Wrapped in timeout so that we can grab input selection
  602. var self = this;
  603. setTimeout(function () {
  604. // Grab selection
  605. var selection = inptSel.get(self.el);
  606. // Char check
  607. var isAfterStart = selection.end > self.focus, isFirstChar = selection.end === 0;
  608. // If clicked in front of start, refocus to start
  609. if (isAfterStart || isFirstChar) {
  610. inptSel.set(self.el, self.focus);
  611. }
  612. }, 0);
  613. };
  614. //
  615. // @private
  616. // Using the provided key information, alter el value.
  617. //
  618. Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) {
  619. // Get current state
  620. this.sel = inptSel.get(this.el);
  621. this.val = this.el.value;
  622. // Init values
  623. this.delta = 0;
  624. // If chars were highlighted, we need to remove them
  625. if (this.sel.begin !== this.sel.end) {
  626. this.delta = -1 * Math.abs(this.sel.begin - this.sel.end);
  627. this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end);
  628. } else if (delKey && delKey === 46) {
  629. this._delete();
  630. } else if (delKey && this.sel.begin - 1 >= 0) {
  631. // Always have a delta of at least -1 for the character being deleted.
  632. this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end);
  633. this.delta -= 1;
  634. } else if (delKey) {
  635. return true;
  636. }
  637. // If the key is not a del key, it should convert to a str
  638. if (!delKey) {
  639. // Add char at position and increment delta
  640. this.val = utils.addChars(this.val, chars, this.sel.begin);
  641. this.delta += chars.length;
  642. }
  643. // Format el.value (also handles updating caret position)
  644. this._formatValue(ignoreCaret);
  645. };
  646. //
  647. // @private
  648. // Deletes the character in front of it
  649. //
  650. Formatter.prototype._delete = function () {
  651. // Adjust focus to make sure its not on a formatted char
  652. while (this.chars[this.sel.begin]) {
  653. this._nextPos();
  654. }
  655. // As long as we are not at the end
  656. if (this.sel.begin < this.val.length) {
  657. // We will simulate a delete by moving the caret to the next char
  658. // and then deleting
  659. this._nextPos();
  660. this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end);
  661. this.delta = -1;
  662. }
  663. };
  664. //
  665. // @private
  666. // Quick helper method to move the caret to the next pos
  667. //
  668. Formatter.prototype._nextPos = function () {
  669. this.sel.end++;
  670. this.sel.begin++;
  671. };
  672. //
  673. // @private
  674. // Alter element value to display characters matching the provided
  675. // instance pattern. Also responsible for updating
  676. //
  677. Formatter.prototype._formatValue = function (ignoreCaret) {
  678. // Set caret pos
  679. this.newPos = this.sel.end + this.delta;
  680. // Remove all formatted chars from val
  681. this._removeChars();
  682. // Switch to first matching pattern based on val
  683. this._updatePattern();
  684. // Validate inputs
  685. this._validateInpts();
  686. // Add formatted characters
  687. this._addChars();
  688. // Set value and adhere to maxLength
  689. this.el.value = this.val.substr(0, this.mLength);
  690. // Set new caret position
  691. if (typeof ignoreCaret === 'undefined' || ignoreCaret === false) {
  692. inptSel.set(this.el, this.newPos);
  693. }
  694. };
  695. //
  696. // @private
  697. // Remove all formatted before and after a specified pos
  698. //
  699. Formatter.prototype._removeChars = function () {
  700. // Delta shouldn't include placeholders
  701. if (this.sel.end > this.focus) {
  702. this.delta += this.sel.end - this.focus;
  703. }
  704. // Account for shifts during removal
  705. var shift = 0;
  706. // Loop through all possible char positions
  707. for (var i = 0; i <= this.mLength; i++) {
  708. // Get transformed position
  709. var curChar = this.chars[i], curHldr = this.hldrs[i], pos = i + shift, val;
  710. // If after selection we need to account for delta
  711. pos = i >= this.sel.begin ? pos + this.delta : pos;
  712. val = this.val.charAt(pos);
  713. // Remove char and account for shift
  714. if (curChar && curChar === val || curHldr && curHldr === val) {
  715. this.val = utils.removeChars(this.val, pos, pos + 1);
  716. shift--;
  717. }
  718. }
  719. // All hldrs should be removed now
  720. this.hldrs = {};
  721. // Set focus to last character
  722. this.focus = this.val.length;
  723. };
  724. //
  725. // @private
  726. // Make sure all inpts are valid, else remove and update delta
  727. //
  728. Formatter.prototype._validateInpts = function () {
  729. // Loop over each char and validate
  730. for (var i = 0; i < this.val.length; i++) {
  731. // Get char inpt type
  732. var inptType = this.inpts[i];
  733. // Checks
  734. var isBadType = !inptRegs[inptType], isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), inBounds = this.inpts[i];
  735. // Remove if incorrect and inbounds
  736. if ((isBadType || isInvalid) && inBounds) {
  737. this.val = utils.removeChars(this.val, i, i + 1);
  738. this.focusStart--;
  739. this.newPos--;
  740. this.delta--;
  741. i--;
  742. }
  743. }
  744. };
  745. //
  746. // @private
  747. // Loop over val and add formatted chars as necessary
  748. //
  749. Formatter.prototype._addChars = function () {
  750. if (this.opts.persistent) {
  751. // Loop over all possible characters
  752. for (var i = 0; i <= this.mLength; i++) {
  753. if (!this.val.charAt(i)) {
  754. // Add placeholder at pos
  755. this.val = utils.addChars(this.val, this.opts.placeholder, i);
  756. this.hldrs[i] = this.opts.placeholder;
  757. }
  758. this._addChar(i);
  759. }
  760. // Adjust focus to make sure its not on a formatted char
  761. while (this.chars[this.focus]) {
  762. this.focus++;
  763. }
  764. } else {
  765. // Avoid caching val.length, as they may change in _addChar.
  766. for (var j = 0; j <= this.val.length; j++) {
  767. // When moving backwards there are some race conditions where we
  768. // dont want to add the character
  769. if (this.delta <= 0 && j === this.focus) {
  770. return true;
  771. }
  772. // Place character in current position of the formatted string.
  773. this._addChar(j);
  774. }
  775. }
  776. };
  777. //
  778. // @private
  779. // Add formattted char at position
  780. //
  781. Formatter.prototype._addChar = function (i) {
  782. // If char exists at position
  783. var chr = this.chars[i];
  784. if (!chr) {
  785. return true;
  786. }
  787. // If chars are added in between the old pos and new pos
  788. // we need to increment pos and delta
  789. if (utils.isBetween(i, [
  790. this.sel.begin - 1,
  791. this.newPos + 1
  792. ])) {
  793. this.newPos++;
  794. this.delta++;
  795. }
  796. // If character added before focus, incr
  797. if (i <= this.focus) {
  798. this.focus++;
  799. }
  800. // Updateholder
  801. if (this.hldrs[i]) {
  802. delete this.hldrs[i];
  803. this.hldrs[i + 1] = this.opts.placeholder;
  804. }
  805. // Update value
  806. this.val = utils.addChars(this.val, chr, i);
  807. };
  808. //
  809. // @private
  810. // Create a patternSpec for passing into patternMatcher that
  811. // has exactly one catch all pattern.
  812. //
  813. Formatter.prototype._specFromSinglePattern = function (patternStr) {
  814. return [{ '*': patternStr }];
  815. };
  816. // Expose
  817. return Formatter;
  818. }(patternMatcher, inptSel, utils);
  819. // A really lightweight plugin wrapper around the constructor,
  820. // preventing against multiple instantiations
  821. var pluginName = 'formatter';
  822. $.fn[pluginName] = function (options) {
  823. // Initiate plugin if options passed
  824. if (typeof options == 'object') {
  825. this.each(function () {
  826. if (!$.data(this, 'plugin_' + pluginName)) {
  827. $.data(this, 'plugin_' + pluginName,
  828. new formatter(this, options));
  829. }
  830. });
  831. }
  832. // Add resetPattern method to plugin
  833. this.resetPattern = function (str) {
  834. this.each(function () {
  835. var formatted = $.data(this, 'plugin_' + pluginName);
  836. // resetPattern for instance
  837. if (formatted) { formatted.resetPattern(str); }
  838. });
  839. // Chainable please
  840. return this;
  841. };
  842. // Chainable please
  843. return this;
  844. };
  845. $.fn[pluginName].addInptType = function (chr, regexp) {
  846. formatter.addInptType(chr, regexp);
  847. };
  848. }));