├── .gitignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── README.md ├── bower.json ├── dist ├── amd │ ├── formatter.js │ ├── inpt-sel.js │ ├── pattern-matcher.js │ ├── pattern.js │ └── utils.js ├── common │ ├── formatter.js │ ├── inpt-sel.js │ ├── pattern-matcher.js │ ├── pattern.js │ └── utils.js ├── formatter.js ├── formatter.min.js ├── jquery.formatter.js └── jquery.formatter.min.js ├── docs ├── _layouts │ └── master.html ├── demos.html ├── index.md ├── javascripts │ ├── formatter.js │ ├── formatter.min.js │ ├── jquery.formatter.js │ ├── jquery.formatter.min.js │ └── scale.fix.js └── stylesheets │ ├── pygment_trac.css │ └── styles.css ├── package.json ├── src ├── formatter.js ├── inpt-sel.js ├── pattern-matcher.js ├── pattern.js ├── tmpls │ ├── jquery.hbs │ └── umd.hbs └── utils.js └── test ├── _runner.html ├── formatter.js ├── pattern-matcher.js ├── pattern.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | tmp 16 | 17 | npm-debug.log 18 | node_modules 19 | bower_components 20 | .grunt 21 | _site 22 | 23 | sauce_connect.log 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "immed": true, 7 | "indent": false, 8 | "latedef": true, 9 | "newcap": true, 10 | "noarg": true, 11 | "quotmark": "single", 12 | "undef": false, 13 | "unused": false, 14 | "trailing": true, 15 | "smarttabs": true, 16 | "laxbreak": true, 17 | "boss": true, 18 | "sub": true, 19 | "immed": false, 20 | "predef": { 21 | "describe": true, 22 | "it": true, 23 | "before": true, 24 | "beforeEach": true, 25 | "after": true, 26 | "afterEach": true 27 | } 28 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.1 4 | before_script: 5 | - npm install -g grunt-cli 6 | - npm install -g bower 7 | - npm install --no-bin-links 8 | - bower install 9 | notifications: 10 | email: 11 | - jarid@firstopinion.co -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Gruntfile.js 3 | * 4 | * Copyright (c) 2014 5 | */ 6 | 7 | 8 | module.exports = function (grunt) { 9 | 10 | 11 | // Load tasks 12 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 13 | 14 | 15 | // Browsers 16 | var browsers = [ 17 | // Latest Versions 18 | { browserName: 'firefox', platform: 'WIN8' }, 19 | { browserName: 'chrome', platform: 'WIN8' }, 20 | { browserName: 'opera', platform: 'WIN7' }, 21 | 22 | // Internet Explorer 23 | { browserName: 'internet explorer', platform: 'WIN8', version: '10' }, 24 | { browserName: 'internet explorer', platform: 'VISTA', version: '9' }, 25 | { browserName: 'internet explorer', platform: 'XP', version: '8' } 26 | ]; 27 | 28 | 29 | // Config 30 | grunt.initConfig({ 31 | 32 | // -------------------------------------------------------------------------- 33 | // PKG CONFIG 34 | // -------------------------------------------------------------------------- 35 | 36 | 'pkg': grunt.file.readJSON('package.json'), 37 | 38 | 39 | // -------------------------------------------------------------------------- 40 | // JSHINT 41 | // -------------------------------------------------------------------------- 42 | 43 | 'jshint': { 44 | src: [ 45 | 'Gruntfile.js', 46 | 'src/**/*.js', 47 | 'test/**/*.js' 48 | ], 49 | build: [ 50 | 'dist/**/*.js', 51 | '!dist/**/*.min.js' 52 | ], 53 | options: { 54 | jshintrc: '.jshintrc', 55 | force: true 56 | } 57 | }, 58 | 59 | 60 | // -------------------------------------------------------------------------- 61 | // CLEAN (EMPTY DIRECTORY) 62 | // -------------------------------------------------------------------------- 63 | 64 | 'clean': { 65 | dist: [ 66 | 'dist' 67 | ], 68 | docs: [ 69 | 'docs/javascripts/*formatter.js', 70 | 'docs/javascripts/*formatter.min.js', 71 | 'docs/index.md' 72 | ] 73 | }, 74 | 75 | 76 | // -------------------------------------------------------------------------- 77 | // REQUIREJS BUILD 78 | // -------------------------------------------------------------------------- 79 | 80 | 'requirejs': { 81 | compile: { 82 | options: { 83 | name: 'formatter', 84 | baseUrl: 'src', 85 | out: 'dist/formatter.js', 86 | optimize: 'none', 87 | skipModuleInsertion: true, 88 | onBuildWrite: function(name, path, contents) { 89 | return require('amdclean').clean({ 90 | code: contents, 91 | prefixMode: 'camelCase', 92 | escodegen: { 93 | format: { 94 | indent: { style: ' ' } 95 | } 96 | } 97 | }); 98 | } 99 | } 100 | } 101 | }, 102 | 103 | 104 | // -------------------------------------------------------------------------- 105 | // UMD WRAP 106 | // -------------------------------------------------------------------------- 107 | 108 | 'umd': { 109 | jquery: { 110 | src: 'dist/formatter.js', 111 | dest: 'dist/jquery.formatter.js', 112 | template: 'src/tmpls/jquery.hbs', 113 | deps: { 'default': ['jQuery'] } 114 | }, 115 | umd: { 116 | src: 'dist/formatter.js', 117 | objectToExport: 'formatter', 118 | globalAlias: 'Formatter', 119 | template: 'src/tmpls/umd.hbs', 120 | dest: 'dist/formatter.js' 121 | } 122 | }, 123 | 124 | 125 | // -------------------------------------------------------------------------- 126 | // ADD BANNER 127 | // -------------------------------------------------------------------------- 128 | 129 | 'concat': { 130 | options: { 131 | banner: '/*!\n' + 132 | ' * v<%= pkg.version %>\n' + 133 | ' * Copyright (c) 2014 First Opinion\n' + 134 | ' * formatter.js is open sourced under the MIT license.\n' + 135 | ' *\n' + 136 | ' * thanks to digitalBush/jquery.maskedinput for some of the trickier\n' + 137 | ' * keycode handling\n' + 138 | ' */ \n\n', 139 | stripBanners: true 140 | }, 141 | umd: { 142 | src: 'dist/formatter.js', 143 | dest: 'dist/formatter.js' 144 | }, 145 | jquery: { 146 | src: 'dist/jquery.formatter.js', 147 | dest: 'dist/jquery.formatter.js' 148 | } 149 | }, 150 | 151 | 152 | // -------------------------------------------------------------------------- 153 | // MINIFY JS 154 | // -------------------------------------------------------------------------- 155 | 156 | 'uglify': { 157 | umd: { 158 | src: 'dist/formatter.js', 159 | dest: 'dist/formatter.min.js' 160 | }, 161 | jquery: { 162 | src: 'dist/jquery.formatter.js', 163 | dest: 'dist/jquery.formatter.min.js' 164 | } 165 | }, 166 | 167 | 168 | // -------------------------------------------------------------------------- 169 | // CREATE COMMONJS VERSION IN DIST 170 | // -------------------------------------------------------------------------- 171 | 172 | 'nodefy': { 173 | all: { 174 | expand: true, 175 | src: ['**/*.js'], 176 | cwd: 'src/', 177 | dest: 'dist/common' 178 | } 179 | }, 180 | 181 | 182 | // -------------------------------------------------------------------------- 183 | // COPY AMD TO DIST 184 | // -------------------------------------------------------------------------- 185 | 186 | 'copy': { 187 | amd: { 188 | expand: true, 189 | src: ['**/*.js'], 190 | cwd: 'src/', 191 | dest: 'dist/amd' 192 | }, 193 | javascripts: { 194 | expand: true, 195 | src: ['*.js'], 196 | cwd: 'dist', 197 | dest: 'docs/javascripts' 198 | }, 199 | readme: { 200 | src: 'README.md', 201 | dest: 'docs/index.md' 202 | } 203 | }, 204 | 205 | 206 | // -------------------------------------------------------------------------- 207 | // WRAP 208 | // -------------------------------------------------------------------------- 209 | 210 | 'wrap': { 211 | readme: { 212 | src: ['docs/index.md'], 213 | dest: 'docs/index.md', 214 | options: { 215 | wrapper: ['---\nlayout: master\n---\n{% raw %}', '{% endraw %}'] 216 | } 217 | } 218 | }, 219 | 220 | 221 | // -------------------------------------------------------------------------- 222 | // WATCH FILES 223 | // -------------------------------------------------------------------------- 224 | 225 | 'watch': { 226 | options: { spawn: true }, 227 | build: { 228 | files: ['Gruntfile.js'], 229 | tasks: ['build', 'docs'], 230 | options: { livereload: true } 231 | }, 232 | src: { 233 | files: ['src/**/*.js'], 234 | tasks: ['build'], 235 | options: { livereload: true } 236 | }, 237 | docs: { 238 | files: ['docs/**/*'], 239 | tasks: ['jekyll'], 240 | options: { livereload: true } 241 | }, 242 | test: { 243 | files: ['test/**/*'], 244 | options: { livereload: true } 245 | } 246 | }, 247 | 248 | 249 | // -------------------------------------------------------------------------- 250 | // STATIC SERVER 251 | // -------------------------------------------------------------------------- 252 | 253 | 'connect': { 254 | docs: { 255 | options: { base: '_site', port: 9998 } 256 | }, 257 | test: { 258 | options: { base: '', port: 9999 } 259 | } 260 | }, 261 | 262 | 263 | // -------------------------------------------------------------------------- 264 | // BUILD AND SERVE JEKYLL DOCS 265 | // -------------------------------------------------------------------------- 266 | 267 | 'jekyll': { 268 | all: { 269 | options: { 270 | src : 'docs', 271 | dest: '_site' 272 | } 273 | } 274 | }, 275 | 276 | 277 | // -------------------------------------------------------------------------- 278 | // PUSH DOCS LIVE 279 | // -------------------------------------------------------------------------- 280 | 281 | 'gh-pages': { 282 | options: { 283 | base: 'docs' 284 | }, 285 | src: ['**'] 286 | }, 287 | 288 | 289 | // -------------------------------------------------------------------------- 290 | // TESTS 291 | // -------------------------------------------------------------------------- 292 | 293 | 'saucelabs-mocha': { 294 | all: { 295 | options: { 296 | urls: ['http://127.0.0.1:9999/test/_runner.html'], 297 | build: process.env.TRAVIS_JOB_ID || '<%= pkg.version %>', 298 | tunnelTimeout: 5, 299 | concurrency: 3, 300 | browsers: browsers, 301 | testname: 'formatter.js' 302 | } 303 | } 304 | }, 305 | 306 | 307 | // -------------------------------------------------------------------------- 308 | // MOCHA 309 | // -------------------------------------------------------------------------- 310 | 311 | 'mocha_phantomjs': { 312 | all: ['test/_runner.html'] 313 | } 314 | 315 | }); 316 | 317 | 318 | // Tasks 319 | grunt.registerTask('default', ['build']); 320 | grunt.registerTask('dev', ['build', 'docs', 'connect', 'watch']); 321 | grunt.registerTask('test', ['build', 'mocha_phantomjs']); 322 | grunt.registerTask('test-cloud', ['build', 'connect:test', 'saucelabs-mocha']); 323 | grunt.registerTask('docs', ['clean:docs', 'copy:javascripts', 'copy:readme', 'wrap:readme', 'jekyll']); 324 | grunt.registerTask('build', ['jshint:src', 'clean:dist', 'requirejs', 'umd:jquery', 'umd:umd', 'concat:umd', 'concat:jquery', 'uglify:umd', 'uglify:jquery', 'nodefy', 'copy:amd']); 325 | 326 | 327 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | formatter.js 2 | ============ 3 | 4 | ___ __ __ _ 5 | / _/__ ______ _ ___ _/ /_/ /____ ____ (_)__ 6 | / _/ _ \/ __/ ' \/ _ `/ __/ __/ -_) __/ / (_-< 7 | /_/ \___/_/ /_/_/_/\_,_/\__/\__/\__/_/ (_)_/ /___/ 8 | |___/ 9 | 10 | Format user input to match a specified pattern 11 | 12 | Pebble fork to keep things up to date 13 | 14 | 15 | 16 | Demos/Examples 17 | -------------- 18 | 19 | [view demo](http://firstopinion.github.io/formatter.js/demos.html) 20 | 21 | 22 | 23 | Why? 24 | ---- 25 | 26 | Sometimes it is useful to format user input as they type. Existing libraries lacked proper functionality / flexibility. Formatter was built from the ground up with no dependencies. There is however a jquery wrapper version for quick use. 27 | 28 | 29 | 30 | On NPM 31 | -------- 32 | 33 | npm install formatter.js-pebble 34 | 35 | 36 | 37 | Usage 38 | ----- 39 | 40 | ### Vanilla Javascript 41 | 42 | * **uncompressed**: formatter.js 43 | * **compressed**: formatter.min.js 44 | 45 | #### new Formatter(el, opts) 46 | 47 | var formatted = new Formatter(document.getElementById('credit-input'), { 48 | 'pattern': '{{999}}-{{999}}-{{999}}-{{9999}}', 49 | 'persistent': true 50 | }); 51 | 52 | 53 | ### Jquery 54 | 55 | * **uncompressed**: jquery.formatter.js 56 | * **compressed**: jquery.formatter.min.js 57 | 58 | #### $(selector).formatter(opts) 59 | 60 | $('#credit-input').formatter({ 61 | 'pattern': '{{999}}-{{999}}-{{999}}-{{9999}}', 62 | 'persistent': true 63 | }); 64 | 65 | 66 | 67 | Opts 68 | ---- 69 | 70 | * **pattern** (required): String representing the pattern of your formatted input. User input areas begin with `{{` and end with `}}`. For example, a phone number may be represented: `({{999}}) {{999}}-{{999}}`. You can specify numbers, letters, or numbers and letters. When using `?` or `A` lower-case letters will automatically be converted to uppercase. 71 | * 9: [0-9] 72 | * a: [A-Za-z] 73 | * \*: [A-Za-z0-9] 74 | * A: [A-Z] 75 | * ?: [A-Z0-9] 76 | * **persistent**: \[False\] Boolean representing if the formatted characters are always visible (persistent), or if they appear as you type. 77 | * **patterns** (optional, replaces *pattern*): Array representing a priority ordered set of patterns that may apply dynamically based on the current input value. Each value in the array is an object, whose key is a regular expression string and value is a *pattern* (see above). The regular expression is tested against the unformatted input value. You may use the special key `'*'` to catch all input values. 78 | ``` 79 | [ 80 | { '^\d{5}$': 'zip: {{99999}}' }, 81 | { '^.{6,8}$: 'postal code: {{********}}' }, 82 | { '*': 'unknown: {{**********}}' } 83 | ] 84 | ``` 85 | 86 | 87 | 88 | Class Methods 89 | ------------- 90 | 91 | #### addInptType(char, regexp) 92 | 93 | Add regular expressions for different input types. 94 | 95 | **Vanilla Javascript** 96 | 97 | Formatter.addInptType('L', /[A-Z]/); 98 | 99 | **Jquery** 100 | 101 | $.fn.formatter.addInptType('L', /[A-Z]/); 102 | 103 | 104 | 105 | Instance Methods 106 | ---------------- 107 | 108 | #### resetPattern(pattern) 109 | 110 | Fairly self explanatory here :) reset the pattern on an existing Formatter instance. 111 | 112 | **Vanilla Javascript** 113 | 114 | (assuming you already created a new instance and saved it to the var `formatted`) 115 | 116 | formatted.resetPattern('{{999}}.{{999}}.{{9999}}'); 117 | 118 | **Jquery** 119 | 120 | (assuming you already initiated formatter on `#selector`) 121 | 122 | $('#selector').formatter().resetPattern(); 123 | 124 | 125 | 126 | Tests 127 | ----- 128 | 129 | Install Dependencies: 130 | 131 | npm install 132 | 133 | Run Tests: 134 | 135 | npm test 136 | 137 | 138 | 139 | License 140 | ------- 141 | 142 | The MIT License (MIT) Copyright (c) 2013 First Opinion 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 147 | 148 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formatter.js", 3 | "version": "0.1.5", 4 | "devDependencies": { 5 | "easy-amdtest": "~0.0.1", 6 | "requirejs": "~2.1.11", 7 | "jquery": "~1.11.0", 8 | "fakey": "~0.0.8" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "Gruntfile.js", 13 | "src", 14 | "docs", 15 | "test" 16 | ], 17 | "main": [ 18 | "dist/formatter.js", 19 | "dist/formatter.min.js", 20 | "dist/jquery.formatter.js", 21 | "dist/jquery.formatter.min.js" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /dist/amd/formatter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * formatter.js 3 | * 4 | * Class used to format input based on passed pattern 5 | * 6 | */ 7 | 8 | define([ 9 | './pattern-matcher', 10 | './inpt-sel', 11 | './utils' 12 | ], function (patternMatcher, inptSel, utils) { 13 | 14 | 15 | // Defaults 16 | var defaults = { 17 | persistent: false, 18 | repeat: false, 19 | placeholder: ' ' 20 | }; 21 | 22 | // Regexs for input validation 23 | var inptRegs = { 24 | '9': /[0-9]/, 25 | 'a': /[A-Za-z]/, 26 | 'A': /[A-Z]/, 27 | '?': /[A-Z0-9]/, 28 | '*': /[A-Za-z0-9]/ 29 | }; 30 | 31 | // 32 | // Class Constructor - Called with new Formatter(el, opts) 33 | // Responsible for setting up required instance variables, and 34 | // attaching the event listener to the element. 35 | // 36 | function Formatter(el, opts) { 37 | // Cache this 38 | var self = this; 39 | 40 | // Make sure we have an element. Make accesible to instance 41 | self.el = el; 42 | if (!self.el) { 43 | throw new TypeError('Must provide an existing element'); 44 | } 45 | 46 | // Merge opts with defaults 47 | self.opts = utils.extend({}, defaults, opts); 48 | 49 | // 1 pattern is special case 50 | if (typeof self.opts.pattern !== 'undefined') { 51 | self.opts.patterns = self._specFromSinglePattern(self.opts.pattern); 52 | delete self.opts.pattern; 53 | } 54 | 55 | // Make sure we have valid opts 56 | if (typeof self.opts.patterns === 'undefined') { 57 | throw new TypeError('Must provide a pattern or array of patterns'); 58 | } 59 | 60 | self.patternMatcher = patternMatcher(self.opts.patterns); 61 | 62 | // Upate pattern with initial value 63 | self._updatePattern(); 64 | 65 | // Init values 66 | self.hldrs = {}; 67 | self.focus = 0; 68 | 69 | // Add Listeners 70 | utils.addListener(self.el, 'keydown', function (evt) { 71 | self._keyDown(evt); 72 | }); 73 | utils.addListener(self.el, 'keypress', function (evt) { 74 | self._keyPress(evt); 75 | }); 76 | utils.addListener(self.el, 'paste', function (evt) { 77 | self._paste(evt); 78 | }); 79 | 80 | // Persistence 81 | if (self.opts.persistent) { 82 | // Format on start 83 | self._processKey('', false); 84 | self.el.blur(); 85 | 86 | // Add Listeners 87 | utils.addListener(self.el, 'focus', function (evt) { 88 | self._focus(evt); 89 | }); 90 | utils.addListener(self.el, 'click', function (evt) { 91 | self._focus(evt); 92 | }); 93 | utils.addListener(self.el, 'touchstart', function (evt) { 94 | self._focus(evt); 95 | }); 96 | } 97 | } 98 | 99 | // 100 | // @public 101 | // Add new char 102 | // 103 | Formatter.addInptType = function (chr, reg) { 104 | inptRegs[chr] = reg; 105 | }; 106 | 107 | // 108 | // @public 109 | // Apply the given pattern to the current input without moving caret. 110 | // 111 | Formatter.prototype.resetPattern = function (str) { 112 | // Update opts to hold new pattern 113 | this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns; 114 | 115 | // Get current state 116 | this.sel = inptSel.get(this.el); 117 | this.val = this.el.value; 118 | 119 | // Init values 120 | this.delta = 0; 121 | 122 | // Remove all formatted chars from val 123 | this._removeChars(); 124 | 125 | this.patternMatcher = patternMatcher(this.opts.patterns); 126 | 127 | // Update pattern 128 | var newPattern = this.patternMatcher.getPattern(this.val); 129 | this.mLength = newPattern.mLength; 130 | this.chars = newPattern.chars; 131 | this.inpts = newPattern.inpts; 132 | 133 | // Format on start 134 | this._processKey('', false, true); 135 | }; 136 | 137 | // 138 | // @private 139 | // Determine correct format pattern based on input val 140 | // 141 | Formatter.prototype._updatePattern = function () { 142 | // Determine appropriate pattern 143 | var newPattern = this.patternMatcher.getPattern(this.val); 144 | 145 | // Only update the pattern if there is an appropriate pattern for the value. 146 | // Otherwise, leave the current pattern (and likely delete the latest character.) 147 | if (newPattern) { 148 | // Get info about the given pattern 149 | this.mLength = newPattern.mLength; 150 | this.chars = newPattern.chars; 151 | this.inpts = newPattern.inpts; 152 | } 153 | }; 154 | 155 | // 156 | // @private 157 | // Handler called on all keyDown strokes. All keys trigger 158 | // this handler. Only process delete keys. 159 | // 160 | Formatter.prototype._keyDown = function (evt) { 161 | // The first thing we need is the character code 162 | var k = evt.which || evt.keyCode; 163 | 164 | // If delete key 165 | if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) { 166 | // Process the keyCode and prevent default 167 | this._processKey(null, k); 168 | return utils.preventDefault(evt); 169 | } 170 | }; 171 | 172 | // 173 | // @private 174 | // Handler called on all keyPress strokes. Only processes 175 | // character keys (as long as no modifier key is in use). 176 | // 177 | Formatter.prototype._keyPress = function (evt) { 178 | // The first thing we need is the character code 179 | var k, isSpecial; 180 | // Mozilla will trigger on special keys and assign the the value 0 181 | // We want to use that 0 rather than the keyCode it assigns. 182 | k = evt.which || evt.keyCode; 183 | isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode); 184 | 185 | // Process the keyCode and prevent default 186 | if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) { 187 | this._processKey(String.fromCharCode(k), false); 188 | return utils.preventDefault(evt); 189 | } 190 | }; 191 | 192 | // 193 | // @private 194 | // Handler called on paste event. 195 | // 196 | Formatter.prototype._paste = function (evt) { 197 | // Process the clipboard paste and prevent default 198 | this._processKey(utils.getClip(evt), false); 199 | return utils.preventDefault(evt); 200 | }; 201 | 202 | // 203 | // @private 204 | // Handle called on focus event. 205 | // 206 | Formatter.prototype._focus = function () { 207 | // Wrapped in timeout so that we can grab input selection 208 | var self = this; 209 | setTimeout(function () { 210 | // Grab selection 211 | var selection = inptSel.get(self.el); 212 | // Char check 213 | var isAfterStart = selection.end > self.focus, 214 | isFirstChar = selection.end === 0; 215 | // If clicked in front of start, refocus to start 216 | if (isAfterStart || isFirstChar) { 217 | inptSel.set(self.el, self.focus); 218 | } 219 | }, 0); 220 | }; 221 | 222 | // 223 | // @private 224 | // Using the provided key information, alter el value. 225 | // 226 | Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) { 227 | // Get current state 228 | this.sel = inptSel.get(this.el); 229 | this.val = this.el.value; 230 | 231 | // Init values 232 | this.delta = 0; 233 | 234 | // If chars were highlighted, we need to remove them 235 | if (this.sel.begin !== this.sel.end) { 236 | this.delta = (-1) * Math.abs(this.sel.begin - this.sel.end); 237 | this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end); 238 | } 239 | 240 | // Delete key (moves opposite direction) 241 | else if (delKey && delKey === 46) { 242 | this._delete(); 243 | 244 | // or Backspace and not at start 245 | } else if (delKey && this.sel.begin - 1 >= 0) { 246 | 247 | // Always have a delta of at least -1 for the character being deleted. 248 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 249 | this.delta -= 1; 250 | 251 | // or Backspace and at start - exit 252 | } else if (delKey) { 253 | return true; 254 | } 255 | 256 | // If the key is not a del key, it should convert to a str 257 | if (!delKey) { 258 | // Add char at position and increment delta 259 | this.val = utils.addChars(this.val, chars, this.sel.begin); 260 | this.delta += chars.length; 261 | } 262 | 263 | // Format el.value (also handles updating caret position) 264 | this._formatValue(ignoreCaret); 265 | }; 266 | 267 | // 268 | // @private 269 | // Deletes the character in front of it 270 | // 271 | Formatter.prototype._delete = function () { 272 | // Adjust focus to make sure its not on a formatted char 273 | while (this.chars[this.sel.begin]) { 274 | this._nextPos(); 275 | } 276 | 277 | // As long as we are not at the end 278 | if (this.sel.begin < this.val.length) { 279 | // We will simulate a delete by moving the caret to the next char 280 | // and then deleting 281 | this._nextPos(); 282 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 283 | this.delta = -1; 284 | } 285 | }; 286 | 287 | // 288 | // @private 289 | // Quick helper method to move the caret to the next pos 290 | // 291 | Formatter.prototype._nextPos = function () { 292 | this.sel.end ++; 293 | this.sel.begin ++; 294 | }; 295 | 296 | // 297 | // @private 298 | // Alter element value to display characters matching the provided 299 | // instance pattern. Also responsible for updating 300 | // 301 | Formatter.prototype._formatValue = function (ignoreCaret) { 302 | // Set caret pos 303 | this.newPos = this.sel.end + this.delta; 304 | 305 | // Remove all formatted chars from val 306 | this._removeChars(); 307 | 308 | // Switch to first matching pattern based on val 309 | this._updatePattern(); 310 | 311 | // Validate inputs 312 | this._validateInpts(); 313 | 314 | // Add formatted characters 315 | this._addChars(); 316 | 317 | // Set value and adhere to maxLength 318 | this.el.value = this.val.substr(0, this.mLength); 319 | 320 | // Set new caret position 321 | if ((typeof ignoreCaret) === 'undefined' || ignoreCaret === false) { 322 | inptSel.set(this.el, this.newPos); 323 | } 324 | }; 325 | 326 | // 327 | // @private 328 | // Remove all formatted before and after a specified pos 329 | // 330 | Formatter.prototype._removeChars = function () { 331 | // Delta shouldn't include placeholders 332 | if (this.sel.end > this.focus) { 333 | this.delta += this.sel.end - this.focus; 334 | } 335 | 336 | // Account for shifts during removal 337 | var shift = 0; 338 | 339 | // Loop through all possible char positions 340 | for (var i = 0; i <= this.mLength; i++) { 341 | // Get transformed position 342 | var curChar = this.chars[i], 343 | curHldr = this.hldrs[i], 344 | pos = i + shift, 345 | val; 346 | 347 | // If after selection we need to account for delta 348 | pos = (i >= this.sel.begin) ? pos + this.delta : pos; 349 | val = this.val.charAt(pos); 350 | // Remove char and account for shift 351 | if (curChar && curChar === val || curHldr && curHldr === val) { 352 | this.val = utils.removeChars(this.val, pos, pos + 1); 353 | shift--; 354 | } 355 | } 356 | 357 | // All hldrs should be removed now 358 | this.hldrs = {}; 359 | 360 | // Set focus to last character 361 | this.focus = this.val.length; 362 | }; 363 | 364 | // 365 | // @private 366 | // Make sure all inpts are valid, else remove and update delta 367 | // 368 | Formatter.prototype._validateInpts = function () { 369 | // Loop over each char and validate 370 | for (var i = 0; i < this.val.length; i++) { 371 | // Get char inpt type 372 | var inptType = this.inpts[i]; 373 | 374 | // When only allowing capitals, ensure this char is capitalized! 375 | if (inptType === '?' || inptType === 'A'){ 376 | var up = this.val.charAt(i).toUpperCase(); 377 | this.val = utils.addChars(utils.removeChars(this.val, i, i+1), up, i); 378 | } 379 | 380 | // Checks 381 | var isBadType = !inptRegs[inptType], 382 | isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), 383 | inBounds = this.inpts[i]; 384 | 385 | // Remove if incorrect and inbounds 386 | if ((isBadType || isInvalid) && inBounds) { 387 | this.val = utils.removeChars(this.val, i, i + 1); 388 | this.focusStart--; 389 | this.newPos--; 390 | this.delta--; 391 | i--; 392 | } 393 | } 394 | }; 395 | 396 | // 397 | // @private 398 | // Loop over val and add formatted chars as necessary 399 | // 400 | Formatter.prototype._addChars = function () { 401 | if (this.opts.persistent) { 402 | // Loop over all possible characters 403 | for (var i = 0; i <= this.mLength; i++) { 404 | if (!this.val.charAt(i)) { 405 | // Add placeholder at pos 406 | this.val = utils.addChars(this.val, this.opts.placeholder, i); 407 | this.hldrs[i] = this.opts.placeholder; 408 | } 409 | this._addChar(i); 410 | } 411 | 412 | // Adjust focus to make sure its not on a formatted char 413 | while (this.chars[this.focus]) { 414 | this.focus++; 415 | } 416 | } else { 417 | // Avoid caching val.length, as they may change in _addChar. 418 | for (var j = 0; j <= this.val.length; j++) { 419 | // When moving backwards there are some race conditions where we 420 | // dont want to add the character 421 | if (this.delta <= 0 && (j === this.focus)) { return true; } 422 | 423 | // Place character in current position of the formatted string. 424 | this._addChar(j); 425 | } 426 | } 427 | }; 428 | 429 | // 430 | // @private 431 | // Add formattted char at position 432 | // 433 | Formatter.prototype._addChar = function (i) { 434 | // If char exists at position 435 | var chr = this.chars[i]; 436 | if (!chr) { return true; } 437 | 438 | // If chars are added in between the old pos and new pos 439 | // we need to increment pos and delta 440 | if (utils.isBetween(i, [this.sel.begin -1, this.newPos +1])) { 441 | this.newPos ++; 442 | this.delta ++; 443 | } 444 | 445 | // If character added before focus, incr 446 | if (i <= this.focus) { 447 | this.focus++; 448 | } 449 | 450 | // Updateholder 451 | if (this.hldrs[i]) { 452 | delete this.hldrs[i]; 453 | this.hldrs[i + 1] = this.opts.placeholder; 454 | } 455 | 456 | // Update value 457 | this.val = utils.addChars(this.val, chr, i); 458 | }; 459 | 460 | // 461 | // @private 462 | // Create a patternSpec for passing into patternMatcher that 463 | // has exactly one catch all pattern. 464 | // 465 | Formatter.prototype._specFromSinglePattern = function (patternStr) { 466 | return [{ '*': patternStr }]; 467 | }; 468 | 469 | 470 | // Expose 471 | return Formatter; 472 | 473 | 474 | }); 475 | -------------------------------------------------------------------------------- /dist/amd/inpt-sel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * inpt-sel.js 3 | * 4 | * Cross browser implementation to get and set input selections 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var inptSel = {}; 14 | 15 | // 16 | // Get begin and end positions of selected input. Return 0's 17 | // if there is no selectiion data 18 | // 19 | inptSel.get = function (el) { 20 | // If normal browser return with result 21 | if (typeof el.selectionStart === 'number') { 22 | return { 23 | begin: el.selectionStart, 24 | end: el.selectionEnd 25 | }; 26 | } 27 | 28 | // Uh-Oh. We must be IE. Fun with TextRange!! 29 | var range = document.selection.createRange(); 30 | // Determine if there is a selection 31 | if (range && range.parentElement() === el) { 32 | var inputRange = el.createTextRange(), 33 | endRange = el.createTextRange(), 34 | length = el.value.length; 35 | 36 | // Create a working TextRange for the input selection 37 | inputRange.moveToBookmark(range.getBookmark()); 38 | 39 | // Move endRange begin pos to end pos (hence endRange) 40 | endRange.collapse(false); 41 | 42 | // If we are at the very end of the input, begin and end 43 | // must both be the length of the el.value 44 | if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) { 45 | return { begin: length, end: length }; 46 | } 47 | 48 | // Note: moveStart usually returns the units moved, which 49 | // one may think is -length, however, it will stop when it 50 | // gets to the begin of the range, thus giving us the 51 | // negative value of the pos. 52 | return { 53 | begin: -inputRange.moveStart('character', -length), 54 | end: -inputRange.moveEnd('character', -length) 55 | }; 56 | } 57 | 58 | //Return 0's on no selection data 59 | return { begin: 0, end: 0 }; 60 | }; 61 | 62 | // 63 | // Set the caret position at a specified location 64 | // 65 | inptSel.set = function (el, pos) { 66 | // Normalize pos 67 | if (typeof pos !== 'object') { 68 | pos = { begin: pos, end: pos }; 69 | } 70 | 71 | // If normal browser 72 | if (el.setSelectionRange) { 73 | el.focus(); 74 | el.setSelectionRange(pos.begin, pos.end); 75 | 76 | // IE = TextRange fun 77 | } else if (el.createTextRange) { 78 | var range = el.createTextRange(); 79 | range.collapse(true); 80 | range.moveEnd('character', pos.end); 81 | range.moveStart('character', pos.begin); 82 | range.select(); 83 | } 84 | }; 85 | 86 | 87 | // Expose 88 | return inptSel; 89 | 90 | 91 | }); -------------------------------------------------------------------------------- /dist/amd/pattern-matcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern-matcher.js 3 | * 4 | * Parses a pattern specification and determines appropriate pattern for an 5 | * input string 6 | * 7 | */ 8 | 9 | 10 | define([ 11 | './pattern', 12 | './utils' 13 | ], function (pattern, utils) { 14 | 15 | 16 | // 17 | // Parse a matcher string into a RegExp. Accepts valid regular 18 | // expressions and the catchall '*'. 19 | // @private 20 | // 21 | var parseMatcher = function (matcher) { 22 | if (matcher === '*') { 23 | return /.*/; 24 | } 25 | return new RegExp(matcher); 26 | }; 27 | 28 | // 29 | // Parse a pattern spec and return a function that returns a pattern 30 | // based on user input. The first matching pattern will be chosen. 31 | // Pattern spec format: 32 | // Array [ 33 | // Object: { Matcher(RegExp String) : Pattern(Pattern String) }, 34 | // ... 35 | // ] 36 | function patternMatcher (patternSpec) { 37 | var matchers = [], 38 | patterns = []; 39 | 40 | // Iterate over each pattern in order. 41 | utils.forEach(patternSpec, function (patternMatcher) { 42 | // Process single property object to obtain pattern and matcher. 43 | utils.forEach(patternMatcher, function (patternStr, matcherStr) { 44 | var parsedPattern = pattern.parse(patternStr), 45 | regExpMatcher = parseMatcher(matcherStr); 46 | 47 | matchers.push(regExpMatcher); 48 | patterns.push(parsedPattern); 49 | 50 | // Stop after one iteration. 51 | return false; 52 | }); 53 | }); 54 | 55 | var getPattern = function (input) { 56 | var matchedIndex; 57 | utils.forEach(matchers, function (matcher, index) { 58 | if (matcher.test(input)) { 59 | matchedIndex = index; 60 | return false; 61 | } 62 | }); 63 | 64 | return matchedIndex === undefined ? null : patterns[matchedIndex]; 65 | }; 66 | 67 | return { 68 | getPattern: getPattern, 69 | patterns: patterns, 70 | matchers: matchers 71 | }; 72 | } 73 | 74 | 75 | // Expose 76 | return patternMatcher; 77 | 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /dist/amd/pattern.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern.js 3 | * 4 | * Utilities to parse str pattern and return info 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var pattern = {}; 14 | 15 | // Match information 16 | var DELIM_SIZE = 4; 17 | 18 | // Our regex used to parse 19 | var regexp = new RegExp('{{([^}]+)}}', 'g'); 20 | 21 | // 22 | // Helper method to parse pattern str 23 | // 24 | var getMatches = function (pattern) { 25 | // Populate array of matches 26 | var matches = [], 27 | match; 28 | while(match = regexp.exec(pattern)) { 29 | matches.push(match); 30 | } 31 | 32 | return matches; 33 | }; 34 | 35 | // 36 | // Create an object holding all formatted characters 37 | // with corresponding positions 38 | // 39 | pattern.parse = function (pattern) { 40 | // Our obj to populate 41 | var info = { inpts: {}, chars: {} }; 42 | 43 | // Pattern information 44 | var matches = getMatches(pattern), 45 | pLength = pattern.length; 46 | 47 | // Counters 48 | var mCount = 0, 49 | iCount = 0, 50 | i = 0; 51 | 52 | // Add inpts, move to end of match, and process 53 | var processMatch = function (val) { 54 | var valLength = val.length; 55 | for (var j = 0; j < valLength; j++) { 56 | info.inpts[iCount] = val.charAt(j); 57 | iCount++; 58 | } 59 | mCount ++; 60 | i += (val.length + DELIM_SIZE - 1); 61 | }; 62 | 63 | // Process match or add chars 64 | for (i; i < pLength; i++) { 65 | if (mCount < matches.length && i === matches[mCount].index) { 66 | processMatch(matches[mCount][1]); 67 | } else { 68 | info.chars[i - (mCount * DELIM_SIZE)] = pattern.charAt(i); 69 | } 70 | } 71 | 72 | // Set mLength and return 73 | info.mLength = i - (mCount * DELIM_SIZE); 74 | return info; 75 | }; 76 | 77 | 78 | // Expose 79 | return pattern; 80 | 81 | 82 | }); -------------------------------------------------------------------------------- /dist/amd/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * utils.js 3 | * 4 | * Independent helper methods (cross browser, etc..) 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var utils = {}; 14 | 15 | // Useragent info for keycode handling 16 | var uAgent = (typeof navigator !== 'undefined') ? navigator.userAgent : null; 17 | 18 | // 19 | // Shallow copy properties from n objects to destObj 20 | // 21 | utils.extend = function (destObj) { 22 | for (var i = 1; i < arguments.length; i++) { 23 | for (var key in arguments[i]) { 24 | destObj[key] = arguments[i][key]; 25 | } 26 | } 27 | return destObj; 28 | }; 29 | 30 | // 31 | // Add a given character to a string at a defined pos 32 | // 33 | utils.addChars = function (str, chars, pos) { 34 | return str.substr(0, pos) + chars + str.substr(pos, str.length); 35 | }; 36 | 37 | // 38 | // Remove a span of characters 39 | // 40 | utils.removeChars = function (str, start, end) { 41 | return str.substr(0, start) + str.substr(end, str.length); 42 | }; 43 | 44 | // 45 | // Return true/false is num false between bounds 46 | // 47 | utils.isBetween = function (num, bounds) { 48 | bounds.sort(function(a,b) { return a-b; }); 49 | return (num > bounds[0] && num < bounds[1]); 50 | }; 51 | 52 | // 53 | // Helper method for cross browser event listeners 54 | // 55 | utils.addListener = function (el, evt, handler) { 56 | return (typeof el.addEventListener !== 'undefined') 57 | ? el.addEventListener(evt, handler, false) 58 | : el.attachEvent('on' + evt, handler); 59 | }; 60 | 61 | // 62 | // Helper method for cross browser implementation of preventDefault 63 | // 64 | utils.preventDefault = function (evt) { 65 | return (evt.preventDefault) ? evt.preventDefault() : (evt.returnValue = false); 66 | }; 67 | 68 | // 69 | // Helper method for cross browser implementation for grabbing 70 | // clipboard data 71 | // 72 | utils.getClip = function (evt) { 73 | if (evt.clipboardData) { return evt.clipboardData.getData('Text'); } 74 | if (window.clipboardData) { return window.clipboardData.getData('Text'); } 75 | }; 76 | 77 | // 78 | // Loop over object and checking for matching properties 79 | // 80 | utils.getMatchingKey = function (which, keyCode, keys) { 81 | // Loop over and return if matched. 82 | for (var k in keys) { 83 | var key = keys[k]; 84 | if (which === key.which && keyCode === key.keyCode) { 85 | return k; 86 | } 87 | } 88 | }; 89 | 90 | // 91 | // Returns true/false if k is a del keyDown 92 | // 93 | utils.isDelKeyDown = function (which, keyCode) { 94 | var keys = { 95 | 'backspace': { 'which': 8, 'keyCode': 8 }, 96 | 'delete': { 'which': 46, 'keyCode': 46 } 97 | }; 98 | 99 | return utils.getMatchingKey(which, keyCode, keys); 100 | }; 101 | 102 | // 103 | // Returns true/false if k is a del keyPress 104 | // 105 | utils.isDelKeyPress = function (which, keyCode) { 106 | var keys = { 107 | 'backspace': { 'which': 8, 'keyCode': 8, 'shiftKey': false }, 108 | 'delete': { 'which': 0, 'keyCode': 46 } 109 | }; 110 | 111 | return utils.getMatchingKey(which, keyCode, keys); 112 | }; 113 | 114 | // // 115 | // // Determine if keydown relates to specialKey 116 | // // 117 | // utils.isSpecialKeyDown = function (which, keyCode) { 118 | // var keys = { 119 | // 'tab': { 'which': 9, 'keyCode': 9 }, 120 | // 'enter': { 'which': 13, 'keyCode': 13 }, 121 | // 'end': { 'which': 35, 'keyCode': 35 }, 122 | // 'home': { 'which': 36, 'keyCode': 36 }, 123 | // 'leftarrow': { 'which': 37, 'keyCode': 37 }, 124 | // 'uparrow': { 'which': 38, 'keyCode': 38 }, 125 | // 'rightarrow': { 'which': 39, 'keyCode': 39 }, 126 | // 'downarrow': { 'which': 40, 'keyCode': 40 }, 127 | // 'F5': { 'which': 116, 'keyCode': 116 } 128 | // }; 129 | 130 | // return utils.getMatchingKey(which, keyCode, keys); 131 | // }; 132 | 133 | // 134 | // Determine if keypress relates to specialKey 135 | // 136 | utils.isSpecialKeyPress = function (which, keyCode) { 137 | var keys = { 138 | 'tab': { 'which': 0, 'keyCode': 9 }, 139 | 'enter': { 'which': 13, 'keyCode': 13 }, 140 | 'end': { 'which': 0, 'keyCode': 35 }, 141 | 'home': { 'which': 0, 'keyCode': 36 }, 142 | 'leftarrow': { 'which': 0, 'keyCode': 37 }, 143 | 'uparrow': { 'which': 0, 'keyCode': 38 }, 144 | 'rightarrow': { 'which': 0, 'keyCode': 39 }, 145 | 'downarrow': { 'which': 0, 'keyCode': 40 }, 146 | 'F5': { 'which': 116, 'keyCode': 116 } 147 | }; 148 | 149 | return utils.getMatchingKey(which, keyCode, keys); 150 | }; 151 | 152 | // 153 | // Returns true/false if modifier key is held down 154 | // 155 | utils.isModifier = function (evt) { 156 | return evt.ctrlKey || evt.altKey || evt.metaKey; 157 | }; 158 | 159 | // 160 | // Iterates over each property of object or array. 161 | // 162 | utils.forEach = function (collection, callback, thisArg) { 163 | if (collection.hasOwnProperty('length')) { 164 | for (var index = 0, len = collection.length; index < len; index++) { 165 | if (callback.call(thisArg, collection[index], index, collection) === false) { 166 | break; 167 | } 168 | } 169 | } else { 170 | for (var key in collection) { 171 | if (collection.hasOwnProperty(key)) { 172 | if (callback.call(thisArg, collection[key], key, collection) === false) { 173 | break; 174 | } 175 | } 176 | } 177 | } 178 | }; 179 | 180 | 181 | // Expose 182 | return utils; 183 | 184 | 185 | }); -------------------------------------------------------------------------------- /dist/common/formatter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * formatter.js 3 | * 4 | * Class used to format input based on passed pattern 5 | * 6 | */ 7 | 8 | var patternMatcher = require('./pattern-matcher'); 9 | var inptSel = require('./inpt-sel'); 10 | var utils = require('./utils'); 11 | 12 | 13 | // Defaults 14 | var defaults = { 15 | persistent: false, 16 | repeat: false, 17 | placeholder: ' ' 18 | }; 19 | 20 | // Regexs for input validation 21 | var inptRegs = { 22 | '9': /[0-9]/, 23 | 'a': /[A-Za-z]/, 24 | 'A': /[A-Z]/, 25 | '?': /[A-Z0-9]/, 26 | '*': /[A-Za-z0-9]/ 27 | }; 28 | 29 | // 30 | // Class Constructor - Called with new Formatter(el, opts) 31 | // Responsible for setting up required instance variables, and 32 | // attaching the event listener to the element. 33 | // 34 | function Formatter(el, opts) { 35 | // Cache this 36 | var self = this; 37 | 38 | // Make sure we have an element. Make accesible to instance 39 | self.el = el; 40 | if (!self.el) { 41 | throw new TypeError('Must provide an existing element'); 42 | } 43 | 44 | // Merge opts with defaults 45 | self.opts = utils.extend({}, defaults, opts); 46 | 47 | // 1 pattern is special case 48 | if (typeof self.opts.pattern !== 'undefined') { 49 | self.opts.patterns = self._specFromSinglePattern(self.opts.pattern); 50 | delete self.opts.pattern; 51 | } 52 | 53 | // Make sure we have valid opts 54 | if (typeof self.opts.patterns === 'undefined') { 55 | throw new TypeError('Must provide a pattern or array of patterns'); 56 | } 57 | 58 | self.patternMatcher = patternMatcher(self.opts.patterns); 59 | 60 | // Upate pattern with initial value 61 | self._updatePattern(); 62 | 63 | // Init values 64 | self.hldrs = {}; 65 | self.focus = 0; 66 | 67 | // Add Listeners 68 | utils.addListener(self.el, 'keydown', function (evt) { 69 | self._keyDown(evt); 70 | }); 71 | utils.addListener(self.el, 'keypress', function (evt) { 72 | self._keyPress(evt); 73 | }); 74 | utils.addListener(self.el, 'paste', function (evt) { 75 | self._paste(evt); 76 | }); 77 | 78 | // Persistence 79 | if (self.opts.persistent) { 80 | // Format on start 81 | self._processKey('', false); 82 | self.el.blur(); 83 | 84 | // Add Listeners 85 | utils.addListener(self.el, 'focus', function (evt) { 86 | self._focus(evt); 87 | }); 88 | utils.addListener(self.el, 'click', function (evt) { 89 | self._focus(evt); 90 | }); 91 | utils.addListener(self.el, 'touchstart', function (evt) { 92 | self._focus(evt); 93 | }); 94 | } 95 | } 96 | 97 | // 98 | // @public 99 | // Add new char 100 | // 101 | Formatter.addInptType = function (chr, reg) { 102 | inptRegs[chr] = reg; 103 | }; 104 | 105 | // 106 | // @public 107 | // Apply the given pattern to the current input without moving caret. 108 | // 109 | Formatter.prototype.resetPattern = function (str) { 110 | // Update opts to hold new pattern 111 | this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns; 112 | 113 | // Get current state 114 | this.sel = inptSel.get(this.el); 115 | this.val = this.el.value; 116 | 117 | // Init values 118 | this.delta = 0; 119 | 120 | // Remove all formatted chars from val 121 | this._removeChars(); 122 | 123 | this.patternMatcher = patternMatcher(this.opts.patterns); 124 | 125 | // Update pattern 126 | var newPattern = this.patternMatcher.getPattern(this.val); 127 | this.mLength = newPattern.mLength; 128 | this.chars = newPattern.chars; 129 | this.inpts = newPattern.inpts; 130 | 131 | // Format on start 132 | this._processKey('', false, true); 133 | }; 134 | 135 | // 136 | // @private 137 | // Determine correct format pattern based on input val 138 | // 139 | Formatter.prototype._updatePattern = function () { 140 | // Determine appropriate pattern 141 | var newPattern = this.patternMatcher.getPattern(this.val); 142 | 143 | // Only update the pattern if there is an appropriate pattern for the value. 144 | // Otherwise, leave the current pattern (and likely delete the latest character.) 145 | if (newPattern) { 146 | // Get info about the given pattern 147 | this.mLength = newPattern.mLength; 148 | this.chars = newPattern.chars; 149 | this.inpts = newPattern.inpts; 150 | } 151 | }; 152 | 153 | // 154 | // @private 155 | // Handler called on all keyDown strokes. All keys trigger 156 | // this handler. Only process delete keys. 157 | // 158 | Formatter.prototype._keyDown = function (evt) { 159 | // The first thing we need is the character code 160 | var k = evt.which || evt.keyCode; 161 | 162 | // If delete key 163 | if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) { 164 | // Process the keyCode and prevent default 165 | this._processKey(null, k); 166 | return utils.preventDefault(evt); 167 | } 168 | }; 169 | 170 | // 171 | // @private 172 | // Handler called on all keyPress strokes. Only processes 173 | // character keys (as long as no modifier key is in use). 174 | // 175 | Formatter.prototype._keyPress = function (evt) { 176 | // The first thing we need is the character code 177 | var k, isSpecial; 178 | // Mozilla will trigger on special keys and assign the the value 0 179 | // We want to use that 0 rather than the keyCode it assigns. 180 | k = evt.which || evt.keyCode; 181 | isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode); 182 | 183 | // Process the keyCode and prevent default 184 | if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) { 185 | this._processKey(String.fromCharCode(k), false); 186 | return utils.preventDefault(evt); 187 | } 188 | }; 189 | 190 | // 191 | // @private 192 | // Handler called on paste event. 193 | // 194 | Formatter.prototype._paste = function (evt) { 195 | // Process the clipboard paste and prevent default 196 | this._processKey(utils.getClip(evt), false); 197 | return utils.preventDefault(evt); 198 | }; 199 | 200 | // 201 | // @private 202 | // Handle called on focus event. 203 | // 204 | Formatter.prototype._focus = function () { 205 | // Wrapped in timeout so that we can grab input selection 206 | var self = this; 207 | setTimeout(function () { 208 | // Grab selection 209 | var selection = inptSel.get(self.el); 210 | // Char check 211 | var isAfterStart = selection.end > self.focus, 212 | isFirstChar = selection.end === 0; 213 | // If clicked in front of start, refocus to start 214 | if (isAfterStart || isFirstChar) { 215 | inptSel.set(self.el, self.focus); 216 | } 217 | }, 0); 218 | }; 219 | 220 | // 221 | // @private 222 | // Using the provided key information, alter el value. 223 | // 224 | Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) { 225 | // Get current state 226 | this.sel = inptSel.get(this.el); 227 | this.val = this.el.value; 228 | 229 | // Init values 230 | this.delta = 0; 231 | 232 | // If chars were highlighted, we need to remove them 233 | if (this.sel.begin !== this.sel.end) { 234 | this.delta = (-1) * Math.abs(this.sel.begin - this.sel.end); 235 | this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end); 236 | } 237 | 238 | // Delete key (moves opposite direction) 239 | else if (delKey && delKey === 46) { 240 | this._delete(); 241 | 242 | // or Backspace and not at start 243 | } else if (delKey && this.sel.begin - 1 >= 0) { 244 | 245 | // Always have a delta of at least -1 for the character being deleted. 246 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 247 | this.delta -= 1; 248 | 249 | // or Backspace and at start - exit 250 | } else if (delKey) { 251 | return true; 252 | } 253 | 254 | // If the key is not a del key, it should convert to a str 255 | if (!delKey) { 256 | // Add char at position and increment delta 257 | this.val = utils.addChars(this.val, chars, this.sel.begin); 258 | this.delta += chars.length; 259 | } 260 | 261 | // Format el.value (also handles updating caret position) 262 | this._formatValue(ignoreCaret); 263 | }; 264 | 265 | // 266 | // @private 267 | // Deletes the character in front of it 268 | // 269 | Formatter.prototype._delete = function () { 270 | // Adjust focus to make sure its not on a formatted char 271 | while (this.chars[this.sel.begin]) { 272 | this._nextPos(); 273 | } 274 | 275 | // As long as we are not at the end 276 | if (this.sel.begin < this.val.length) { 277 | // We will simulate a delete by moving the caret to the next char 278 | // and then deleting 279 | this._nextPos(); 280 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 281 | this.delta = -1; 282 | } 283 | }; 284 | 285 | // 286 | // @private 287 | // Quick helper method to move the caret to the next pos 288 | // 289 | Formatter.prototype._nextPos = function () { 290 | this.sel.end ++; 291 | this.sel.begin ++; 292 | }; 293 | 294 | // 295 | // @private 296 | // Alter element value to display characters matching the provided 297 | // instance pattern. Also responsible for updating 298 | // 299 | Formatter.prototype._formatValue = function (ignoreCaret) { 300 | // Set caret pos 301 | this.newPos = this.sel.end + this.delta; 302 | 303 | // Remove all formatted chars from val 304 | this._removeChars(); 305 | 306 | // Switch to first matching pattern based on val 307 | this._updatePattern(); 308 | 309 | // Validate inputs 310 | this._validateInpts(); 311 | 312 | // Add formatted characters 313 | this._addChars(); 314 | 315 | // Set value and adhere to maxLength 316 | this.el.value = this.val.substr(0, this.mLength); 317 | 318 | // Set new caret position 319 | if ((typeof ignoreCaret) === 'undefined' || ignoreCaret === false) { 320 | inptSel.set(this.el, this.newPos); 321 | } 322 | }; 323 | 324 | // 325 | // @private 326 | // Remove all formatted before and after a specified pos 327 | // 328 | Formatter.prototype._removeChars = function () { 329 | // Delta shouldn't include placeholders 330 | if (this.sel.end > this.focus) { 331 | this.delta += this.sel.end - this.focus; 332 | } 333 | 334 | // Account for shifts during removal 335 | var shift = 0; 336 | 337 | // Loop through all possible char positions 338 | for (var i = 0; i <= this.mLength; i++) { 339 | // Get transformed position 340 | var curChar = this.chars[i], 341 | curHldr = this.hldrs[i], 342 | pos = i + shift, 343 | val; 344 | 345 | // If after selection we need to account for delta 346 | pos = (i >= this.sel.begin) ? pos + this.delta : pos; 347 | val = this.val.charAt(pos); 348 | // Remove char and account for shift 349 | if (curChar && curChar === val || curHldr && curHldr === val) { 350 | this.val = utils.removeChars(this.val, pos, pos + 1); 351 | shift--; 352 | } 353 | } 354 | 355 | // All hldrs should be removed now 356 | this.hldrs = {}; 357 | 358 | // Set focus to last character 359 | this.focus = this.val.length; 360 | }; 361 | 362 | // 363 | // @private 364 | // Make sure all inpts are valid, else remove and update delta 365 | // 366 | Formatter.prototype._validateInpts = function () { 367 | // Loop over each char and validate 368 | for (var i = 0; i < this.val.length; i++) { 369 | // Get char inpt type 370 | var inptType = this.inpts[i]; 371 | 372 | // When only allowing capitals, ensure this char is capitalized! 373 | if (inptType === '?' || inptType === 'A'){ 374 | var up = this.val.charAt(i).toUpperCase(); 375 | this.val = utils.addChars(utils.removeChars(this.val, i, i+1), up, i); 376 | } 377 | 378 | // Checks 379 | var isBadType = !inptRegs[inptType], 380 | isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), 381 | inBounds = this.inpts[i]; 382 | 383 | // Remove if incorrect and inbounds 384 | if ((isBadType || isInvalid) && inBounds) { 385 | this.val = utils.removeChars(this.val, i, i + 1); 386 | this.focusStart--; 387 | this.newPos--; 388 | this.delta--; 389 | i--; 390 | } 391 | } 392 | }; 393 | 394 | // 395 | // @private 396 | // Loop over val and add formatted chars as necessary 397 | // 398 | Formatter.prototype._addChars = function () { 399 | if (this.opts.persistent) { 400 | // Loop over all possible characters 401 | for (var i = 0; i <= this.mLength; i++) { 402 | if (!this.val.charAt(i)) { 403 | // Add placeholder at pos 404 | this.val = utils.addChars(this.val, this.opts.placeholder, i); 405 | this.hldrs[i] = this.opts.placeholder; 406 | } 407 | this._addChar(i); 408 | } 409 | 410 | // Adjust focus to make sure its not on a formatted char 411 | while (this.chars[this.focus]) { 412 | this.focus++; 413 | } 414 | } else { 415 | // Avoid caching val.length, as they may change in _addChar. 416 | for (var j = 0; j <= this.val.length; j++) { 417 | // When moving backwards there are some race conditions where we 418 | // dont want to add the character 419 | if (this.delta <= 0 && (j === this.focus)) { return true; } 420 | 421 | // Place character in current position of the formatted string. 422 | this._addChar(j); 423 | } 424 | } 425 | }; 426 | 427 | // 428 | // @private 429 | // Add formattted char at position 430 | // 431 | Formatter.prototype._addChar = function (i) { 432 | // If char exists at position 433 | var chr = this.chars[i]; 434 | if (!chr) { return true; } 435 | 436 | // If chars are added in between the old pos and new pos 437 | // we need to increment pos and delta 438 | if (utils.isBetween(i, [this.sel.begin -1, this.newPos +1])) { 439 | this.newPos ++; 440 | this.delta ++; 441 | } 442 | 443 | // If character added before focus, incr 444 | if (i <= this.focus) { 445 | this.focus++; 446 | } 447 | 448 | // Updateholder 449 | if (this.hldrs[i]) { 450 | delete this.hldrs[i]; 451 | this.hldrs[i + 1] = this.opts.placeholder; 452 | } 453 | 454 | // Update value 455 | this.val = utils.addChars(this.val, chr, i); 456 | }; 457 | 458 | // 459 | // @private 460 | // Create a patternSpec for passing into patternMatcher that 461 | // has exactly one catch all pattern. 462 | // 463 | Formatter.prototype._specFromSinglePattern = function (patternStr) { 464 | return [{ '*': patternStr }]; 465 | }; 466 | 467 | 468 | // Expose 469 | module.exports = Formatter; 470 | 471 | 472 | 473 | -------------------------------------------------------------------------------- /dist/common/inpt-sel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * inpt-sel.js 3 | * 4 | * Cross browser implementation to get and set input selections 5 | * 6 | */ 7 | 8 | 9 | 10 | 11 | 12 | // Define module 13 | var inptSel = {}; 14 | 15 | // 16 | // Get begin and end positions of selected input. Return 0's 17 | // if there is no selectiion data 18 | // 19 | inptSel.get = function (el) { 20 | // If normal browser return with result 21 | if (typeof el.selectionStart === 'number') { 22 | return { 23 | begin: el.selectionStart, 24 | end: el.selectionEnd 25 | }; 26 | } 27 | 28 | // Uh-Oh. We must be IE. Fun with TextRange!! 29 | var range = document.selection.createRange(); 30 | // Determine if there is a selection 31 | if (range && range.parentElement() === el) { 32 | var inputRange = el.createTextRange(), 33 | endRange = el.createTextRange(), 34 | length = el.value.length; 35 | 36 | // Create a working TextRange for the input selection 37 | inputRange.moveToBookmark(range.getBookmark()); 38 | 39 | // Move endRange begin pos to end pos (hence endRange) 40 | endRange.collapse(false); 41 | 42 | // If we are at the very end of the input, begin and end 43 | // must both be the length of the el.value 44 | if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) { 45 | return { begin: length, end: length }; 46 | } 47 | 48 | // Note: moveStart usually returns the units moved, which 49 | // one may think is -length, however, it will stop when it 50 | // gets to the begin of the range, thus giving us the 51 | // negative value of the pos. 52 | return { 53 | begin: -inputRange.moveStart('character', -length), 54 | end: -inputRange.moveEnd('character', -length) 55 | }; 56 | } 57 | 58 | //Return 0's on no selection data 59 | return { begin: 0, end: 0 }; 60 | }; 61 | 62 | // 63 | // Set the caret position at a specified location 64 | // 65 | inptSel.set = function (el, pos) { 66 | // Normalize pos 67 | if (typeof pos !== 'object') { 68 | pos = { begin: pos, end: pos }; 69 | } 70 | 71 | // If normal browser 72 | if (el.setSelectionRange) { 73 | el.focus(); 74 | el.setSelectionRange(pos.begin, pos.end); 75 | 76 | // IE = TextRange fun 77 | } else if (el.createTextRange) { 78 | var range = el.createTextRange(); 79 | range.collapse(true); 80 | range.moveEnd('character', pos.end); 81 | range.moveStart('character', pos.begin); 82 | range.select(); 83 | } 84 | }; 85 | 86 | 87 | // Expose 88 | module.exports = inptSel; 89 | 90 | 91 | -------------------------------------------------------------------------------- /dist/common/pattern-matcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern-matcher.js 3 | * 4 | * Parses a pattern specification and determines appropriate pattern for an 5 | * input string 6 | * 7 | */ 8 | 9 | 10 | var pattern = require('./pattern'); 11 | var utils = require('./utils'); 12 | 13 | 14 | // 15 | // Parse a matcher string into a RegExp. Accepts valid regular 16 | // expressions and the catchall '*'. 17 | // @private 18 | // 19 | var parseMatcher = function (matcher) { 20 | if (matcher === '*') { 21 | return /.*/; 22 | } 23 | return new RegExp(matcher); 24 | }; 25 | 26 | // 27 | // Parse a pattern spec and return a function that returns a pattern 28 | // based on user input. The first matching pattern will be chosen. 29 | // Pattern spec format: 30 | // Array [ 31 | // Object: { Matcher(RegExp String) : Pattern(Pattern String) }, 32 | // ... 33 | // ] 34 | function patternMatcher (patternSpec) { 35 | var matchers = [], 36 | patterns = []; 37 | 38 | // Iterate over each pattern in order. 39 | utils.forEach(patternSpec, function (patternMatcher) { 40 | // Process single property object to obtain pattern and matcher. 41 | utils.forEach(patternMatcher, function (patternStr, matcherStr) { 42 | var parsedPattern = pattern.parse(patternStr), 43 | regExpMatcher = parseMatcher(matcherStr); 44 | 45 | matchers.push(regExpMatcher); 46 | patterns.push(parsedPattern); 47 | 48 | // Stop after one iteration. 49 | return false; 50 | }); 51 | }); 52 | 53 | var getPattern = function (input) { 54 | var matchedIndex; 55 | utils.forEach(matchers, function (matcher, index) { 56 | if (matcher.test(input)) { 57 | matchedIndex = index; 58 | return false; 59 | } 60 | }); 61 | 62 | return matchedIndex === undefined ? null : patterns[matchedIndex]; 63 | }; 64 | 65 | return { 66 | getPattern: getPattern, 67 | patterns: patterns, 68 | matchers: matchers 69 | }; 70 | } 71 | 72 | 73 | // Expose 74 | module.exports = patternMatcher; 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /dist/common/pattern.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern.js 3 | * 4 | * Utilities to parse str pattern and return info 5 | * 6 | */ 7 | 8 | 9 | 10 | 11 | 12 | // Define module 13 | var pattern = {}; 14 | 15 | // Match information 16 | var DELIM_SIZE = 4; 17 | 18 | // Our regex used to parse 19 | var regexp = new RegExp('{{([^}]+)}}', 'g'); 20 | 21 | // 22 | // Helper method to parse pattern str 23 | // 24 | var getMatches = function (pattern) { 25 | // Populate array of matches 26 | var matches = [], 27 | match; 28 | while(match = regexp.exec(pattern)) { 29 | matches.push(match); 30 | } 31 | 32 | return matches; 33 | }; 34 | 35 | // 36 | // Create an object holding all formatted characters 37 | // with corresponding positions 38 | // 39 | pattern.parse = function (pattern) { 40 | // Our obj to populate 41 | var info = { inpts: {}, chars: {} }; 42 | 43 | // Pattern information 44 | var matches = getMatches(pattern), 45 | pLength = pattern.length; 46 | 47 | // Counters 48 | var mCount = 0, 49 | iCount = 0, 50 | i = 0; 51 | 52 | // Add inpts, move to end of match, and process 53 | var processMatch = function (val) { 54 | var valLength = val.length; 55 | for (var j = 0; j < valLength; j++) { 56 | info.inpts[iCount] = val.charAt(j); 57 | iCount++; 58 | } 59 | mCount ++; 60 | i += (val.length + DELIM_SIZE - 1); 61 | }; 62 | 63 | // Process match or add chars 64 | for (i; i < pLength; i++) { 65 | if (mCount < matches.length && i === matches[mCount].index) { 66 | processMatch(matches[mCount][1]); 67 | } else { 68 | info.chars[i - (mCount * DELIM_SIZE)] = pattern.charAt(i); 69 | } 70 | } 71 | 72 | // Set mLength and return 73 | info.mLength = i - (mCount * DELIM_SIZE); 74 | return info; 75 | }; 76 | 77 | 78 | // Expose 79 | module.exports = pattern; 80 | 81 | 82 | -------------------------------------------------------------------------------- /dist/common/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * utils.js 3 | * 4 | * Independent helper methods (cross browser, etc..) 5 | * 6 | */ 7 | 8 | 9 | 10 | 11 | 12 | // Define module 13 | var utils = {}; 14 | 15 | // Useragent info for keycode handling 16 | var uAgent = (typeof navigator !== 'undefined') ? navigator.userAgent : null; 17 | 18 | // 19 | // Shallow copy properties from n objects to destObj 20 | // 21 | utils.extend = function (destObj) { 22 | for (var i = 1; i < arguments.length; i++) { 23 | for (var key in arguments[i]) { 24 | destObj[key] = arguments[i][key]; 25 | } 26 | } 27 | return destObj; 28 | }; 29 | 30 | // 31 | // Add a given character to a string at a defined pos 32 | // 33 | utils.addChars = function (str, chars, pos) { 34 | return str.substr(0, pos) + chars + str.substr(pos, str.length); 35 | }; 36 | 37 | // 38 | // Remove a span of characters 39 | // 40 | utils.removeChars = function (str, start, end) { 41 | return str.substr(0, start) + str.substr(end, str.length); 42 | }; 43 | 44 | // 45 | // Return true/false is num false between bounds 46 | // 47 | utils.isBetween = function (num, bounds) { 48 | bounds.sort(function(a,b) { return a-b; }); 49 | return (num > bounds[0] && num < bounds[1]); 50 | }; 51 | 52 | // 53 | // Helper method for cross browser event listeners 54 | // 55 | utils.addListener = function (el, evt, handler) { 56 | return (typeof el.addEventListener !== 'undefined') 57 | ? el.addEventListener(evt, handler, false) 58 | : el.attachEvent('on' + evt, handler); 59 | }; 60 | 61 | // 62 | // Helper method for cross browser implementation of preventDefault 63 | // 64 | utils.preventDefault = function (evt) { 65 | return (evt.preventDefault) ? evt.preventDefault() : (evt.returnValue = false); 66 | }; 67 | 68 | // 69 | // Helper method for cross browser implementation for grabbing 70 | // clipboard data 71 | // 72 | utils.getClip = function (evt) { 73 | if (evt.clipboardData) { return evt.clipboardData.getData('Text'); } 74 | if (window.clipboardData) { return window.clipboardData.getData('Text'); } 75 | }; 76 | 77 | // 78 | // Loop over object and checking for matching properties 79 | // 80 | utils.getMatchingKey = function (which, keyCode, keys) { 81 | // Loop over and return if matched. 82 | for (var k in keys) { 83 | var key = keys[k]; 84 | if (which === key.which && keyCode === key.keyCode) { 85 | return k; 86 | } 87 | } 88 | }; 89 | 90 | // 91 | // Returns true/false if k is a del keyDown 92 | // 93 | utils.isDelKeyDown = function (which, keyCode) { 94 | var keys = { 95 | 'backspace': { 'which': 8, 'keyCode': 8 }, 96 | 'delete': { 'which': 46, 'keyCode': 46 } 97 | }; 98 | 99 | return utils.getMatchingKey(which, keyCode, keys); 100 | }; 101 | 102 | // 103 | // Returns true/false if k is a del keyPress 104 | // 105 | utils.isDelKeyPress = function (which, keyCode) { 106 | var keys = { 107 | 'backspace': { 'which': 8, 'keyCode': 8, 'shiftKey': false }, 108 | 'delete': { 'which': 0, 'keyCode': 46 } 109 | }; 110 | 111 | return utils.getMatchingKey(which, keyCode, keys); 112 | }; 113 | 114 | // // 115 | // // Determine if keydown relates to specialKey 116 | // // 117 | // utils.isSpecialKeyDown = function (which, keyCode) { 118 | // var keys = { 119 | // 'tab': { 'which': 9, 'keyCode': 9 }, 120 | // 'enter': { 'which': 13, 'keyCode': 13 }, 121 | // 'end': { 'which': 35, 'keyCode': 35 }, 122 | // 'home': { 'which': 36, 'keyCode': 36 }, 123 | // 'leftarrow': { 'which': 37, 'keyCode': 37 }, 124 | // 'uparrow': { 'which': 38, 'keyCode': 38 }, 125 | // 'rightarrow': { 'which': 39, 'keyCode': 39 }, 126 | // 'downarrow': { 'which': 40, 'keyCode': 40 }, 127 | // 'F5': { 'which': 116, 'keyCode': 116 } 128 | // }; 129 | 130 | // return utils.getMatchingKey(which, keyCode, keys); 131 | // }; 132 | 133 | // 134 | // Determine if keypress relates to specialKey 135 | // 136 | utils.isSpecialKeyPress = function (which, keyCode) { 137 | var keys = { 138 | 'tab': { 'which': 0, 'keyCode': 9 }, 139 | 'enter': { 'which': 13, 'keyCode': 13 }, 140 | 'end': { 'which': 0, 'keyCode': 35 }, 141 | 'home': { 'which': 0, 'keyCode': 36 }, 142 | 'leftarrow': { 'which': 0, 'keyCode': 37 }, 143 | 'uparrow': { 'which': 0, 'keyCode': 38 }, 144 | 'rightarrow': { 'which': 0, 'keyCode': 39 }, 145 | 'downarrow': { 'which': 0, 'keyCode': 40 }, 146 | 'F5': { 'which': 116, 'keyCode': 116 } 147 | }; 148 | 149 | return utils.getMatchingKey(which, keyCode, keys); 150 | }; 151 | 152 | // 153 | // Returns true/false if modifier key is held down 154 | // 155 | utils.isModifier = function (evt) { 156 | return evt.ctrlKey || evt.altKey || evt.metaKey; 157 | }; 158 | 159 | // 160 | // Iterates over each property of object or array. 161 | // 162 | utils.forEach = function (collection, callback, thisArg) { 163 | if (collection.hasOwnProperty('length')) { 164 | for (var index = 0, len = collection.length; index < len; index++) { 165 | if (callback.call(thisArg, collection[index], index, collection) === false) { 166 | break; 167 | } 168 | } 169 | } else { 170 | for (var key in collection) { 171 | if (collection.hasOwnProperty(key)) { 172 | if (callback.call(thisArg, collection[key], key, collection) === false) { 173 | break; 174 | } 175 | } 176 | } 177 | } 178 | }; 179 | 180 | 181 | // Expose 182 | module.exports = utils; 183 | 184 | 185 | -------------------------------------------------------------------------------- /dist/formatter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * v0.1.8 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 | // 11 | // Uses Node, AMD or browser globals to create a module. This example creates 12 | // a global even when AMD is used. This is useful if you have some scripts 13 | // that are loaded by an AMD loader, but they still want access to globals. 14 | // If you do not need to export a global for the AMD case, 15 | // see returnExports.js. 16 | // 17 | // If you want something that will work in other stricter CommonJS environments, 18 | // or if you need to create a circular dependency, see commonJsStrictGlobal.js 19 | // 20 | // Defines a module "returnExportsGlobal" that depends another module called 21 | // "b". Note that the name of the module is implied by the file name. It is 22 | // best if the file name and the exported global have matching names. 23 | // 24 | // If the 'b' module also uses this type of boilerplate, then 25 | // in the browser, it will create a global .b that is used below. 26 | // 27 | (function (root, factory) { 28 | if (typeof define === 'function' && define.amd) { 29 | // AMD. Register as an anonymous module. 30 | define([], function () { 31 | return (root.returnExportsGlobal = factory()); 32 | }); 33 | } else if (typeof exports === 'object') { 34 | // Node. Does not work with strict CommonJS, but 35 | // only CommonJS-like enviroments that support module.exports, 36 | // like Node. 37 | module.exports = factory(); 38 | } else { 39 | root['Formatter'] = factory(); 40 | } 41 | }(this, function () { 42 | 43 | 44 | /* 45 | * pattern.js 46 | * 47 | * Utilities to parse str pattern and return info 48 | * 49 | */ 50 | var pattern = function () { 51 | // Define module 52 | var pattern = {}; 53 | // Match information 54 | var DELIM_SIZE = 4; 55 | // Our regex used to parse 56 | var regexp = new RegExp('{{([^}]+)}}', 'g'); 57 | // 58 | // Helper method to parse pattern str 59 | // 60 | var getMatches = function (pattern) { 61 | // Populate array of matches 62 | var matches = [], match; 63 | while (match = regexp.exec(pattern)) { 64 | matches.push(match); 65 | } 66 | return matches; 67 | }; 68 | // 69 | // Create an object holding all formatted characters 70 | // with corresponding positions 71 | // 72 | pattern.parse = function (pattern) { 73 | // Our obj to populate 74 | var info = { 75 | inpts: {}, 76 | chars: {} 77 | }; 78 | // Pattern information 79 | var matches = getMatches(pattern), pLength = pattern.length; 80 | // Counters 81 | var mCount = 0, iCount = 0, i = 0; 82 | // Add inpts, move to end of match, and process 83 | var processMatch = function (val) { 84 | var valLength = val.length; 85 | for (var j = 0; j < valLength; j++) { 86 | info.inpts[iCount] = val.charAt(j); 87 | iCount++; 88 | } 89 | mCount++; 90 | i += val.length + DELIM_SIZE - 1; 91 | }; 92 | // Process match or add chars 93 | for (i; i < pLength; i++) { 94 | if (mCount < matches.length && i === matches[mCount].index) { 95 | processMatch(matches[mCount][1]); 96 | } else { 97 | info.chars[i - mCount * DELIM_SIZE] = pattern.charAt(i); 98 | } 99 | } 100 | // Set mLength and return 101 | info.mLength = i - mCount * DELIM_SIZE; 102 | return info; 103 | }; 104 | // Expose 105 | return pattern; 106 | }(); 107 | /* 108 | * utils.js 109 | * 110 | * Independent helper methods (cross browser, etc..) 111 | * 112 | */ 113 | var utils = function () { 114 | // Define module 115 | var utils = {}; 116 | // Useragent info for keycode handling 117 | var uAgent = typeof navigator !== 'undefined' ? navigator.userAgent : null; 118 | // 119 | // Shallow copy properties from n objects to destObj 120 | // 121 | utils.extend = function (destObj) { 122 | for (var i = 1; i < arguments.length; i++) { 123 | for (var key in arguments[i]) { 124 | destObj[key] = arguments[i][key]; 125 | } 126 | } 127 | return destObj; 128 | }; 129 | // 130 | // Add a given character to a string at a defined pos 131 | // 132 | utils.addChars = function (str, chars, pos) { 133 | return str.substr(0, pos) + chars + str.substr(pos, str.length); 134 | }; 135 | // 136 | // Remove a span of characters 137 | // 138 | utils.removeChars = function (str, start, end) { 139 | return str.substr(0, start) + str.substr(end, str.length); 140 | }; 141 | // 142 | // Return true/false is num false between bounds 143 | // 144 | utils.isBetween = function (num, bounds) { 145 | bounds.sort(function (a, b) { 146 | return a - b; 147 | }); 148 | return num > bounds[0] && num < bounds[1]; 149 | }; 150 | // 151 | // Helper method for cross browser event listeners 152 | // 153 | utils.addListener = function (el, evt, handler) { 154 | return typeof el.addEventListener !== 'undefined' ? el.addEventListener(evt, handler, false) : el.attachEvent('on' + evt, handler); 155 | }; 156 | // 157 | // Helper method for cross browser implementation of preventDefault 158 | // 159 | utils.preventDefault = function (evt) { 160 | return evt.preventDefault ? evt.preventDefault() : evt.returnValue = false; 161 | }; 162 | // 163 | // Helper method for cross browser implementation for grabbing 164 | // clipboard data 165 | // 166 | utils.getClip = function (evt) { 167 | if (evt.clipboardData) { 168 | return evt.clipboardData.getData('Text'); 169 | } 170 | if (window.clipboardData) { 171 | return window.clipboardData.getData('Text'); 172 | } 173 | }; 174 | // 175 | // Loop over object and checking for matching properties 176 | // 177 | utils.getMatchingKey = function (which, keyCode, keys) { 178 | // Loop over and return if matched. 179 | for (var k in keys) { 180 | var key = keys[k]; 181 | if (which === key.which && keyCode === key.keyCode) { 182 | return k; 183 | } 184 | } 185 | }; 186 | // 187 | // Returns true/false if k is a del keyDown 188 | // 189 | utils.isDelKeyDown = function (which, keyCode) { 190 | var keys = { 191 | 'backspace': { 192 | 'which': 8, 193 | 'keyCode': 8 194 | }, 195 | 'delete': { 196 | 'which': 46, 197 | 'keyCode': 46 198 | } 199 | }; 200 | return utils.getMatchingKey(which, keyCode, keys); 201 | }; 202 | // 203 | // Returns true/false if k is a del keyPress 204 | // 205 | utils.isDelKeyPress = function (which, keyCode) { 206 | var keys = { 207 | 'backspace': { 208 | 'which': 8, 209 | 'keyCode': 8, 210 | 'shiftKey': false 211 | }, 212 | 'delete': { 213 | 'which': 0, 214 | 'keyCode': 46 215 | } 216 | }; 217 | return utils.getMatchingKey(which, keyCode, keys); 218 | }; 219 | // // 220 | // // Determine if keydown relates to specialKey 221 | // // 222 | // utils.isSpecialKeyDown = function (which, keyCode) { 223 | // var keys = { 224 | // 'tab': { 'which': 9, 'keyCode': 9 }, 225 | // 'enter': { 'which': 13, 'keyCode': 13 }, 226 | // 'end': { 'which': 35, 'keyCode': 35 }, 227 | // 'home': { 'which': 36, 'keyCode': 36 }, 228 | // 'leftarrow': { 'which': 37, 'keyCode': 37 }, 229 | // 'uparrow': { 'which': 38, 'keyCode': 38 }, 230 | // 'rightarrow': { 'which': 39, 'keyCode': 39 }, 231 | // 'downarrow': { 'which': 40, 'keyCode': 40 }, 232 | // 'F5': { 'which': 116, 'keyCode': 116 } 233 | // }; 234 | // return utils.getMatchingKey(which, keyCode, keys); 235 | // }; 236 | // 237 | // Determine if keypress relates to specialKey 238 | // 239 | utils.isSpecialKeyPress = function (which, keyCode) { 240 | var keys = { 241 | 'tab': { 242 | 'which': 0, 243 | 'keyCode': 9 244 | }, 245 | 'enter': { 246 | 'which': 13, 247 | 'keyCode': 13 248 | }, 249 | 'end': { 250 | 'which': 0, 251 | 'keyCode': 35 252 | }, 253 | 'home': { 254 | 'which': 0, 255 | 'keyCode': 36 256 | }, 257 | 'leftarrow': { 258 | 'which': 0, 259 | 'keyCode': 37 260 | }, 261 | 'uparrow': { 262 | 'which': 0, 263 | 'keyCode': 38 264 | }, 265 | 'rightarrow': { 266 | 'which': 0, 267 | 'keyCode': 39 268 | }, 269 | 'downarrow': { 270 | 'which': 0, 271 | 'keyCode': 40 272 | }, 273 | 'F5': { 274 | 'which': 116, 275 | 'keyCode': 116 276 | } 277 | }; 278 | return utils.getMatchingKey(which, keyCode, keys); 279 | }; 280 | // 281 | // Returns true/false if modifier key is held down 282 | // 283 | utils.isModifier = function (evt) { 284 | return evt.ctrlKey || evt.altKey || evt.metaKey; 285 | }; 286 | // 287 | // Iterates over each property of object or array. 288 | // 289 | utils.forEach = function (collection, callback, thisArg) { 290 | if (collection.hasOwnProperty('length')) { 291 | for (var index = 0, len = collection.length; index < len; index++) { 292 | if (callback.call(thisArg, collection[index], index, collection) === false) { 293 | break; 294 | } 295 | } 296 | } else { 297 | for (var key in collection) { 298 | if (collection.hasOwnProperty(key)) { 299 | if (callback.call(thisArg, collection[key], key, collection) === false) { 300 | break; 301 | } 302 | } 303 | } 304 | } 305 | }; 306 | // Expose 307 | return utils; 308 | }(); 309 | /* 310 | * pattern-matcher.js 311 | * 312 | * Parses a pattern specification and determines appropriate pattern for an 313 | * input string 314 | * 315 | */ 316 | var patternMatcher = function (pattern, utils) { 317 | // 318 | // Parse a matcher string into a RegExp. Accepts valid regular 319 | // expressions and the catchall '*'. 320 | // @private 321 | // 322 | var parseMatcher = function (matcher) { 323 | if (matcher === '*') { 324 | return /.*/; 325 | } 326 | return new RegExp(matcher); 327 | }; 328 | // 329 | // Parse a pattern spec and return a function that returns a pattern 330 | // based on user input. The first matching pattern will be chosen. 331 | // Pattern spec format: 332 | // Array [ 333 | // Object: { Matcher(RegExp String) : Pattern(Pattern String) }, 334 | // ... 335 | // ] 336 | function patternMatcher(patternSpec) { 337 | var matchers = [], patterns = []; 338 | // Iterate over each pattern in order. 339 | utils.forEach(patternSpec, function (patternMatcher) { 340 | // Process single property object to obtain pattern and matcher. 341 | utils.forEach(patternMatcher, function (patternStr, matcherStr) { 342 | var parsedPattern = pattern.parse(patternStr), regExpMatcher = parseMatcher(matcherStr); 343 | matchers.push(regExpMatcher); 344 | patterns.push(parsedPattern); 345 | // Stop after one iteration. 346 | return false; 347 | }); 348 | }); 349 | var getPattern = function (input) { 350 | var matchedIndex; 351 | utils.forEach(matchers, function (matcher, index) { 352 | if (matcher.test(input)) { 353 | matchedIndex = index; 354 | return false; 355 | } 356 | }); 357 | return matchedIndex === undefined ? null : patterns[matchedIndex]; 358 | }; 359 | return { 360 | getPattern: getPattern, 361 | patterns: patterns, 362 | matchers: matchers 363 | }; 364 | } 365 | // Expose 366 | return patternMatcher; 367 | }(pattern, utils); 368 | /* 369 | * inpt-sel.js 370 | * 371 | * Cross browser implementation to get and set input selections 372 | * 373 | */ 374 | var inptSel = function () { 375 | // Define module 376 | var inptSel = {}; 377 | // 378 | // Get begin and end positions of selected input. Return 0's 379 | // if there is no selectiion data 380 | // 381 | inptSel.get = function (el) { 382 | // If normal browser return with result 383 | if (typeof el.selectionStart === 'number') { 384 | return { 385 | begin: el.selectionStart, 386 | end: el.selectionEnd 387 | }; 388 | } 389 | // Uh-Oh. We must be IE. Fun with TextRange!! 390 | var range = document.selection.createRange(); 391 | // Determine if there is a selection 392 | if (range && range.parentElement() === el) { 393 | var inputRange = el.createTextRange(), endRange = el.createTextRange(), length = el.value.length; 394 | // Create a working TextRange for the input selection 395 | inputRange.moveToBookmark(range.getBookmark()); 396 | // Move endRange begin pos to end pos (hence endRange) 397 | endRange.collapse(false); 398 | // If we are at the very end of the input, begin and end 399 | // must both be the length of the el.value 400 | if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) { 401 | return { 402 | begin: length, 403 | end: length 404 | }; 405 | } 406 | // Note: moveStart usually returns the units moved, which 407 | // one may think is -length, however, it will stop when it 408 | // gets to the begin of the range, thus giving us the 409 | // negative value of the pos. 410 | return { 411 | begin: -inputRange.moveStart('character', -length), 412 | end: -inputRange.moveEnd('character', -length) 413 | }; 414 | } 415 | //Return 0's on no selection data 416 | return { 417 | begin: 0, 418 | end: 0 419 | }; 420 | }; 421 | // 422 | // Set the caret position at a specified location 423 | // 424 | inptSel.set = function (el, pos) { 425 | // Normalize pos 426 | if (typeof pos !== 'object') { 427 | pos = { 428 | begin: pos, 429 | end: pos 430 | }; 431 | } 432 | // If normal browser 433 | if (el.setSelectionRange) { 434 | el.focus(); 435 | el.setSelectionRange(pos.begin, pos.end); 436 | } else if (el.createTextRange) { 437 | var range = el.createTextRange(); 438 | range.collapse(true); 439 | range.moveEnd('character', pos.end); 440 | range.moveStart('character', pos.begin); 441 | range.select(); 442 | } 443 | }; 444 | // Expose 445 | return inptSel; 446 | }(); 447 | /* 448 | * formatter.js 449 | * 450 | * Class used to format input based on passed pattern 451 | * 452 | */ 453 | var formatter = function (patternMatcher, inptSel, utils) { 454 | // Defaults 455 | var defaults = { 456 | persistent: false, 457 | repeat: false, 458 | placeholder: ' ' 459 | }; 460 | // Regexs for input validation 461 | var inptRegs = { 462 | '9': /[0-9]/, 463 | 'a': /[A-Za-z]/, 464 | 'A': /[A-Z]/, 465 | '?': /[A-Z0-9]/, 466 | '*': /[A-Za-z0-9]/ 467 | }; 468 | // 469 | // Class Constructor - Called with new Formatter(el, opts) 470 | // Responsible for setting up required instance variables, and 471 | // attaching the event listener to the element. 472 | // 473 | function Formatter(el, opts) { 474 | // Cache this 475 | var self = this; 476 | // Make sure we have an element. Make accesible to instance 477 | self.el = el; 478 | if (!self.el) { 479 | throw new TypeError('Must provide an existing element'); 480 | } 481 | // Merge opts with defaults 482 | self.opts = utils.extend({}, defaults, opts); 483 | // 1 pattern is special case 484 | if (typeof self.opts.pattern !== 'undefined') { 485 | self.opts.patterns = self._specFromSinglePattern(self.opts.pattern); 486 | delete self.opts.pattern; 487 | } 488 | // Make sure we have valid opts 489 | if (typeof self.opts.patterns === 'undefined') { 490 | throw new TypeError('Must provide a pattern or array of patterns'); 491 | } 492 | self.patternMatcher = patternMatcher(self.opts.patterns); 493 | // Upate pattern with initial value 494 | self._updatePattern(); 495 | // Init values 496 | self.hldrs = {}; 497 | self.focus = 0; 498 | // Add Listeners 499 | utils.addListener(self.el, 'keydown', function (evt) { 500 | self._keyDown(evt); 501 | }); 502 | utils.addListener(self.el, 'keypress', function (evt) { 503 | self._keyPress(evt); 504 | }); 505 | utils.addListener(self.el, 'paste', function (evt) { 506 | self._paste(evt); 507 | }); 508 | // Persistence 509 | if (self.opts.persistent) { 510 | // Format on start 511 | self._processKey('', false); 512 | self.el.blur(); 513 | // Add Listeners 514 | utils.addListener(self.el, 'focus', function (evt) { 515 | self._focus(evt); 516 | }); 517 | utils.addListener(self.el, 'click', function (evt) { 518 | self._focus(evt); 519 | }); 520 | utils.addListener(self.el, 'touchstart', function (evt) { 521 | self._focus(evt); 522 | }); 523 | } 524 | } 525 | // 526 | // @public 527 | // Add new char 528 | // 529 | Formatter.addInptType = function (chr, reg) { 530 | inptRegs[chr] = reg; 531 | }; 532 | // 533 | // @public 534 | // Apply the given pattern to the current input without moving caret. 535 | // 536 | Formatter.prototype.resetPattern = function (str) { 537 | // Update opts to hold new pattern 538 | this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns; 539 | // Get current state 540 | this.sel = inptSel.get(this.el); 541 | this.val = this.el.value; 542 | // Init values 543 | this.delta = 0; 544 | // Remove all formatted chars from val 545 | this._removeChars(); 546 | this.patternMatcher = patternMatcher(this.opts.patterns); 547 | // Update pattern 548 | var newPattern = this.patternMatcher.getPattern(this.val); 549 | this.mLength = newPattern.mLength; 550 | this.chars = newPattern.chars; 551 | this.inpts = newPattern.inpts; 552 | // Format on start 553 | this._processKey('', false, true); 554 | }; 555 | // 556 | // @private 557 | // Determine correct format pattern based on input val 558 | // 559 | Formatter.prototype._updatePattern = function () { 560 | // Determine appropriate pattern 561 | var newPattern = this.patternMatcher.getPattern(this.val); 562 | // Only update the pattern if there is an appropriate pattern for the value. 563 | // Otherwise, leave the current pattern (and likely delete the latest character.) 564 | if (newPattern) { 565 | // Get info about the given pattern 566 | this.mLength = newPattern.mLength; 567 | this.chars = newPattern.chars; 568 | this.inpts = newPattern.inpts; 569 | } 570 | }; 571 | // 572 | // @private 573 | // Handler called on all keyDown strokes. All keys trigger 574 | // this handler. Only process delete keys. 575 | // 576 | Formatter.prototype._keyDown = function (evt) { 577 | // The first thing we need is the character code 578 | var k = evt.which || evt.keyCode; 579 | // If delete key 580 | if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) { 581 | // Process the keyCode and prevent default 582 | this._processKey(null, k); 583 | return utils.preventDefault(evt); 584 | } 585 | }; 586 | // 587 | // @private 588 | // Handler called on all keyPress strokes. Only processes 589 | // character keys (as long as no modifier key is in use). 590 | // 591 | Formatter.prototype._keyPress = function (evt) { 592 | // The first thing we need is the character code 593 | var k, isSpecial; 594 | // Mozilla will trigger on special keys and assign the the value 0 595 | // We want to use that 0 rather than the keyCode it assigns. 596 | k = evt.which || evt.keyCode; 597 | isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode); 598 | // Process the keyCode and prevent default 599 | if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) { 600 | this._processKey(String.fromCharCode(k), false); 601 | return utils.preventDefault(evt); 602 | } 603 | }; 604 | // 605 | // @private 606 | // Handler called on paste event. 607 | // 608 | Formatter.prototype._paste = function (evt) { 609 | // Process the clipboard paste and prevent default 610 | this._processKey(utils.getClip(evt), false); 611 | return utils.preventDefault(evt); 612 | }; 613 | // 614 | // @private 615 | // Handle called on focus event. 616 | // 617 | Formatter.prototype._focus = function () { 618 | // Wrapped in timeout so that we can grab input selection 619 | var self = this; 620 | setTimeout(function () { 621 | // Grab selection 622 | var selection = inptSel.get(self.el); 623 | // Char check 624 | var isAfterStart = selection.end > self.focus, isFirstChar = selection.end === 0; 625 | // If clicked in front of start, refocus to start 626 | if (isAfterStart || isFirstChar) { 627 | inptSel.set(self.el, self.focus); 628 | } 629 | }, 0); 630 | }; 631 | // 632 | // @private 633 | // Using the provided key information, alter el value. 634 | // 635 | Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) { 636 | // Get current state 637 | this.sel = inptSel.get(this.el); 638 | this.val = this.el.value; 639 | // Init values 640 | this.delta = 0; 641 | // If chars were highlighted, we need to remove them 642 | if (this.sel.begin !== this.sel.end) { 643 | this.delta = -1 * Math.abs(this.sel.begin - this.sel.end); 644 | this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end); 645 | } else if (delKey && delKey === 46) { 646 | this._delete(); 647 | } else if (delKey && this.sel.begin - 1 >= 0) { 648 | // Always have a delta of at least -1 for the character being deleted. 649 | this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end); 650 | this.delta -= 1; 651 | } else if (delKey) { 652 | return true; 653 | } 654 | // If the key is not a del key, it should convert to a str 655 | if (!delKey) { 656 | // Add char at position and increment delta 657 | this.val = utils.addChars(this.val, chars, this.sel.begin); 658 | this.delta += chars.length; 659 | } 660 | // Format el.value (also handles updating caret position) 661 | this._formatValue(ignoreCaret); 662 | }; 663 | // 664 | // @private 665 | // Deletes the character in front of it 666 | // 667 | Formatter.prototype._delete = function () { 668 | // Adjust focus to make sure its not on a formatted char 669 | while (this.chars[this.sel.begin]) { 670 | this._nextPos(); 671 | } 672 | // As long as we are not at the end 673 | if (this.sel.begin < this.val.length) { 674 | // We will simulate a delete by moving the caret to the next char 675 | // and then deleting 676 | this._nextPos(); 677 | this.val = utils.removeChars(this.val, this.sel.end - 1, this.sel.end); 678 | this.delta = -1; 679 | } 680 | }; 681 | // 682 | // @private 683 | // Quick helper method to move the caret to the next pos 684 | // 685 | Formatter.prototype._nextPos = function () { 686 | this.sel.end++; 687 | this.sel.begin++; 688 | }; 689 | // 690 | // @private 691 | // Alter element value to display characters matching the provided 692 | // instance pattern. Also responsible for updating 693 | // 694 | Formatter.prototype._formatValue = function (ignoreCaret) { 695 | // Set caret pos 696 | this.newPos = this.sel.end + this.delta; 697 | // Remove all formatted chars from val 698 | this._removeChars(); 699 | // Switch to first matching pattern based on val 700 | this._updatePattern(); 701 | // Validate inputs 702 | this._validateInpts(); 703 | // Add formatted characters 704 | this._addChars(); 705 | // Set value and adhere to maxLength 706 | this.el.value = this.val.substr(0, this.mLength); 707 | // Set new caret position 708 | if (typeof ignoreCaret === 'undefined' || ignoreCaret === false) { 709 | inptSel.set(this.el, this.newPos); 710 | } 711 | }; 712 | // 713 | // @private 714 | // Remove all formatted before and after a specified pos 715 | // 716 | Formatter.prototype._removeChars = function () { 717 | // Delta shouldn't include placeholders 718 | if (this.sel.end > this.focus) { 719 | this.delta += this.sel.end - this.focus; 720 | } 721 | // Account for shifts during removal 722 | var shift = 0; 723 | // Loop through all possible char positions 724 | for (var i = 0; i <= this.mLength; i++) { 725 | // Get transformed position 726 | var curChar = this.chars[i], curHldr = this.hldrs[i], pos = i + shift, val; 727 | // If after selection we need to account for delta 728 | pos = i >= this.sel.begin ? pos + this.delta : pos; 729 | val = this.val.charAt(pos); 730 | // Remove char and account for shift 731 | if (curChar && curChar === val || curHldr && curHldr === val) { 732 | this.val = utils.removeChars(this.val, pos, pos + 1); 733 | shift--; 734 | } 735 | } 736 | // All hldrs should be removed now 737 | this.hldrs = {}; 738 | // Set focus to last character 739 | this.focus = this.val.length; 740 | }; 741 | // 742 | // @private 743 | // Make sure all inpts are valid, else remove and update delta 744 | // 745 | Formatter.prototype._validateInpts = function () { 746 | // Loop over each char and validate 747 | for (var i = 0; i < this.val.length; i++) { 748 | // Get char inpt type 749 | var inptType = this.inpts[i]; 750 | // When only allowing capitals, ensure this char is capitalized! 751 | if (inptType === '?' || inptType === 'A') { 752 | var up = this.val.charAt(i).toUpperCase(); 753 | this.val = utils.addChars(utils.removeChars(this.val, i, i + 1), up, i); 754 | } 755 | // Checks 756 | var isBadType = !inptRegs[inptType], isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), inBounds = this.inpts[i]; 757 | // Remove if incorrect and inbounds 758 | if ((isBadType || isInvalid) && inBounds) { 759 | this.val = utils.removeChars(this.val, i, i + 1); 760 | this.focusStart--; 761 | this.newPos--; 762 | this.delta--; 763 | i--; 764 | } 765 | } 766 | }; 767 | // 768 | // @private 769 | // Loop over val and add formatted chars as necessary 770 | // 771 | Formatter.prototype._addChars = function () { 772 | if (this.opts.persistent) { 773 | // Loop over all possible characters 774 | for (var i = 0; i <= this.mLength; i++) { 775 | if (!this.val.charAt(i)) { 776 | // Add placeholder at pos 777 | this.val = utils.addChars(this.val, this.opts.placeholder, i); 778 | this.hldrs[i] = this.opts.placeholder; 779 | } 780 | this._addChar(i); 781 | } 782 | // Adjust focus to make sure its not on a formatted char 783 | while (this.chars[this.focus]) { 784 | this.focus++; 785 | } 786 | } else { 787 | // Avoid caching val.length, as they may change in _addChar. 788 | for (var j = 0; j <= this.val.length; j++) { 789 | // When moving backwards there are some race conditions where we 790 | // dont want to add the character 791 | if (this.delta <= 0 && j === this.focus) { 792 | return true; 793 | } 794 | // Place character in current position of the formatted string. 795 | this._addChar(j); 796 | } 797 | } 798 | }; 799 | // 800 | // @private 801 | // Add formattted char at position 802 | // 803 | Formatter.prototype._addChar = function (i) { 804 | // If char exists at position 805 | var chr = this.chars[i]; 806 | if (!chr) { 807 | return true; 808 | } 809 | // If chars are added in between the old pos and new pos 810 | // we need to increment pos and delta 811 | if (utils.isBetween(i, [ 812 | this.sel.begin - 1, 813 | this.newPos + 1 814 | ])) { 815 | this.newPos++; 816 | this.delta++; 817 | } 818 | // If character added before focus, incr 819 | if (i <= this.focus) { 820 | this.focus++; 821 | } 822 | // Updateholder 823 | if (this.hldrs[i]) { 824 | delete this.hldrs[i]; 825 | this.hldrs[i + 1] = this.opts.placeholder; 826 | } 827 | // Update value 828 | this.val = utils.addChars(this.val, chr, i); 829 | }; 830 | // 831 | // @private 832 | // Create a patternSpec for passing into patternMatcher that 833 | // has exactly one catch all pattern. 834 | // 835 | Formatter.prototype._specFromSinglePattern = function (patternStr) { 836 | return [{ '*': patternStr }]; 837 | }; 838 | // Expose 839 | return Formatter; 840 | }(patternMatcher, inptSel, utils); 841 | 842 | 843 | return formatter; 844 | 845 | 846 | 847 | })); -------------------------------------------------------------------------------- /dist/formatter.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],function(){return a.returnExportsGlobal=b()}):"object"==typeof exports?module.exports=b():a.Formatter=b()}(this,function(){var a=function(){var a={},b=4,c=new RegExp("{{([^}]+)}}","g"),d=function(a){for(var b,d=[];b=c.exec(a);)d.push(b);return d};return a.parse=function(a){var c={inpts:{},chars:{}},e=d(a),f=a.length,g=0,h=0,i=0,j=function(a){for(var d=a.length,e=0;d>e;e++)c.inpts[h]=a.charAt(e),h++;g++,i+=a.length+b-1};for(i;f>i;i++)gb[0]&&ad&&b.call(c,a[d],d,a)!==!1;d++);else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)break},a}(),c=function(a,b){function c(c){var e=[],f=[];b.forEach(c,function(c){b.forEach(c,function(b,c){var g=a.parse(b),h=d(c);return e.push(h),f.push(g),!1})});var g=function(a){var c;return b.forEach(e,function(b,d){return b.test(a)?(c=d,!1):void 0}),void 0===c?null:f[c]};return{getPattern:g,patterns:f,matchers:e}}var d=function(a){return"*"===a?/.*/:new RegExp(a)};return c}(a,b),d=function(){var a={};return a.get=function(a){if("number"==typeof a.selectionStart)return{begin:a.selectionStart,end:a.selectionEnd};var b=document.selection.createRange();if(b&&b.parentElement()===a){var c=a.createTextRange(),d=a.createTextRange(),e=a.value.length;return c.moveToBookmark(b.getBookmark()),d.collapse(!1),c.compareEndPoints("StartToEnd",d)>-1?{begin:e,end:e}:{begin:-c.moveStart("character",-e),end:-c.moveEnd("character",-e)}}return{begin:0,end:0}},a.set=function(a,b){if("object"!=typeof b&&(b={begin:b,end:b}),a.setSelectionRange)a.focus(),a.setSelectionRange(b.begin,b.end);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b.end),c.moveStart("character",b.begin),c.select()}},a}(),e=function(a,b,c){function d(b,d){var f=this;if(f.el=b,!f.el)throw new TypeError("Must provide an existing element");if(f.opts=c.extend({},e,d),"undefined"!=typeof f.opts.pattern&&(f.opts.patterns=f._specFromSinglePattern(f.opts.pattern),delete f.opts.pattern),"undefined"==typeof f.opts.patterns)throw new TypeError("Must provide a pattern or array of patterns");f.patternMatcher=a(f.opts.patterns),f._updatePattern(),f.hldrs={},f.focus=0,c.addListener(f.el,"keydown",function(a){f._keyDown(a)}),c.addListener(f.el,"keypress",function(a){f._keyPress(a)}),c.addListener(f.el,"paste",function(a){f._paste(a)}),f.opts.persistent&&(f._processKey("",!1),f.el.blur(),c.addListener(f.el,"focus",function(a){f._focus(a)}),c.addListener(f.el,"click",function(a){f._focus(a)}),c.addListener(f.el,"touchstart",function(a){f._focus(a)}))}var e={persistent:!1,repeat:!1,placeholder:" "},f={9:/[0-9]/,a:/[A-Za-z]/,A:/[A-Z]/,"?":/[A-Z0-9]/,"*":/[A-Za-z0-9]/};return d.addInptType=function(a,b){f[a]=b},d.prototype.resetPattern=function(c){this.opts.patterns=c?this._specFromSinglePattern(c):this.opts.patterns,this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this._removeChars(),this.patternMatcher=a(this.opts.patterns);var d=this.patternMatcher.getPattern(this.val);this.mLength=d.mLength,this.chars=d.chars,this.inpts=d.inpts,this._processKey("",!1,!0)},d.prototype._updatePattern=function(){var a=this.patternMatcher.getPattern(this.val);a&&(this.mLength=a.mLength,this.chars=a.chars,this.inpts=a.inpts)},d.prototype._keyDown=function(a){var b=a.which||a.keyCode;return b&&c.isDelKeyDown(a.which,a.keyCode)?(this._processKey(null,b),c.preventDefault(a)):void 0},d.prototype._keyPress=function(a){var b,d;return b=a.which||a.keyCode,d=c.isSpecialKeyPress(a.which,a.keyCode),c.isDelKeyPress(a.which,a.keyCode)||d||c.isModifier(a)?void 0:(this._processKey(String.fromCharCode(b),!1),c.preventDefault(a))},d.prototype._paste=function(a){return this._processKey(c.getClip(a),!1),c.preventDefault(a)},d.prototype._focus=function(){var a=this;setTimeout(function(){var c=b.get(a.el),d=c.end>a.focus,e=0===c.end;(d||e)&&b.set(a.el,a.focus)},0)},d.prototype._processKey=function(a,d,e){if(this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this.sel.begin!==this.sel.end)this.delta=-1*Math.abs(this.sel.begin-this.sel.end),this.val=c.removeChars(this.val,this.sel.begin,this.sel.end);else if(d&&46===d)this._delete();else if(d&&this.sel.begin-1>=0)this.val=c.removeChars(this.val,this.sel.end-1,this.sel.end),this.delta-=1;else if(d)return!0;d||(this.val=c.addChars(this.val,a,this.sel.begin),this.delta+=a.length),this._formatValue(e)},d.prototype._delete=function(){for(;this.chars[this.sel.begin];)this._nextPos();this.sel.beginthis.focus&&(this.delta+=this.sel.end-this.focus);for(var a=0,b=0;b<=this.mLength;b++){var d,e=this.chars[b],f=this.hldrs[b],g=b+a;g=b>=this.sel.begin?g+this.delta:g,d=this.val.charAt(g),(e&&e===d||f&&f===d)&&(this.val=c.removeChars(this.val,g,g+1),a--)}this.hldrs={},this.focus=this.val.length},d.prototype._validateInpts=function(){for(var a=0;ae;e++)c.inpts[h]=a.charAt(e),h++;g++,i+=a.length+b-1};for(i;f>i;i++)gb[0]&&ad&&b.call(c,a[d],d,a)!==!1;d++);else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)break},a}(),d=function(a,b){function c(c){var e=[],f=[];b.forEach(c,function(c){b.forEach(c,function(b,c){var g=a.parse(b),h=d(c);return e.push(h),f.push(g),!1})});var g=function(a){var c;return b.forEach(e,function(b,d){return b.test(a)?(c=d,!1):void 0}),void 0===c?null:f[c]};return{getPattern:g,patterns:f,matchers:e}}var d=function(a){return"*"===a?/.*/:new RegExp(a)};return c}(b,c),e=function(){var a={};return a.get=function(a){if("number"==typeof a.selectionStart)return{begin:a.selectionStart,end:a.selectionEnd};var b=document.selection.createRange();if(b&&b.parentElement()===a){var c=a.createTextRange(),d=a.createTextRange(),e=a.value.length;return c.moveToBookmark(b.getBookmark()),d.collapse(!1),c.compareEndPoints("StartToEnd",d)>-1?{begin:e,end:e}:{begin:-c.moveStart("character",-e),end:-c.moveEnd("character",-e)}}return{begin:0,end:0}},a.set=function(a,b){if("object"!=typeof b&&(b={begin:b,end:b}),a.setSelectionRange)a.focus(),a.setSelectionRange(b.begin,b.end);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b.end),c.moveStart("character",b.begin),c.select()}},a}(),f=function(a,b,c){function d(b,d){var f=this;if(f.el=b,!f.el)throw new TypeError("Must provide an existing element");if(f.opts=c.extend({},e,d),"undefined"!=typeof f.opts.pattern&&(f.opts.patterns=f._specFromSinglePattern(f.opts.pattern),delete f.opts.pattern),"undefined"==typeof f.opts.patterns)throw new TypeError("Must provide a pattern or array of patterns");f.patternMatcher=a(f.opts.patterns),f._updatePattern(),f.hldrs={},f.focus=0,c.addListener(f.el,"keydown",function(a){f._keyDown(a)}),c.addListener(f.el,"keypress",function(a){f._keyPress(a)}),c.addListener(f.el,"paste",function(a){f._paste(a)}),f.opts.persistent&&(f._processKey("",!1),f.el.blur(),c.addListener(f.el,"focus",function(a){f._focus(a)}),c.addListener(f.el,"click",function(a){f._focus(a)}),c.addListener(f.el,"touchstart",function(a){f._focus(a)}))}var e={persistent:!1,repeat:!1,placeholder:" "},f={9:/[0-9]/,a:/[A-Za-z]/,A:/[A-Z]/,"?":/[A-Z0-9]/,"*":/[A-Za-z0-9]/};return d.addInptType=function(a,b){f[a]=b},d.prototype.resetPattern=function(c){this.opts.patterns=c?this._specFromSinglePattern(c):this.opts.patterns,this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this._removeChars(),this.patternMatcher=a(this.opts.patterns);var d=this.patternMatcher.getPattern(this.val);this.mLength=d.mLength,this.chars=d.chars,this.inpts=d.inpts,this._processKey("",!1,!0)},d.prototype._updatePattern=function(){var a=this.patternMatcher.getPattern(this.val);a&&(this.mLength=a.mLength,this.chars=a.chars,this.inpts=a.inpts)},d.prototype._keyDown=function(a){var b=a.which||a.keyCode;return b&&c.isDelKeyDown(a.which,a.keyCode)?(this._processKey(null,b),c.preventDefault(a)):void 0},d.prototype._keyPress=function(a){var b,d;return b=a.which||a.keyCode,d=c.isSpecialKeyPress(a.which,a.keyCode),c.isDelKeyPress(a.which,a.keyCode)||d||c.isModifier(a)?void 0:(this._processKey(String.fromCharCode(b),!1),c.preventDefault(a))},d.prototype._paste=function(a){return this._processKey(c.getClip(a),!1),c.preventDefault(a)},d.prototype._focus=function(){var a=this;setTimeout(function(){var c=b.get(a.el),d=c.end>a.focus,e=0===c.end;(d||e)&&b.set(a.el,a.focus)},0)},d.prototype._processKey=function(a,d,e){if(this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this.sel.begin!==this.sel.end)this.delta=-1*Math.abs(this.sel.begin-this.sel.end),this.val=c.removeChars(this.val,this.sel.begin,this.sel.end);else if(d&&46===d)this._delete();else if(d&&this.sel.begin-1>=0)this.val=c.removeChars(this.val,this.sel.end-1,this.sel.end),this.delta-=1;else if(d)return!0;d||(this.val=c.addChars(this.val,a,this.sel.begin),this.delta+=a.length),this._formatValue(e)},d.prototype._delete=function(){for(;this.chars[this.sel.begin];)this._nextPos();this.sel.beginthis.focus&&(this.delta+=this.sel.end-this.focus);for(var a=0,b=0;b<=this.mLength;b++){var d,e=this.chars[b],f=this.hldrs[b],g=b+a;g=b>=this.sel.begin?g+this.delta:g,d=this.val.charAt(g),(e&&e===d||f&&f===d)&&(this.val=c.removeChars(this.val,g,g+1),a--)}this.hldrs={},this.focus=this.val.length},d.prototype._validateInpts=function(){for(var a=0;a 2 | 3 | 4 | 5 | 6 | formatter.js by firstopinion 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 |
18 |
19 |

formatter.js

20 |

Format user input to match a specified pattern

21 | 22 |

View the Project on GitHub firstopinion/formatter.js

23 | 24 | 28 | 29 | 34 |
35 |
36 | 37 | {{ content }} 38 | 39 |
40 |
41 | 42 | 43 | 44 | {% raw %} 45 | 67 | {% endraw %} 68 | 69 | -------------------------------------------------------------------------------- /docs/demos.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: master 3 | --- 4 | 5 | {% raw %} 6 | 7 |

Demos

8 | 9 |

Credit Card

10 |

Ex: 4242-4242-4242-4242

11 |
12 |
13 | 14 |
15 |
16 |
new Formatter(document.getElementById('credit-input'), {
17 |   'pattern': '{{9999}}-{{9999}}-{{9999}}-{{9999}}'
18 | });
19 |
$('#credit-input').formatter({
20 |   'pattern': '{{9999}}-{{9999}}-{{9999}}-{{9999}}'
21 | });
22 | 23 |

Phone Number

24 |

Ex: (802) 415-3411

25 |
26 |
27 | 28 |
29 |
30 |
new Formatter(document.getElementById('phone-input'), {
31 |   'pattern': '({{999}}) {{999}}.{{9999}}',
32 |   'persistent': true
33 | });
34 |
$('#phone-input').formatter({
35 |   'pattern': '({{999}}) {{999}}-{{9999}}',
36 |   'persistent': true
37 | });
38 | 39 | {% endraw %} -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: master 3 | --- 4 | {% raw %} 5 | formatter.js [![Build Status](https://travis-ci.org/firstopinion/formatter.js.png)](https://travis-ci.org/firstopinion/formatter.js) 6 | ============ 7 | 8 | ___ __ __ _ 9 | / _/__ ______ _ ___ _/ /_/ /____ ____ (_)__ 10 | / _/ _ \/ __/ ' \/ _ `/ __/ __/ -_) __/ / (_-< 11 | /_/ \___/_/ /_/_/_/\_,_/\__/\__/\__/_/ (_)_/ /___/ 12 | |___/ 13 | 14 | Format user input to match a specified pattern 15 | 16 | 17 | 18 | Demos/Examples 19 | -------------- 20 | 21 | [view demo](http://firstopinion.github.io/formatter.js/demos.html) 22 | 23 | 24 | 25 | Why? 26 | ---- 27 | 28 | Sometimes it is useful to format user input as they type. Existing libraries lacked proper functionality / flexibility. Formatter was built from the ground up with no dependencies. There is however a jquery wrapper version for quick use. 29 | 30 | 31 | 32 | On Bower 33 | -------- 34 | 35 | bower install formatter 36 | 37 | 38 | 39 | Usage 40 | ----- 41 | 42 | ### Vanilla Javascript 43 | 44 | * **uncompressed**: formatter.js 45 | * **compressed**: formatter.min.js 46 | 47 | #### new Formatter(el, opts) 48 | 49 | var formatted = new Formatter(document.getElementById('credit-input'), { 50 | 'pattern': '{{999}}-{{999}}-{{999}}-{{9999}}', 51 | 'persistent': true 52 | }); 53 | 54 | 55 | ### Jquery 56 | 57 | * **uncompressed**: jquery.formatter.js 58 | * **compressed**: jquery.formatter.min.js 59 | 60 | #### $(selector).formatter(opts) 61 | 62 | $('#credit-input').formatter({ 63 | 'pattern': '{{999}}-{{999}}-{{999}}-{{9999}}', 64 | 'persistent': true 65 | }); 66 | 67 | 68 | 69 | Opts 70 | ---- 71 | 72 | * **pattern** (required): String representing the pattern of your formatted input. User input areas begin with `{{` and end with `}}`. For example, a phone number may be represented: `({{999}}) {{999}}-{{999}}`. You can specify numbers, letters, or numbers and letters. 73 | * 9: [0-9] 74 | * a: [A-Za-z] 75 | * \*: [A-Za-z0-9] 76 | * **persistent**: \[False\] Boolean representing if the formatted characters are always visible (persistent), or if they appear as you type. 77 | * **patterns** (optional, replaces *pattern*): Array representing a priority ordered set of patterns that may apply dynamically based on the current input value. Each value in the array is an object, whose key is a regular expression string and value is a *pattern* (see above). The regular expression is tested against the unformatted input value. You may use the special key `'*'` to catch all input values. 78 | ``` 79 | [ 80 | { '^\d{5}$': 'zip: {{99999}}' }, 81 | { '^.{6,8}$: 'postal code: {{********}}' }, 82 | { '*': 'unknown: {{**********}}' } 83 | ] 84 | ``` 85 | 86 | 87 | 88 | Class Methods 89 | ------------- 90 | 91 | #### addInptType(char, regexp) 92 | 93 | Add regular expressions for different input types. 94 | 95 | **Vanilla Javascript** 96 | 97 | Formatter.addInptType('L', /[A-Z]/); 98 | 99 | **Jquery** 100 | 101 | $.fn.formatter.addInptType('L', /[A-Z]/); 102 | 103 | 104 | 105 | Instance Methods 106 | ---------------- 107 | 108 | #### resetPattern(pattern) 109 | 110 | Fairly self explanatory here :) reset the pattern on an existing Formatter instance. 111 | 112 | **Vanilla Javascript** 113 | 114 | (assuming you already created a new instance and saved it to the var `formatted`) 115 | 116 | formatted.resetPattern('{{999}}.{{999}}.{{9999}}'); 117 | 118 | **Jquery** 119 | 120 | (assuming you already initiated formatter on `#selector`) 121 | 122 | $('#selector').formatter().resetPattern(); 123 | 124 | 125 | 126 | Tests 127 | ----- 128 | 129 | Install Dependencies: 130 | 131 | npm install 132 | 133 | Run Tests: 134 | 135 | npm test 136 | 137 | 138 | 139 | License 140 | ------- 141 | 142 | The MIT License (MIT) Copyright (c) 2013 First Opinion 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 147 | 148 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | 150 | {% endraw %} -------------------------------------------------------------------------------- /docs/javascripts/formatter.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"function"==typeof define&&define.amd?define([],function(){return a.returnExportsGlobal=b()}):"object"==typeof exports?module.exports=b():a.Formatter=b()}(this,function(){var a=function(){var a={},b=4,c=new RegExp("{{([^}]+)}}","g"),d=function(a){for(var b,d=[];b=c.exec(a);)d.push(b);return d};return a.parse=function(a){var c={inpts:{},chars:{}},e=d(a),f=a.length,g=0,h=0,i=0,j=function(a){for(var d=a.length,e=0;d>e;e++)c.inpts[h]=a.charAt(e),h++;g++,i+=a.length+b-1};for(i;f>i;i++)gb[0]&&ad&&b.call(c,a[d],d,a)!==!1;d++);else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)break},a}(),c=function(a,b){function c(c){var e=[],f=[];b.forEach(c,function(c){b.forEach(c,function(b,c){var g=a.parse(b),h=d(c);return e.push(h),f.push(g),!1})});var g=function(a){var c;return b.forEach(e,function(b,d){return b.test(a)?(c=d,!1):void 0}),void 0===c?null:f[c]};return{getPattern:g,patterns:f,matchers:e}}var d=function(a){return"*"===a?/.*/:new RegExp(a)};return c}(a,b),d=function(){var a={};return a.get=function(a){if("number"==typeof a.selectionStart)return{begin:a.selectionStart,end:a.selectionEnd};var b=document.selection.createRange();if(b&&b.parentElement()===a){var c=a.createTextRange(),d=a.createTextRange(),e=a.value.length;return c.moveToBookmark(b.getBookmark()),d.collapse(!1),c.compareEndPoints("StartToEnd",d)>-1?{begin:e,end:e}:{begin:-c.moveStart("character",-e),end:-c.moveEnd("character",-e)}}return{begin:0,end:0}},a.set=function(a,b){if("object"!=typeof b&&(b={begin:b,end:b}),a.setSelectionRange)a.focus(),a.setSelectionRange(b.begin,b.end);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b.end),c.moveStart("character",b.begin),c.select()}},a}(),e=function(a,b,c){function d(b,d){var f=this;if(f.el=b,!f.el)throw new TypeError("Must provide an existing element");if(f.opts=c.extend({},e,d),"undefined"!=typeof f.opts.pattern&&(f.opts.patterns=f._specFromSinglePattern(f.opts.pattern),delete f.opts.pattern),"undefined"==typeof f.opts.patterns)throw new TypeError("Must provide a pattern or array of patterns");f.patternMatcher=a(f.opts.patterns),f._updatePattern(),f.hldrs={},f.focus=0,c.addListener(f.el,"keydown",function(a){f._keyDown(a)}),c.addListener(f.el,"keypress",function(a){f._keyPress(a)}),c.addListener(f.el,"paste",function(a){f._paste(a)}),f.opts.persistent&&(f._processKey("",!1),f.el.blur(),c.addListener(f.el,"focus",function(a){f._focus(a)}),c.addListener(f.el,"click",function(a){f._focus(a)}),c.addListener(f.el,"touchstart",function(a){f._focus(a)}))}var e={persistent:!1,repeat:!1,placeholder:" "},f={9:/[0-9]/,a:/[A-Za-z]/,"*":/[A-Za-z0-9]/};return d.addInptType=function(a,b){f[a]=b},d.prototype.resetPattern=function(c){this.opts.patterns=c?this._specFromSinglePattern(c):this.opts.patterns,this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this._removeChars(),this.patternMatcher=a(this.opts.patterns);var d=this.patternMatcher.getPattern(this.val);this.mLength=d.mLength,this.chars=d.chars,this.inpts=d.inpts,this._processKey("",!1,!0)},d.prototype._updatePattern=function(){var a=this.patternMatcher.getPattern(this.val);a&&(this.mLength=a.mLength,this.chars=a.chars,this.inpts=a.inpts)},d.prototype._keyDown=function(a){var b=a.which||a.keyCode;return b&&c.isDelKeyDown(a.which,a.keyCode)?(this._processKey(null,b),c.preventDefault(a)):void 0},d.prototype._keyPress=function(a){var b,d;return b=a.which||a.keyCode,d=c.isSpecialKeyPress(a.which,a.keyCode),c.isDelKeyPress(a.which,a.keyCode)||d||c.isModifier(a)?void 0:(this._processKey(String.fromCharCode(b),!1),c.preventDefault(a))},d.prototype._paste=function(a){return this._processKey(c.getClip(a),!1),c.preventDefault(a)},d.prototype._focus=function(){var a=this;setTimeout(function(){var c=b.get(a.el),d=c.end>a.focus,e=0===c.end;(d||e)&&b.set(a.el,a.focus)},0)},d.prototype._processKey=function(a,d,e){if(this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this.sel.begin!==this.sel.end)this.delta=-1*Math.abs(this.sel.begin-this.sel.end),this.val=c.removeChars(this.val,this.sel.begin,this.sel.end);else if(d&&46===d)this._delete();else if(d&&this.sel.begin-1>=0)this.val=c.removeChars(this.val,this.sel.end-1,this.sel.end),this.delta-=1;else if(d)return!0;d||(this.val=c.addChars(this.val,a,this.sel.begin),this.delta+=a.length),this._formatValue(e)},d.prototype._delete=function(){for(;this.chars[this.sel.begin];)this._nextPos();this.sel.beginthis.focus&&(this.delta+=this.sel.end-this.focus);for(var a=0,b=0;b<=this.mLength;b++){var d,e=this.chars[b],f=this.hldrs[b],g=b+a;g=b>=this.sel.begin?g+this.delta:g,d=this.val.charAt(g),(e&&e===d||f&&f===d)&&(this.val=c.removeChars(this.val,g,g+1),a--)}this.hldrs={},this.focus=this.val.length},d.prototype._validateInpts=function(){for(var a=0;ae;e++)c.inpts[h]=a.charAt(e),h++;g++,i+=a.length+b-1};for(i;f>i;i++)gb[0]&&ad&&b.call(c,a[d],d,a)!==!1;d++);else for(var f in a)if(a.hasOwnProperty(f)&&b.call(c,a[f],f,a)===!1)break},a}(),c=function(a,b){function c(c){var e=[],f=[];b.forEach(c,function(c){b.forEach(c,function(b,c){var g=a.parse(b),h=d(c);return e.push(h),f.push(g),!1})});var g=function(a){var c;return b.forEach(e,function(b,d){return b.test(a)?(c=d,!1):void 0}),void 0===c?null:f[c]};return{getPattern:g,patterns:f,matchers:e}}var d=function(a){return"*"===a?/.*/:new RegExp(a)};return c}(a,b),d=function(){var a={};return a.get=function(a){if("number"==typeof a.selectionStart)return{begin:a.selectionStart,end:a.selectionEnd};var b=document.selection.createRange();if(b&&b.parentElement()===a){var c=a.createTextRange(),d=a.createTextRange(),e=a.value.length;return c.moveToBookmark(b.getBookmark()),d.collapse(!1),c.compareEndPoints("StartToEnd",d)>-1?{begin:e,end:e}:{begin:-c.moveStart("character",-e),end:-c.moveEnd("character",-e)}}return{begin:0,end:0}},a.set=function(a,b){if("object"!=typeof b&&(b={begin:b,end:b}),a.setSelectionRange)a.focus(),a.setSelectionRange(b.begin,b.end);else if(a.createTextRange){var c=a.createTextRange();c.collapse(!0),c.moveEnd("character",b.end),c.moveStart("character",b.begin),c.select()}},a}(),e=function(a,b,c){function d(b,d){var f=this;if(f.el=b,!f.el)throw new TypeError("Must provide an existing element");if(f.opts=c.extend({},e,d),"undefined"!=typeof f.opts.pattern&&(f.opts.patterns=f._specFromSinglePattern(f.opts.pattern),delete f.opts.pattern),"undefined"==typeof f.opts.patterns)throw new TypeError("Must provide a pattern or array of patterns");f.patternMatcher=a(f.opts.patterns),f._updatePattern(),f.hldrs={},f.focus=0,c.addListener(f.el,"keydown",function(a){f._keyDown(a)}),c.addListener(f.el,"keypress",function(a){f._keyPress(a)}),c.addListener(f.el,"paste",function(a){f._paste(a)}),f.opts.persistent&&(f._processKey("",!1),f.el.blur(),c.addListener(f.el,"focus",function(a){f._focus(a)}),c.addListener(f.el,"click",function(a){f._focus(a)}),c.addListener(f.el,"touchstart",function(a){f._focus(a)}))}var e={persistent:!1,repeat:!1,placeholder:" "},f={9:/[0-9]/,a:/[A-Za-z]/,"*":/[A-Za-z0-9]/};return d.addInptType=function(a,b){f[a]=b},d.prototype.resetPattern=function(c){this.opts.patterns=c?this._specFromSinglePattern(c):this.opts.patterns,this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this._removeChars(),this.patternMatcher=a(this.opts.patterns);var d=this.patternMatcher.getPattern(this.val);this.mLength=d.mLength,this.chars=d.chars,this.inpts=d.inpts,this._processKey("",!1,!0)},d.prototype._updatePattern=function(){var a=this.patternMatcher.getPattern(this.val);a&&(this.mLength=a.mLength,this.chars=a.chars,this.inpts=a.inpts)},d.prototype._keyDown=function(a){var b=a.which||a.keyCode;return b&&c.isDelKeyDown(a.which,a.keyCode)?(this._processKey(null,b),c.preventDefault(a)):void 0},d.prototype._keyPress=function(a){var b,d;return b=a.which||a.keyCode,d=c.isSpecialKeyPress(a.which,a.keyCode),c.isDelKeyPress(a.which,a.keyCode)||d||c.isModifier(a)?void 0:(this._processKey(String.fromCharCode(b),!1),c.preventDefault(a))},d.prototype._paste=function(a){return this._processKey(c.getClip(a),!1),c.preventDefault(a)},d.prototype._focus=function(){var a=this;setTimeout(function(){var c=b.get(a.el),d=c.end>a.focus,e=0===c.end;(d||e)&&b.set(a.el,a.focus)},0)},d.prototype._processKey=function(a,d,e){if(this.sel=b.get(this.el),this.val=this.el.value,this.delta=0,this.sel.begin!==this.sel.end)this.delta=-1*Math.abs(this.sel.begin-this.sel.end),this.val=c.removeChars(this.val,this.sel.begin,this.sel.end);else if(d&&46===d)this._delete();else if(d&&this.sel.begin-1>=0)this.val=c.removeChars(this.val,this.sel.end-1,this.sel.end),this.delta-=1;else if(d)return!0;d||(this.val=c.addChars(this.val,a,this.sel.begin),this.delta+=a.length),this._formatValue(e)},d.prototype._delete=function(){for(;this.chars[this.sel.begin];)this._nextPos();this.sel.beginthis.focus&&(this.delta+=this.sel.end-this.focus);for(var a=0,b=0;b<=this.mLength;b++){var d,e=this.chars[b],f=this.hldrs[b],g=b+a;g=b>=this.sel.begin?g+this.delta:g,d=this.val.charAt(g),(e&&e===d||f&&f===d)&&(this.val=c.removeChars(this.val,g,g+1),a--)}this.hldrs={},this.focus=this.val.length},d.prototype._validateInpts=function(){for(var a=0;a a { 269 | display: block; 270 | padding: 10px 0; 271 | } 272 | .demo-input { 273 | margin-bottom: 20px; 274 | padding: 15px; 275 | border-radius: 4px; 276 | background: #fafafa; 277 | } 278 | /* 2. form elements 279 | ----------------------------------------------------------------------------------------------------*/ 280 | input { 281 | display: block; 282 | margin: 0; 283 | padding: 0; 284 | border: none; 285 | outline: none; 286 | border: 0; 287 | outline: none; 288 | background: none; 289 | font-family: "Lucida Console", Monaco, monospace; 290 | } 291 | input[type=file] { 292 | width: 1px; 293 | } 294 | input[type=number]::-webkit-inner-spin-button, 295 | input[type=number]::-webkit-outer-spin-button { 296 | -webkit-appearance: none; 297 | margin: 0; 298 | } 299 | 300 | /* 3. input 301 | ----------------------------------------------------------------------------------------------------*/ 302 | .input-wrap { 303 | display: block; 304 | position: relative; 305 | *position: static; 306 | overflow: hidden; 307 | line-height: 0; 308 | vertical-align: middle; 309 | } 310 | .input { 311 | width: 100%; 312 | border: medium none; 313 | } 314 | 315 | .input-s1 { 316 | padding: 21px 21px 17px 21px; 317 | height: 16px; 318 | line-height: 1; 319 | font-size: 16px; 320 | font-weight: light; 321 | border-radius: 4px; 322 | border: 1px solid rgb(250, 250, 250); 323 | } 324 | .input-s1 > .input { 325 | margin: -19px -21px -17px -21px; 326 | padding: 19px 21px 17px 21px; 327 | height: 16px; 328 | border-radius: 4px; 329 | } 330 | .input-white { 331 | background: #d3d3ce; /* Old browsers */ 332 | background: linear-gradient(to bottom, #d3d3ce 0%,#fff 100%); /* W3C */ 333 | } 334 | .input-white > .input { 335 | color: #676a6c; 336 | background: #fff; 337 | } 338 | .input-white > .input::-webkit-input-placeholder { 339 | color: #bcbec0; 340 | } 341 | .input-white > .input:-moz-placeholder { 342 | color: #bcbec0; 343 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formatter.js-pebble", 3 | "version": "0.1.8", 4 | "description": "Format user input to match a specified pattern", 5 | "author": "Joe Simpson ", 6 | "contributors": [ 7 | { 8 | "name": "Jarid Margolin", 9 | "email": "jarid@firstopinion.com" 10 | } 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mypebble/formatter.js" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mypebble/formatter.js/issues" 18 | }, 19 | "scripts": { 20 | "test": "grunt test" 21 | }, 22 | "main": "./dist/common/formatter.js", 23 | "devDependencies": { 24 | "amdclean": "~1.5.0", 25 | "grunt": "~0.4.3", 26 | "grunt-contrib-clean": "~0.5.0", 27 | "grunt-contrib-concat": "~0.4.0", 28 | "grunt-contrib-connect": "~0.7.1", 29 | "grunt-contrib-copy": "~0.5.0", 30 | "grunt-contrib-jshint": "~0.10.0", 31 | "grunt-contrib-nodefy": "~0.2.1", 32 | "grunt-contrib-requirejs": "~0.4.1", 33 | "grunt-contrib-uglify": "~0.4.0", 34 | "grunt-contrib-watch": "~0.6.1", 35 | "grunt-gh-pages": "~0.9.1", 36 | "grunt-jekyll": "~0.4.2", 37 | "grunt-mocha-phantomjs": "~0.5.0", 38 | "grunt-saucelabs": "~5.1.0", 39 | "grunt-umd": "~1.7.3", 40 | "grunt-wrap": "~0.3.0", 41 | "matchdep": "~0.3.0", 42 | "mocha": "~1.18.2", 43 | "proclaim": "~2.0.0", 44 | "sinon": "~1.9.0" 45 | }, 46 | "license": "MIT" 47 | } 48 | -------------------------------------------------------------------------------- /src/formatter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * formatter.js 3 | * 4 | * Class used to format input based on passed pattern 5 | * 6 | */ 7 | 8 | define([ 9 | './pattern-matcher', 10 | './inpt-sel', 11 | './utils' 12 | ], function (patternMatcher, inptSel, utils) { 13 | 14 | 15 | // Defaults 16 | var defaults = { 17 | persistent: false, 18 | repeat: false, 19 | placeholder: ' ' 20 | }; 21 | 22 | // Regexs for input validation 23 | var inptRegs = { 24 | '9': /[0-9]/, 25 | 'a': /[A-Za-z]/, 26 | 'A': /[A-Z]/, 27 | '?': /[A-Z0-9]/, 28 | '*': /[A-Za-z0-9]/ 29 | }; 30 | 31 | // 32 | // Class Constructor - Called with new Formatter(el, opts) 33 | // Responsible for setting up required instance variables, and 34 | // attaching the event listener to the element. 35 | // 36 | function Formatter(el, opts) { 37 | // Cache this 38 | var self = this; 39 | 40 | // Make sure we have an element. Make accesible to instance 41 | self.el = el; 42 | if (!self.el) { 43 | throw new TypeError('Must provide an existing element'); 44 | } 45 | 46 | // Merge opts with defaults 47 | self.opts = utils.extend({}, defaults, opts); 48 | 49 | // 1 pattern is special case 50 | if (typeof self.opts.pattern !== 'undefined') { 51 | self.opts.patterns = self._specFromSinglePattern(self.opts.pattern); 52 | delete self.opts.pattern; 53 | } 54 | 55 | // Make sure we have valid opts 56 | if (typeof self.opts.patterns === 'undefined') { 57 | throw new TypeError('Must provide a pattern or array of patterns'); 58 | } 59 | 60 | self.patternMatcher = patternMatcher(self.opts.patterns); 61 | 62 | // Upate pattern with initial value 63 | self._updatePattern(); 64 | 65 | // Init values 66 | self.hldrs = {}; 67 | self.focus = 0; 68 | 69 | // Add Listeners 70 | utils.addListener(self.el, 'keydown', function (evt) { 71 | self._keyDown(evt); 72 | }); 73 | utils.addListener(self.el, 'keypress', function (evt) { 74 | self._keyPress(evt); 75 | }); 76 | utils.addListener(self.el, 'paste', function (evt) { 77 | self._paste(evt); 78 | }); 79 | 80 | // Persistence 81 | if (self.opts.persistent) { 82 | // Format on start 83 | self._processKey('', false); 84 | self.el.blur(); 85 | 86 | // Add Listeners 87 | utils.addListener(self.el, 'focus', function (evt) { 88 | self._focus(evt); 89 | }); 90 | utils.addListener(self.el, 'click', function (evt) { 91 | self._focus(evt); 92 | }); 93 | utils.addListener(self.el, 'touchstart', function (evt) { 94 | self._focus(evt); 95 | }); 96 | } 97 | } 98 | 99 | // 100 | // @public 101 | // Add new char 102 | // 103 | Formatter.addInptType = function (chr, reg) { 104 | inptRegs[chr] = reg; 105 | }; 106 | 107 | // 108 | // @public 109 | // Apply the given pattern to the current input without moving caret. 110 | // 111 | Formatter.prototype.resetPattern = function (str) { 112 | // Update opts to hold new pattern 113 | this.opts.patterns = str ? this._specFromSinglePattern(str) : this.opts.patterns; 114 | 115 | // Get current state 116 | this.sel = inptSel.get(this.el); 117 | this.val = this.el.value; 118 | 119 | // Init values 120 | this.delta = 0; 121 | 122 | // Remove all formatted chars from val 123 | this._removeChars(); 124 | 125 | this.patternMatcher = patternMatcher(this.opts.patterns); 126 | 127 | // Update pattern 128 | var newPattern = this.patternMatcher.getPattern(this.val); 129 | this.mLength = newPattern.mLength; 130 | this.chars = newPattern.chars; 131 | this.inpts = newPattern.inpts; 132 | 133 | // Format on start 134 | this._processKey('', false, true); 135 | }; 136 | 137 | // 138 | // @private 139 | // Determine correct format pattern based on input val 140 | // 141 | Formatter.prototype._updatePattern = function () { 142 | // Determine appropriate pattern 143 | var newPattern = this.patternMatcher.getPattern(this.val); 144 | 145 | // Only update the pattern if there is an appropriate pattern for the value. 146 | // Otherwise, leave the current pattern (and likely delete the latest character.) 147 | if (newPattern) { 148 | // Get info about the given pattern 149 | this.mLength = newPattern.mLength; 150 | this.chars = newPattern.chars; 151 | this.inpts = newPattern.inpts; 152 | } 153 | }; 154 | 155 | // 156 | // @private 157 | // Handler called on all keyDown strokes. All keys trigger 158 | // this handler. Only process delete keys. 159 | // 160 | Formatter.prototype._keyDown = function (evt) { 161 | // The first thing we need is the character code 162 | var k = evt.which || evt.keyCode; 163 | 164 | // If delete key 165 | if (k && utils.isDelKeyDown(evt.which, evt.keyCode)) { 166 | // Process the keyCode and prevent default 167 | this._processKey(null, k); 168 | return utils.preventDefault(evt); 169 | } 170 | }; 171 | 172 | // 173 | // @private 174 | // Handler called on all keyPress strokes. Only processes 175 | // character keys (as long as no modifier key is in use). 176 | // 177 | Formatter.prototype._keyPress = function (evt) { 178 | // The first thing we need is the character code 179 | var k, isSpecial; 180 | // Mozilla will trigger on special keys and assign the the value 0 181 | // We want to use that 0 rather than the keyCode it assigns. 182 | k = evt.which || evt.keyCode; 183 | isSpecial = utils.isSpecialKeyPress(evt.which, evt.keyCode); 184 | 185 | // Process the keyCode and prevent default 186 | if (!utils.isDelKeyPress(evt.which, evt.keyCode) && !isSpecial && !utils.isModifier(evt)) { 187 | this._processKey(String.fromCharCode(k), false); 188 | return utils.preventDefault(evt); 189 | } 190 | }; 191 | 192 | // 193 | // @private 194 | // Handler called on paste event. 195 | // 196 | Formatter.prototype._paste = function (evt) { 197 | // Process the clipboard paste and prevent default 198 | this._processKey(utils.getClip(evt), false); 199 | return utils.preventDefault(evt); 200 | }; 201 | 202 | // 203 | // @private 204 | // Handle called on focus event. 205 | // 206 | Formatter.prototype._focus = function () { 207 | // Wrapped in timeout so that we can grab input selection 208 | var self = this; 209 | setTimeout(function () { 210 | // Grab selection 211 | var selection = inptSel.get(self.el); 212 | // Char check 213 | var isAfterStart = selection.end > self.focus, 214 | isFirstChar = selection.end === 0; 215 | // If clicked in front of start, refocus to start 216 | if (isAfterStart || isFirstChar) { 217 | inptSel.set(self.el, self.focus); 218 | } 219 | }, 0); 220 | }; 221 | 222 | // 223 | // @private 224 | // Using the provided key information, alter el value. 225 | // 226 | Formatter.prototype._processKey = function (chars, delKey, ignoreCaret) { 227 | // Get current state 228 | this.sel = inptSel.get(this.el); 229 | this.val = this.el.value; 230 | 231 | // Init values 232 | this.delta = 0; 233 | 234 | // If chars were highlighted, we need to remove them 235 | if (this.sel.begin !== this.sel.end) { 236 | this.delta = (-1) * Math.abs(this.sel.begin - this.sel.end); 237 | this.val = utils.removeChars(this.val, this.sel.begin, this.sel.end); 238 | } 239 | 240 | // Delete key (moves opposite direction) 241 | else if (delKey && delKey === 46) { 242 | this._delete(); 243 | 244 | // or Backspace and not at start 245 | } else if (delKey && this.sel.begin - 1 >= 0) { 246 | 247 | // Always have a delta of at least -1 for the character being deleted. 248 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 249 | this.delta -= 1; 250 | 251 | // or Backspace and at start - exit 252 | } else if (delKey) { 253 | return true; 254 | } 255 | 256 | // If the key is not a del key, it should convert to a str 257 | if (!delKey) { 258 | // Add char at position and increment delta 259 | this.val = utils.addChars(this.val, chars, this.sel.begin); 260 | this.delta += chars.length; 261 | } 262 | 263 | // Format el.value (also handles updating caret position) 264 | this._formatValue(ignoreCaret); 265 | }; 266 | 267 | // 268 | // @private 269 | // Deletes the character in front of it 270 | // 271 | Formatter.prototype._delete = function () { 272 | // Adjust focus to make sure its not on a formatted char 273 | while (this.chars[this.sel.begin]) { 274 | this._nextPos(); 275 | } 276 | 277 | // As long as we are not at the end 278 | if (this.sel.begin < this.val.length) { 279 | // We will simulate a delete by moving the caret to the next char 280 | // and then deleting 281 | this._nextPos(); 282 | this.val = utils.removeChars(this.val, this.sel.end -1, this.sel.end); 283 | this.delta = -1; 284 | } 285 | }; 286 | 287 | // 288 | // @private 289 | // Quick helper method to move the caret to the next pos 290 | // 291 | Formatter.prototype._nextPos = function () { 292 | this.sel.end ++; 293 | this.sel.begin ++; 294 | }; 295 | 296 | // 297 | // @private 298 | // Alter element value to display characters matching the provided 299 | // instance pattern. Also responsible for updating 300 | // 301 | Formatter.prototype._formatValue = function (ignoreCaret) { 302 | // Set caret pos 303 | this.newPos = this.sel.end + this.delta; 304 | 305 | // Remove all formatted chars from val 306 | this._removeChars(); 307 | 308 | // Switch to first matching pattern based on val 309 | this._updatePattern(); 310 | 311 | // Validate inputs 312 | this._validateInpts(); 313 | 314 | // Add formatted characters 315 | this._addChars(); 316 | 317 | // Set value and adhere to maxLength 318 | this.el.value = this.val.substr(0, this.mLength); 319 | 320 | // Set new caret position 321 | if ((typeof ignoreCaret) === 'undefined' || ignoreCaret === false) { 322 | inptSel.set(this.el, this.newPos); 323 | } 324 | }; 325 | 326 | // 327 | // @private 328 | // Remove all formatted before and after a specified pos 329 | // 330 | Formatter.prototype._removeChars = function () { 331 | // Delta shouldn't include placeholders 332 | if (this.sel.end > this.focus) { 333 | this.delta += this.sel.end - this.focus; 334 | } 335 | 336 | // Account for shifts during removal 337 | var shift = 0; 338 | 339 | // Loop through all possible char positions 340 | for (var i = 0; i <= this.mLength; i++) { 341 | // Get transformed position 342 | var curChar = this.chars[i], 343 | curHldr = this.hldrs[i], 344 | pos = i + shift, 345 | val; 346 | 347 | // If after selection we need to account for delta 348 | pos = (i >= this.sel.begin) ? pos + this.delta : pos; 349 | val = this.val.charAt(pos); 350 | // Remove char and account for shift 351 | if (curChar && curChar === val || curHldr && curHldr === val) { 352 | this.val = utils.removeChars(this.val, pos, pos + 1); 353 | shift--; 354 | } 355 | } 356 | 357 | // All hldrs should be removed now 358 | this.hldrs = {}; 359 | 360 | // Set focus to last character 361 | this.focus = this.val.length; 362 | }; 363 | 364 | // 365 | // @private 366 | // Make sure all inpts are valid, else remove and update delta 367 | // 368 | Formatter.prototype._validateInpts = function () { 369 | // Loop over each char and validate 370 | for (var i = 0; i < this.val.length; i++) { 371 | // Get char inpt type 372 | var inptType = this.inpts[i]; 373 | 374 | // When only allowing capitals, ensure this char is capitalized! 375 | if (inptType === '?' || inptType === 'A'){ 376 | var up = this.val.charAt(i).toUpperCase(); 377 | this.val = utils.addChars(utils.removeChars(this.val, i, i+1), up, i); 378 | } 379 | 380 | // Checks 381 | var isBadType = !inptRegs[inptType], 382 | isInvalid = !isBadType && !inptRegs[inptType].test(this.val.charAt(i)), 383 | inBounds = this.inpts[i]; 384 | 385 | // Remove if incorrect and inbounds 386 | if ((isBadType || isInvalid) && inBounds) { 387 | this.val = utils.removeChars(this.val, i, i + 1); 388 | this.focusStart--; 389 | this.newPos--; 390 | this.delta--; 391 | i--; 392 | } 393 | } 394 | }; 395 | 396 | // 397 | // @private 398 | // Loop over val and add formatted chars as necessary 399 | // 400 | Formatter.prototype._addChars = function () { 401 | if (this.opts.persistent) { 402 | // Loop over all possible characters 403 | for (var i = 0; i <= this.mLength; i++) { 404 | if (!this.val.charAt(i)) { 405 | // Add placeholder at pos 406 | this.val = utils.addChars(this.val, this.opts.placeholder, i); 407 | this.hldrs[i] = this.opts.placeholder; 408 | } 409 | this._addChar(i); 410 | } 411 | 412 | // Adjust focus to make sure its not on a formatted char 413 | while (this.chars[this.focus]) { 414 | this.focus++; 415 | } 416 | } else { 417 | // Avoid caching val.length, as they may change in _addChar. 418 | for (var j = 0; j <= this.val.length; j++) { 419 | // When moving backwards there are some race conditions where we 420 | // dont want to add the character 421 | if (this.delta <= 0 && (j === this.focus)) { return true; } 422 | 423 | // Place character in current position of the formatted string. 424 | this._addChar(j); 425 | } 426 | } 427 | }; 428 | 429 | // 430 | // @private 431 | // Add formattted char at position 432 | // 433 | Formatter.prototype._addChar = function (i) { 434 | // If char exists at position 435 | var chr = this.chars[i]; 436 | if (!chr) { return true; } 437 | 438 | // If chars are added in between the old pos and new pos 439 | // we need to increment pos and delta 440 | if (utils.isBetween(i, [this.sel.begin -1, this.newPos +1])) { 441 | this.newPos ++; 442 | this.delta ++; 443 | } 444 | 445 | // If character added before focus, incr 446 | if (i <= this.focus) { 447 | this.focus++; 448 | } 449 | 450 | // Updateholder 451 | if (this.hldrs[i]) { 452 | delete this.hldrs[i]; 453 | this.hldrs[i + 1] = this.opts.placeholder; 454 | } 455 | 456 | // Update value 457 | this.val = utils.addChars(this.val, chr, i); 458 | }; 459 | 460 | // 461 | // @private 462 | // Create a patternSpec for passing into patternMatcher that 463 | // has exactly one catch all pattern. 464 | // 465 | Formatter.prototype._specFromSinglePattern = function (patternStr) { 466 | return [{ '*': patternStr }]; 467 | }; 468 | 469 | 470 | // Expose 471 | return Formatter; 472 | 473 | 474 | }); 475 | -------------------------------------------------------------------------------- /src/inpt-sel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * inpt-sel.js 3 | * 4 | * Cross browser implementation to get and set input selections 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var inptSel = {}; 14 | 15 | // 16 | // Get begin and end positions of selected input. Return 0's 17 | // if there is no selectiion data 18 | // 19 | inptSel.get = function (el) { 20 | // If normal browser return with result 21 | if (typeof el.selectionStart === 'number') { 22 | return { 23 | begin: el.selectionStart, 24 | end: el.selectionEnd 25 | }; 26 | } 27 | 28 | // Uh-Oh. We must be IE. Fun with TextRange!! 29 | var range = document.selection.createRange(); 30 | // Determine if there is a selection 31 | if (range && range.parentElement() === el) { 32 | var inputRange = el.createTextRange(), 33 | endRange = el.createTextRange(), 34 | length = el.value.length; 35 | 36 | // Create a working TextRange for the input selection 37 | inputRange.moveToBookmark(range.getBookmark()); 38 | 39 | // Move endRange begin pos to end pos (hence endRange) 40 | endRange.collapse(false); 41 | 42 | // If we are at the very end of the input, begin and end 43 | // must both be the length of the el.value 44 | if (inputRange.compareEndPoints('StartToEnd', endRange) > -1) { 45 | return { begin: length, end: length }; 46 | } 47 | 48 | // Note: moveStart usually returns the units moved, which 49 | // one may think is -length, however, it will stop when it 50 | // gets to the begin of the range, thus giving us the 51 | // negative value of the pos. 52 | return { 53 | begin: -inputRange.moveStart('character', -length), 54 | end: -inputRange.moveEnd('character', -length) 55 | }; 56 | } 57 | 58 | //Return 0's on no selection data 59 | return { begin: 0, end: 0 }; 60 | }; 61 | 62 | // 63 | // Set the caret position at a specified location 64 | // 65 | inptSel.set = function (el, pos) { 66 | // Normalize pos 67 | if (typeof pos !== 'object') { 68 | pos = { begin: pos, end: pos }; 69 | } 70 | 71 | // If normal browser 72 | if (el.setSelectionRange) { 73 | el.focus(); 74 | el.setSelectionRange(pos.begin, pos.end); 75 | 76 | // IE = TextRange fun 77 | } else if (el.createTextRange) { 78 | var range = el.createTextRange(); 79 | range.collapse(true); 80 | range.moveEnd('character', pos.end); 81 | range.moveStart('character', pos.begin); 82 | range.select(); 83 | } 84 | }; 85 | 86 | 87 | // Expose 88 | return inptSel; 89 | 90 | 91 | }); -------------------------------------------------------------------------------- /src/pattern-matcher.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern-matcher.js 3 | * 4 | * Parses a pattern specification and determines appropriate pattern for an 5 | * input string 6 | * 7 | */ 8 | 9 | 10 | define([ 11 | './pattern', 12 | './utils' 13 | ], function (pattern, utils) { 14 | 15 | 16 | // 17 | // Parse a matcher string into a RegExp. Accepts valid regular 18 | // expressions and the catchall '*'. 19 | // @private 20 | // 21 | var parseMatcher = function (matcher) { 22 | if (matcher === '*') { 23 | return /.*/; 24 | } 25 | return new RegExp(matcher); 26 | }; 27 | 28 | // 29 | // Parse a pattern spec and return a function that returns a pattern 30 | // based on user input. The first matching pattern will be chosen. 31 | // Pattern spec format: 32 | // Array [ 33 | // Object: { Matcher(RegExp String) : Pattern(Pattern String) }, 34 | // ... 35 | // ] 36 | function patternMatcher (patternSpec) { 37 | var matchers = [], 38 | patterns = []; 39 | 40 | // Iterate over each pattern in order. 41 | utils.forEach(patternSpec, function (patternMatcher) { 42 | // Process single property object to obtain pattern and matcher. 43 | utils.forEach(patternMatcher, function (patternStr, matcherStr) { 44 | var parsedPattern = pattern.parse(patternStr), 45 | regExpMatcher = parseMatcher(matcherStr); 46 | 47 | matchers.push(regExpMatcher); 48 | patterns.push(parsedPattern); 49 | 50 | // Stop after one iteration. 51 | return false; 52 | }); 53 | }); 54 | 55 | var getPattern = function (input) { 56 | var matchedIndex; 57 | utils.forEach(matchers, function (matcher, index) { 58 | if (matcher.test(input)) { 59 | matchedIndex = index; 60 | return false; 61 | } 62 | }); 63 | 64 | return matchedIndex === undefined ? null : patterns[matchedIndex]; 65 | }; 66 | 67 | return { 68 | getPattern: getPattern, 69 | patterns: patterns, 70 | matchers: matchers 71 | }; 72 | } 73 | 74 | 75 | // Expose 76 | return patternMatcher; 77 | 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /src/pattern.js: -------------------------------------------------------------------------------- 1 | /* 2 | * pattern.js 3 | * 4 | * Utilities to parse str pattern and return info 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var pattern = {}; 14 | 15 | // Match information 16 | var DELIM_SIZE = 4; 17 | 18 | // Our regex used to parse 19 | var regexp = new RegExp('{{([^}]+)}}', 'g'); 20 | 21 | // 22 | // Helper method to parse pattern str 23 | // 24 | var getMatches = function (pattern) { 25 | // Populate array of matches 26 | var matches = [], 27 | match; 28 | while(match = regexp.exec(pattern)) { 29 | matches.push(match); 30 | } 31 | 32 | return matches; 33 | }; 34 | 35 | // 36 | // Create an object holding all formatted characters 37 | // with corresponding positions 38 | // 39 | pattern.parse = function (pattern) { 40 | // Our obj to populate 41 | var info = { inpts: {}, chars: {} }; 42 | 43 | // Pattern information 44 | var matches = getMatches(pattern), 45 | pLength = pattern.length; 46 | 47 | // Counters 48 | var mCount = 0, 49 | iCount = 0, 50 | i = 0; 51 | 52 | // Add inpts, move to end of match, and process 53 | var processMatch = function (val) { 54 | var valLength = val.length; 55 | for (var j = 0; j < valLength; j++) { 56 | info.inpts[iCount] = val.charAt(j); 57 | iCount++; 58 | } 59 | mCount ++; 60 | i += (val.length + DELIM_SIZE - 1); 61 | }; 62 | 63 | // Process match or add chars 64 | for (i; i < pLength; i++) { 65 | if (mCount < matches.length && i === matches[mCount].index) { 66 | processMatch(matches[mCount][1]); 67 | } else { 68 | info.chars[i - (mCount * DELIM_SIZE)] = pattern.charAt(i); 69 | } 70 | } 71 | 72 | // Set mLength and return 73 | info.mLength = i - (mCount * DELIM_SIZE); 74 | return info; 75 | }; 76 | 77 | 78 | // Expose 79 | return pattern; 80 | 81 | 82 | }); -------------------------------------------------------------------------------- /src/tmpls/jquery.hbs: -------------------------------------------------------------------------------- 1 | // 2 | // Uses CommonJS, AMD or browser globals to create a jQuery plugin. 3 | // 4 | // Similar to jqueryPlugin.js but also tries to 5 | // work in a CommonJS environment. 6 | // It is unlikely jQuery will run in a CommonJS 7 | // environment. See jqueryPlugin.js if you do 8 | // not want to add the extra CommonJS detection. 9 | // 10 | (function (root, factory) { 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD. Register as an anonymous module. 13 | define({{#if amdModuleId}}'{{amdModuleId}}', {{/if}}[{{{amdDependencies}}}], factory); 14 | } else if (typeof exports === 'object') { 15 | factory({{{cjsDependencies}}}); 16 | } else { 17 | // Browser globals 18 | factory({{{globalDependencies}}}); 19 | } 20 | }(this, function ({{dependencies}}) { 21 | 22 | 23 | {{{code}}} 24 | 25 | 26 | // A really lightweight plugin wrapper around the constructor, 27 | // preventing against multiple instantiations 28 | var pluginName = 'formatter'; 29 | 30 | $.fn[pluginName] = function (options) { 31 | 32 | // Initiate plugin if options passed 33 | if (typeof options == 'object') { 34 | this.each(function () { 35 | if (!$.data(this, 'plugin_' + pluginName)) { 36 | $.data(this, 'plugin_' + pluginName, 37 | new formatter(this, options)); 38 | } 39 | }); 40 | } 41 | 42 | // Add resetPattern method to plugin 43 | this.resetPattern = function (str) { 44 | this.each(function () { 45 | var formatted = $.data(this, 'plugin_' + pluginName); 46 | // resetPattern for instance 47 | if (formatted) { formatted.resetPattern(str); } 48 | }); 49 | // Chainable please 50 | return this; 51 | }; 52 | 53 | // Chainable please 54 | return this; 55 | }; 56 | 57 | $.fn[pluginName].addInptType = function (chr, regexp) { 58 | formatter.addInptType(chr, regexp); 59 | }; 60 | 61 | 62 | })); -------------------------------------------------------------------------------- /src/tmpls/umd.hbs: -------------------------------------------------------------------------------- 1 | // 2 | // Uses Node, AMD or browser globals to create a module. This example creates 3 | // a global even when AMD is used. This is useful if you have some scripts 4 | // that are loaded by an AMD loader, but they still want access to globals. 5 | // If you do not need to export a global for the AMD case, 6 | // see returnExports.js. 7 | // 8 | // If you want something that will work in other stricter CommonJS environments, 9 | // or if you need to create a circular dependency, see commonJsStrictGlobal.js 10 | // 11 | // Defines a module "returnExportsGlobal" that depends another module called 12 | // "b". Note that the name of the module is implied by the file name. It is 13 | // best if the file name and the exported global have matching names. 14 | // 15 | // If the 'b' module also uses this type of boilerplate, then 16 | // in the browser, it will create a global .b that is used below. 17 | // 18 | (function (root, factory) { 19 | if (typeof define === 'function' && define.amd) { 20 | // AMD. Register as an anonymous module. 21 | define({{#if amdModuleId}}'{{amdModuleId}}', {{/if}}[{{{amdDependencies}}}], function () { 22 | return (root.returnExportsGlobal = factory({{{amdDependencies}}})); 23 | }); 24 | } else if (typeof exports === 'object') { 25 | // Node. Does not work with strict CommonJS, but 26 | // only CommonJS-like enviroments that support module.exports, 27 | // like Node. 28 | module.exports = factory({{{cjsDependencies}}}); 29 | } else { 30 | {{#if globalAlias}}root['{{{globalAlias}}}'] = {{else}}{{#if objectToExport}}root['{{{objectToExport}}}'] = {{/if}}{{/if}}factory({{{globalDependencies}}}); 31 | } 32 | }(this, function ({{dependencies}}) { 33 | 34 | 35 | {{{code}}} 36 | {{#if objectToExport}} 37 | {{indent}}return {{{objectToExport}}}; 38 | {{/if}} 39 | 40 | 41 | })); -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * utils.js 3 | * 4 | * Independent helper methods (cross browser, etc..) 5 | * 6 | */ 7 | 8 | 9 | define(function () { 10 | 11 | 12 | // Define module 13 | var utils = {}; 14 | 15 | // Useragent info for keycode handling 16 | var uAgent = (typeof navigator !== 'undefined') ? navigator.userAgent : null; 17 | 18 | // 19 | // Shallow copy properties from n objects to destObj 20 | // 21 | utils.extend = function (destObj) { 22 | for (var i = 1; i < arguments.length; i++) { 23 | for (var key in arguments[i]) { 24 | destObj[key] = arguments[i][key]; 25 | } 26 | } 27 | return destObj; 28 | }; 29 | 30 | // 31 | // Add a given character to a string at a defined pos 32 | // 33 | utils.addChars = function (str, chars, pos) { 34 | return str.substr(0, pos) + chars + str.substr(pos, str.length); 35 | }; 36 | 37 | // 38 | // Remove a span of characters 39 | // 40 | utils.removeChars = function (str, start, end) { 41 | return str.substr(0, start) + str.substr(end, str.length); 42 | }; 43 | 44 | // 45 | // Return true/false is num false between bounds 46 | // 47 | utils.isBetween = function (num, bounds) { 48 | bounds.sort(function(a,b) { return a-b; }); 49 | return (num > bounds[0] && num < bounds[1]); 50 | }; 51 | 52 | // 53 | // Helper method for cross browser event listeners 54 | // 55 | utils.addListener = function (el, evt, handler) { 56 | return (typeof el.addEventListener !== 'undefined') 57 | ? el.addEventListener(evt, handler, false) 58 | : el.attachEvent('on' + evt, handler); 59 | }; 60 | 61 | // 62 | // Helper method for cross browser implementation of preventDefault 63 | // 64 | utils.preventDefault = function (evt) { 65 | return (evt.preventDefault) ? evt.preventDefault() : (evt.returnValue = false); 66 | }; 67 | 68 | // 69 | // Helper method for cross browser implementation for grabbing 70 | // clipboard data 71 | // 72 | utils.getClip = function (evt) { 73 | if (evt.clipboardData) { return evt.clipboardData.getData('Text'); } 74 | if (window.clipboardData) { return window.clipboardData.getData('Text'); } 75 | }; 76 | 77 | // 78 | // Loop over object and checking for matching properties 79 | // 80 | utils.getMatchingKey = function (which, keyCode, keys) { 81 | // Loop over and return if matched. 82 | for (var k in keys) { 83 | var key = keys[k]; 84 | if (which === key.which && keyCode === key.keyCode) { 85 | return k; 86 | } 87 | } 88 | }; 89 | 90 | // 91 | // Returns true/false if k is a del keyDown 92 | // 93 | utils.isDelKeyDown = function (which, keyCode) { 94 | var keys = { 95 | 'backspace': { 'which': 8, 'keyCode': 8 }, 96 | 'delete': { 'which': 46, 'keyCode': 46 } 97 | }; 98 | 99 | return utils.getMatchingKey(which, keyCode, keys); 100 | }; 101 | 102 | // 103 | // Returns true/false if k is a del keyPress 104 | // 105 | utils.isDelKeyPress = function (which, keyCode) { 106 | var keys = { 107 | 'backspace': { 'which': 8, 'keyCode': 8, 'shiftKey': false }, 108 | 'delete': { 'which': 0, 'keyCode': 46 } 109 | }; 110 | 111 | return utils.getMatchingKey(which, keyCode, keys); 112 | }; 113 | 114 | // // 115 | // // Determine if keydown relates to specialKey 116 | // // 117 | // utils.isSpecialKeyDown = function (which, keyCode) { 118 | // var keys = { 119 | // 'tab': { 'which': 9, 'keyCode': 9 }, 120 | // 'enter': { 'which': 13, 'keyCode': 13 }, 121 | // 'end': { 'which': 35, 'keyCode': 35 }, 122 | // 'home': { 'which': 36, 'keyCode': 36 }, 123 | // 'leftarrow': { 'which': 37, 'keyCode': 37 }, 124 | // 'uparrow': { 'which': 38, 'keyCode': 38 }, 125 | // 'rightarrow': { 'which': 39, 'keyCode': 39 }, 126 | // 'downarrow': { 'which': 40, 'keyCode': 40 }, 127 | // 'F5': { 'which': 116, 'keyCode': 116 } 128 | // }; 129 | 130 | // return utils.getMatchingKey(which, keyCode, keys); 131 | // }; 132 | 133 | // 134 | // Determine if keypress relates to specialKey 135 | // 136 | utils.isSpecialKeyPress = function (which, keyCode) { 137 | var keys = { 138 | 'tab': { 'which': 0, 'keyCode': 9 }, 139 | 'enter': { 'which': 13, 'keyCode': 13 }, 140 | 'end': { 'which': 0, 'keyCode': 35 }, 141 | 'home': { 'which': 0, 'keyCode': 36 }, 142 | 'leftarrow': { 'which': 0, 'keyCode': 37 }, 143 | 'uparrow': { 'which': 0, 'keyCode': 38 }, 144 | 'rightarrow': { 'which': 0, 'keyCode': 39 }, 145 | 'downarrow': { 'which': 0, 'keyCode': 40 }, 146 | 'F5': { 'which': 116, 'keyCode': 116 } 147 | }; 148 | 149 | return utils.getMatchingKey(which, keyCode, keys); 150 | }; 151 | 152 | // 153 | // Returns true/false if modifier key is held down 154 | // 155 | utils.isModifier = function (evt) { 156 | return evt.ctrlKey || evt.altKey || evt.metaKey; 157 | }; 158 | 159 | // 160 | // Iterates over each property of object or array. 161 | // 162 | utils.forEach = function (collection, callback, thisArg) { 163 | if (collection.hasOwnProperty('length')) { 164 | for (var index = 0, len = collection.length; index < len; index++) { 165 | if (callback.call(thisArg, collection[index], index, collection) === false) { 166 | break; 167 | } 168 | } 169 | } else { 170 | for (var key in collection) { 171 | if (collection.hasOwnProperty(key)) { 172 | if (callback.call(thisArg, collection[key], key, collection) === false) { 173 | break; 174 | } 175 | } 176 | } 177 | } 178 | }; 179 | 180 | 181 | // Expose 182 | return utils; 183 | 184 | 185 | }); -------------------------------------------------------------------------------- /test/_runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | formatter.js 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 51 | 52 | -------------------------------------------------------------------------------- /test/formatter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * test/formatter.js 3 | * 4 | * Copyright (c) 2014 First Opinion 5 | */ 6 | 7 | 8 | define([ 9 | 'jquery', 10 | 'proclaim', 11 | 'sinon', 12 | 'fakey', 13 | 'formatter', 14 | 'inpt-sel', 15 | 'pattern-matcher', 16 | 'pattern', 17 | 'utils' 18 | ], function ($, assert, sinon, fakey, Formatter, inptSel, utils) { 19 | 20 | 21 | // 22 | // formatter.js tests 23 | // 24 | describe('formatter.js', function () { 25 | 26 | // Global vars 27 | var $workboard = $('#workboard'); 28 | 29 | // Test vars 30 | var formatted, $el, el; 31 | 32 | // Define new inptType 33 | Formatter.addInptType('D', /[A-Za-z0-9.]/); 34 | 35 | // Add fresh element 36 | beforeEach(function () { 37 | var html = $('#tmpl-input-text').html(); 38 | $el = $(html); 39 | el = $el[0]; 40 | 41 | $('#workboard').append($el); 42 | }); 43 | 44 | // Remove element 45 | afterEach(function () { 46 | $el.remove(); 47 | }); 48 | 49 | // 50 | // Formatter global tests 51 | // 52 | describe('global', function () { 53 | 54 | // Create new instance 55 | var createInstance = function (str) { 56 | formatted = new Formatter($el[0], { 57 | pattern: str, 58 | persistent: true 59 | }); 60 | }; 61 | 62 | it('Should set init values and merge defaults', function () { 63 | createInstance('({{999}}) {{999}}-{{9999}}'); 64 | 65 | // Check opts 66 | assert.equal(formatted.opts.patterns[0]['*'], '({{999}}) {{999}}-{{9999}}'); 67 | assert.isTrue(formatted.opts.persistent); 68 | // Check pattern 69 | assert.isObject(formatted.chars); 70 | assert.isObject(formatted.inpts); 71 | assert.isNumber(formatted.mLength); 72 | // Check init values 73 | assert.isObject(formatted.hldrs); 74 | assert.isNumber(formatted.focus); 75 | }); 76 | 77 | it('Should natively handle home, end, and arrow keys', function (done) { 78 | createInstance('({{999}}) {{999}}-{{9999}}'); 79 | 80 | sinon.spy(Formatter.prototype, '_processKey'); 81 | 82 | fakey.seq(el, [ 83 | { key: 'leftarrow' }, 84 | { key: 'rightarrow' }, 85 | { key: 'uparrow' }, 86 | { key: 'downarrow' }, 87 | { key: 'home' }, 88 | { key: 'end' }, 89 | { key: 'enter' }, 90 | { key: 'tab' } 91 | ], function () { 92 | assert.ok(Formatter.prototype._processKey.notCalled); 93 | Formatter.prototype._processKey.restore(); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('Should be capable of containg a period in the pattern', function (done) { 99 | createInstance('http://www.{{DDDDDDDD}}'); 100 | 101 | fakey.seq(el, [ 102 | { str: 'abcd' }, 103 | { key: '.' }, 104 | { str: 'com'} 105 | ], function () { 106 | assert.equal(formatted.el.value, 'http://www.abcd.com'); 107 | done(); 108 | }); 109 | }); 110 | 111 | it('Should update value when resetPattern method is called', function (done) { 112 | createInstance('({{999}}) {{999}}-{{9999}}'); 113 | 114 | fakey.str(el, '24567890', function () { 115 | formatted.resetPattern('{{999}}.{{999}}.{{9999}}'); 116 | assert.equal(formatted.el.value, '245.678.90 '); 117 | done(); 118 | }); 119 | }); 120 | 121 | it('Should focus to the next available inpt position', function (done) { 122 | createInstance('({{999}}) {{999}}-{{9999}}'); 123 | 124 | fakey.str(el, '1237890', function () { 125 | assert.equal(formatted.focus, 11); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('Should not focus on a formatted char', function (done) { 131 | createInstance('({{999}}) {{999}}-{{9999}}'); 132 | 133 | fakey.str(el, '123', function () { 134 | assert.equal(formatted.focus, 6); 135 | done(); 136 | }); 137 | }); 138 | 139 | it('Should enforce pattern maxLength', function (done) { 140 | createInstance('({{999}}) {{999}}-{{9999}}'); 141 | 142 | fakey.str(el, '12345678901', function () { 143 | assert.equal(formatted.el.value, '(123) 456-7890'); 144 | done(); 145 | }); 146 | }); 147 | 148 | it('Should add regex inpts', function (done) { 149 | Formatter.addInptType('L', /[A-Z]/); 150 | createInstance('{{LLL}}'); 151 | 152 | fakey.str(el, 'AaAaA', function () { 153 | assert.equal(formatted.el.value, 'AAA'); 154 | done(); 155 | }); 156 | }); 157 | 158 | }); 159 | 160 | // 161 | // Formatter with value dependent patterns 162 | // 163 | describe('value dependent patterns', function () { 164 | 165 | it('Should apply the default format', function (done) { 166 | formatted = new Formatter(el, { 167 | patterns: [ 168 | { '*': '!{{9}}' } 169 | ] 170 | }); 171 | 172 | fakey.key(el, '1', function () { 173 | assert.equal(formatted.el.value, '!1'); 174 | done(); 175 | }); 176 | }); 177 | 178 | it('Should apply appropriate format based on current value', function (done) { 179 | formatted = new Formatter(el, { 180 | patterns: [ 181 | { '^0': '!{{9999}}' }, 182 | { '*': '{{9999}}' } 183 | ] 184 | }); 185 | 186 | fakey.str(el, '0123', function () { 187 | assert.equal(formatted.el.value, '!0123'); 188 | done(); 189 | }); 190 | }); 191 | 192 | it('Should apply the first appropriate format that matches the current value', function (done) { 193 | formatted = new Formatter(el, { 194 | patterns: [ 195 | { '^0': 'first:{{9999}}' }, 196 | { '^00': 'second:{{9999}}' } 197 | ] 198 | }); 199 | 200 | fakey.str(el, '00', function () { 201 | assert.equal(formatted.el.value, 'first:00'); 202 | done(); 203 | }); 204 | }); 205 | }); 206 | 207 | // 208 | // Formatter with persistence 209 | // 210 | describe('persistent: true', function () { 211 | 212 | beforeEach(function () { 213 | formatted = new Formatter(el, { 214 | pattern: '({{999}}) {{999}}-{{9999}}', 215 | persistent: true 216 | }); 217 | }); 218 | 219 | it('Should format chars as they are entered', function (done) { 220 | fakey.str(el, '1237890', function () { 221 | assert.equal(formatted.el.value, '(123) 789-0 '); 222 | done(); 223 | }); 224 | }); 225 | 226 | it('Should fromat chars entered mid str', function (done) { 227 | fakey.str(el, '1237890', function () { 228 | inptSel.set(el, 6); 229 | fakey.str(el, '456', function () { 230 | assert.equal(formatted.el.value, '(123) 456-7890'); 231 | done(); 232 | }); 233 | }); 234 | }); 235 | 236 | it('Should delete chars when highlighted', function (done) { 237 | fakey.str(el, '1234567890', function () { 238 | inptSel.set(el, { begin: 2, end: 8 }); 239 | fakey.key(el, 'backspace', function () { 240 | assert.equal(formatted.el.value, '(167) 890- '); 241 | done(); 242 | }); 243 | }); 244 | }); 245 | 246 | // it('Should handle pasting multiple characters', function (done) { 247 | // user.keySeq('167890', function () { 248 | // sel = { begin: 2, end: 2 }; 249 | // user.paste('2345', function () { 250 | // assert.equal(formatted.el.value, '(123) 456-7890'); 251 | // done(); 252 | // }); 253 | // }); 254 | // }); 255 | 256 | it('Should remove previous character on backspace key', function (done) { 257 | fakey.str(el, '1234567890', function () { 258 | inptSel.set(el, 2); 259 | fakey.key(el, 'backspace', function () { 260 | assert.equal(formatted.el.value, '(234) 567-890 '); 261 | done(); 262 | }); 263 | }); 264 | }); 265 | 266 | it('Should remove characters in correct order when backspacing over a formatted character.', function (done) { 267 | fakey.str(el, '1234567890', function () { 268 | fakey.key(el, 'backspace', 6, function () { 269 | assert.equal(formatted.el.value, '(123) 45 - '); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | it('Should remove next character on delete key', function (done) { 276 | fakey.str(el, '234567890', function () { 277 | inptSel.set(el, 2); 278 | fakey.key(el, 'delete', function () { 279 | assert.equal(formatted.el.value, '(245) 678-90 '); 280 | done(); 281 | }); 282 | }); 283 | }); 284 | 285 | }); 286 | 287 | // 288 | // Formatter without persistence 289 | // 290 | describe('persistent: false', function () { 291 | 292 | beforeEach(function () { 293 | formatted = new Formatter(el, { 294 | pattern: '({{999}}) {{999}}-{{9999}}' 295 | }); 296 | }); 297 | 298 | it('Should format chars as they are entered', function (done) { 299 | fakey.str(el, '1237890', function () { 300 | assert.equal(formatted.el.value, '(123) 789-0'); 301 | done(); 302 | }); 303 | }); 304 | 305 | it('Should fromat chars entered mid str', function (done) { 306 | fakey.str(el, '1237890', function () { 307 | inptSel.set(el, 6); 308 | fakey.str(el, '456', function () { 309 | assert.equal(formatted.el.value, '(123) 456-7890'); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | 315 | it('Should delete chars when highlighted', function (done) { 316 | fakey.str(el, '1234567890', function () { 317 | inptSel.set(el, { begin: 2, end: 8 }); 318 | fakey.key(el, 'backspace', function () { 319 | assert.equal(formatted.el.value, '(167) 890'); 320 | done(); 321 | }); 322 | }); 323 | }); 324 | 325 | // it('Should handle pasting multiple characters', function (done) { 326 | // fakey.str(el, '167890', function () { 327 | // inptSel.set(el, 2); 328 | // user.paste('2345', function () { 329 | // assert.equal(formatted.el.value, '(123) 456-7890'); 330 | // done(); 331 | // }); 332 | // }); 333 | // }); 334 | 335 | it('Should remove previous character on backspace key', function (done) { 336 | fakey.str(el, '1234567890', function () { 337 | inptSel.set(el, 2); 338 | fakey.key(el, 'backspace', function () { 339 | assert.equal(formatted.el.value, '(234) 567-890'); 340 | done(); 341 | }); 342 | }); 343 | }); 344 | 345 | it('Should remove a format character when it is the last character on backspace key', function (done) { 346 | fakey.str(el, '123', function () { 347 | fakey.key(el, 'backspace', function () { 348 | assert.equal(formatted.el.value, '(123'); 349 | done(); 350 | }); 351 | }); 352 | }); 353 | 354 | it('Should completely empty field', function (done) { 355 | fakey.key(el, '1', function () { 356 | fakey.key(el, 'backspace', function () { 357 | assert.equal(formatted.el.value, ''); 358 | done(); 359 | }); 360 | }); 361 | }); 362 | 363 | it('Should not add chars past focus location if deleting', function (done) { 364 | fakey.str(el, '1234567', function () { 365 | fakey.key(el, 'backspace', function () { 366 | assert.equal(formatted.el.value, '(123) 456'); 367 | done(); 368 | }); 369 | }); 370 | }); 371 | 372 | it('Should remove next character on delete key', function (done) { 373 | fakey.str(el, '234567890', function () { 374 | inptSel.set(el, 2); 375 | fakey.key(el, 'delete', function () { 376 | assert.equal(formatted.el.value, '(245) 678-90'); 377 | done(); 378 | }); 379 | }); 380 | }); 381 | 382 | it('Should update value when resetPattern method is called', function (done) { 383 | fakey.str(el, '24567890', function () { 384 | formatted.resetPattern('{{999}}.{{999}}.{{9999}}'); 385 | assert.equal(formatted.el.value, '245.678.90'); 386 | done(); 387 | }); 388 | }); 389 | 390 | it('Should update value when resetPattern method is called without changing pattern', function (done) { 391 | fakey.str(el, '2456789013', function () { 392 | formatted.resetPattern(); 393 | assert.equal(formatted.el.value, '(245) 678-9013'); 394 | done(); 395 | }); 396 | }); 397 | 398 | it('Should not reset caret position on format', function (done) { 399 | fakey.str(el, '24567890', function () { 400 | formatted.resetPattern(); 401 | assert.equal(formatted.el.value, '(245) 678-90'); 402 | assert.deepEqual({ begin: 12, end: 12 }, inptSel.get(el)); 403 | done(); 404 | }); 405 | }); 406 | 407 | }); 408 | 409 | }); 410 | 411 | 412 | }); -------------------------------------------------------------------------------- /test/pattern-matcher.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * test/pattern-matcher.js 3 | * 4 | * Copyright (c) 2014 First Opinion 5 | */ 6 | 7 | 8 | define([ 9 | 'proclaim', 10 | 'pattern-matcher', 11 | 'pattern' 12 | ], function (assert, patternMatcher, pattern) { 13 | 14 | 15 | // 16 | // pattern-matcher.js tests 17 | // 18 | describe('pattern-matcher.js', function () { 19 | 20 | // Pattern Strings 21 | var patternStringA = '!{{*}}{{*}}', 22 | patternStringB = '@{{*}}{{*}}'; 23 | 24 | // Parsed Patterns 25 | var patternA = pattern.parse(patternStringA), 26 | patternB = pattern.parse(patternStringB); 27 | 28 | it('Should parse each matcher as a regex', function () { 29 | var myPatternMatcher = patternMatcher([ 30 | { '^abc$': '{{*}}-{{*}}-{{*}}' } 31 | ]); 32 | 33 | assert.isTrue(myPatternMatcher.matchers[0].test('abc')); 34 | assert.isFalse(myPatternMatcher.matchers[0].test('xyz')); 35 | }); 36 | 37 | it('Should parse each pattern as a pattern', function () { 38 | var myPatternMatcher = patternMatcher([ 39 | { '^abc$': '{{***}}' } 40 | ]); 41 | 42 | assert.deepEqual(myPatternMatcher.patterns[0], pattern.parse('{{***}}')); 43 | }); 44 | 45 | describe('getPattern', function () { 46 | 47 | it('Should return the appropriate pattern for the input', function () { 48 | var myPatternMatcher = patternMatcher([ 49 | { '^a': patternStringA }, 50 | { '^b': patternStringB } 51 | ]); 52 | 53 | assert.deepEqual(myPatternMatcher.getPattern('a'), patternA); 54 | assert.deepEqual(myPatternMatcher.getPattern('abc'), patternA); 55 | assert.deepEqual(myPatternMatcher.getPattern('bac'), patternB); 56 | }); 57 | 58 | it('Should return the first matching pattern', function () { 59 | var myPatternMatcher = patternMatcher([ 60 | { '^a': patternStringA }, 61 | { '.*': patternStringB } 62 | ]); 63 | 64 | assert.deepEqual(myPatternMatcher.getPattern('a'), patternA); 65 | assert.deepEqual(myPatternMatcher.getPattern('aa'), patternA); 66 | }); 67 | 68 | it('Should return the wildcard pattern "*" if no other matches', function () { 69 | var myPatternMatcher = patternMatcher([ 70 | { 'wont-match': patternStringA }, 71 | { '*': patternStringB } 72 | ]); 73 | 74 | assert.deepEqual(myPatternMatcher.getPattern('a'), patternB); 75 | }); 76 | 77 | it('Should return null if no pattern matches', function () { 78 | var myPatternMatcher = patternMatcher([ 79 | { 'wont-match': patternStringA }, 80 | { '*': patternStringB } 81 | ]); 82 | 83 | assert.deepEqual(myPatternMatcher.getPattern('a'), patternB); 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | 90 | 91 | }); -------------------------------------------------------------------------------- /test/pattern.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * test/pattern.js 3 | * 4 | * Copyright (c) 2014 First Opinion 5 | */ 6 | 7 | 8 | define([ 9 | 'proclaim', 10 | 'pattern' 11 | ], function (assert, pattern) { 12 | 13 | 14 | // 15 | // pattern.js tests 16 | // 17 | describe('pattern.js', function () { 18 | 19 | // pattern.parse 20 | // Create an object holding all formatted characters 21 | // with corresponding positions 22 | describe('parse', function () { 23 | 24 | it('Should return an obj with pattern info', function () { 25 | var result = pattern.parse('({{9A*}}) {{9A*}}-{{AAAA}}'); 26 | assert.deepEqual(result.chars, { 27 | '0': '(', 28 | '4': ')', 29 | '5': ' ', 30 | '9': '-' 31 | }); 32 | assert.deepEqual(result.inpts, { 33 | '0': '9', 34 | '1': 'A', 35 | '2': '*', 36 | '3': '9', 37 | '4': 'A', 38 | '5': '*', 39 | '6': 'A', 40 | '7': 'A', 41 | '8': 'A', 42 | '9': 'A' 43 | }); 44 | assert.equal(result.mLength, 14); 45 | }); 46 | 47 | describe('edge cases', function () { 48 | 49 | it('Should parse a pattern with a leading char', function () { 50 | var result = pattern.parse('_{{**}}'); 51 | 52 | assert.deepEqual(result.chars, { 53 | '0': '_' 54 | }); 55 | assert.deepEqual(result.inpts, { 56 | '0': '*', 57 | '1': '*' 58 | }); 59 | assert.equal(result.mLength, 3); 60 | }); 61 | 62 | it('Should parse a pattern with a trailing char', function () { 63 | var result = pattern.parse('{{**}}_'); 64 | 65 | assert.deepEqual(result.chars, { 66 | '2': '_' 67 | }); 68 | assert.deepEqual(result.inpts, { 69 | '0': '*', 70 | '1': '*' 71 | }); 72 | assert.equal(result.mLength, 3); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | 81 | 82 | }); -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * test/utils.js 3 | * 4 | * Copyright (c) 2014 First Opinion 5 | */ 6 | 7 | 8 | define([ 9 | 'proclaim', 10 | 'utils' 11 | ], function (assert, utils) { 12 | 13 | 14 | // 15 | // utils.js tests 16 | // 17 | describe('utils.js', function () { 18 | 19 | // utils.extend 20 | // Shallow copy properties from n objects to destObj 21 | describe('extend', function () { 22 | 23 | it('Should return an obj with merged props', function () { 24 | // Setup Data 25 | var defaults, opts; 26 | defaults = { 27 | 'extend' : 'should', 28 | 'overwrite': 'all' 29 | }; 30 | opts = { 31 | 'overwrite': 'default', 32 | 'values' : 'to', 33 | 'the' : 'destObj' 34 | }; 35 | 36 | // Run extend 37 | var result = utils.extend({}, defaults, opts); 38 | // Check results 39 | assert.deepEqual(result, { 40 | 'extend' : 'should', 41 | 'overwrite': 'default', 42 | 'values' : 'to', 43 | 'the' : 'destObj' 44 | }); 45 | // Make sure defaults & opts were not changed 46 | assert.deepEqual({ 47 | 'extend' : 'should', 48 | 'overwrite': 'all' 49 | }, defaults); 50 | assert.deepEqual({ 51 | 'overwrite': 'default', 52 | 'values' : 'to', 53 | 'the' : 'destObj' 54 | }, opts); 55 | }); 56 | 57 | }); 58 | 59 | // utils.addChars 60 | // Add a given character to a string at a defined pos 61 | describe('addChars', function () { 62 | 63 | it('Should add chars to str starting at pos', function () { 64 | var result = utils.addChars('add the str', 'inbetween ', 4); 65 | assert.equal(result, 'add inbetween the str'); 66 | }); 67 | 68 | }); 69 | 70 | // utils.removeChars 71 | // Remove a span of characters 72 | describe('removeChars', function () { 73 | 74 | it('Should remove span of chars', function () { 75 | var result = utils.removeChars('remove uneccesary chars', 6, 17); 76 | assert.equal(result, 'remove chars'); 77 | }); 78 | 79 | }); 80 | 81 | // utils.isBetween 82 | // Return true/false is num false between bounds 83 | describe('removeChars', function () { 84 | 85 | it('Should return true when between range', function () { 86 | var result = utils.isBetween(2, [1, 3]); 87 | assert.isTrue(result); 88 | }); 89 | 90 | it('Should return true regardless of order', function () { 91 | var result = utils.isBetween(2, [3, 1]); 92 | assert.isTrue(result); 93 | }); 94 | 95 | it('Should return false when not between range', function () { 96 | var result = utils.isBetween(4, [1, 3]); 97 | assert.isFalse(result); 98 | }); 99 | 100 | }); 101 | 102 | 103 | // utils.forEach 104 | // Iterate over a collection 105 | describe('forEach', function () { 106 | 107 | it('Should iterate over an array', function () { 108 | var result = []; 109 | utils.forEach(['a','b','c'], function (val, key) { 110 | result.push(key); 111 | result.push(val); 112 | }); 113 | 114 | assert.deepEqual(result, [0,'a',1,'b',2,'c']); 115 | }); 116 | 117 | it('Should iterate over an object', function () { 118 | var result = []; 119 | utils.forEach({ 'first': 'a', second: 'b' }, function (val, key) { 120 | result.push(key); 121 | result.push(val); 122 | }); 123 | 124 | assert.deepEqual(result, ['first', 'a', 'second', 'b']); 125 | }); 126 | 127 | it('Should ignore prototypically inherited properties', function () { 128 | var Parent = function () { 129 | this.property = 'property'; 130 | }; 131 | Parent.prototype = { protoProperty: 'protoProperty' } ; 132 | 133 | var result = []; 134 | utils.forEach(new Parent(), function (val) { 135 | result.push(val); 136 | }); 137 | 138 | assert.deepEqual(result, ['property']); 139 | }); 140 | 141 | it('Should stop short when callback returns false', function () { 142 | var result = []; 143 | utils.forEach(['a','b','c'], function (val, key) { 144 | result.push(key); 145 | result.push(val); 146 | return false; 147 | }); 148 | 149 | assert.deepEqual(result, [0,'a']); 150 | }); 151 | 152 | it('Should bind the callback to the given thisArg', function () { 153 | var result = []; 154 | utils.forEach(['a','b','c'], function (val, key) { 155 | this.push(key); 156 | this.push(val); 157 | }, result); 158 | 159 | assert.deepEqual(result, [0,'a',1,'b',2,'c']); 160 | }); 161 | 162 | }); 163 | 164 | }); 165 | 166 | 167 | }); --------------------------------------------------------------------------------