├── .gitignore ├── .npmignore ├── .travis.yml ├── CARDRULES.md ├── CHANGELOG ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bower.json ├── dist ├── jquery.payform.js ├── jquery.payform.min.js ├── payform.js └── payform.min.js ├── index.html ├── jquery.html ├── package-lock.json ├── package.json ├── src ├── jquery.payform.coffee └── payform.coffee └── test ├── cardType_spec.coffee ├── formatCardExpiry_spec.coffee ├── formatCardNumber_spec.coffee ├── mocha.opts ├── parseCardExpiry_spec.coffee ├── validateCardCVC_spec.coffee ├── validateCardExpiry_spec.coffee └── validateCardNumber_spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower.json 2 | .npmignore 3 | CONTRIBUTING.md 4 | Makefile 5 | index.html 6 | jquery.html 7 | .travis.yml 8 | .lvimrc 9 | test/* 10 | src/* 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" 5 | - "7" 6 | - "6" 7 | -------------------------------------------------------------------------------- /CARDRULES.md: -------------------------------------------------------------------------------- 1 | # Supported Card Types 2 | 3 | The following list contains Issuer Identification Number (IIN) patterns and length for all debit and credit card types supported by Payform. Please note that while references are provided, there may be some missing matching patterns. Nevertheless, the current regular expressions used are valid with respect to these sources. 4 | 5 | ## Credit Cards 6 | 7 | ### American Express 8 | 9 | **IIN Pattern:** 34, 37 [1] 10 | 11 | **Length:** 15 [2] 12 | 13 | ### Diners Club 14 | 15 | **IIN Pattern:** 36, 38, 30[0-5] [3] 16 | 17 | **Length:** 14 [3] 18 | 19 | ### Discover 20 | 21 | **IIN Pattern:** 6011, 65, 64[4-9], 622 [3] 22 | 23 | **Length:** 16 [3] 24 | 25 | ### Hipercard 26 | 27 | **IIN Pattern:** 384100, 384140, 384160, 606282, 637095, 637568, 60(?!11) [4], [5] 28 | 29 | **Length:** 14-19 30 | 31 | ### JCB 32 | 33 | **IIN Pattern:** 35 [3] 34 | 35 | **Length:** 16-19 [3] 36 | 37 | ### Mastercard 38 | 39 | **IIN Pattern:** 222100...272099, 510000..559999, 677189 [1], [6], [13] 40 | 41 | **Length:** 16 42 | 43 | ### Unionpay 44 | 45 | **IIN Pattern:** 62 [3] 46 | 47 | **Length:** 16-19 [3] 48 | 49 | ### Visa 50 | 51 | **IIN Pattern:** 4 [7] 52 | 53 | **Length:** 13, 16, 19 54 | 55 | ## Debit Cards 56 | 57 | ### Dankkort 58 | 59 | **IIN Pattern:** 5019 [8] 60 | 61 | **Length:** 16 62 | 63 | ### Elo 64 | 65 | **IIN Pattern:** (4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021) [9], [10] 66 | 67 | **Length:** 16 68 | 69 | ### Forbrugsforeningen 70 | 71 | **IIN Pattern:** 600 [11] 72 | 73 | **Length:** 16 74 | 75 | ### Maestro 76 | 77 | **IIN Pattern:** 5018, 5020, 5038, 6304, 639000 to 639099, 670000 to 679999 [12], [13] 78 | 79 | **Length:** 12-19 80 | 81 | ### Visa Electron 82 | 83 | **IIN Pattern:** 4026, 417500, 4405, 4508, 4844, 4913, 4917 [7] 84 | 85 | **Length:** 16 86 | 87 | 88 | 89 | [1]: https://www.moneris.com/-/media/Moneris/Files/EN/Support/Compliance-Information/CAG_booklet.pdf 90 | [2]: https://www.cybersource.com/developers/getting_started/test_and_manage/best_practices/card_type_id/ 91 | [3]: https://www.discovernetwork.com/downloads/IPP_VAR_Compliance.pdf 92 | [4]: https://mage2.pro/t/topic/3865 93 | [5]: https://stevemorse.org/ssn/List_of_Bank_Identification_Numbers.html 94 | [6]: https://www.mastercard.us/en-us/issuers/get-support/2-series-bin-expansion.html 95 | [7]: https://baymard.com/checkout-usability/credit-card-patterns 96 | [8]: https://www.nets.eu/dk-da/kundeservice/Verifikation%20af%20betalingsl%C3%B8sninger/Documents/ct-trg-otrs-en.pdf 97 | [9]: https://mage2.pro/t/topic/3867 98 | [10]: https://github.com/Adyen/adyen-magento/issues/236 99 | [11]: https://tech.dibspayment.com/D2/Toolbox/Test_information/Cards 100 | [12]: http://blog.unibulmerchantservices.com/12-signs-of-a-valid-mastercard-card/ 101 | [13]: https://www.mastercard.us/content/dam/mccom/global/documents/mastercard-rules.pdf 102 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | = 1.4.0 2 | * Add support for detaching events from input fields (see PR #62) 3 | * Use a 2-digit precision for Mastercard BIN validation patterns (see PR #62) 4 | 5 | = 1.3.0 6 | * Updated Issuer Identification Number (IIN) patterns with documentaion (see PR #44 and #47) 7 | * Allow right and left arrow keys to be used while navigating inside all input types (see PR #45) 8 | * Fix issue with clearning selected text when typing (see PR #48) 9 | * Fix issuewith the expiry field parsing when typing in a RTL context (see PR #50) 10 | * Allow cursor repositioning when pasting full card numbers (see PR #51) 11 | 12 | = 1.2.5 13 | * Fixes #37, allowing for vendoring and fix event normalization (PR #39) 14 | * Fixes #38, full width character fixes for Safari 15 | * Fixes #41, improve RTL support 16 | 17 | = 1.2.4 18 | * Fix issue with cutting off last 2 digits of some cards (see #34 and #25) 19 | * Update Mocha to 3.5.3 (CVE) 20 | 21 | = 1.2.3 22 | * Fix issue in handling full width characters (see PR #36) 23 | 24 | = 1.2.2 25 | * Fix IE11 hanging on input (see PR #32) 26 | 27 | = 1.2.1 28 | * Correct Diners Club Pattern (regarding #19 and #22) 29 | 30 | = 1.2.0 31 | * Updated Diners Club Pattern 32 | * New Mastercard Ranges 33 | * Support for full width input modes 34 | 35 | = 1.1.0 36 | * Add jQuery Plugin shim 37 | * Add numeric input formatter `numericInput` 38 | * Move build process to makefile 39 | 40 | = 1.0.2 41 | * Fix bug with 1 or 0 in expiry formatting 42 | * Fix strange behavior with cursor position on 'change' events 43 | * Fix FF bug navigating CVC field with arrows 44 | * IE8 Support 45 | 46 | = 1.0.1 47 | * Fix cursor issue when editing fields 48 | 49 | = 1.0.0 50 | * Initial Release 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Yay, you're interested in helping this thing suck less. Thank you! 4 | 5 | ## Project Layout 6 | 7 | - `src/` - Coffeescript Source 8 | - `dist/` - Compiled and Minified 9 | - `test/` - Unit Tests 10 | 11 | ## Having a problem? 12 | 13 | A **great** way to start a discussion about a potential issue is to 14 | submit a pull request including a failing test case. It's really hard to 15 | misunderstand a problem presented this way. This way it's clear what the 16 | problem is before you spend your valuable time trying to fix it. 17 | 18 | ## Have an idea to make it better? 19 | 20 | Again, guard your time and effort. Make sure that you don't spend a lot 21 | of time on an improvement without talking through it first. 22 | 23 | ## Getting to work 24 | 25 | ```sh 26 | npm install 27 | npm run build 28 | npm test 29 | ``` 30 | 31 | ## Pull Requests 32 | 33 | **Make sure to send pull requests to `develop`.** 34 | 35 | Good Pull Requests include: 36 | 37 | - A clear explaination of the problem (or enhancement) 38 | - Clean commit history (squash where it makes sense) 39 | - Relevant Tests (either updated and/or new) 40 | 41 | ## Release Process 42 | 43 | We strive for [semantic versioning](https://semver.org/) for our version number assignment, and utilize the [git flow](https://github.com/nvie/gitflow) tool to execute releases in the repository. 44 | 45 | You can initialize git flow once it is installed with 46 | 47 | ``` 48 | $> git flow init -d 49 | ``` 50 | 51 | This will use the default branch naming conventions for git flow. 52 | 53 | All new functionality should come in on the `develop` branch and when you're ready to cut a new release, start the process by using 54 | 55 | ``` 56 | $> git flow release start 1.x.x 57 | ``` 58 | 59 | This should give you a release branch off develop and some relevant instructions. 60 | 61 | This is when you should: 62 | - Bump the version numbers in both `src/payform.coffee` and `package.json` 63 | - Update the `CHANGELOG` by adding a new section for this version 64 | - Ensure the tests pas with `make test` 65 | - Run `make clean && make build` 66 | 67 | Once you've done this and committed these changes to the release branch, you are ready to run: 68 | 69 | ``` 70 | $> git flow release finish 1.x.x 71 | ``` 72 | 73 | This will: 74 | - Merge the release branch into `master` and also back into `develop` 75 | - Create a tag for the release and prompt you for an annotation (I usually paste in the relevant `CHANGELOG` entry) 76 | 77 | At this point you should push `master` and `develop`, and also the new tag with `git push --tags` 78 | 79 | ### Publishing to npm 80 | 81 | Once the release process is complete, and you're confident it is correct, you should be able to publish to npm with 82 | 83 | ``` 84 | $> npm publish 85 | ``` 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original work as `jquery.payment` Copyright (c) 2014 Stripe 2 | Modified work as `payform` Copyright (c) 2015 Jonathan D. Johnson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | BIN := node_modules/.bin/ 3 | 4 | build: dist/payform.js dist/payform.min.js dist/jquery.payform.js dist/jquery.payform.min.js 5 | 6 | dist/payform.js: src/payform.coffee 7 | $(BIN)coffee -c --no-header -o dist/ src/payform.coffee 8 | 9 | dist/payform.min.js: dist/payform.js 10 | $(BIN)uglifyjs dist/payform.js -o dist/payform.min.js 11 | 12 | dist/jquery.payform.js: src/jquery.payform.coffee 13 | $(BIN)browserify \ 14 | -p bundle-collapser/plugin \ 15 | -t coffeeify \ 16 | --extension='.coffee' \ 17 | src/jquery.payform.coffee > dist/jquery.payform.js 18 | 19 | dist/jquery.payform.min.js: dist/jquery.payform.js 20 | $(BIN)uglifyjs dist/jquery.payform.js -o dist/jquery.payform.min.js 21 | 22 | watch: build 23 | $(BIN)watch 'make build' src 24 | 25 | test: 26 | $(BIN)mocha test/**_spec.coffee 27 | 28 | clean: 29 | rm -rf dist/*.js 30 | 31 | .PHONY: build test watch 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 |

3 |

Do you use webpack?

4 |

5 |

6 | Wish your team made reducing the size of your webpack builds a priority? Want to know how the changes you're making impact your asset profile for every pull request? 7 |

8 |

9 | Check it out at packtracker.io. 10 |

11 | 12 | 13 | --- 14 | # payform 15 | 16 | [![Build Status](https://travis-ci.org/jondavidjohn/payform.svg?branch=master)](https://travis-ci.org/jondavidjohn/payform) 17 | ![Dependencies](https://david-dm.org/jondavidjohn/payform.svg) 18 | 19 | A general purpose library for building credit card forms, validating inputs, and formatting numbers. 20 | 21 | Supported card types: 22 | 23 | * Visa 24 | * MasterCard 25 | * American Express 26 | * Diners Club 27 | * Discover 28 | * UnionPay 29 | * JCB 30 | * Visa Electron 31 | * Maestro 32 | * Forbrugsforeningen 33 | * Dankort 34 | 35 | (Custom card types are also [supported](#custom-cards)) 36 | 37 | Works in IE8+ and all other modern browsers. 38 | 39 | [**Demo**](https://jondavidjohn.github.io/payform) 40 | 41 | ## Installation / Usage 42 | 43 | ### npm (Node and Browserify) 44 | 45 | ```sh 46 | npm install payform --save 47 | ``` 48 | 49 | ```javascript 50 | var payform = require('payform'); 51 | 52 | // Format input for card number entry 53 | var input = document.getElementById('ccnum'); 54 | payform.cardNumberInput(input) 55 | 56 | // Validate a credit card number 57 | payform.validateCardNumber('4242 4242 4242 4242'); //=> true 58 | 59 | // Get card type from number 60 | payform.parseCardType('4242 4242 4242 4242'); //=> 'visa' 61 | ``` 62 | 63 | ### AMD / Require.js 64 | 65 | ```javascript 66 | require.config({ 67 | paths: { "payform": "path/to/payform" } 68 | }); 69 | 70 | require(["payform"], function (payform) { 71 | // Format input for card number entry 72 | var input = document.getElementById('ccnum'); 73 | payform.cardNumberInput(input) 74 | 75 | // Validate a credit card number 76 | payform.validateCardNumber('4242 4242 4242 4242'); //=> true 77 | 78 | // Get card type from number 79 | payform.parseCardType('4242 4242 4242 4242'); //=> 'visa' 80 | }); 81 | ``` 82 | 83 | ### Direct script include / Bower 84 | 85 | Optionally via bower (or simply via download) 86 | ```sh 87 | bower install payform --save 88 | ``` 89 | 90 | ```html 91 | 92 | 103 | ``` 104 | 105 | ### jQuery Plugin (also supports Zepto) 106 | 107 | This library also includes a jquery plugin. The primary `payform` object 108 | can be found at `$.payform`, and there are jquery centric ways to utilize the [browser 109 | input formatters.](#browser-input-formatting-helpers) 110 | 111 | ```html 112 | 113 | 123 | ``` 124 | 125 | ## API 126 | 127 | ### General Formatting and Validation 128 | 129 | #### payform.validateCardNumber(number) 130 | 131 | Validates a card number: 132 | 133 | * Validates numbers 134 | * Validates Luhn algorithm 135 | * Validates length 136 | 137 | Example: 138 | 139 | ``` javascript 140 | payform.validateCardNumber('4242 4242 4242 4242'); //=> true 141 | ``` 142 | 143 | #### payform.validateCardExpiry(month, year) 144 | 145 | Validates a card expiry: 146 | 147 | * Validates numbers 148 | * Validates in the future 149 | * Supports year shorthand 150 | 151 | Example: 152 | 153 | ``` javascript 154 | payform.validateCardExpiry('05', '20'); //=> true 155 | payform.validateCardExpiry('05', '2015'); //=> true 156 | payform.validateCardExpiry('05', '05'); //=> false 157 | ``` 158 | 159 | #### payform.validateCardCVC(cvc, type) 160 | 161 | Validates a card CVC: 162 | 163 | * Validates number 164 | * Validates length to 4 165 | 166 | Example: 167 | 168 | ``` javascript 169 | payform.validateCardCVC('123'); //=> true 170 | payform.validateCardCVC('123', 'amex'); //=> true 171 | payform.validateCardCVC('1234', 'amex'); //=> true 172 | payform.validateCardCVC('12344'); //=> false 173 | ``` 174 | 175 | #### payform.parseCardType(number) 176 | 177 | Returns a card type. Either: 178 | 179 | * `visa` 180 | * `mastercard` 181 | * `amex` 182 | * `dinersclub` 183 | * `discover` 184 | * `unionpay` 185 | * `jcb` 186 | * `visaelectron` 187 | * `maestro` 188 | * `forbrugsforeningen` 189 | * `dankort` 190 | 191 | The function will return `null` if the card type can't be determined. 192 | 193 | Example: 194 | 195 | ``` javascript 196 | payform.parseCardType('4242 4242 4242 4242'); //=> 'visa' 197 | payform.parseCardType('hello world?'); //=> null 198 | ``` 199 | 200 | #### payform.parseCardExpiry(string) 201 | 202 | Parses a credit card expiry in the form of MM/YYYY, returning an object containing the `month` and `year`. Shorthand years, such as `13` are also supported (and converted into the longhand, e.g. `2013`). 203 | 204 | ``` javascript 205 | payform.parseCardExpiry('03 / 2025'); //=> {month: 3: year: 2025} 206 | payform.parseCardExpiry('05 / 04'); //=> {month: 5, year: 2004} 207 | ``` 208 | 209 | This function doesn't perform any validation of the month or year; use `payform.validateCardExpiry(month, year)` for that. 210 | 211 | ### Browser `` formatting helpers 212 | 213 | These methods are specifically for use in the browser to attach `` formatters. 214 | 215 | (alternate [jQuery Plugin](#jquery-plugin) syntax is also provided) 216 | 217 | #### payform.cardNumberInput(input) 218 | 219 | _jQuery plugin:_ `$(...).payform('formatCardNumber')` 220 | 221 | Formats card numbers: 222 | 223 | * Includes a space between every 4 digits 224 | * Restricts input to numbers 225 | * Limits to 16 numbers 226 | * Supports American Express formatting 227 | 228 | Example: 229 | 230 | ``` javascript 231 | var input = document.getElementById('ccnum'); 232 | payform.cardNumberInput(input); 233 | ``` 234 | 235 | #### payform.expiryInput(input) 236 | 237 | _jQuery plugin:_ `$(...).payform('formatCardExpiry')` 238 | 239 | Formats card expiry: 240 | 241 | * Includes a `/` between the month and year 242 | * Restricts input to numbers 243 | * Restricts length 244 | 245 | Example: 246 | 247 | ``` javascript 248 | var input = document.getElementById('expiry'); 249 | payform.expiryInput(input); 250 | ``` 251 | 252 | #### payform.cvcInput(input) 253 | 254 | _jQuery plugin:_ `$(...).payform('formatCardCVC')` 255 | 256 | Formats card CVC: 257 | 258 | * Restricts length to 4 numbers 259 | * Restricts input to numbers 260 | 261 | Example: 262 | 263 | ``` javascript 264 | var input = document.getElementById('cvc'); 265 | payform.cvcInput(input); 266 | ``` 267 | 268 | #### payform.numericInput(input) 269 | 270 | _jQuery plugin:_ `$(...).payform('formatNumeric')` 271 | 272 | General numeric input restriction. 273 | 274 | Example: 275 | 276 | ``` javascript 277 | var input = document.getElementById('numeric'); 278 | payform.numericInput(input); 279 | ``` 280 | 281 | ### Detaching formatting helpers from `` 282 | 283 | Once you have used the formatting helpers available, you might also want to remove them from your input elements. Being able to remove them is especially useful in a Single Page Application (SPA) environment where you want to make sure you're properly unsubscribing events from elements before removing them from the DOM. Detaching events will assure you will not encounter any memory leaks while using this library. 284 | 285 | These methods are specifically for use in the browser to detach `` formatters. 286 | 287 | #### payform.detachCardNumberInput(input) 288 | 289 | _jQuery plugin:_ `$(...).payform('detachFormatCardNumber')` 290 | 291 | Example: 292 | 293 | ``` javascript 294 | var input = document.getElementById('ccnum'); 295 | // now you're able to detach: 296 | payform.detachCardNumberInput(input); 297 | ``` 298 | 299 | #### payform.detachExpiryInput(input) 300 | 301 | _jQuery plugin:_ `$(...).payform('detachFormatCardExpiry')` 302 | 303 | Example: 304 | 305 | ``` javascript 306 | var input = document.getElementById('expiry'); 307 | payform.expiryInput(input); 308 | // now you're able to detach: 309 | payform.detachExpiryInput(input); 310 | ``` 311 | 312 | #### payform.detachCvcInput(input) 313 | 314 | _jQuery plugin:_ `$(...).payform('detachFormatCardCVC')` 315 | 316 | Example: 317 | 318 | ``` javascript 319 | var input = document.getElementById('cvc'); 320 | payform.cvcInput(input); 321 | // now you're able to detach: 322 | payform.detachCvcInput(input); 323 | ``` 324 | 325 | #### payform.detachNumericInput(input) 326 | 327 | _jQuery plugin:_ `$(...).payform('detachFormatNumeric')` 328 | 329 | Example: 330 | 331 | ``` javascript 332 | var input = document.getElementById('numeric'); 333 | payform.numericInput(input); 334 | // now you're able to detach: 335 | payform.detachNumericInput(input); 336 | ``` 337 | 338 | ### Custom Cards 339 | 340 | #### payform.cards 341 | 342 | Array of objects that describe valid card types. Each object should contain the following fields: 343 | 344 | ``` javascript 345 | { 346 | // Card type, as returned by payform.parseCardType. 347 | type: 'mastercard', 348 | // Regex used to identify the card type. For the best experience, this should be 349 | // the shortest pattern that can guarantee the card is of a particular type. 350 | pattern: /^5[0-5]/, 351 | // Array of valid card number lengths. 352 | length: [16], 353 | // Array of valid card CVC lengths. 354 | cvcLength: [3], 355 | // Boolean indicating whether a valid card number should satisfy the Luhn check. 356 | luhn: true, 357 | // Regex used to format the card number. Each match is joined with a space. 358 | format: /(\d{1,4})/g 359 | } 360 | ``` 361 | 362 | When identifying a card type, the array is traversed in order until the card number matches a `pattern`. For this reason, patterns with higher specificity should appear towards the beginning of the array. 363 | 364 | ## Development 365 | 366 | Please see [CONTRIBUTING.md](https://github.com/jondavidjohn/payform/blob/develop/CONTRIBUTING.md). 367 | 368 | ## Autocomplete recommendations 369 | 370 | We recommend you turn autocomplete on for credit card forms, except for the CVC field (which should never be stored). You can do this by setting the `autocomplete` attribute: 371 | 372 | ``` html 373 |
374 | 375 | 376 |
377 | ``` 378 | 379 | You should also mark up your fields using the [Autofill spec](https://html.spec.whatwg.org/multipage/forms.html#autofill). These are respected by a number of browsers, including Chrome. 380 | 381 | ``` html 382 | 383 | ``` 384 | 385 | Set `autocomplete` to `cc-number` for credit card numbers and `cc-exp` for credit card expiry. 386 | 387 | ## Mobile recommendations 388 | 389 | We recommend you to use `` which will cause the numeric keyboard to be displayed on mobile devices: 390 | 391 | ``` html 392 | 393 | ``` 394 | 395 | ## A derived work 396 | 397 | This library is derived from a lot of great work done on [`jquery.payment`](https://github.com/stripe/jquery.payment) by the folks at [Stripe](https://stripe.com/). This aims to 398 | build upon that work, in a module that can be consumed in more diverse situations. 399 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payform", 3 | "main": "dist/payform.js" 4 | } 5 | -------------------------------------------------------------------------------- /dist/jquery.payform.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 50 | License: MIT 51 | Version: 1.4.0 52 | */ 53 | var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 54 | 55 | (function(name, definition) { 56 | if (typeof module !== "undefined" && module !== null) { 57 | return module.exports = definition(); 58 | } else if (typeof define === 'function' && typeof define.amd === 'object') { 59 | return define(name, definition); 60 | } else { 61 | return this[name] = definition(); 62 | } 63 | })('payform', function() { 64 | var _eventNormalize, _getCaretPos, _off, _on, attachEvents, cardFromNumber, cardFromType, defaultFormat, eventList, formatBackCardNumber, formatBackExpiry, formatCardExpiry, formatCardNumber, formatForwardExpiry, formatForwardSlashAndSpace, getDirectionality, hasTextSelected, keyCodes, luhnCheck, payform, reFormatCVC, reFormatCardNumber, reFormatExpiry, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric; 65 | _getCaretPos = function(ele) { 66 | var r, rc, re; 67 | if (ele.selectionStart != null) { 68 | return ele.selectionStart; 69 | } else if (document.selection != null) { 70 | ele.focus(); 71 | r = document.selection.createRange(); 72 | re = ele.createTextRange(); 73 | rc = re.duplicate(); 74 | re.moveToBookmark(r.getBookmark()); 75 | rc.setEndPoint('EndToStart', re); 76 | return rc.text.length; 77 | } 78 | }; 79 | _eventNormalize = function(listener) { 80 | return function(e) { 81 | var newEvt; 82 | if (e == null) { 83 | e = window.event; 84 | } 85 | if (e.inputType === 'insertCompositionText' && !e.isComposing) { 86 | return; 87 | } 88 | newEvt = { 89 | target: e.target || e.srcElement, 90 | which: e.which || e.keyCode, 91 | type: e.type, 92 | metaKey: e.metaKey, 93 | ctrlKey: e.ctrlKey, 94 | preventDefault: function() { 95 | if (e.preventDefault) { 96 | e.preventDefault(); 97 | } else { 98 | e.returnValue = false; 99 | } 100 | } 101 | }; 102 | return listener(newEvt); 103 | }; 104 | }; 105 | _on = function(ele, event, listener) { 106 | if (ele.addEventListener != null) { 107 | return ele.addEventListener(event, listener, false); 108 | } else { 109 | return ele.attachEvent("on" + event, listener); 110 | } 111 | }; 112 | _off = function(ele, event, listener) { 113 | if (ele.removeEventListener != null) { 114 | return ele.removeEventListener(event, listener, false); 115 | } else { 116 | return ele.detachEvent("on" + event, listener); 117 | } 118 | }; 119 | payform = {}; 120 | keyCodes = { 121 | UNKNOWN: 0, 122 | BACKSPACE: 8, 123 | PAGE_UP: 33, 124 | ARROW_LEFT: 37, 125 | ARROW_RIGHT: 39 126 | }; 127 | defaultFormat = /(\d{1,4})/g; 128 | payform.cards = [ 129 | { 130 | type: 'elo', 131 | pattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/, 132 | format: defaultFormat, 133 | length: [16], 134 | cvcLength: [3], 135 | luhn: true 136 | }, { 137 | type: 'visaelectron', 138 | pattern: /^4(026|17500|405|508|844|91[37])/, 139 | format: defaultFormat, 140 | length: [16], 141 | cvcLength: [3], 142 | luhn: true 143 | }, { 144 | type: 'maestro', 145 | pattern: /^(5018|5020|5038|6304|6390[0-9]{2}|67[0-9]{4})/, 146 | format: defaultFormat, 147 | length: [12, 13, 14, 15, 16, 17, 18, 19], 148 | cvcLength: [3], 149 | luhn: true 150 | }, { 151 | type: 'forbrugsforeningen', 152 | pattern: /^600/, 153 | format: defaultFormat, 154 | length: [16], 155 | cvcLength: [3], 156 | luhn: true 157 | }, { 158 | type: 'dankort', 159 | pattern: /^5019/, 160 | format: defaultFormat, 161 | length: [16], 162 | cvcLength: [3], 163 | luhn: true 164 | }, { 165 | type: 'visa', 166 | pattern: /^4/, 167 | format: defaultFormat, 168 | length: [13, 16, 19], 169 | cvcLength: [3], 170 | luhn: true 171 | }, { 172 | type: 'mastercard', 173 | pattern: /^(5[1-5][0-9]{4}|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)([0-9]{2})/, 174 | format: defaultFormat, 175 | length: [16], 176 | cvcLength: [3], 177 | luhn: true 178 | }, { 179 | type: 'amex', 180 | pattern: /^3[47]/, 181 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 182 | length: [15], 183 | cvcLength: [4], 184 | luhn: true 185 | }, { 186 | type: 'hipercard', 187 | pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/, 188 | format: defaultFormat, 189 | length: [14, 15, 16, 17, 18, 19], 190 | cvcLength: [3], 191 | luhn: true 192 | }, { 193 | type: 'dinersclub', 194 | pattern: /^(36|38|30[0-5])/, 195 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 196 | length: [14], 197 | cvcLength: [3], 198 | luhn: true 199 | }, { 200 | type: 'discover', 201 | pattern: /^(6011|65|64[4-9]|622)/, 202 | format: defaultFormat, 203 | length: [16], 204 | cvcLength: [3], 205 | luhn: true 206 | }, { 207 | type: 'unionpay', 208 | pattern: /^62/, 209 | format: defaultFormat, 210 | length: [16, 17, 18, 19], 211 | cvcLength: [3], 212 | luhn: false 213 | }, { 214 | type: 'jcb', 215 | pattern: /^35/, 216 | format: defaultFormat, 217 | length: [16, 17, 18, 19], 218 | cvcLength: [3], 219 | luhn: true 220 | }, { 221 | type: 'laser', 222 | pattern: /^(6706|6771|6709)/, 223 | format: defaultFormat, 224 | length: [16, 17, 18, 19], 225 | cvcLength: [3], 226 | luhn: true 227 | } 228 | ]; 229 | cardFromNumber = function(num) { 230 | var card, i, len, ref; 231 | num = (num + '').replace(/\D/g, ''); 232 | ref = payform.cards; 233 | for (i = 0, len = ref.length; i < len; i++) { 234 | card = ref[i]; 235 | if (card.pattern.test(num)) { 236 | return card; 237 | } 238 | } 239 | }; 240 | cardFromType = function(type) { 241 | var card, i, len, ref; 242 | ref = payform.cards; 243 | for (i = 0, len = ref.length; i < len; i++) { 244 | card = ref[i]; 245 | if (card.type === type) { 246 | return card; 247 | } 248 | } 249 | }; 250 | getDirectionality = function(target) { 251 | var style; 252 | style = getComputedStyle(target); 253 | return style && style['direction'] || document.dir; 254 | }; 255 | luhnCheck = function(num) { 256 | var digit, digits, i, len, odd, sum; 257 | odd = true; 258 | sum = 0; 259 | digits = (num + '').split('').reverse(); 260 | for (i = 0, len = digits.length; i < len; i++) { 261 | digit = digits[i]; 262 | digit = parseInt(digit, 10); 263 | if ((odd = !odd)) { 264 | digit *= 2; 265 | } 266 | if (digit > 9) { 267 | digit -= 9; 268 | } 269 | sum += digit; 270 | } 271 | return sum % 10 === 0; 272 | }; 273 | hasTextSelected = function(target) { 274 | var ref; 275 | if ((typeof document !== "undefined" && document !== null ? (ref = document.selection) != null ? ref.createRange : void 0 : void 0) != null) { 276 | if (document.selection.createRange().text) { 277 | return true; 278 | } 279 | } 280 | return (target.selectionStart != null) && target.selectionStart !== target.selectionEnd; 281 | }; 282 | replaceFullWidthChars = function(str) { 283 | var char, chars, fullWidth, halfWidth, i, idx, len, value; 284 | if (str == null) { 285 | str = ''; 286 | } 287 | fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 288 | halfWidth = '0123456789'; 289 | value = ''; 290 | chars = str.split(''); 291 | for (i = 0, len = chars.length; i < len; i++) { 292 | char = chars[i]; 293 | idx = fullWidth.indexOf(char); 294 | if (idx > -1) { 295 | char = halfWidth[idx]; 296 | } 297 | value += char; 298 | } 299 | return value; 300 | }; 301 | reFormatCardNumber = function(e) { 302 | var cursor; 303 | cursor = _getCaretPos(e.target); 304 | if (e.target.value === "") { 305 | return; 306 | } 307 | if (getDirectionality(e.target) === 'ltr') { 308 | cursor = _getCaretPos(e.target); 309 | } 310 | e.target.value = payform.formatCardNumber(e.target.value); 311 | if (getDirectionality(e.target) === 'ltr' && cursor !== e.target.selectionStart) { 312 | cursor = _getCaretPos(e.target); 313 | } 314 | if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) { 315 | e.target.value = '‎\u200e'.concat(e.target.value); 316 | } 317 | cursor = _getCaretPos(e.target); 318 | if ((cursor != null) && cursor !== 0 && e.type !== 'change') { 319 | return e.target.setSelectionRange(cursor, cursor); 320 | } 321 | }; 322 | formatCardNumber = function(e) { 323 | var card, cursor, digit, length, re, upperLength, value; 324 | digit = String.fromCharCode(e.which); 325 | if (!/^\d+$/.test(digit)) { 326 | return; 327 | } 328 | value = e.target.value; 329 | card = cardFromNumber(value + digit); 330 | length = (value.replace(/\D/g, '') + digit).length; 331 | upperLength = 16; 332 | if (card) { 333 | upperLength = card.length[card.length.length - 1]; 334 | } 335 | if (length >= upperLength) { 336 | return; 337 | } 338 | cursor = _getCaretPos(e.target); 339 | if (cursor && cursor !== value.length) { 340 | return; 341 | } 342 | if (card && card.type === 'amex') { 343 | re = /^(\d{4}|\d{4}\s\d{6})$/; 344 | } else { 345 | re = /(?:^|\s)(\d{4})$/; 346 | } 347 | if (re.test(value)) { 348 | e.preventDefault(); 349 | return setTimeout(function() { 350 | return e.target.value = value + " " + digit; 351 | }); 352 | } else if (re.test(value + digit)) { 353 | e.preventDefault(); 354 | return setTimeout(function() { 355 | return e.target.value = (value + digit) + " "; 356 | }); 357 | } 358 | }; 359 | formatBackCardNumber = function(e) { 360 | var cursor, value; 361 | value = e.target.value; 362 | if (e.which !== keyCodes.BACKSPACE) { 363 | return; 364 | } 365 | cursor = _getCaretPos(e.target); 366 | if (cursor && cursor !== value.length) { 367 | return; 368 | } 369 | if ((e.target.selectionEnd - e.target.selectionStart) > 1) { 370 | return; 371 | } 372 | if (/\d\s$/.test(value)) { 373 | e.preventDefault(); 374 | return setTimeout(function() { 375 | return e.target.value = value.replace(/\d\s$/, ''); 376 | }); 377 | } else if (/\s\d?$/.test(value)) { 378 | e.preventDefault(); 379 | return setTimeout(function() { 380 | return e.target.value = value.replace(/\d$/, ''); 381 | }); 382 | } 383 | }; 384 | reFormatExpiry = function(e) { 385 | var cursor; 386 | if (e.target.value === "") { 387 | return; 388 | } 389 | e.target.value = payform.formatCardExpiry(e.target.value); 390 | if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) { 391 | e.target.value = '‎\u200e'.concat(e.target.value); 392 | } 393 | cursor = _getCaretPos(e.target); 394 | if ((cursor != null) && e.type !== 'change') { 395 | return e.target.setSelectionRange(cursor, cursor); 396 | } 397 | }; 398 | formatCardExpiry = function(e) { 399 | var digit, val; 400 | digit = String.fromCharCode(e.which); 401 | if (!/^\d+$/.test(digit)) { 402 | return; 403 | } 404 | val = e.target.value + digit; 405 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 406 | e.preventDefault(); 407 | return setTimeout(function() { 408 | return e.target.value = "0" + val + " / "; 409 | }); 410 | } else if (/^\d\d$/.test(val)) { 411 | e.preventDefault(); 412 | return setTimeout(function() { 413 | return e.target.value = val + " / "; 414 | }); 415 | } 416 | }; 417 | formatForwardExpiry = function(e) { 418 | var digit, val; 419 | digit = String.fromCharCode(e.which); 420 | if (!/^\d+$/.test(digit)) { 421 | return; 422 | } 423 | val = e.target.value; 424 | if (/^\d\d$/.test(val)) { 425 | return e.target.value = val + " / "; 426 | } 427 | }; 428 | formatForwardSlashAndSpace = function(e) { 429 | var val, which; 430 | which = String.fromCharCode(e.which); 431 | if (!(which === '/' || which === ' ')) { 432 | return; 433 | } 434 | val = e.target.value; 435 | if (/^\d$/.test(val) && val !== '0') { 436 | return e.target.value = "0" + val + " / "; 437 | } 438 | }; 439 | formatBackExpiry = function(e) { 440 | var cursor, value; 441 | value = e.target.value; 442 | if (e.which !== keyCodes.BACKSPACE) { 443 | return; 444 | } 445 | cursor = _getCaretPos(e.target); 446 | if (cursor && cursor !== value.length) { 447 | return; 448 | } 449 | if (/\d\s\/\s$/.test(value)) { 450 | e.preventDefault(); 451 | return setTimeout(function() { 452 | return e.target.value = value.replace(/\d\s\/\s$/, ''); 453 | }); 454 | } 455 | }; 456 | reFormatCVC = function(e) { 457 | var cursor; 458 | if (e.target.value === "") { 459 | return; 460 | } 461 | cursor = _getCaretPos(e.target); 462 | e.target.value = replaceFullWidthChars(e.target.value).replace(/\D/g, '').slice(0, 4); 463 | if ((cursor != null) && e.type !== 'change') { 464 | return e.target.setSelectionRange(cursor, cursor); 465 | } 466 | }; 467 | restrictNumeric = function(e) { 468 | var input; 469 | if (e.metaKey || e.ctrlKey) { 470 | return; 471 | } 472 | if ([keyCodes.UNKNOWN, keyCodes.ARROW_LEFT, keyCodes.ARROW_RIGHT].indexOf(e.which) > -1) { 473 | return; 474 | } 475 | if (e.which < keyCodes.PAGE_UP) { 476 | return; 477 | } 478 | input = String.fromCharCode(e.which); 479 | if (!/^\d+$/.test(input)) { 480 | return e.preventDefault(); 481 | } 482 | }; 483 | restrictCardNumber = function(e) { 484 | var card, digit, maxLength, value; 485 | digit = String.fromCharCode(e.which); 486 | if (!/^\d+$/.test(digit)) { 487 | return; 488 | } 489 | if (hasTextSelected(e.target)) { 490 | return; 491 | } 492 | value = (e.target.value + digit).replace(/\D/g, ''); 493 | card = cardFromNumber(value); 494 | maxLength = card ? card.length[card.length.length - 1] : 16; 495 | if (value.length > maxLength) { 496 | return e.preventDefault(); 497 | } 498 | }; 499 | restrictExpiry = function(e) { 500 | var digit, value; 501 | digit = String.fromCharCode(e.which); 502 | if (!/^\d+$/.test(digit)) { 503 | return; 504 | } 505 | if (hasTextSelected(e.target)) { 506 | return; 507 | } 508 | value = e.target.value + digit; 509 | value = value.replace(/\D/g, ''); 510 | if (value.length > 6) { 511 | return e.preventDefault(); 512 | } 513 | }; 514 | restrictCVC = function(e) { 515 | var digit, val; 516 | digit = String.fromCharCode(e.which); 517 | if (!/^\d+$/.test(digit)) { 518 | return; 519 | } 520 | if (hasTextSelected(e.target)) { 521 | return; 522 | } 523 | val = e.target.value + digit; 524 | if (val.length > 4) { 525 | return e.preventDefault(); 526 | } 527 | }; 528 | eventList = { 529 | cvcInput: [ 530 | { 531 | eventName: 'keypress', 532 | eventHandler: _eventNormalize(restrictNumeric) 533 | }, { 534 | eventName: 'keypress', 535 | eventHandler: _eventNormalize(restrictCVC) 536 | }, { 537 | eventName: 'paste', 538 | eventHandler: _eventNormalize(reFormatCVC) 539 | }, { 540 | eventName: 'change', 541 | eventHandler: _eventNormalize(reFormatCVC) 542 | }, { 543 | eventName: 'input', 544 | eventHandler: _eventNormalize(reFormatCVC) 545 | } 546 | ], 547 | expiryInput: [ 548 | { 549 | eventName: 'keypress', 550 | eventHandler: _eventNormalize(restrictNumeric) 551 | }, { 552 | eventName: 'keypress', 553 | eventHandler: _eventNormalize(restrictExpiry) 554 | }, { 555 | eventName: 'keypress', 556 | eventHandler: _eventNormalize(formatCardExpiry) 557 | }, { 558 | eventName: 'keypress', 559 | eventHandler: _eventNormalize(formatForwardSlashAndSpace) 560 | }, { 561 | eventName: 'keypress', 562 | eventHandler: _eventNormalize(formatForwardExpiry) 563 | }, { 564 | eventName: 'keydown', 565 | eventHandler: _eventNormalize(formatBackExpiry) 566 | }, { 567 | eventName: 'change', 568 | eventHandler: _eventNormalize(reFormatExpiry) 569 | }, { 570 | eventName: 'input', 571 | eventHandler: _eventNormalize(reFormatExpiry) 572 | } 573 | ], 574 | cardNumberInput: [ 575 | { 576 | eventName: 'keypress', 577 | eventHandler: _eventNormalize(restrictNumeric) 578 | }, { 579 | eventName: 'keypress', 580 | eventHandler: _eventNormalize(restrictCardNumber) 581 | }, { 582 | eventName: 'keypress', 583 | eventHandler: _eventNormalize(formatCardNumber) 584 | }, { 585 | eventName: 'keydown', 586 | eventHandler: _eventNormalize(formatBackCardNumber) 587 | }, { 588 | eventName: 'paste', 589 | eventHandler: _eventNormalize(reFormatCardNumber) 590 | }, { 591 | eventName: 'change', 592 | eventHandler: _eventNormalize(reFormatCardNumber) 593 | }, { 594 | eventName: 'input', 595 | eventHandler: _eventNormalize(reFormatCardNumber) 596 | } 597 | ], 598 | numericInput: [ 599 | { 600 | eventName: 'keypress', 601 | eventHandler: _eventNormalize(restrictNumeric) 602 | }, { 603 | eventName: 'paste', 604 | eventHandler: _eventNormalize(restrictNumeric) 605 | }, { 606 | eventName: 'change', 607 | eventHandler: _eventNormalize(restrictNumeric) 608 | }, { 609 | eventName: 'input', 610 | eventHandler: _eventNormalize(restrictNumeric) 611 | } 612 | ] 613 | }; 614 | attachEvents = function(input, events, detach) { 615 | var evt, i, len; 616 | for (i = 0, len = events.length; i < len; i++) { 617 | evt = events[i]; 618 | if (detach) { 619 | _off(input, evt.eventName, evt.eventHandler); 620 | } else { 621 | _on(input, evt.eventName, evt.eventHandler); 622 | } 623 | } 624 | }; 625 | payform.cvcInput = function(input) { 626 | return attachEvents(input, eventList.cvcInput); 627 | }; 628 | payform.expiryInput = function(input) { 629 | return attachEvents(input, eventList.expiryInput); 630 | }; 631 | payform.cardNumberInput = function(input) { 632 | return attachEvents(input, eventList.cardNumberInput); 633 | }; 634 | payform.numericInput = function(input) { 635 | return attachEvents(input, eventList.numericInput); 636 | }; 637 | payform.detachCvcInput = function(input) { 638 | return attachEvents(input, eventList.cvcInput, true); 639 | }; 640 | payform.detachExpiryInput = function(input) { 641 | return attachEvents(input, eventList.expiryInput, true); 642 | }; 643 | payform.detachCardNumberInput = function(input) { 644 | return attachEvents(input, eventList.cardNumberInput, true); 645 | }; 646 | payform.detachNumericInput = function(input) { 647 | return attachEvents(input, eventList.numericInput, true); 648 | }; 649 | payform.parseCardExpiry = function(value) { 650 | var month, prefix, ref, year; 651 | value = value.replace(/\s/g, ''); 652 | ref = value.split('/', 2), month = ref[0], year = ref[1]; 653 | if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { 654 | prefix = (new Date).getFullYear(); 655 | prefix = prefix.toString().slice(0, 2); 656 | year = prefix + year; 657 | } 658 | month = parseInt(month.replace(/[\u200e]/g, ""), 10); 659 | year = parseInt(year, 10); 660 | return { 661 | month: month, 662 | year: year 663 | }; 664 | }; 665 | payform.validateCardNumber = function(num) { 666 | var card, ref; 667 | num = (num + '').replace(/\s+|-/g, ''); 668 | if (!/^\d+$/.test(num)) { 669 | return false; 670 | } 671 | card = cardFromNumber(num); 672 | if (!card) { 673 | return false; 674 | } 675 | return (ref = num.length, indexOf.call(card.length, ref) >= 0) && (card.luhn === false || luhnCheck(num)); 676 | }; 677 | payform.validateCardExpiry = function(month, year) { 678 | var currentTime, expiry, ref; 679 | if (typeof month === 'object' && 'month' in month) { 680 | ref = month, month = ref.month, year = ref.year; 681 | } 682 | if (!(month && year)) { 683 | return false; 684 | } 685 | month = String(month).trim(); 686 | year = String(year).trim(); 687 | if (!/^\d+$/.test(month)) { 688 | return false; 689 | } 690 | if (!/^\d+$/.test(year)) { 691 | return false; 692 | } 693 | if (!((1 <= month && month <= 12))) { 694 | return false; 695 | } 696 | if (year.length === 2) { 697 | if (year < 70) { 698 | year = "20" + year; 699 | } else { 700 | year = "19" + year; 701 | } 702 | } 703 | if (year.length !== 4) { 704 | return false; 705 | } 706 | expiry = new Date(year, month); 707 | currentTime = new Date; 708 | expiry.setMonth(expiry.getMonth() - 1); 709 | expiry.setMonth(expiry.getMonth() + 1, 1); 710 | return expiry > currentTime; 711 | }; 712 | payform.validateCardCVC = function(cvc, type) { 713 | var card, ref; 714 | cvc = String(cvc).trim(); 715 | if (!/^\d+$/.test(cvc)) { 716 | return false; 717 | } 718 | card = cardFromType(type); 719 | if (card != null) { 720 | return ref = cvc.length, indexOf.call(card.cvcLength, ref) >= 0; 721 | } else { 722 | return cvc.length >= 3 && cvc.length <= 4; 723 | } 724 | }; 725 | payform.parseCardType = function(num) { 726 | var ref; 727 | if (!num) { 728 | return null; 729 | } 730 | return ((ref = cardFromNumber(num)) != null ? ref.type : void 0) || null; 731 | }; 732 | payform.formatCardNumber = function(num) { 733 | var card, groups, ref, upperLength; 734 | num = replaceFullWidthChars(num); 735 | num = num.replace(/\D/g, ''); 736 | card = cardFromNumber(num); 737 | if (!card) { 738 | return num; 739 | } 740 | upperLength = card.length[card.length.length - 1]; 741 | num = num.slice(0, upperLength); 742 | if (card.format.global) { 743 | return (ref = num.match(card.format)) != null ? ref.join(' ') : void 0; 744 | } else { 745 | groups = card.format.exec(num); 746 | if (groups == null) { 747 | return; 748 | } 749 | groups.shift(); 750 | groups = groups.filter(Boolean); 751 | return groups.join(' '); 752 | } 753 | }; 754 | payform.formatCardExpiry = function(expiry) { 755 | var mon, parts, sep, year; 756 | expiry = replaceFullWidthChars(expiry); 757 | parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 758 | if (!parts) { 759 | return ''; 760 | } 761 | mon = parts[1] || ''; 762 | sep = parts[2] || ''; 763 | year = parts[3] || ''; 764 | if (year.length > 0) { 765 | sep = ' / '; 766 | } else if (sep === ' /') { 767 | mon = mon.substring(0, 1); 768 | sep = ''; 769 | } else if (mon.length === 2 || sep.length > 0) { 770 | sep = ' / '; 771 | } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { 772 | mon = "0" + mon; 773 | sep = ' / '; 774 | } 775 | return mon + sep + year; 776 | }; 777 | return payform; 778 | }); 779 | 780 | 781 | },{}]},{},[1]); 782 | -------------------------------------------------------------------------------- /dist/jquery.payform.min.js: -------------------------------------------------------------------------------- 1 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i9){digit-=9}sum+=digit}return sum%10===0};hasTextSelected=function(target){var ref;if((typeof document!=="undefined"&&document!==null?(ref=document.selection)!=null?ref.createRange:void 0:void 0)!=null){if(document.selection.createRange().text){return true}}return target.selectionStart!=null&&target.selectionStart!==target.selectionEnd};replaceFullWidthChars=function(str){var char,chars,fullWidth,halfWidth,i,idx,len,value;if(str==null){str=""}fullWidth="0123456789";halfWidth="0123456789";value="";chars=str.split("");for(i=0,len=chars.length;i-1){char=halfWidth[idx]}value+=char}return value};reFormatCardNumber=function(e){var cursor;cursor=_getCaretPos(e.target);if(e.target.value===""){return}if(getDirectionality(e.target)==="ltr"){cursor=_getCaretPos(e.target)}e.target.value=payform.formatCardNumber(e.target.value);if(getDirectionality(e.target)==="ltr"&&cursor!==e.target.selectionStart){cursor=_getCaretPos(e.target)}if(getDirectionality(e.target)==="rtl"&&e.target.value.indexOf("‎‎")===-1){e.target.value="‎‎".concat(e.target.value)}cursor=_getCaretPos(e.target);if(cursor!=null&&cursor!==0&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};formatCardNumber=function(e){var card,cursor,digit,length,re,upperLength,value;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}value=e.target.value;card=cardFromNumber(value+digit);length=(value.replace(/\D/g,"")+digit).length;upperLength=16;if(card){upperLength=card.length[card.length.length-1]}if(length>=upperLength){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(card&&card.type==="amex"){re=/^(\d{4}|\d{4}\s\d{6})$/}else{re=/(?:^|\s)(\d{4})$/}if(re.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value+" "+digit})}else if(re.test(value+digit)){e.preventDefault();return setTimeout(function(){return e.target.value=value+digit+" "})}};formatBackCardNumber=function(e){var cursor,value;value=e.target.value;if(e.which!==keyCodes.BACKSPACE){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(e.target.selectionEnd-e.target.selectionStart>1){return}if(/\d\s$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d\s$/,"")})}else if(/\s\d?$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d$/,"")})}};reFormatExpiry=function(e){var cursor;if(e.target.value===""){return}e.target.value=payform.formatCardExpiry(e.target.value);if(getDirectionality(e.target)==="rtl"&&e.target.value.indexOf("‎‎")===-1){e.target.value="‎‎".concat(e.target.value)}cursor=_getCaretPos(e.target);if(cursor!=null&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};formatCardExpiry=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}val=e.target.value+digit;if(/^\d$/.test(val)&&(val!=="0"&&val!=="1")){e.preventDefault();return setTimeout(function(){return e.target.value="0"+val+" / "})}else if(/^\d\d$/.test(val)){e.preventDefault();return setTimeout(function(){return e.target.value=val+" / "})}};formatForwardExpiry=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}val=e.target.value;if(/^\d\d$/.test(val)){return e.target.value=val+" / "}};formatForwardSlashAndSpace=function(e){var val,which;which=String.fromCharCode(e.which);if(!(which==="/"||which===" ")){return}val=e.target.value;if(/^\d$/.test(val)&&val!=="0"){return e.target.value="0"+val+" / "}};formatBackExpiry=function(e){var cursor,value;value=e.target.value;if(e.which!==keyCodes.BACKSPACE){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(/\d\s\/\s$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d\s\/\s$/,"")})}};reFormatCVC=function(e){var cursor;if(e.target.value===""){return}cursor=_getCaretPos(e.target);e.target.value=replaceFullWidthChars(e.target.value).replace(/\D/g,"").slice(0,4);if(cursor!=null&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};restrictNumeric=function(e){var input;if(e.metaKey||e.ctrlKey){return}if([keyCodes.UNKNOWN,keyCodes.ARROW_LEFT,keyCodes.ARROW_RIGHT].indexOf(e.which)>-1){return}if(e.whichmaxLength){return e.preventDefault()}};restrictExpiry=function(e){var digit,value;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}if(hasTextSelected(e.target)){return}value=e.target.value+digit;value=value.replace(/\D/g,"");if(value.length>6){return e.preventDefault()}};restrictCVC=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}if(hasTextSelected(e.target)){return}val=e.target.value+digit;if(val.length>4){return e.preventDefault()}};eventList={cvcInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictCVC)},{eventName:"paste",eventHandler:_eventNormalize(reFormatCVC)},{eventName:"change",eventHandler:_eventNormalize(reFormatCVC)},{eventName:"input",eventHandler:_eventNormalize(reFormatCVC)}],expiryInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictExpiry)},{eventName:"keypress",eventHandler:_eventNormalize(formatCardExpiry)},{eventName:"keypress",eventHandler:_eventNormalize(formatForwardSlashAndSpace)},{eventName:"keypress",eventHandler:_eventNormalize(formatForwardExpiry)},{eventName:"keydown",eventHandler:_eventNormalize(formatBackExpiry)},{eventName:"change",eventHandler:_eventNormalize(reFormatExpiry)},{eventName:"input",eventHandler:_eventNormalize(reFormatExpiry)}],cardNumberInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictCardNumber)},{eventName:"keypress",eventHandler:_eventNormalize(formatCardNumber)},{eventName:"keydown",eventHandler:_eventNormalize(formatBackCardNumber)},{eventName:"paste",eventHandler:_eventNormalize(reFormatCardNumber)},{eventName:"change",eventHandler:_eventNormalize(reFormatCardNumber)},{eventName:"input",eventHandler:_eventNormalize(reFormatCardNumber)}],numericInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"paste",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"change",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"input",eventHandler:_eventNormalize(restrictNumeric)}]};attachEvents=function(input,events,detach){var evt,i,len;for(i=0,len=events.length;i=0)&&(card.luhn===false||luhnCheck(num))};payform.validateCardExpiry=function(month,year){var currentTime,expiry,ref;if(typeof month==="object"&&"month"in month){ref=month,month=ref.month,year=ref.year}if(!(month&&year)){return false}month=String(month).trim();year=String(year).trim();if(!/^\d+$/.test(month)){return false}if(!/^\d+$/.test(year)){return false}if(!(1<=month&&month<=12)){return false}if(year.length===2){if(year<70){year="20"+year}else{year="19"+year}}if(year.length!==4){return false}expiry=new Date(year,month);currentTime=new Date;expiry.setMonth(expiry.getMonth()-1);expiry.setMonth(expiry.getMonth()+1,1);return expiry>currentTime};payform.validateCardCVC=function(cvc,type){var card,ref;cvc=String(cvc).trim();if(!/^\d+$/.test(cvc)){return false}card=cardFromType(type);if(card!=null){return ref=cvc.length,indexOf.call(card.cvcLength,ref)>=0}else{return cvc.length>=3&&cvc.length<=4}};payform.parseCardType=function(num){var ref;if(!num){return null}return((ref=cardFromNumber(num))!=null?ref.type:void 0)||null};payform.formatCardNumber=function(num){var card,groups,ref,upperLength;num=replaceFullWidthChars(num);num=num.replace(/\D/g,"");card=cardFromNumber(num);if(!card){return num}upperLength=card.length[card.length.length-1];num=num.slice(0,upperLength);if(card.format.global){return(ref=num.match(card.format))!=null?ref.join(" "):void 0}else{groups=card.format.exec(num);if(groups==null){return}groups.shift();groups=groups.filter(Boolean);return groups.join(" ")}};payform.formatCardExpiry=function(expiry){var mon,parts,sep,year;expiry=replaceFullWidthChars(expiry);parts=expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);if(!parts){return""}mon=parts[1]||"";sep=parts[2]||"";year=parts[3]||"";if(year.length>0){sep=" / "}else if(sep===" /"){mon=mon.substring(0,1);sep=""}else if(mon.length===2||sep.length>0){sep=" / "}else if(mon.length===1&&(mon!=="0"&&mon!=="1")){mon="0"+mon;sep=" / "}return mon+sep+year};return payform})},{}]},{},[1]); 2 | -------------------------------------------------------------------------------- /dist/payform.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Payform Javascript Library 4 | 5 | URL: https://github.com/jondavidjohn/payform 6 | Author: Jonathan D. Johnson 7 | License: MIT 8 | Version: 1.4.0 9 | */ 10 | 11 | (function() { 12 | var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 13 | 14 | (function(name, definition) { 15 | if (typeof module !== "undefined" && module !== null) { 16 | return module.exports = definition(); 17 | } else if (typeof define === 'function' && typeof define.amd === 'object') { 18 | return define(name, definition); 19 | } else { 20 | return this[name] = definition(); 21 | } 22 | })('payform', function() { 23 | var _eventNormalize, _getCaretPos, _off, _on, attachEvents, cardFromNumber, cardFromType, defaultFormat, eventList, formatBackCardNumber, formatBackExpiry, formatCardExpiry, formatCardNumber, formatForwardExpiry, formatForwardSlashAndSpace, getDirectionality, hasTextSelected, keyCodes, luhnCheck, payform, reFormatCVC, reFormatCardNumber, reFormatExpiry, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric; 24 | _getCaretPos = function(ele) { 25 | var r, rc, re; 26 | if (ele.selectionStart != null) { 27 | return ele.selectionStart; 28 | } else if (document.selection != null) { 29 | ele.focus(); 30 | r = document.selection.createRange(); 31 | re = ele.createTextRange(); 32 | rc = re.duplicate(); 33 | re.moveToBookmark(r.getBookmark()); 34 | rc.setEndPoint('EndToStart', re); 35 | return rc.text.length; 36 | } 37 | }; 38 | _eventNormalize = function(listener) { 39 | return function(e) { 40 | var newEvt; 41 | if (e == null) { 42 | e = window.event; 43 | } 44 | if (e.inputType === 'insertCompositionText' && !e.isComposing) { 45 | return; 46 | } 47 | newEvt = { 48 | target: e.target || e.srcElement, 49 | which: e.which || e.keyCode, 50 | type: e.type, 51 | metaKey: e.metaKey, 52 | ctrlKey: e.ctrlKey, 53 | preventDefault: function() { 54 | if (e.preventDefault) { 55 | e.preventDefault(); 56 | } else { 57 | e.returnValue = false; 58 | } 59 | } 60 | }; 61 | return listener(newEvt); 62 | }; 63 | }; 64 | _on = function(ele, event, listener) { 65 | if (ele.addEventListener != null) { 66 | return ele.addEventListener(event, listener, false); 67 | } else { 68 | return ele.attachEvent("on" + event, listener); 69 | } 70 | }; 71 | _off = function(ele, event, listener) { 72 | if (ele.removeEventListener != null) { 73 | return ele.removeEventListener(event, listener, false); 74 | } else { 75 | return ele.detachEvent("on" + event, listener); 76 | } 77 | }; 78 | payform = {}; 79 | keyCodes = { 80 | UNKNOWN: 0, 81 | BACKSPACE: 8, 82 | PAGE_UP: 33, 83 | ARROW_LEFT: 37, 84 | ARROW_RIGHT: 39 85 | }; 86 | defaultFormat = /(\d{1,4})/g; 87 | payform.cards = [ 88 | { 89 | type: 'elo', 90 | pattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/, 91 | format: defaultFormat, 92 | length: [16], 93 | cvcLength: [3], 94 | luhn: true 95 | }, { 96 | type: 'visaelectron', 97 | pattern: /^4(026|17500|405|508|844|91[37])/, 98 | format: defaultFormat, 99 | length: [16], 100 | cvcLength: [3], 101 | luhn: true 102 | }, { 103 | type: 'maestro', 104 | pattern: /^(5018|5020|5038|6304|6390[0-9]{2}|67[0-9]{4})/, 105 | format: defaultFormat, 106 | length: [12, 13, 14, 15, 16, 17, 18, 19], 107 | cvcLength: [3], 108 | luhn: true 109 | }, { 110 | type: 'forbrugsforeningen', 111 | pattern: /^600/, 112 | format: defaultFormat, 113 | length: [16], 114 | cvcLength: [3], 115 | luhn: true 116 | }, { 117 | type: 'dankort', 118 | pattern: /^5019/, 119 | format: defaultFormat, 120 | length: [16], 121 | cvcLength: [3], 122 | luhn: true 123 | }, { 124 | type: 'visa', 125 | pattern: /^4/, 126 | format: defaultFormat, 127 | length: [13, 16, 19], 128 | cvcLength: [3], 129 | luhn: true 130 | }, { 131 | type: 'mastercard', 132 | pattern: /^(5[1-5][0-9]{4}|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)([0-9]{2})/, 133 | format: defaultFormat, 134 | length: [16], 135 | cvcLength: [3], 136 | luhn: true 137 | }, { 138 | type: 'amex', 139 | pattern: /^3[47]/, 140 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 141 | length: [15], 142 | cvcLength: [4], 143 | luhn: true 144 | }, { 145 | type: 'hipercard', 146 | pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/, 147 | format: defaultFormat, 148 | length: [14, 15, 16, 17, 18, 19], 149 | cvcLength: [3], 150 | luhn: true 151 | }, { 152 | type: 'dinersclub', 153 | pattern: /^(36|38|30[0-5])/, 154 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 155 | length: [14], 156 | cvcLength: [3], 157 | luhn: true 158 | }, { 159 | type: 'discover', 160 | pattern: /^(6011|65|64[4-9]|622)/, 161 | format: defaultFormat, 162 | length: [16], 163 | cvcLength: [3], 164 | luhn: true 165 | }, { 166 | type: 'unionpay', 167 | pattern: /^62/, 168 | format: defaultFormat, 169 | length: [16, 17, 18, 19], 170 | cvcLength: [3], 171 | luhn: false 172 | }, { 173 | type: 'jcb', 174 | pattern: /^35/, 175 | format: defaultFormat, 176 | length: [16, 17, 18, 19], 177 | cvcLength: [3], 178 | luhn: true 179 | }, { 180 | type: 'laser', 181 | pattern: /^(6706|6771|6709)/, 182 | format: defaultFormat, 183 | length: [16, 17, 18, 19], 184 | cvcLength: [3], 185 | luhn: true 186 | } 187 | ]; 188 | cardFromNumber = function(num) { 189 | var card, i, len, ref; 190 | num = (num + '').replace(/\D/g, ''); 191 | ref = payform.cards; 192 | for (i = 0, len = ref.length; i < len; i++) { 193 | card = ref[i]; 194 | if (card.pattern.test(num)) { 195 | return card; 196 | } 197 | } 198 | }; 199 | cardFromType = function(type) { 200 | var card, i, len, ref; 201 | ref = payform.cards; 202 | for (i = 0, len = ref.length; i < len; i++) { 203 | card = ref[i]; 204 | if (card.type === type) { 205 | return card; 206 | } 207 | } 208 | }; 209 | getDirectionality = function(target) { 210 | var style; 211 | style = getComputedStyle(target); 212 | return style && style['direction'] || document.dir; 213 | }; 214 | luhnCheck = function(num) { 215 | var digit, digits, i, len, odd, sum; 216 | odd = true; 217 | sum = 0; 218 | digits = (num + '').split('').reverse(); 219 | for (i = 0, len = digits.length; i < len; i++) { 220 | digit = digits[i]; 221 | digit = parseInt(digit, 10); 222 | if ((odd = !odd)) { 223 | digit *= 2; 224 | } 225 | if (digit > 9) { 226 | digit -= 9; 227 | } 228 | sum += digit; 229 | } 230 | return sum % 10 === 0; 231 | }; 232 | hasTextSelected = function(target) { 233 | var ref; 234 | if ((typeof document !== "undefined" && document !== null ? (ref = document.selection) != null ? ref.createRange : void 0 : void 0) != null) { 235 | if (document.selection.createRange().text) { 236 | return true; 237 | } 238 | } 239 | return (target.selectionStart != null) && target.selectionStart !== target.selectionEnd; 240 | }; 241 | replaceFullWidthChars = function(str) { 242 | var char, chars, fullWidth, halfWidth, i, idx, len, value; 243 | if (str == null) { 244 | str = ''; 245 | } 246 | fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'; 247 | halfWidth = '0123456789'; 248 | value = ''; 249 | chars = str.split(''); 250 | for (i = 0, len = chars.length; i < len; i++) { 251 | char = chars[i]; 252 | idx = fullWidth.indexOf(char); 253 | if (idx > -1) { 254 | char = halfWidth[idx]; 255 | } 256 | value += char; 257 | } 258 | return value; 259 | }; 260 | reFormatCardNumber = function(e) { 261 | var cursor; 262 | cursor = _getCaretPos(e.target); 263 | if (e.target.value === "") { 264 | return; 265 | } 266 | if (getDirectionality(e.target) === 'ltr') { 267 | cursor = _getCaretPos(e.target); 268 | } 269 | e.target.value = payform.formatCardNumber(e.target.value); 270 | if (getDirectionality(e.target) === 'ltr' && cursor !== e.target.selectionStart) { 271 | cursor = _getCaretPos(e.target); 272 | } 273 | if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) { 274 | e.target.value = '‎\u200e'.concat(e.target.value); 275 | } 276 | cursor = _getCaretPos(e.target); 277 | if ((cursor != null) && cursor !== 0 && e.type !== 'change') { 278 | return e.target.setSelectionRange(cursor, cursor); 279 | } 280 | }; 281 | formatCardNumber = function(e) { 282 | var card, cursor, digit, length, re, upperLength, value; 283 | digit = String.fromCharCode(e.which); 284 | if (!/^\d+$/.test(digit)) { 285 | return; 286 | } 287 | value = e.target.value; 288 | card = cardFromNumber(value + digit); 289 | length = (value.replace(/\D/g, '') + digit).length; 290 | upperLength = 16; 291 | if (card) { 292 | upperLength = card.length[card.length.length - 1]; 293 | } 294 | if (length >= upperLength) { 295 | return; 296 | } 297 | cursor = _getCaretPos(e.target); 298 | if (cursor && cursor !== value.length) { 299 | return; 300 | } 301 | if (card && card.type === 'amex') { 302 | re = /^(\d{4}|\d{4}\s\d{6})$/; 303 | } else { 304 | re = /(?:^|\s)(\d{4})$/; 305 | } 306 | if (re.test(value)) { 307 | e.preventDefault(); 308 | return setTimeout(function() { 309 | return e.target.value = value + " " + digit; 310 | }); 311 | } else if (re.test(value + digit)) { 312 | e.preventDefault(); 313 | return setTimeout(function() { 314 | return e.target.value = (value + digit) + " "; 315 | }); 316 | } 317 | }; 318 | formatBackCardNumber = function(e) { 319 | var cursor, value; 320 | value = e.target.value; 321 | if (e.which !== keyCodes.BACKSPACE) { 322 | return; 323 | } 324 | cursor = _getCaretPos(e.target); 325 | if (cursor && cursor !== value.length) { 326 | return; 327 | } 328 | if ((e.target.selectionEnd - e.target.selectionStart) > 1) { 329 | return; 330 | } 331 | if (/\d\s$/.test(value)) { 332 | e.preventDefault(); 333 | return setTimeout(function() { 334 | return e.target.value = value.replace(/\d\s$/, ''); 335 | }); 336 | } else if (/\s\d?$/.test(value)) { 337 | e.preventDefault(); 338 | return setTimeout(function() { 339 | return e.target.value = value.replace(/\d$/, ''); 340 | }); 341 | } 342 | }; 343 | reFormatExpiry = function(e) { 344 | var cursor; 345 | if (e.target.value === "") { 346 | return; 347 | } 348 | e.target.value = payform.formatCardExpiry(e.target.value); 349 | if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) { 350 | e.target.value = '‎\u200e'.concat(e.target.value); 351 | } 352 | cursor = _getCaretPos(e.target); 353 | if ((cursor != null) && e.type !== 'change') { 354 | return e.target.setSelectionRange(cursor, cursor); 355 | } 356 | }; 357 | formatCardExpiry = function(e) { 358 | var digit, val; 359 | digit = String.fromCharCode(e.which); 360 | if (!/^\d+$/.test(digit)) { 361 | return; 362 | } 363 | val = e.target.value + digit; 364 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 365 | e.preventDefault(); 366 | return setTimeout(function() { 367 | return e.target.value = "0" + val + " / "; 368 | }); 369 | } else if (/^\d\d$/.test(val)) { 370 | e.preventDefault(); 371 | return setTimeout(function() { 372 | return e.target.value = val + " / "; 373 | }); 374 | } 375 | }; 376 | formatForwardExpiry = function(e) { 377 | var digit, val; 378 | digit = String.fromCharCode(e.which); 379 | if (!/^\d+$/.test(digit)) { 380 | return; 381 | } 382 | val = e.target.value; 383 | if (/^\d\d$/.test(val)) { 384 | return e.target.value = val + " / "; 385 | } 386 | }; 387 | formatForwardSlashAndSpace = function(e) { 388 | var val, which; 389 | which = String.fromCharCode(e.which); 390 | if (!(which === '/' || which === ' ')) { 391 | return; 392 | } 393 | val = e.target.value; 394 | if (/^\d$/.test(val) && val !== '0') { 395 | return e.target.value = "0" + val + " / "; 396 | } 397 | }; 398 | formatBackExpiry = function(e) { 399 | var cursor, value; 400 | value = e.target.value; 401 | if (e.which !== keyCodes.BACKSPACE) { 402 | return; 403 | } 404 | cursor = _getCaretPos(e.target); 405 | if (cursor && cursor !== value.length) { 406 | return; 407 | } 408 | if (/\d\s\/\s$/.test(value)) { 409 | e.preventDefault(); 410 | return setTimeout(function() { 411 | return e.target.value = value.replace(/\d\s\/\s$/, ''); 412 | }); 413 | } 414 | }; 415 | reFormatCVC = function(e) { 416 | var cursor; 417 | if (e.target.value === "") { 418 | return; 419 | } 420 | cursor = _getCaretPos(e.target); 421 | e.target.value = replaceFullWidthChars(e.target.value).replace(/\D/g, '').slice(0, 4); 422 | if ((cursor != null) && e.type !== 'change') { 423 | return e.target.setSelectionRange(cursor, cursor); 424 | } 425 | }; 426 | restrictNumeric = function(e) { 427 | var input; 428 | if (e.metaKey || e.ctrlKey) { 429 | return; 430 | } 431 | if ([keyCodes.UNKNOWN, keyCodes.ARROW_LEFT, keyCodes.ARROW_RIGHT].indexOf(e.which) > -1) { 432 | return; 433 | } 434 | if (e.which < keyCodes.PAGE_UP) { 435 | return; 436 | } 437 | input = String.fromCharCode(e.which); 438 | if (!/^\d+$/.test(input)) { 439 | return e.preventDefault(); 440 | } 441 | }; 442 | restrictCardNumber = function(e) { 443 | var card, digit, maxLength, value; 444 | digit = String.fromCharCode(e.which); 445 | if (!/^\d+$/.test(digit)) { 446 | return; 447 | } 448 | if (hasTextSelected(e.target)) { 449 | return; 450 | } 451 | value = (e.target.value + digit).replace(/\D/g, ''); 452 | card = cardFromNumber(value); 453 | maxLength = card ? card.length[card.length.length - 1] : 16; 454 | if (value.length > maxLength) { 455 | return e.preventDefault(); 456 | } 457 | }; 458 | restrictExpiry = function(e) { 459 | var digit, value; 460 | digit = String.fromCharCode(e.which); 461 | if (!/^\d+$/.test(digit)) { 462 | return; 463 | } 464 | if (hasTextSelected(e.target)) { 465 | return; 466 | } 467 | value = e.target.value + digit; 468 | value = value.replace(/\D/g, ''); 469 | if (value.length > 6) { 470 | return e.preventDefault(); 471 | } 472 | }; 473 | restrictCVC = function(e) { 474 | var digit, val; 475 | digit = String.fromCharCode(e.which); 476 | if (!/^\d+$/.test(digit)) { 477 | return; 478 | } 479 | if (hasTextSelected(e.target)) { 480 | return; 481 | } 482 | val = e.target.value + digit; 483 | if (val.length > 4) { 484 | return e.preventDefault(); 485 | } 486 | }; 487 | eventList = { 488 | cvcInput: [ 489 | { 490 | eventName: 'keypress', 491 | eventHandler: _eventNormalize(restrictNumeric) 492 | }, { 493 | eventName: 'keypress', 494 | eventHandler: _eventNormalize(restrictCVC) 495 | }, { 496 | eventName: 'paste', 497 | eventHandler: _eventNormalize(reFormatCVC) 498 | }, { 499 | eventName: 'change', 500 | eventHandler: _eventNormalize(reFormatCVC) 501 | }, { 502 | eventName: 'input', 503 | eventHandler: _eventNormalize(reFormatCVC) 504 | } 505 | ], 506 | expiryInput: [ 507 | { 508 | eventName: 'keypress', 509 | eventHandler: _eventNormalize(restrictNumeric) 510 | }, { 511 | eventName: 'keypress', 512 | eventHandler: _eventNormalize(restrictExpiry) 513 | }, { 514 | eventName: 'keypress', 515 | eventHandler: _eventNormalize(formatCardExpiry) 516 | }, { 517 | eventName: 'keypress', 518 | eventHandler: _eventNormalize(formatForwardSlashAndSpace) 519 | }, { 520 | eventName: 'keypress', 521 | eventHandler: _eventNormalize(formatForwardExpiry) 522 | }, { 523 | eventName: 'keydown', 524 | eventHandler: _eventNormalize(formatBackExpiry) 525 | }, { 526 | eventName: 'change', 527 | eventHandler: _eventNormalize(reFormatExpiry) 528 | }, { 529 | eventName: 'input', 530 | eventHandler: _eventNormalize(reFormatExpiry) 531 | } 532 | ], 533 | cardNumberInput: [ 534 | { 535 | eventName: 'keypress', 536 | eventHandler: _eventNormalize(restrictNumeric) 537 | }, { 538 | eventName: 'keypress', 539 | eventHandler: _eventNormalize(restrictCardNumber) 540 | }, { 541 | eventName: 'keypress', 542 | eventHandler: _eventNormalize(formatCardNumber) 543 | }, { 544 | eventName: 'keydown', 545 | eventHandler: _eventNormalize(formatBackCardNumber) 546 | }, { 547 | eventName: 'paste', 548 | eventHandler: _eventNormalize(reFormatCardNumber) 549 | }, { 550 | eventName: 'change', 551 | eventHandler: _eventNormalize(reFormatCardNumber) 552 | }, { 553 | eventName: 'input', 554 | eventHandler: _eventNormalize(reFormatCardNumber) 555 | } 556 | ], 557 | numericInput: [ 558 | { 559 | eventName: 'keypress', 560 | eventHandler: _eventNormalize(restrictNumeric) 561 | }, { 562 | eventName: 'paste', 563 | eventHandler: _eventNormalize(restrictNumeric) 564 | }, { 565 | eventName: 'change', 566 | eventHandler: _eventNormalize(restrictNumeric) 567 | }, { 568 | eventName: 'input', 569 | eventHandler: _eventNormalize(restrictNumeric) 570 | } 571 | ] 572 | }; 573 | attachEvents = function(input, events, detach) { 574 | var evt, i, len; 575 | for (i = 0, len = events.length; i < len; i++) { 576 | evt = events[i]; 577 | if (detach) { 578 | _off(input, evt.eventName, evt.eventHandler); 579 | } else { 580 | _on(input, evt.eventName, evt.eventHandler); 581 | } 582 | } 583 | }; 584 | payform.cvcInput = function(input) { 585 | return attachEvents(input, eventList.cvcInput); 586 | }; 587 | payform.expiryInput = function(input) { 588 | return attachEvents(input, eventList.expiryInput); 589 | }; 590 | payform.cardNumberInput = function(input) { 591 | return attachEvents(input, eventList.cardNumberInput); 592 | }; 593 | payform.numericInput = function(input) { 594 | return attachEvents(input, eventList.numericInput); 595 | }; 596 | payform.detachCvcInput = function(input) { 597 | return attachEvents(input, eventList.cvcInput, true); 598 | }; 599 | payform.detachExpiryInput = function(input) { 600 | return attachEvents(input, eventList.expiryInput, true); 601 | }; 602 | payform.detachCardNumberInput = function(input) { 603 | return attachEvents(input, eventList.cardNumberInput, true); 604 | }; 605 | payform.detachNumericInput = function(input) { 606 | return attachEvents(input, eventList.numericInput, true); 607 | }; 608 | payform.parseCardExpiry = function(value) { 609 | var month, prefix, ref, year; 610 | value = value.replace(/\s/g, ''); 611 | ref = value.split('/', 2), month = ref[0], year = ref[1]; 612 | if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { 613 | prefix = (new Date).getFullYear(); 614 | prefix = prefix.toString().slice(0, 2); 615 | year = prefix + year; 616 | } 617 | month = parseInt(month.replace(/[\u200e]/g, ""), 10); 618 | year = parseInt(year, 10); 619 | return { 620 | month: month, 621 | year: year 622 | }; 623 | }; 624 | payform.validateCardNumber = function(num) { 625 | var card, ref; 626 | num = (num + '').replace(/\s+|-/g, ''); 627 | if (!/^\d+$/.test(num)) { 628 | return false; 629 | } 630 | card = cardFromNumber(num); 631 | if (!card) { 632 | return false; 633 | } 634 | return (ref = num.length, indexOf.call(card.length, ref) >= 0) && (card.luhn === false || luhnCheck(num)); 635 | }; 636 | payform.validateCardExpiry = function(month, year) { 637 | var currentTime, expiry, ref; 638 | if (typeof month === 'object' && 'month' in month) { 639 | ref = month, month = ref.month, year = ref.year; 640 | } 641 | if (!(month && year)) { 642 | return false; 643 | } 644 | month = String(month).trim(); 645 | year = String(year).trim(); 646 | if (!/^\d+$/.test(month)) { 647 | return false; 648 | } 649 | if (!/^\d+$/.test(year)) { 650 | return false; 651 | } 652 | if (!((1 <= month && month <= 12))) { 653 | return false; 654 | } 655 | if (year.length === 2) { 656 | if (year < 70) { 657 | year = "20" + year; 658 | } else { 659 | year = "19" + year; 660 | } 661 | } 662 | if (year.length !== 4) { 663 | return false; 664 | } 665 | expiry = new Date(year, month); 666 | currentTime = new Date; 667 | expiry.setMonth(expiry.getMonth() - 1); 668 | expiry.setMonth(expiry.getMonth() + 1, 1); 669 | return expiry > currentTime; 670 | }; 671 | payform.validateCardCVC = function(cvc, type) { 672 | var card, ref; 673 | cvc = String(cvc).trim(); 674 | if (!/^\d+$/.test(cvc)) { 675 | return false; 676 | } 677 | card = cardFromType(type); 678 | if (card != null) { 679 | return ref = cvc.length, indexOf.call(card.cvcLength, ref) >= 0; 680 | } else { 681 | return cvc.length >= 3 && cvc.length <= 4; 682 | } 683 | }; 684 | payform.parseCardType = function(num) { 685 | var ref; 686 | if (!num) { 687 | return null; 688 | } 689 | return ((ref = cardFromNumber(num)) != null ? ref.type : void 0) || null; 690 | }; 691 | payform.formatCardNumber = function(num) { 692 | var card, groups, ref, upperLength; 693 | num = replaceFullWidthChars(num); 694 | num = num.replace(/\D/g, ''); 695 | card = cardFromNumber(num); 696 | if (!card) { 697 | return num; 698 | } 699 | upperLength = card.length[card.length.length - 1]; 700 | num = num.slice(0, upperLength); 701 | if (card.format.global) { 702 | return (ref = num.match(card.format)) != null ? ref.join(' ') : void 0; 703 | } else { 704 | groups = card.format.exec(num); 705 | if (groups == null) { 706 | return; 707 | } 708 | groups.shift(); 709 | groups = groups.filter(Boolean); 710 | return groups.join(' '); 711 | } 712 | }; 713 | payform.formatCardExpiry = function(expiry) { 714 | var mon, parts, sep, year; 715 | expiry = replaceFullWidthChars(expiry); 716 | parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/); 717 | if (!parts) { 718 | return ''; 719 | } 720 | mon = parts[1] || ''; 721 | sep = parts[2] || ''; 722 | year = parts[3] || ''; 723 | if (year.length > 0) { 724 | sep = ' / '; 725 | } else if (sep === ' /') { 726 | mon = mon.substring(0, 1); 727 | sep = ''; 728 | } else if (mon.length === 2 || sep.length > 0) { 729 | sep = ' / '; 730 | } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) { 731 | mon = "0" + mon; 732 | sep = ' / '; 733 | } 734 | return mon + sep + year; 735 | }; 736 | return payform; 737 | }); 738 | 739 | }).call(this); 740 | -------------------------------------------------------------------------------- /dist/payform.min.js: -------------------------------------------------------------------------------- 1 | (function(){var indexOf=[].indexOf||function(item){for(var i=0,l=this.length;i9){digit-=9}sum+=digit}return sum%10===0};hasTextSelected=function(target){var ref;if((typeof document!=="undefined"&&document!==null?(ref=document.selection)!=null?ref.createRange:void 0:void 0)!=null){if(document.selection.createRange().text){return true}}return target.selectionStart!=null&&target.selectionStart!==target.selectionEnd};replaceFullWidthChars=function(str){var char,chars,fullWidth,halfWidth,i,idx,len,value;if(str==null){str=""}fullWidth="0123456789";halfWidth="0123456789";value="";chars=str.split("");for(i=0,len=chars.length;i-1){char=halfWidth[idx]}value+=char}return value};reFormatCardNumber=function(e){var cursor;cursor=_getCaretPos(e.target);if(e.target.value===""){return}if(getDirectionality(e.target)==="ltr"){cursor=_getCaretPos(e.target)}e.target.value=payform.formatCardNumber(e.target.value);if(getDirectionality(e.target)==="ltr"&&cursor!==e.target.selectionStart){cursor=_getCaretPos(e.target)}if(getDirectionality(e.target)==="rtl"&&e.target.value.indexOf("‎‎")===-1){e.target.value="‎‎".concat(e.target.value)}cursor=_getCaretPos(e.target);if(cursor!=null&&cursor!==0&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};formatCardNumber=function(e){var card,cursor,digit,length,re,upperLength,value;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}value=e.target.value;card=cardFromNumber(value+digit);length=(value.replace(/\D/g,"")+digit).length;upperLength=16;if(card){upperLength=card.length[card.length.length-1]}if(length>=upperLength){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(card&&card.type==="amex"){re=/^(\d{4}|\d{4}\s\d{6})$/}else{re=/(?:^|\s)(\d{4})$/}if(re.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value+" "+digit})}else if(re.test(value+digit)){e.preventDefault();return setTimeout(function(){return e.target.value=value+digit+" "})}};formatBackCardNumber=function(e){var cursor,value;value=e.target.value;if(e.which!==keyCodes.BACKSPACE){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(e.target.selectionEnd-e.target.selectionStart>1){return}if(/\d\s$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d\s$/,"")})}else if(/\s\d?$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d$/,"")})}};reFormatExpiry=function(e){var cursor;if(e.target.value===""){return}e.target.value=payform.formatCardExpiry(e.target.value);if(getDirectionality(e.target)==="rtl"&&e.target.value.indexOf("‎‎")===-1){e.target.value="‎‎".concat(e.target.value)}cursor=_getCaretPos(e.target);if(cursor!=null&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};formatCardExpiry=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}val=e.target.value+digit;if(/^\d$/.test(val)&&(val!=="0"&&val!=="1")){e.preventDefault();return setTimeout(function(){return e.target.value="0"+val+" / "})}else if(/^\d\d$/.test(val)){e.preventDefault();return setTimeout(function(){return e.target.value=val+" / "})}};formatForwardExpiry=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}val=e.target.value;if(/^\d\d$/.test(val)){return e.target.value=val+" / "}};formatForwardSlashAndSpace=function(e){var val,which;which=String.fromCharCode(e.which);if(!(which==="/"||which===" ")){return}val=e.target.value;if(/^\d$/.test(val)&&val!=="0"){return e.target.value="0"+val+" / "}};formatBackExpiry=function(e){var cursor,value;value=e.target.value;if(e.which!==keyCodes.BACKSPACE){return}cursor=_getCaretPos(e.target);if(cursor&&cursor!==value.length){return}if(/\d\s\/\s$/.test(value)){e.preventDefault();return setTimeout(function(){return e.target.value=value.replace(/\d\s\/\s$/,"")})}};reFormatCVC=function(e){var cursor;if(e.target.value===""){return}cursor=_getCaretPos(e.target);e.target.value=replaceFullWidthChars(e.target.value).replace(/\D/g,"").slice(0,4);if(cursor!=null&&e.type!=="change"){return e.target.setSelectionRange(cursor,cursor)}};restrictNumeric=function(e){var input;if(e.metaKey||e.ctrlKey){return}if([keyCodes.UNKNOWN,keyCodes.ARROW_LEFT,keyCodes.ARROW_RIGHT].indexOf(e.which)>-1){return}if(e.whichmaxLength){return e.preventDefault()}};restrictExpiry=function(e){var digit,value;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}if(hasTextSelected(e.target)){return}value=e.target.value+digit;value=value.replace(/\D/g,"");if(value.length>6){return e.preventDefault()}};restrictCVC=function(e){var digit,val;digit=String.fromCharCode(e.which);if(!/^\d+$/.test(digit)){return}if(hasTextSelected(e.target)){return}val=e.target.value+digit;if(val.length>4){return e.preventDefault()}};eventList={cvcInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictCVC)},{eventName:"paste",eventHandler:_eventNormalize(reFormatCVC)},{eventName:"change",eventHandler:_eventNormalize(reFormatCVC)},{eventName:"input",eventHandler:_eventNormalize(reFormatCVC)}],expiryInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictExpiry)},{eventName:"keypress",eventHandler:_eventNormalize(formatCardExpiry)},{eventName:"keypress",eventHandler:_eventNormalize(formatForwardSlashAndSpace)},{eventName:"keypress",eventHandler:_eventNormalize(formatForwardExpiry)},{eventName:"keydown",eventHandler:_eventNormalize(formatBackExpiry)},{eventName:"change",eventHandler:_eventNormalize(reFormatExpiry)},{eventName:"input",eventHandler:_eventNormalize(reFormatExpiry)}],cardNumberInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"keypress",eventHandler:_eventNormalize(restrictCardNumber)},{eventName:"keypress",eventHandler:_eventNormalize(formatCardNumber)},{eventName:"keydown",eventHandler:_eventNormalize(formatBackCardNumber)},{eventName:"paste",eventHandler:_eventNormalize(reFormatCardNumber)},{eventName:"change",eventHandler:_eventNormalize(reFormatCardNumber)},{eventName:"input",eventHandler:_eventNormalize(reFormatCardNumber)}],numericInput:[{eventName:"keypress",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"paste",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"change",eventHandler:_eventNormalize(restrictNumeric)},{eventName:"input",eventHandler:_eventNormalize(restrictNumeric)}]};attachEvents=function(input,events,detach){var evt,i,len;for(i=0,len=events.length;i=0)&&(card.luhn===false||luhnCheck(num))};payform.validateCardExpiry=function(month,year){var currentTime,expiry,ref;if(typeof month==="object"&&"month"in month){ref=month,month=ref.month,year=ref.year}if(!(month&&year)){return false}month=String(month).trim();year=String(year).trim();if(!/^\d+$/.test(month)){return false}if(!/^\d+$/.test(year)){return false}if(!(1<=month&&month<=12)){return false}if(year.length===2){if(year<70){year="20"+year}else{year="19"+year}}if(year.length!==4){return false}expiry=new Date(year,month);currentTime=new Date;expiry.setMonth(expiry.getMonth()-1);expiry.setMonth(expiry.getMonth()+1,1);return expiry>currentTime};payform.validateCardCVC=function(cvc,type){var card,ref;cvc=String(cvc).trim();if(!/^\d+$/.test(cvc)){return false}card=cardFromType(type);if(card!=null){return ref=cvc.length,indexOf.call(card.cvcLength,ref)>=0}else{return cvc.length>=3&&cvc.length<=4}};payform.parseCardType=function(num){var ref;if(!num){return null}return((ref=cardFromNumber(num))!=null?ref.type:void 0)||null};payform.formatCardNumber=function(num){var card,groups,ref,upperLength;num=replaceFullWidthChars(num);num=num.replace(/\D/g,"");card=cardFromNumber(num);if(!card){return num}upperLength=card.length[card.length.length-1];num=num.slice(0,upperLength);if(card.format.global){return(ref=num.match(card.format))!=null?ref.join(" "):void 0}else{groups=card.format.exec(num);if(groups==null){return}groups.shift();groups=groups.filter(Boolean);return groups.join(" ")}};payform.formatCardExpiry=function(expiry){var mon,parts,sep,year;expiry=replaceFullWidthChars(expiry);parts=expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);if(!parts){return""}mon=parts[1]||"";sep=parts[2]||"";year=parts[3]||"";if(year.length>0){sep=" / "}else if(sep===" /"){mon=mon.substring(0,1);sep=""}else if(mon.length===2||sep.length>0){sep=" / "}else if(mon.length===1&&(mon!=="0"&&mon!=="1")){mon="0"+mon;sep=" / "}return mon+sep+year};return payform})}).call(this); 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Payform Demo 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 | 14 | 15 | 20 |
21 | 22 | 23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Payform Demo 5 | 6 | 7 | 8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payform", 3 | "version": "1.4.0", 4 | "description": "A general purpose library for building credit card forms, validating inputs, and formatting numbers.", 5 | "keywords": [ 6 | "payment", 7 | "form", 8 | "cc", 9 | "card", 10 | "credit card", 11 | "formatting", 12 | "validation", 13 | "jquery-plugin", 14 | "ecosystem:jquery", 15 | "ecosystem:browserify" 16 | ], 17 | "author": "Jonathan D. Johnson ", 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/jondavidjohn/payform.git" 22 | }, 23 | "main": "dist/payform.js", 24 | "scripts": { 25 | "test": "make test", 26 | "build": "make build", 27 | "watch": "make watch" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^16.2.3", 31 | "bundle-collapser": "~1.1.1", 32 | "coffeeify": "~1.0.0", 33 | "coffeescript": "~1.9.0", 34 | "mocha": "^5.2.0", 35 | "uglify-js": "~3.3.7", 36 | "watch": "~0.13.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/jquery.payform.coffee: -------------------------------------------------------------------------------- 1 | payform = require './payform' 2 | 3 | do ($ = window.jQuery || window.Zepto) -> 4 | 5 | $.payform = payform 6 | $.payform.fn = 7 | formatCardNumber: -> 8 | payform.cardNumberInput @get(0) 9 | formatCardExpiry: -> 10 | payform.expiryInput @get(0) 11 | formatCardCVC: -> 12 | payform.cvcInput @get(0) 13 | formatNumeric: -> 14 | payform.numericInput @get(0) 15 | detachFormatCardNumber: -> 16 | payform.detachCardNumberInput @get(0) 17 | detachFormatCardExpiry: -> 18 | payform.detachExpiryInput @get(0) 19 | detachFormatCardCVC: -> 20 | payform.detachCvcInput @get(0) 21 | detachFormatNumeric: -> 22 | payform.detachNumericInput @get(0) 23 | 24 | $.fn.payform = (method) -> 25 | $.payform.fn[method].call(this) if $.payform.fn[method]? 26 | return this 27 | -------------------------------------------------------------------------------- /src/payform.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Payform Javascript Library 3 | 4 | URL: https://github.com/jondavidjohn/payform 5 | Author: Jonathan D. Johnson 6 | License: MIT 7 | Version: 1.4.0 8 | ### 9 | ((name, definition) -> 10 | if module? 11 | module.exports = definition() 12 | else if typeof define is 'function' and typeof define.amd is 'object' 13 | define(name, definition) 14 | else 15 | this[name] = definition() 16 | )('payform', -> 17 | 18 | _getCaretPos = (ele) -> 19 | if ele.selectionStart? 20 | return ele.selectionStart 21 | else if document.selection? 22 | ele.focus() 23 | r = document.selection.createRange() 24 | re = ele.createTextRange() 25 | rc = re.duplicate() 26 | re.moveToBookmark(r.getBookmark()) 27 | rc.setEndPoint('EndToStart', re) 28 | return rc.text.length 29 | 30 | _eventNormalize = (listener) -> 31 | return (e = window.event) -> 32 | if e.inputType == 'insertCompositionText' and !e.isComposing 33 | return 34 | newEvt = 35 | target: e.target or e.srcElement 36 | which: e.which or e.keyCode 37 | type: e.type 38 | metaKey: e.metaKey 39 | ctrlKey: e.ctrlKey 40 | preventDefault: -> 41 | if e.preventDefault 42 | e.preventDefault() 43 | else 44 | e.returnValue = false 45 | return 46 | listener(newEvt) 47 | 48 | _on = (ele, event, listener) -> 49 | if ele.addEventListener? 50 | ele.addEventListener(event, listener, false) 51 | else 52 | ele.attachEvent("on#{event}", listener) 53 | 54 | _off = (ele, event, listener) -> 55 | if ele.removeEventListener? 56 | ele.removeEventListener(event, listener, false) 57 | else 58 | ele.detachEvent("on#{event}", listener) 59 | 60 | payform = {} 61 | 62 | # Key Codes 63 | 64 | keyCodes = { 65 | UNKNOWN : 0, 66 | BACKSPACE : 8, 67 | PAGE_UP : 33, 68 | ARROW_LEFT : 37, 69 | ARROW_RIGHT : 39, 70 | } 71 | 72 | # Utils 73 | 74 | defaultFormat = /(\d{1,4})/g 75 | 76 | payform.cards = [ 77 | # Debit cards must come first, since they have more 78 | # specific patterns than their credit-card equivalents. 79 | { 80 | type: 'elo' 81 | pattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/ 82 | format: defaultFormat 83 | length: [16] 84 | cvcLength: [3] 85 | luhn: true 86 | } 87 | { 88 | type: 'visaelectron' 89 | pattern: /^4(026|17500|405|508|844|91[37])/ 90 | format: defaultFormat 91 | length: [16] 92 | cvcLength: [3] 93 | luhn: true 94 | } 95 | { 96 | type: 'maestro' 97 | pattern: /^(5018|5020|5038|6304|6390[0-9]{2}|67[0-9]{4})/ 98 | format: defaultFormat 99 | length: [12..19] 100 | cvcLength: [3] 101 | luhn: true 102 | } 103 | { 104 | type: 'forbrugsforeningen' 105 | pattern: /^600/ 106 | format: defaultFormat 107 | length: [16] 108 | cvcLength: [3] 109 | luhn: true 110 | } 111 | { 112 | type: 'dankort' 113 | pattern: /^5019/ 114 | format: defaultFormat 115 | length: [16] 116 | cvcLength: [3] 117 | luhn: true 118 | } 119 | # Credit cards 120 | { 121 | type: 'visa' 122 | pattern: /^4/ 123 | format: defaultFormat 124 | length: [13, 16, 19] 125 | cvcLength: [3] 126 | luhn: true 127 | } 128 | { 129 | type: 'mastercard' 130 | pattern: /^(5[1-5][0-9]{4}|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)([0-9]{2})/ 131 | format: defaultFormat 132 | length: [16] 133 | cvcLength: [3] 134 | luhn: true 135 | } 136 | { 137 | type: 'amex' 138 | pattern: /^3[47]/ 139 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/ 140 | length: [15] 141 | cvcLength: [4] 142 | luhn: true 143 | } 144 | # Must be above dinersclub. 145 | { 146 | type: 'hipercard' 147 | pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/ 148 | format: defaultFormat 149 | length: [14..19] 150 | cvcLength: [3] 151 | luhn: true 152 | } 153 | { 154 | type: 'dinersclub' 155 | pattern: /^(36|38|30[0-5])/ 156 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/ 157 | length: [14] 158 | cvcLength: [3] 159 | luhn: true 160 | } 161 | { 162 | type: 'discover' 163 | pattern: /^(6011|65|64[4-9]|622)/ 164 | format: defaultFormat 165 | length: [16] 166 | cvcLength: [3] 167 | luhn: true 168 | } 169 | { 170 | type: 'unionpay' 171 | pattern: /^62/ 172 | format: defaultFormat 173 | length: [16..19] 174 | cvcLength: [3] 175 | luhn: false 176 | } 177 | { 178 | type: 'jcb' 179 | pattern: /^35/ 180 | format: defaultFormat 181 | length: [16..19] 182 | cvcLength: [3] 183 | luhn: true 184 | } 185 | { 186 | type: 'laser' 187 | pattern: /^(6706|6771|6709)/ 188 | format: defaultFormat 189 | length: [16..19] 190 | cvcLength: [3] 191 | luhn: true 192 | } 193 | ] 194 | 195 | cardFromNumber = (num) -> 196 | num = (num + '').replace(/\D/g, '') 197 | return card for card in payform.cards when card.pattern.test(num) 198 | 199 | cardFromType = (type) -> 200 | return card for card in payform.cards when card.type is type 201 | 202 | getDirectionality = (target) -> 203 | # Work around Firefox not returning the styles in some edge cases. 204 | # In Firefox < 62, style can be `null`. 205 | # In Firefox 62+, `style['direction']` can be an empty string. 206 | # See https://bugzilla.mozilla.org/show_bug.cgi?id=1467722. 207 | style = getComputedStyle(target) 208 | style and style['direction'] or document.dir 209 | 210 | luhnCheck = (num) -> 211 | odd = true 212 | sum = 0 213 | 214 | digits = (num + '').split('').reverse() 215 | 216 | for digit in digits 217 | digit = parseInt(digit, 10) 218 | digit *= 2 if (odd = !odd) 219 | digit -= 9 if digit > 9 220 | sum += digit 221 | 222 | sum % 10 == 0 223 | 224 | hasTextSelected = (target) -> 225 | # If some text is selected in IE 226 | if document?.selection?.createRange? 227 | return true if document.selection.createRange().text 228 | target.selectionStart? and target.selectionStart isnt target.selectionEnd 229 | 230 | # Private 231 | 232 | # Replace Full-Width Chars 233 | 234 | replaceFullWidthChars = (str = '') -> 235 | fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19' 236 | halfWidth = '0123456789' 237 | 238 | value = '' 239 | chars = str.split('') 240 | 241 | for char in chars 242 | idx = fullWidth.indexOf(char) 243 | char = halfWidth[idx] if idx > -1 244 | value += char 245 | 246 | value 247 | 248 | # Format Card Number 249 | 250 | reFormatCardNumber = (e) -> 251 | cursor = _getCaretPos(e.target) 252 | return if e.target.value is "" 253 | 254 | if getDirectionality(e.target) == 'ltr' 255 | cursor = _getCaretPos(e.target) 256 | 257 | e.target.value = payform.formatCardNumber(e.target.value) 258 | 259 | if getDirectionality(e.target) == 'ltr' and cursor isnt e.target.selectionStart 260 | cursor = _getCaretPos(e.target) 261 | 262 | if getDirectionality(e.target) == 'rtl' and e.target.value.indexOf('‎\u200e') == -1 263 | e.target.value = '‎\u200e'.concat(e.target.value) 264 | 265 | cursor = _getCaretPos(e.target) 266 | 267 | if cursor? and cursor isnt 0 and e.type isnt 'change' 268 | e.target.setSelectionRange(cursor, cursor) 269 | 270 | formatCardNumber = (e) -> 271 | # Only format if input is a number 272 | digit = String.fromCharCode(e.which) 273 | return unless /^\d+$/.test(digit) 274 | 275 | value = e.target.value 276 | card = cardFromNumber(value + digit) 277 | length = (value.replace(/\D/g, '') + digit).length 278 | 279 | upperLength = 16 280 | upperLength = card.length[card.length.length - 1] if card 281 | return if length >= upperLength 282 | 283 | # Return if focus isn't at the end of the text 284 | cursor = _getCaretPos(e.target) 285 | return if cursor and cursor isnt value.length 286 | 287 | if card && card.type is 'amex' 288 | # AMEX cards are formatted differently 289 | re = /^(\d{4}|\d{4}\s\d{6})$/ 290 | else 291 | re = /(?:^|\s)(\d{4})$/ 292 | 293 | # If '4242' + 4 294 | if re.test(value) 295 | e.preventDefault() 296 | setTimeout -> e.target.value = "#{value} #{digit}" 297 | 298 | # If '424' + 2 299 | else if re.test(value + digit) 300 | e.preventDefault() 301 | setTimeout -> e.target.value = "#{value + digit} " 302 | 303 | formatBackCardNumber = (e) -> 304 | value = e.target.value 305 | 306 | # Return unless backspacing 307 | return unless e.which is keyCodes.BACKSPACE 308 | 309 | # Return if focus isn't at the end of the text 310 | cursor = _getCaretPos(e.target) 311 | return if cursor and cursor isnt value.length 312 | 313 | return if (e.target.selectionEnd - e.target.selectionStart) > 1 314 | 315 | # Remove the digit + trailing space 316 | if /\d\s$/.test(value) 317 | e.preventDefault() 318 | setTimeout -> e.target.value = value.replace /\d\s$/, '' 319 | # Remove digit if ends in space + digit 320 | else if /\s\d?$/.test(value) 321 | e.preventDefault() 322 | setTimeout -> e.target.value = value.replace /\d$/, '' 323 | 324 | # Format Expiry 325 | 326 | reFormatExpiry = (e) -> 327 | return if e.target.value is "" 328 | e.target.value = payform.formatCardExpiry(e.target.value) 329 | if getDirectionality(e.target) == 'rtl' and e.target.value.indexOf('‎\u200e') == -1 330 | e.target.value = '‎\u200e'.concat(e.target.value) 331 | cursor = _getCaretPos(e.target) 332 | if cursor? and e.type isnt 'change' 333 | e.target.setSelectionRange(cursor, cursor) 334 | 335 | formatCardExpiry = (e) -> 336 | # Only format if input is a number 337 | digit = String.fromCharCode(e.which) 338 | return unless /^\d+$/.test(digit) 339 | 340 | val = e.target.value + digit 341 | 342 | if /^\d$/.test(val) and val not in ['0', '1'] 343 | e.preventDefault() 344 | setTimeout -> e.target.value = "0#{val} / " 345 | 346 | else if /^\d\d$/.test(val) 347 | e.preventDefault() 348 | setTimeout -> e.target.value = "#{val} / " 349 | 350 | formatForwardExpiry = (e) -> 351 | digit = String.fromCharCode(e.which) 352 | return unless /^\d+$/.test(digit) 353 | val = e.target.value 354 | if /^\d\d$/.test(val) 355 | e.target.value = "#{val} / " 356 | 357 | formatForwardSlashAndSpace = (e) -> 358 | which = String.fromCharCode(e.which) 359 | return unless which is '/' or which is ' ' 360 | val = e.target.value 361 | if /^\d$/.test(val) and val isnt '0' 362 | e.target.value = "0#{val} / " 363 | 364 | formatBackExpiry = (e) -> 365 | value = e.target.value 366 | 367 | # Return unless backspacing 368 | return unless e.which is keyCodes.BACKSPACE 369 | 370 | # Return if focus isn't at the end of the text 371 | cursor = _getCaretPos(e.target) 372 | return if cursor and cursor isnt value.length 373 | 374 | # Remove the trailing space + last digit 375 | if /\d\s\/\s$/.test(value) 376 | e.preventDefault() 377 | setTimeout -> e.target.value = value.replace(/\d\s\/\s$/, '') 378 | 379 | # Format CVC 380 | 381 | reFormatCVC = (e) -> 382 | return if e.target.value is "" 383 | cursor = _getCaretPos(e.target) 384 | e.target.value = replaceFullWidthChars(e.target.value).replace(/\D/g, '')[0...4] 385 | if cursor? and e.type isnt 'change' 386 | e.target.setSelectionRange(cursor, cursor) 387 | 388 | # Restrictions 389 | 390 | restrictNumeric = (e) -> 391 | # Key event is for a browser shortcut 392 | return if e.metaKey or e.ctrlKey 393 | 394 | # If keycode is a special char (WebKit) 395 | return if [keyCodes.UNKNOWN, keyCodes.ARROW_LEFT, keyCodes.ARROW_RIGHT].indexOf(e.which) > -1 396 | 397 | # If char is a special char (Firefox) 398 | return if e.which < keyCodes.PAGE_UP 399 | 400 | input = String.fromCharCode(e.which) 401 | 402 | # Char is a number 403 | unless /^\d+$/.test(input) 404 | e.preventDefault() 405 | 406 | restrictCardNumber = (e) -> 407 | digit = String.fromCharCode(e.which) 408 | return unless /^\d+$/.test(digit) 409 | 410 | return if hasTextSelected(e.target) 411 | 412 | # Restrict number of digits 413 | value = (e.target.value + digit).replace(/\D/g, '') 414 | card = cardFromNumber(value) 415 | maxLength = if card then card.length[card.length.length - 1] else 16 416 | 417 | if value.length > maxLength 418 | e.preventDefault() 419 | 420 | restrictExpiry = (e) -> 421 | digit = String.fromCharCode(e.which) 422 | return unless /^\d+$/.test(digit) 423 | 424 | return if hasTextSelected(e.target) 425 | 426 | value = e.target.value + digit 427 | value = value.replace(/\D/g, '') 428 | 429 | if value.length > 6 430 | e.preventDefault() 431 | 432 | restrictCVC = (e) -> 433 | digit = String.fromCharCode(e.which) 434 | return unless /^\d+$/.test(digit) 435 | return if hasTextSelected(e.target) 436 | val = e.target.value + digit 437 | if val.length > 4 438 | e.preventDefault() 439 | 440 | # Public 441 | 442 | # Formatting 443 | 444 | eventList = { 445 | cvcInput: [ 446 | { 447 | eventName: 'keypress', 448 | eventHandler: _eventNormalize(restrictNumeric), 449 | }, 450 | { 451 | eventName: 'keypress', 452 | eventHandler: _eventNormalize(restrictCVC), 453 | }, 454 | { 455 | eventName: 'paste', 456 | eventHandler: _eventNormalize(reFormatCVC), 457 | }, 458 | { 459 | eventName: 'change', 460 | eventHandler: _eventNormalize(reFormatCVC), 461 | }, 462 | { 463 | eventName: 'input', 464 | eventHandler: _eventNormalize(reFormatCVC), 465 | }, 466 | ], 467 | 468 | expiryInput: [ 469 | { 470 | eventName: 'keypress', 471 | eventHandler: _eventNormalize(restrictNumeric), 472 | }, 473 | { 474 | eventName: 'keypress', 475 | eventHandler: _eventNormalize(restrictExpiry), 476 | }, 477 | { 478 | eventName: 'keypress', 479 | eventHandler: _eventNormalize(formatCardExpiry), 480 | }, 481 | { 482 | eventName: 'keypress', 483 | eventHandler: _eventNormalize(formatForwardSlashAndSpace), 484 | }, 485 | { 486 | eventName: 'keypress', 487 | eventHandler: _eventNormalize(formatForwardExpiry), 488 | }, 489 | { 490 | eventName: 'keydown', 491 | eventHandler: _eventNormalize(formatBackExpiry), 492 | }, 493 | { 494 | eventName: 'change', 495 | eventHandler: _eventNormalize(reFormatExpiry), 496 | }, 497 | { 498 | eventName: 'input', 499 | eventHandler: _eventNormalize(reFormatExpiry), 500 | }, 501 | ], 502 | 503 | cardNumberInput: [ 504 | { 505 | eventName: 'keypress', 506 | eventHandler: _eventNormalize(restrictNumeric), 507 | }, 508 | { 509 | eventName: 'keypress', 510 | eventHandler: _eventNormalize(restrictCardNumber), 511 | }, 512 | { 513 | eventName: 'keypress', 514 | eventHandler: _eventNormalize(formatCardNumber), 515 | }, 516 | { 517 | eventName: 'keydown', 518 | eventHandler: _eventNormalize(formatBackCardNumber), 519 | }, 520 | { 521 | eventName: 'paste', 522 | eventHandler: _eventNormalize(reFormatCardNumber), 523 | }, 524 | { 525 | eventName: 'change', 526 | eventHandler: _eventNormalize(reFormatCardNumber), 527 | }, 528 | { 529 | eventName: 'input', 530 | eventHandler: _eventNormalize(reFormatCardNumber), 531 | }, 532 | ], 533 | 534 | numericInput: [ 535 | { 536 | eventName: 'keypress', 537 | eventHandler: _eventNormalize(restrictNumeric), 538 | }, 539 | { 540 | eventName: 'paste', 541 | eventHandler: _eventNormalize(restrictNumeric), 542 | }, 543 | { 544 | eventName: 'change', 545 | eventHandler: _eventNormalize(restrictNumeric), 546 | }, 547 | { 548 | eventName: 'input', 549 | eventHandler: _eventNormalize(restrictNumeric), 550 | }, 551 | ], 552 | } 553 | 554 | attachEvents = (input, events, detach) -> 555 | for evt in events 556 | if (detach) 557 | _off(input, evt.eventName, evt.eventHandler) 558 | else 559 | _on(input, evt.eventName, evt.eventHandler) 560 | return 561 | 562 | payform.cvcInput = (input) -> 563 | attachEvents(input, eventList.cvcInput) 564 | 565 | payform.expiryInput = (input) -> 566 | attachEvents(input, eventList.expiryInput) 567 | 568 | payform.cardNumberInput = (input) -> 569 | attachEvents(input, eventList.cardNumberInput) 570 | 571 | payform.numericInput = (input) -> 572 | attachEvents(input, eventList.numericInput) 573 | 574 | payform.detachCvcInput = (input) -> 575 | attachEvents(input, eventList.cvcInput, true) 576 | 577 | payform.detachExpiryInput = (input) -> 578 | attachEvents(input, eventList.expiryInput, true) 579 | 580 | payform.detachCardNumberInput = (input) -> 581 | attachEvents(input, eventList.cardNumberInput, true) 582 | 583 | payform.detachNumericInput = (input) -> 584 | attachEvents(input, eventList.numericInput, true) 585 | 586 | # Validations 587 | 588 | payform.parseCardExpiry = (value) -> 589 | value = value.replace(/\s/g, '') 590 | [month, year] = value.split('/', 2) 591 | 592 | # Allow for year shortcut 593 | if year?.length is 2 and /^\d+$/.test(year) 594 | prefix = (new Date).getFullYear() 595 | prefix = prefix.toString()[0..1] 596 | year = prefix + year 597 | 598 | # Remove left-to-right mark LTR invisible unicode control character used in right-to-left contexts 599 | month = parseInt(month.replace(/[\u200e]/g, ""), 10); 600 | year = parseInt(year, 10) 601 | 602 | month: month, year: year 603 | 604 | payform.validateCardNumber = (num) -> 605 | num = (num + '').replace(/\s+|-/g, '') 606 | return false unless /^\d+$/.test(num) 607 | 608 | card = cardFromNumber(num) 609 | return false unless card 610 | 611 | num.length in card.length and 612 | (card.luhn is false or luhnCheck(num)) 613 | 614 | payform.validateCardExpiry = (month, year) -> 615 | # Allow passing an object 616 | if typeof month is 'object' and 'month' of month 617 | {month, year} = month 618 | 619 | return false unless month and year 620 | 621 | month = String(month).trim() 622 | year = String(year).trim() 623 | 624 | return false unless /^\d+$/.test(month) 625 | return false unless /^\d+$/.test(year) 626 | return false unless 1 <= month <= 12 627 | 628 | if year.length == 2 629 | if year < 70 630 | year = "20#{year}" 631 | else 632 | year = "19#{year}" 633 | 634 | return false unless year.length == 4 635 | 636 | expiry = new Date(year, month) 637 | currentTime = new Date 638 | 639 | # Months start from 0 in JavaScript 640 | expiry.setMonth(expiry.getMonth() - 1) 641 | 642 | # The cc expires at the end of the month, 643 | # so we need to make the expiry the first day 644 | # of the month after 645 | expiry.setMonth(expiry.getMonth() + 1, 1) 646 | 647 | expiry > currentTime 648 | 649 | payform.validateCardCVC = (cvc, type) -> 650 | cvc = String(cvc).trim() 651 | return false unless /^\d+$/.test(cvc) 652 | 653 | card = cardFromType(type) 654 | if card? 655 | # Check against a explicit card type 656 | cvc.length in card.cvcLength 657 | else 658 | # Check against all types 659 | cvc.length >= 3 and cvc.length <= 4 660 | 661 | payform.parseCardType = (num) -> 662 | return null unless num 663 | cardFromNumber(num)?.type or null 664 | 665 | payform.formatCardNumber = (num) -> 666 | num = replaceFullWidthChars(num) 667 | num = num.replace(/\D/g, '') 668 | card = cardFromNumber(num) 669 | return num unless card 670 | 671 | upperLength = card.length[card.length.length - 1] 672 | num = num[0...upperLength] 673 | 674 | if card.format.global 675 | num.match(card.format)?.join(' ') 676 | else 677 | groups = card.format.exec(num) 678 | return unless groups? 679 | groups.shift() 680 | groups = groups.filter(Boolean) 681 | groups.join(' ') 682 | 683 | payform.formatCardExpiry = (expiry) -> 684 | expiry = replaceFullWidthChars(expiry) 685 | parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/) 686 | return '' unless parts 687 | 688 | mon = parts[1] || '' 689 | sep = parts[2] || '' 690 | year = parts[3] || '' 691 | 692 | if year.length > 0 693 | sep = ' / ' 694 | 695 | else if sep is ' /' 696 | mon = mon.substring(0, 1) 697 | sep = '' 698 | 699 | else if mon.length == 2 or sep.length > 0 700 | sep = ' / ' 701 | 702 | else if mon.length == 1 and mon not in ['0', '1'] 703 | mon = "0#{mon}" 704 | sep = ' / ' 705 | 706 | return mon + sep + year 707 | 708 | payform 709 | ) 710 | -------------------------------------------------------------------------------- /test/cardType_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#parseCardType()', -> 6 | it 'should return Visa that begins with 40', -> 7 | topic = payform.parseCardType '4012121212121212' 8 | assert.equal topic, 'visa' 9 | 10 | it 'that begins with 5 should return MasterCard', -> 11 | topic = payform.parseCardType '5555555555554444' 12 | assert.equal topic, 'mastercard' 13 | 14 | it 'that begins with 2 should return MasterCard', -> 15 | topic = payform.parseCardType '2221000002222221' 16 | assert.equal topic, 'mastercard' 17 | 18 | it 'that begins with 34 should return American Express', -> 19 | topic = payform.parseCardType '3412121212121212' 20 | assert.equal topic, 'amex' 21 | 22 | it 'that is not numbers should return null', -> 23 | topic = payform.parseCardType 'aoeu' 24 | assert.equal topic, null 25 | 26 | it 'that has unrecognized beginning numbers should return null', -> 27 | topic = payform.parseCardType 'aoeu' 28 | assert.equal topic, null 29 | 30 | it 'should return correct type for all test numbers', -> 31 | assert.equal(payform.parseCardType('4917300800000000'), 'visaelectron') 32 | 33 | assert.equal(payform.parseCardType('6759649826438453'), 'maestro') 34 | 35 | assert.equal(payform.parseCardType('6007220000000004'), 'forbrugsforeningen') 36 | 37 | assert.equal(payform.parseCardType('5019717010103742'), 'dankort') 38 | 39 | assert.equal(payform.parseCardType('4111111111111111'), 'visa') 40 | assert.equal(payform.parseCardType('4012888888881881'), 'visa') 41 | assert.equal(payform.parseCardType('4222222222222'), 'visa') 42 | assert.equal(payform.parseCardType('4462030000000000'), 'visa') 43 | assert.equal(payform.parseCardType('4484070000000000'), 'visa') 44 | 45 | assert.equal(payform.parseCardType('5555555555554444'), 'mastercard') 46 | assert.equal(payform.parseCardType('5454545454545454'), 'mastercard') 47 | assert.equal(payform.parseCardType('2221000002222221'), 'mastercard') 48 | 49 | assert.equal(payform.parseCardType('378282246310005'), 'amex') 50 | assert.equal(payform.parseCardType('371449635398431'), 'amex') 51 | assert.equal(payform.parseCardType('378734493671000'), 'amex') 52 | 53 | assert.equal(payform.parseCardType('30569309025904'), 'dinersclub') 54 | assert.equal(payform.parseCardType('38520000023237'), 'dinersclub') 55 | assert.equal(payform.parseCardType('36700102000000'), 'dinersclub') 56 | assert.equal(payform.parseCardType('36148900647913'), 'dinersclub') 57 | 58 | assert.equal(payform.parseCardType('6011111111111117'), 'discover') 59 | assert.equal(payform.parseCardType('6011000990139424'), 'discover') 60 | 61 | assert.equal(payform.parseCardType('6271136264806203568'), 'unionpay') 62 | assert.equal(payform.parseCardType('6236265930072952775'), 'unionpay') 63 | assert.equal(payform.parseCardType('6204679475679144515'), 'unionpay') 64 | assert.equal(payform.parseCardType('6216657720782466507'), 'unionpay') 65 | 66 | assert.equal(payform.parseCardType('3530111333300000'), 'jcb') 67 | assert.equal(payform.parseCardType('3566002020360505'), 'jcb') 68 | assert.equal(payform.parseCardType('3536408073177691495'), 'jcb') 69 | 70 | assert.equal(payform.parseCardType('6062821086773091'), 'hipercard') 71 | assert.equal(payform.parseCardType('6375683647504601'), 'hipercard') 72 | assert.equal(payform.parseCardType('6370957513839696'), 'hipercard') 73 | assert.equal(payform.parseCardType('6375688248373892'), 'hipercard') 74 | assert.equal(payform.parseCardType('6012135281693108'), 'hipercard') 75 | assert.equal(payform.parseCardType('38410036464094'), 'hipercard') 76 | assert.equal(payform.parseCardType('38414050328938'), 'hipercard') 77 | 78 | describe '#cards', -> 79 | it 'should expose an array of standard card types', -> 80 | cards = payform.cards 81 | assert Array.isArray(cards) 82 | 83 | visa = card for card in cards when card.type is 'visa' 84 | assert.notEqual visa, null 85 | 86 | it 'should support new card types', -> 87 | wing = 88 | type: 'wing' 89 | pattern: /^501818/ 90 | length: [16] 91 | luhn: false 92 | 93 | payform.cards.unshift wing 94 | 95 | wingCard = '5018 1818 1818 1818' 96 | assert.equal payform.parseCardType(wingCard), 'wing' 97 | assert.equal payform.validateCardNumber(wingCard), true 98 | -------------------------------------------------------------------------------- /test/formatCardExpiry_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#formatCardExpiry', -> 6 | it 'should format month shorthand correctly', -> 7 | assert.equal payform.formatCardExpiry('4'), '04 / ' 8 | 9 | it 'should only allow numbers', -> 10 | assert.equal payform.formatCardExpiry('1d'), '1 / ' 11 | 12 | it 'should format full-width expiry correctly', -> 13 | assert.equal payform.formatCardExpiry('\uff18'), '08 / ' 14 | assert.equal payform.formatCardExpiry('\uff10\uff17\uff12\uff10\uff11\uff18'), '07 / 2018' 15 | assert.equal payform.formatCardExpiry('\uff10\uff18\uff12\uff10\uff11\uff18\uff12\uff10\uff11\uff18'), '08 / 2018' 16 | -------------------------------------------------------------------------------- /test/formatCardNumber_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#formatCardNumber', -> 6 | it 'should format cc number correctly', -> 7 | assert.equal payform.formatCardNumber('42424'), '4242 4' 8 | assert.equal payform.formatCardNumber('42424242'), '4242 4242' 9 | assert.equal payform.formatCardNumber('4242424242'), '4242 4242 42' 10 | assert.equal payform.formatCardNumber('4242424242424242'), '4242 4242 4242 4242' 11 | assert.equal payform.formatCardNumber('4242424242424242424'), '4242 4242 4242 4242 424' 12 | 13 | it 'should format amex cc number correctly', -> 14 | assert.equal payform.formatCardNumber('37828'), '3782 8' 15 | assert.equal payform.formatCardNumber('3782822'), '3782 822' 16 | assert.equal payform.formatCardNumber('378282246310'), '3782 822463 10' 17 | assert.equal payform.formatCardNumber('378282246310005'), '3782 822463 10005' 18 | 19 | it 'should format full-width cc number correctly', -> 20 | assert.equal payform.formatCardNumber('\uff14\uff12\uff14\uff12'), '4242' 21 | assert.equal payform.formatCardNumber('\uff14\uff12\uff14\uff12\uff14\uff12'), '4242 42' 22 | 23 | it 'should only allow numbers', -> 24 | assert.equal payform.formatCardNumber('42424242424242A22'), '4242 4242 4242 4222' 25 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require coffeescript/register 2 | --compilers coffee:coffeescript/register 3 | -------------------------------------------------------------------------------- /test/parseCardExpiry_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#parseCardExpiry', -> 6 | it 'should parse string expiry', -> 7 | topic = payform.parseCardExpiry '03 / 2025' 8 | assert.deepEqual topic, month: 3, year: 2025 9 | 10 | it 'should support shorthand year', -> 11 | topic = payform.parseCardExpiry '05/04' 12 | assert.deepEqual topic, month: 5, year: 2004 13 | 14 | it 'should return NaN when it cannot parse', -> 15 | topic = payform.parseCardExpiry '05/dd' 16 | assert isNaN(topic.year) 17 | -------------------------------------------------------------------------------- /test/validateCardCVC_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#validateCardCVC', -> 6 | it 'should fail if is empty', -> 7 | topic = payform.validateCardCVC '' 8 | assert.equal topic, false 9 | 10 | it 'should pass if is valid', -> 11 | topic = payform.validateCardCVC '123' 12 | assert.equal topic, true 13 | 14 | it 'should fail with non-digits', -> 15 | topic = payform.validateCardCVC '12e' 16 | assert.equal topic, false 17 | 18 | it 'should fail with less than 3 digits', -> 19 | topic = payform.validateCardCVC '12' 20 | assert.equal topic, false 21 | 22 | it 'should fail with more than 4 digits', -> 23 | topic = payform.validateCardCVC '12345' 24 | assert.equal topic, false 25 | 26 | it 'should validate a three digit number with no card type', -> 27 | topic = payform.validateCardCVC('123') 28 | assert.equal topic, true 29 | 30 | it 'should fail a three digit number with card type amex', -> 31 | topic = payform.validateCardCVC('123', 'amex') 32 | assert.equal topic, false 33 | 34 | it 'should validate a four digit number with card type amex', -> 35 | topic = payform.validateCardCVC('1234', 'amex') 36 | assert.equal topic, true 37 | 38 | it 'should validate a three digit number with card type other than amex', -> 39 | topic = payform.validateCardCVC('123', 'visa') 40 | assert.equal topic, true 41 | 42 | it 'should not validate a four digit number with a card type other than amex', -> 43 | topic = payform.validateCardCVC('1234', 'visa') 44 | assert.equal topic, false 45 | 46 | it 'should validate a four digit number with card type amex', -> 47 | topic = payform.validateCardCVC('1234', 'amex') 48 | assert.equal topic, true 49 | 50 | it 'should not validate a number larger than 4 digits', -> 51 | topic = payform.validateCardCVC('12344') 52 | assert.equal topic, false 53 | -------------------------------------------------------------------------------- /test/validateCardExpiry_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#validateCardExpiry()', -> 6 | it 'should fail expires is before the current year', -> 7 | currentTime = new Date() 8 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() - 1 9 | assert.equal topic, false 10 | 11 | it 'that expires in the current year but before current month', -> 12 | currentTime = new Date() 13 | topic = payform.validateCardExpiry currentTime.getMonth(), currentTime.getFullYear() 14 | assert.equal topic, false 15 | 16 | it 'that has an invalid month', -> 17 | currentTime = new Date() 18 | topic = payform.validateCardExpiry 13, currentTime.getFullYear() 19 | assert.equal topic, false 20 | 21 | it 'that is this year and month', -> 22 | currentTime = new Date() 23 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 24 | assert.equal topic, true 25 | 26 | it 'that is just after this month', -> 27 | # Remember - months start with 0 in JavaScript! 28 | currentTime = new Date() 29 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 30 | assert.equal topic, true 31 | 32 | it 'that is after this year', -> 33 | currentTime = new Date() 34 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() + 1 35 | assert.equal topic, true 36 | 37 | it 'that is a two-digit year', -> 38 | currentTime = new Date() 39 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, ('' + currentTime.getFullYear())[0...2] 40 | assert.equal topic, true 41 | 42 | it 'that is a two-digit year in the past (i.e. 1990s)', -> 43 | currentTime = new Date() 44 | topic = payform.validateCardExpiry currentTime.getMonth() + 1, 99 45 | assert.equal topic, false 46 | 47 | it 'that has string numbers', -> 48 | currentTime = new Date() 49 | currentTime.setFullYear(currentTime.getFullYear() + 1, currentTime.getMonth() + 2) 50 | topic = payform.validateCardExpiry currentTime.getMonth() + 1 + '', currentTime.getFullYear() + '' 51 | assert.equal topic, true 52 | 53 | it 'that has non-numbers', -> 54 | topic = payform.validateCardExpiry 'h12', '3300' 55 | assert.equal topic, false 56 | 57 | it 'should fail if year or month is NaN', -> 58 | topic = payform.validateCardExpiry '12', NaN 59 | assert.equal topic, false 60 | 61 | it 'should support year shorthand', -> 62 | assert.equal payform.validateCardExpiry('05', '20'), true 63 | -------------------------------------------------------------------------------- /test/validateCardNumber_spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | payform = require('../src/payform') 3 | 4 | describe 'payform', -> 5 | describe '#validateCardNumber()', -> 6 | it 'should fail if empty', -> 7 | topic = payform.validateCardNumber '' 8 | assert.equal topic, false 9 | 10 | it 'should fail if is a bunch of spaces', -> 11 | topic = payform.validateCardNumber ' ' 12 | assert.equal topic, false 13 | 14 | it 'should success if is valid', -> 15 | topic = payform.validateCardNumber '4242424242424242' 16 | assert.equal topic, true 17 | 18 | it 'that has dashes in it but is valid', -> 19 | topic = payform.validateCardNumber '4242-4242-4242-4242' 20 | assert.equal topic, true 21 | 22 | it 'should succeed if it has spaces in it but is valid', -> 23 | topic = payform.validateCardNumber '4242 4242 4242 4242' 24 | assert.equal topic, true 25 | 26 | it 'that does not pass the luhn checker', -> 27 | topic = payform.validateCardNumber '4242424242424241' 28 | assert.equal topic, false 29 | 30 | it 'should fail if is more than 16 digits', -> 31 | topic = payform.validateCardNumber '42424242424242424' 32 | assert.equal topic, false 33 | 34 | it 'should fail if is less than 10 digits', -> 35 | topic = payform.validateCardNumber '424242424' 36 | assert.equal topic, false 37 | 38 | it 'should fail with non-digits', -> 39 | topic = payform.validateCardNumber '4242424e42424241' 40 | assert.equal topic, false 41 | 42 | it 'should validate for all card types', -> 43 | assert(payform.validateCardNumber('4917300800000000'), 'visaelectron') 44 | 45 | assert(payform.validateCardNumber('6759649826438453'), 'maestro') 46 | assert(payform.validateCardNumber('639002000000000003'), 'maestro') 47 | assert(payform.validateCardNumber('6771798021000008'), 'maestro') 48 | assert(payform.validateCardNumber('6771830999991239'), 'maestro') 49 | assert(payform.validateCardNumber('6799990100000000019'), 'maestro') 50 | 51 | assert(payform.validateCardNumber('6007220000000004'), 'forbrugsforeningen') 52 | 53 | assert(payform.validateCardNumber('5019717010103742'), 'dankort') 54 | 55 | assert(payform.validateCardNumber('4111111111111111'), 'visa') 56 | assert(payform.validateCardNumber('4012888888881881'), 'visa') 57 | assert(payform.validateCardNumber('4222222222222'), 'visa') 58 | assert(payform.validateCardNumber('4462030000000000'), 'visa') 59 | assert(payform.validateCardNumber('4484070000000000'), 'visa') 60 | 61 | assert(payform.validateCardNumber('5105105105105100'), 'mastercard') 62 | assert(payform.validateCardNumber('5555555555554444'), 'mastercard') 63 | assert(payform.validateCardNumber('5454545454545454'), 'mastercard') 64 | assert(payform.validateCardNumber('2223000048400011'), 'mastercard') 65 | assert(payform.validateCardNumber('2720990010089800'), 'mastercard') 66 | 67 | assert(payform.validateCardNumber('378282246310005'), 'amex') 68 | assert(payform.validateCardNumber('371449635398431'), 'amex') 69 | assert(payform.validateCardNumber('378734493671000'), 'amex') 70 | 71 | assert(payform.validateCardNumber('30569309025904'), 'dinersclub') 72 | assert(payform.validateCardNumber('38520000023237'), 'dinersclub') 73 | assert(payform.validateCardNumber('36700102000000'), 'dinersclub') 74 | assert(payform.validateCardNumber('36148900647913'), 'dinersclub') 75 | 76 | assert(payform.validateCardNumber('6011111111111117'), 'discover') 77 | assert(payform.validateCardNumber('6011000990139424'), 'discover') 78 | 79 | assert(payform.validateCardNumber('6271136264806203568'), 'unionpay') 80 | assert(payform.validateCardNumber('6204679475679144515'), 'unionpay') 81 | assert(payform.validateCardNumber('6216657720782466507'), 'unionpay') 82 | 83 | assert(payform.validateCardNumber('3530111333300000'), 'jcb') 84 | assert(payform.validateCardNumber('3566002020360505'), 'jcb') 85 | assert(payform.validateCardNumber('6362970000457013'), 'elo') 86 | 87 | assert(payform.validateCardNumber('6062821086773091'), 'hipercard') 88 | assert(payform.validateCardNumber('6375683647504601'), 'hipercard') 89 | assert(payform.validateCardNumber('6370957513839696'), 'hipercard') 90 | assert(payform.validateCardNumber('6375688248373892'), 'hipercard') 91 | assert(payform.validateCardNumber('6012135281693108'), 'hipercard') 92 | assert(payform.validateCardNumber('38410036464094'), 'hipercard') 93 | assert(payform.validateCardNumber('38414050328938'), 'hipercard') 94 | --------------------------------------------------------------------------------