├── .gitignore ├── .travis.yml ├── bower.json ├── .github └── dependabot.yml ├── tests.bundle.js ├── webpack.config.js ├── .npmignore ├── karma.conf.js ├── LICENSE ├── package.json ├── example └── index.html ├── README.md ├── src └── index.coffee ├── spec └── index.spec.coffee └── dist └── payment.js /.gitignore: -------------------------------------------------------------------------------- 1 | .tmp 2 | coverage 3 | node_modules 4 | lib 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "13.1.0" 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment", 3 | "version": "2.4.0", 4 | "main": "dist/payment.js" 5 | } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /tests.bundle.js: -------------------------------------------------------------------------------- 1 | var context = require.context('./spec', true, /.+\.spec\.(js|coffee)?$/) 2 | context.keys().forEach(context) 3 | module.exports = context 4 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | resolve: { 5 | extensions: [".js", ".coffee"], 6 | }, 7 | entry: "./src/index.coffee", 8 | mode: "none", 9 | output: { 10 | path: __dirname + "/dist/", 11 | filename: "payment.js", 12 | library: "payment", 13 | libraryTarget: "var", 14 | }, 15 | module: { 16 | rules: [ 17 | { test: /\.json/, loader: "json-loader" }, 18 | { test: /\.coffee$/, loader: "coffee-loader" }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Automatically ignored per: 2 | # https://www.npmjs.org/doc/developers.html#Keeping-files-out-of-your-package 3 | # 4 | # .*.swp 5 | # ._* 6 | # .DS_Store 7 | # .git 8 | # .hg 9 | # .lock-wscript 10 | # .svn 11 | # .wafpickle-* 12 | # CVS 13 | # npm-debug.log 14 | # node_modules 15 | 16 | *.seed 17 | *.log 18 | *.csv 19 | *.dat 20 | *.out 21 | *.pid 22 | *.gz 23 | *.orig 24 | *.jql.js 25 | 26 | work 27 | build 28 | src 29 | test 30 | spec 31 | pids 32 | logs 33 | results 34 | coverage 35 | lib-cov 36 | html-report 37 | xunit.xml 38 | 39 | .project 40 | .idea 41 | .settings 42 | .iml 43 | *.sublime-workspace 44 | *.sublime-project 45 | 46 | ehthumbs.db 47 | Icon? 48 | Thumbs.db 49 | .AppleDouble 50 | .LSOverride 51 | .Spotlight-V100 52 | .Trashes 53 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | var webpackConfig = require("./webpack.config.js"); 2 | webpackConfig.devtool = "inline-source-map"; 3 | delete webpackConfig.externals; 4 | delete webpackConfig.entry; 5 | delete webpackConfig.output; 6 | 7 | module.exports = function (config) { 8 | config.set({ 9 | basePath: ".", 10 | frameworks: ["es6-shim", "chai", "mocha", "sinon"], 11 | files: ["tests.bundle.js"], 12 | preprocessors: { 13 | "tests.bundle.js": ["webpack", "sourcemap"], 14 | }, 15 | reporters: ["dots", "coverage"], 16 | port: 9876, 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | autoWatch: true, 20 | browsers: ["Chrome"], 21 | singleRun: false, 22 | concurrency: Infinity, 23 | webpack: webpackConfig, 24 | webpackMiddleware: { 25 | noInfo: false, 26 | }, 27 | coverageReporter: { 28 | type: "lcov", 29 | dir: "coverage/", 30 | }, 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Jesse Pollak 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment", 3 | "version": "2.4.5", 4 | "description": "A general purpose library for building credit card forms, validating inputs and formatting numbers. Base on jquery.payment by @stripe, but without the jQuery.", 5 | "keywords": [ 6 | "payment", 7 | "cc", 8 | "card" 9 | ], 10 | "author": "Jesse Pollak", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jessepollak/payment.git" 15 | }, 16 | "main": "lib/index.js", 17 | "scripts": { 18 | "compile": "coffee -o lib/ --compile ./src/index.coffee", 19 | "build": "npm run env NODE_ENV=production && webpack", 20 | "development": "coffee -o lib/ --compile --watch ./src/index.coffee", 21 | "preversion": "npm run test", 22 | "prepare": "npm run env NODE_ENV=production && npm run compile", 23 | "postpublish": "git push origin master && git push --tags", 24 | "test": "karma start --single-run --browsers PhantomJS" 25 | }, 26 | "devDependencies": { 27 | "browserify": "~17.0.0", 28 | "coffee-loader": "^0.7.2", 29 | "coffee-script": "~1.12", 30 | "jsdom": "~16.6", 31 | "json-loader": "^0.5.4", 32 | "karma": "^5.2.3", 33 | "karma-chai": "^0.1.0", 34 | "karma-chai-plugins": "^0.9.0", 35 | "karma-chrome-launcher": "^3.1.0", 36 | "karma-coverage": "^2.0.3", 37 | "karma-es6-shim": "^1.0.0", 38 | "karma-mocha": "^2.0.1", 39 | "karma-phantomjs-launcher": "^1.0.0", 40 | "karma-sinon": "^1.0.4", 41 | "karma-sourcemap-loader": "^0.3.8", 42 | "karma-webpack": "^4.0.2", 43 | "mocha": "^8.2.1", 44 | "mocha-webpack": "^1.1.0", 45 | "mversion": "^2.0.0", 46 | "nodemon": "^2.0.6", 47 | "phantomjs-prebuilt": "^2.1.7", 48 | "run-sequence": "~2.2.1", 49 | "tiny-lr": "^2.0.0", 50 | "vinyl-source-stream": "~2.0.0", 51 | "webpack": "^4.46.0", 52 | "webpack-cli": "^4.7.0" 53 | }, 54 | "dependencies": { 55 | "globalthis": "^1.0.2", 56 | "qj": "~2.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | 24 | 25 |
26 |

Card number formatting

27 | 28 | 29 | 30 |

Expiry formatting

31 | 32 | 33 | 34 |

CVC formatting

35 | 36 | 37 |

Restrict Numeric

38 | 39 | 40 | 41 |

42 | 43 | 44 |
45 | 46 | 47 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Payment [![Build Status](https://travis-ci.org/jessepollak/payment.svg?branch=master)](https://travis-ci.org/jessepollak/payment) ![npm](https://img.shields.io/npm/v/payment) 2 | 3 | 4 | A jQuery-free general purpose library for building credit card forms, validating inputs and formatting numbers. Heavily, heavily based on [@stripe's jquery.payment library](http://github.com/stripe/jquery.payment), but without the jQuery. 5 | 6 | For example, you can make a input act like a credit card field (with number formatting and length restriction): 7 | 8 | ``` javascript 9 | Payment.formatCardNumber(document.querySelector('input.cc-num')); 10 | ``` 11 | 12 | Then, when the payment form is submitted, you can validate the card number on the client-side: 13 | 14 | ``` javascript 15 | var valid = Payment.fns.validateCardNumber(document.querySelector('input.cc-num').value); 16 | 17 | if (!valid) { 18 | alert('Your card is not valid!'); 19 | return false; 20 | } 21 | ``` 22 | 23 | You can find a full [demo here](http://jessepollak.github.io/payment/example). 24 | 25 | Supported card types are: 26 | 27 | * Visa 28 | * MasterCard 29 | * American Express 30 | * Discover 31 | * JCB 32 | * Diners Club 33 | * Maestro 34 | * Laser 35 | * UnionPay 36 | * Elo 37 | * Hipercard 38 | * Troy 39 | 40 | ## API 41 | 42 | ### Payment.formatCardNumber(element[, maxLength]) 43 | Formats card numbers: 44 | 45 | * Includes a space between every 4 digits 46 | * Restricts input to numbers 47 | * Limits to 16 numbers 48 | * Supports American Express formatting 49 | * Adds a class of the card type (e.g. 'visa') to the input 50 | * If second parameter is specified then card length will be limited to its value (19 digits cards are not in use despite being included in specifications) 51 | 52 | Example: 53 | 54 | ``` javascript 55 | Payment.formatCardNumber(document.querySelector('input.cc-num')); 56 | ``` 57 | 58 | ### Payment.formatCardExpiry(element) 59 | 60 | Formats card expiry: 61 | 62 | * Includes a `/` between the month and year 63 | * Restricts input to numbers 64 | * Restricts length 65 | 66 | Example: 67 | 68 | ``` javascript 69 | Payment.formatCardExpiry(document.querySelector('input.cc-exp')); 70 | ``` 71 | 72 | ### Payment.formatCardCVC(element) 73 | Formats card CVC: 74 | 75 | * Restricts length to 4 numbers 76 | * Restricts input to numbers 77 | 78 | Example: 79 | 80 | ``` javascript 81 | Payment.formatCardCVC(document.querySelector('input.cc-cvc')); 82 | ``` 83 | 84 | ### Payment.restrictNumeric(element) 85 | 86 | General numeric input restriction. 87 | 88 | Example: 89 | 90 | ``` javascript 91 | Payment.restrictNumeric(document.querySelector('[data-numeric]')); 92 | ``` 93 | 94 | ### Payment.fns.validateCardNumber(number) 95 | 96 | Validates a card number: 97 | 98 | * Validates numbers 99 | * Validates Luhn algorithm 100 | * Validates length 101 | 102 | Example: 103 | 104 | ``` javascript 105 | Payment.fns.validateCardNumber('4242 4242 4242 4242'); //=> true 106 | ``` 107 | 108 | ### Payment.fns.validateCardExpiry(month, year), Payment.fns.validateCardExpiry('month / year') 109 | 110 | Validates a card expiry: 111 | 112 | * Validates numbers 113 | * Validates in the future 114 | * Supports year shorthand 115 | * Supports formatted as `formatCardExpiry` input value 116 | 117 | Example: 118 | 119 | ``` javascript 120 | Payment.fns.validateCardExpiry('05', '20'); //=> true 121 | Payment.fns.validateCardExpiry('05', '2015'); //=> true 122 | Payment.fns.validateCardExpiry('05', '05'); //=> false 123 | Payment.fns.validateCardExpiry('05 / 25'); //=> true 124 | Payment.fns.validateCardExpiry('05 / 2015'); //=> false 125 | ``` 126 | 127 | ### Payment.fns.validateCardCVC(cvc, type) 128 | 129 | Validates a card CVC: 130 | 131 | * Validates number 132 | * Validates length to 4 133 | 134 | Example: 135 | 136 | ``` javascript 137 | Payment.fns.validateCardCVC('123'); //=> true 138 | Payment.fns.validateCardCVC('123', 'amex'); //=> true 139 | Payment.fns.validateCardCVC('1234', 'amex'); //=> true 140 | Payment.fns.validateCardCVC('12344'); //=> false 141 | ``` 142 | 143 | ### Payment.fns.cardType(number) 144 | 145 | Returns a card type. Either: 146 | 147 | * `visa` 148 | * `mastercard` 149 | * `discover` 150 | * `amex` 151 | * `jcb` 152 | * `dinersclub` 153 | * `maestro` 154 | * `laser` 155 | * `unionpay` 156 | * `elo` 157 | * `hipercard` 158 | 159 | The function will return `null` if the card type can't be determined. 160 | 161 | Example: 162 | 163 | ``` javascript 164 | Payment.fns.cardType('4242 4242 4242 4242'); //=> 'visa' 165 | ``` 166 | 167 | ### Payment.fns.cardExpiryVal(string) and Payment.cardExpiryVal(el) 168 | 169 | 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`). 170 | 171 | ``` javascript 172 | Payment.fns.cardExpiryVal('03 / 2025'); //=> {month: 3: year: 2025} 173 | Payment.fns.cardExpiryVal('05 / 04'); //=> {month: 5, year: 2004} 174 | Payment.fns.cardExpiryVal(document.querySelector('input.cc-exp')) //=> {month: 4, year: 2020} 175 | ``` 176 | 177 | This function doesn't perform any validation of the month or year; use `Payment.fns.validateCardExpiry(month, year)` for that. 178 | 179 | ## Card Type functions 180 | 181 | We've provided utility functions to change which card types can be identified by Payment. 182 | 183 | ### Payment.getCardArray() 184 | 185 | Returns the array of card types. 186 | 187 | ### Payment.setCardArray(cardTypes) 188 | 189 | Overrides the array of card types with a new array. 190 | 191 | ### Payment.addToCardArray(cardType) 192 | 193 | Add a new card type to the card array. 194 | 195 | ### Payment.removeFromCardArray(cardName) 196 | 197 | Remove a card type from the card array. 198 | 199 | ## Example 200 | 201 | Look in [`./example/index.html`](example/index.html) 202 | 203 | ## Building 204 | 205 | Run `npm run build` 206 | 207 | ## Running tests 208 | 209 | Run `npm run test` 210 | 211 | ## Autocomplete recommendations 212 | 213 | We recommend you turn autocomplete on for credit card forms, except for the CVC field. You can do this by setting the `autocomplete` attribute: 214 | 215 | ``` html 216 |
217 | 218 | 219 |
220 | ``` 221 | 222 | 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. 223 | 224 | ``` html 225 | 226 | ``` 227 | 228 | Set `autocompletetype` to `cc-number` for credit card numbers, `cc-exp` for credit card expiry and `cc-csc` for the CVC (security code). 229 | 230 | ## Mobile recommendations 231 | 232 | We recommend you set the `pattern` attribute which will cause the numeric keyboard to be displayed on mobiles: 233 | 234 | ``` html 235 | 236 | ``` 237 | 238 | You may have to turn off HTML5 validation (using the `novalidate` form attribute) when using this `pattern`, as it won't match space formatting. 239 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | globalThis = require('globalthis/polyfill')() 2 | QJ = require 'qj' 3 | 4 | defaultFormat = /(\d{1,4})/g 5 | 6 | cards = [ 7 | { 8 | type: 'amex', 9 | pattern: /^3[47]/, 10 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 11 | length: [15], 12 | cvcLength: [4], 13 | luhn: true 14 | } 15 | { 16 | type: 'dankort', 17 | pattern: /^5019/, 18 | format: defaultFormat, 19 | length: [16], 20 | cvcLength: [3], 21 | luhn: true 22 | } 23 | { 24 | type: 'dinersclub', 25 | pattern: /^(36|38|30[0-5])/, 26 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 27 | length: [14], 28 | cvcLength: [3], 29 | luhn: true 30 | } 31 | { 32 | type: 'discover', 33 | pattern: /^(6011|65|64[4-9]|622)/, 34 | format: defaultFormat, 35 | length: [16], 36 | cvcLength: [3], 37 | luhn: true 38 | } 39 | { 40 | type: 'elo', 41 | pattern: /^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175|^627780|^636297|^636369|^636368|^(506699|5067[0-6]\d|50677[0-8])|^(50900\d|5090[1-9]\d|509[1-9]\d{2})|^65003[1-3]|^(65003[5-9]|65004\d|65005[0-1])|^(65040[5-9]|6504[1-3]\d)|^(65048[5-9]|65049\d|6505[0-2]\d|65053[0-8])|^(65054[1-9]|6505[5-8]\d|65059[0-8])|^(65070\d|65071[0-8])|^65072[0-7]|^(65090[1-9]|65091\d|650920)|^(65165[2-9]|6516[6-7]\d)|^(65500\d|65501\d)|^(65502[1-9]|6550[3-4]\d|65505[0-8])|^(65092[1-9]|65097[0-8])/, 42 | format: defaultFormat, 43 | length: [16], 44 | cvcLength: [3], 45 | luhn: true 46 | } 47 | { 48 | type: 'hipercard', 49 | pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/, 50 | format: defaultFormat, 51 | length: [14..19], 52 | cvcLength: [3], 53 | luhn: true 54 | } 55 | { 56 | type: 'jcb', 57 | pattern: /^(308[8-9]|309[0-3]|3094[0]{4}|309[6-9]|310[0-2]|311[2-9]|3120|315[8-9]|333[7-9]|334[0-9]|35)/, 58 | format: defaultFormat, 59 | length: [16, 19], 60 | cvcLength: [3], 61 | luhn: true 62 | } 63 | { 64 | type: 'laser', 65 | pattern: /^(6706|6771|6709)/, 66 | format: defaultFormat, 67 | length: [16..19], 68 | cvcLength: [3], 69 | luhn: true 70 | } 71 | { 72 | type: 'maestro', 73 | pattern: /^(50|5[6-9]|6007|6220|6304|6703|6708|6759|676[1-3])/, 74 | format: defaultFormat, 75 | length: [12..19], 76 | cvcLength: [3], 77 | luhn: true 78 | } 79 | { 80 | type: 'mastercard', 81 | pattern: /^(5[1-5]|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)/, 82 | format: defaultFormat, 83 | length: [16], 84 | cvcLength: [3], 85 | luhn: true 86 | } 87 | { 88 | type: 'mir', 89 | pattern: /^220[0-4][0-9][0-9]\d{10}$/, 90 | format: defaultFormat, 91 | length: [16], 92 | cvcLength: [3], 93 | luhn: true 94 | } 95 | { 96 | type: 'troy', 97 | pattern: /^9792/, 98 | format: defaultFormat, 99 | length: [16], 100 | cvcLength: [3], 101 | luhn: true 102 | } 103 | { 104 | type: 'unionpay', 105 | pattern: /^62/, 106 | format: defaultFormat, 107 | length: [16..19], 108 | cvcLength: [3], 109 | luhn: false 110 | } 111 | { 112 | type: 'visaelectron', 113 | pattern: /^4(026|17500|405|508|844|91[37])/, 114 | format: defaultFormat, 115 | length: [16], 116 | cvcLength: [3], 117 | luhn: true 118 | } 119 | { 120 | type: 'visa', 121 | pattern: /^4/, 122 | format: defaultFormat, 123 | length: [13, 16], 124 | cvcLength: [3], 125 | luhn: true 126 | } 127 | ] 128 | 129 | cardFromNumber = (num) -> 130 | num = (num + '').replace(/\D/g, '') 131 | foundCard = undefined 132 | for card in cards 133 | if match = num.match(card.pattern) 134 | # Use card with higher match length / specificity 135 | if !foundCard or match[0].length > foundCard[1][0].length 136 | foundCard = [card, match] 137 | foundCard && foundCard[0] 138 | 139 | cardFromType = (type) -> 140 | return card for card in cards when card.type is type 141 | 142 | luhnCheck = (num) -> 143 | odd = true 144 | sum = 0 145 | 146 | digits = (num + '').split('').reverse() 147 | 148 | for digit in digits 149 | digit = parseInt(digit, 10) 150 | digit *= 2 if (odd = !odd) 151 | digit -= 9 if digit > 9 152 | sum += digit 153 | 154 | sum % 10 == 0 155 | 156 | hasTextSelected = (target) -> 157 | try 158 | # If some text is selected 159 | return true if target.selectionStart? and 160 | target.selectionStart isnt target.selectionEnd 161 | 162 | # If some text is selected in IE 163 | if document?.selection?.createRange? 164 | return true if document.selection.createRange().text 165 | catch e 166 | 167 | false 168 | 169 | # Private 170 | 171 | # Format Card Number 172 | 173 | reFormatCardNumber = (e) -> 174 | setTimeout => 175 | target = e.target 176 | value = QJ.val(target) 177 | value = Payment.fns.formatCardNumber(value) 178 | cursorSafeAssignValue(target, value) 179 | QJ.trigger(target, 'change') 180 | 181 | formatCardNumber = (maxLength) -> (e) -> 182 | # Only format if input is a number 183 | if e.which > 0 184 | digit = String.fromCharCode(e.which); 185 | value = QJ.val(e.target) + digit 186 | else # android keycode not provided workaround 187 | digit = e.data; 188 | value = QJ.val(e.target) 189 | 190 | return unless /^\d+$/.test(digit) 191 | 192 | target = e.target 193 | card = cardFromNumber(value) 194 | length = (value.replace(/\D/g, '')).length 195 | 196 | upperLengths = [16] 197 | upperLengths = card.length if card 198 | upperLengths = upperLengths.filter((x) -> x <= maxLength) if maxLength 199 | 200 | # Return if an upper length has been reached 201 | for upperLength, i in upperLengths 202 | continue if length >= upperLength and upperLengths[i+1] 203 | return if length >= upperLength 204 | 205 | # Return if focus isn't at the end of the text 206 | return if hasTextSelected(target) 207 | 208 | if card && card.type is 'amex' 209 | # Amex cards are formatted differently 210 | re = /^(\d{4}|\d{4}\s\d{6})$/ 211 | else 212 | re = /(?:^|\s)(\d{4})$/ 213 | 214 | # If '4242' + 4 215 | value = value.substring(0, value.length - 1) 216 | if re.test(value) 217 | e.preventDefault() 218 | QJ.val(target, value + ' ' + digit) 219 | QJ.trigger(target, 'change') 220 | 221 | formatBackCardNumber = (e) -> 222 | target = e.target 223 | value = QJ.val(target) 224 | 225 | return if e.meta 226 | 227 | # Return unless backspacing 228 | return unless e.which is 8 229 | 230 | # Return if focus isn't at the end of the text 231 | return if hasTextSelected(target) 232 | 233 | # Remove the trailing space 234 | if /\d\s$/.test(value) 235 | e.preventDefault() 236 | QJ.val(target, value.replace(/\d\s$/, '')) 237 | QJ.trigger(target, 'change') 238 | else if /\s\d?$/.test(value) 239 | e.preventDefault() 240 | QJ.val(target, value.replace(/\s\d?$/, '')) 241 | QJ.trigger(target, 'change') 242 | 243 | # Format Expiry 244 | 245 | formatExpiry = (e) -> 246 | target = e.target 247 | # Only format if input is a number 248 | if e.which > 0 249 | digit = String.fromCharCode(e.which); 250 | val = QJ.val(target) + digit 251 | else # android keycode not provided workaround 252 | digit = e.data; 253 | val = QJ.val(target) 254 | 255 | return unless /^\d+$/.test(digit) 256 | 257 | if /^\d$/.test(val) and val not in ['0', '1'] 258 | e.preventDefault() 259 | QJ.val(target, "0#{val} / ") 260 | QJ.trigger(target, 'change') 261 | 262 | else if /^\d\d$/.test(val) 263 | e.preventDefault() 264 | QJ.val(target, "#{val} / ") 265 | QJ.trigger(target, 'change') 266 | 267 | formatMonthExpiry = (e) -> 268 | digit = String.fromCharCode(e.which) 269 | return unless /^\d+$/.test(digit) 270 | 271 | target = e.target 272 | val = QJ.val(target) + digit 273 | 274 | if /^\d$/.test(val) and val not in ['0', '1'] 275 | e.preventDefault() 276 | QJ.val(target, "0#{val}") 277 | QJ.trigger(target, 'change') 278 | 279 | else if /^\d\d$/.test(val) 280 | e.preventDefault() 281 | QJ.val(target, "#{val}") 282 | QJ.trigger(target, 'change') 283 | 284 | formatForwardExpiry = (e) -> 285 | digit = String.fromCharCode(e.which) 286 | return unless /^\d+$/.test(digit) 287 | 288 | target = e.target 289 | val = QJ.val(target) 290 | 291 | if /^\d\d$/.test(val) 292 | QJ.val(target, "#{val} / ") 293 | QJ.trigger(target, 'change') 294 | 295 | formatForwardSlash = (e) -> 296 | slash = String.fromCharCode(e.which) 297 | return unless slash is '/' 298 | 299 | target = e.target 300 | val = QJ.val(target) 301 | 302 | if /^\d$/.test(val) and val isnt '0' 303 | QJ.val(target, "0#{val} / ") 304 | QJ.trigger(target, 'change') 305 | 306 | formatBackExpiry = (e) -> 307 | # If shift+backspace is pressed 308 | return if e.metaKey 309 | 310 | target = e.target 311 | value = QJ.val(target) 312 | 313 | # Return unless backspacing 314 | return unless e.which is 8 315 | 316 | # Return if focus isn't at the end of the text 317 | return if hasTextSelected(target) 318 | 319 | # Remove the trailing space 320 | if /\d(\s|\/)+$/.test(value) 321 | e.preventDefault() 322 | QJ.val(target, value.replace(/\d(\s|\/)*$/, '')) 323 | QJ.trigger(target, 'change') 324 | else if /\s\/\s?\d?$/.test(value) 325 | e.preventDefault() 326 | QJ.val(target, value.replace(/\s\/\s?\d?$/, '')) 327 | QJ.trigger(target, 'change') 328 | 329 | # Restrictions 330 | 331 | restrictNumeric = (e) -> 332 | # Key event is for a browser shortcut 333 | return true if e.metaKey or e.ctrlKey 334 | 335 | # If keycode is a space 336 | return e.preventDefault() if e.which is 32 337 | 338 | # If keycode is a special char (WebKit) 339 | return true if e.which is 0 340 | 341 | # If char is a special char (Firefox) 342 | return true if e.which < 33 343 | 344 | input = String.fromCharCode(e.which) 345 | 346 | # Char is a number or a space 347 | return e.preventDefault() if !/[\d\s]/.test(input) 348 | 349 | restrictCardNumber = (maxLength) -> (e) -> 350 | target = e.target 351 | digit = String.fromCharCode(e.which) 352 | return unless /^\d+$/.test(digit) 353 | 354 | return if hasTextSelected(target) 355 | 356 | # Restrict number of digits 357 | value = (QJ.val(target) + digit).replace(/\D/g, '') 358 | card = cardFromNumber(value) 359 | 360 | length = 16 361 | length = card.length[card.length.length - 1] if card 362 | length = Math.min length, maxLength if maxLength 363 | 364 | e.preventDefault() unless value.length <= length 365 | 366 | restrictExpiry = (e, length) -> 367 | target = e.target 368 | digit = String.fromCharCode(e.which) 369 | return unless /^\d+$/.test(digit) 370 | 371 | return if hasTextSelected(target) 372 | 373 | value = QJ.val(target) + digit 374 | value = value.replace(/\D/g, '') 375 | 376 | return e.preventDefault() if value.length > length 377 | 378 | restrictCombinedExpiry = (e) -> 379 | return restrictExpiry e, 6 380 | 381 | restrictMonthExpiry = (e) -> 382 | return restrictExpiry e, 2 383 | 384 | restrictYearExpiry = (e) -> 385 | return restrictExpiry e, 4 386 | 387 | restrictCVC = (e) -> 388 | target = e.target 389 | digit = String.fromCharCode(e.which) 390 | return unless /^\d+$/.test(digit) 391 | 392 | return if hasTextSelected(target) 393 | 394 | val = QJ.val(target) + digit 395 | return e.preventDefault() unless val.length <= 4 396 | 397 | setCardType = (e) -> 398 | target = e.target 399 | val = QJ.val(target) 400 | cardType = Payment.fns.cardType(val) or 'unknown' 401 | 402 | unless QJ.hasClass(target, cardType) 403 | allTypes = (card.type for card in cards) 404 | 405 | QJ.removeClass target, 'unknown' 406 | QJ.removeClass target, allTypes.join(' ') 407 | 408 | QJ.addClass target, cardType 409 | QJ.toggleClass target, 'identified', cardType isnt 'unknown' 410 | QJ.trigger target, 'payment.cardType', cardType 411 | 412 | cursorSafeAssignValue = (target, value) -> 413 | selectionEnd = target.selectionEnd 414 | QJ.val(target, value) 415 | target.selectionEnd = selectionEnd if selectionEnd 416 | 417 | # Public 418 | 419 | class Payment 420 | @J: QJ 421 | @fns: 422 | cardExpiryVal: (value) -> 423 | value = value.replace(/\s/g, '') 424 | [month, year] = value.split('/', 2) 425 | 426 | # Allow for year shortcut 427 | if year?.length is 2 and /^\d+$/.test(year) 428 | prefix = (new Date).getFullYear() 429 | prefix = prefix.toString()[0..1] 430 | year = prefix + year 431 | 432 | month = parseInt(month, 10) 433 | year = parseInt(year, 10) 434 | 435 | month: month, year: year 436 | validateCardNumber: (num) -> 437 | num = (num + '').replace(/\s+|-/g, '') 438 | return false unless /^\d+$/.test(num) 439 | 440 | card = cardFromNumber(num) 441 | return false unless card 442 | 443 | num.length in card.length and 444 | (card.luhn is false or luhnCheck(num)) 445 | validateCardExpiry: (month, year) -> 446 | # Allow passing an object 447 | if typeof month is 'object' and 'month' of month 448 | {month, year} = month 449 | else if typeof month is 'string' and '/' in month 450 | {month, year} = Payment.fns.cardExpiryVal(month) 451 | 452 | return false unless month and year 453 | 454 | month = QJ.trim(month) 455 | year = QJ.trim(year) 456 | 457 | return false unless /^\d+$/.test(month) 458 | return false unless /^\d+$/.test(year) 459 | 460 | month = parseInt(month, 10) 461 | 462 | return false unless month and month <= 12 463 | 464 | if year.length is 2 465 | prefix = (new Date).getFullYear() 466 | prefix = prefix.toString()[0..1] 467 | year = prefix + year 468 | 469 | expiry = new Date(year, month) 470 | currentTime = new Date 471 | 472 | # Months start from 0 in JavaScript 473 | expiry.setMonth(expiry.getMonth() - 1) 474 | 475 | # The cc expires at the end of the month, 476 | # so we need to make the expiry the first day 477 | # of the month after 478 | expiry.setMonth(expiry.getMonth() + 1, 1) 479 | 480 | expiry > currentTime 481 | validateCardCVC: (cvc, type) -> 482 | cvc = QJ.trim(cvc) 483 | return false unless /^\d+$/.test(cvc) 484 | 485 | if type and cardFromType(type) 486 | # Check against a explicit card type 487 | cvc.length in cardFromType(type)?.cvcLength 488 | else 489 | # Check against all types 490 | cvc.length >= 3 and cvc.length <= 4 491 | cardType: (num) -> 492 | return null unless num 493 | cardFromNumber(num)?.type or null 494 | formatCardNumber: (num) -> 495 | card = cardFromNumber(num) 496 | return num unless card 497 | 498 | upperLength = card.length[card.length.length - 1] 499 | 500 | num = num.replace(/\D/g, '') 501 | num = num.slice(0, upperLength) 502 | 503 | if card.format.global 504 | num.match(card.format)?.join(' ') 505 | else 506 | groups = card.format.exec(num) 507 | return unless groups? 508 | groups.shift() 509 | groups = groups.filter((n) -> n) # Filter empty groups 510 | groups.join(' ') 511 | @restrictNumeric: (el) -> 512 | QJ.on el, 'keypress', restrictNumeric 513 | QJ.on el, 'input', restrictNumeric 514 | @cardExpiryVal: (el) -> 515 | Payment.fns.cardExpiryVal(QJ.val(el)) 516 | @formatCardCVC: (el) -> 517 | Payment.restrictNumeric el 518 | QJ.on el, 'keypress', restrictCVC 519 | QJ.on el, 'input', restrictCVC 520 | el 521 | @formatCardExpiry: (el) -> 522 | Payment.restrictNumeric el 523 | if el.length && el.length == 2 524 | [month, year] = el 525 | @formatCardExpiryMultiple month, year 526 | else 527 | QJ.on el, 'keypress', restrictCombinedExpiry 528 | QJ.on el, 'keypress', formatExpiry 529 | QJ.on el, 'keypress', formatForwardSlash 530 | QJ.on el, 'keypress', formatForwardExpiry 531 | QJ.on el, 'keydown', formatBackExpiry 532 | QJ.on el, 'input', formatExpiry 533 | el 534 | @formatCardExpiryMultiple: (month, year) -> 535 | QJ.on month, 'keypress', restrictMonthExpiry 536 | QJ.on month, 'keypress', formatMonthExpiry 537 | QJ.on month, 'input', formatMonthExpiry 538 | QJ.on year, 'keypress', restrictYearExpiry 539 | QJ.on year, 'input', restrictYearExpiry 540 | @formatCardNumber: (el, maxLength) -> 541 | Payment.restrictNumeric el 542 | QJ.on el, 'keypress', restrictCardNumber(maxLength) 543 | QJ.on el, 'keypress', formatCardNumber(maxLength) 544 | QJ.on el, 'keydown', formatBackCardNumber 545 | QJ.on el, 'keyup blur', setCardType 546 | QJ.on el, 'blur', reFormatCardNumber 547 | QJ.on el, 'paste', reFormatCardNumber 548 | QJ.on el, 'input', formatCardNumber(maxLength) 549 | el 550 | @getCardArray: -> return cards 551 | @setCardArray: (cardArray) -> 552 | cards = cardArray 553 | return true 554 | @addToCardArray: (cardObject) -> 555 | cards.push(cardObject) 556 | @removeFromCardArray: (type) -> 557 | for key, value of cards 558 | if value.type == type 559 | cards.splice(key, 1) 560 | return true 561 | 562 | module.exports = Payment 563 | globalThis.Payment = Payment 564 | -------------------------------------------------------------------------------- /spec/index.spec.coffee: -------------------------------------------------------------------------------- 1 | assert = require('assert') 2 | 3 | Payment = require('../src/index') 4 | QJ = require('qj') 5 | 6 | describe 'payment', -> 7 | describe 'Validating a card number', -> 8 | it 'should fail if empty', -> 9 | topic = Payment.fns.validateCardNumber '' 10 | assert.equal topic, false 11 | 12 | it 'should fail if is a bunch of spaces', -> 13 | topic = Payment.fns.validateCardNumber ' ' 14 | assert.equal topic, false 15 | 16 | it 'should success if is valid', -> 17 | topic = Payment.fns.validateCardNumber '4242424242424242' 18 | assert.equal topic, true 19 | 20 | it 'that has dashes in it but is valid', -> 21 | topic = Payment.fns.validateCardNumber '4242-4242-4242-4242' 22 | assert.equal topic, true 23 | 24 | it 'should succeed if it has spaces in it but is valid', -> 25 | topic = Payment.fns.validateCardNumber '4242 4242 4242 4242' 26 | assert.equal topic, true 27 | 28 | it 'that does not pass the luhn checker', -> 29 | topic = Payment.fns.validateCardNumber '4242424242424241' 30 | assert.equal topic, false 31 | 32 | it 'should fail if is more than 16 digits', -> 33 | topic = Payment.fns.validateCardNumber '42424242424242424' 34 | assert.equal topic, false 35 | 36 | it 'should fail if is less than 10 digits', -> 37 | topic = Payment.fns.validateCardNumber '424242424' 38 | assert.equal topic, false 39 | 40 | it 'should fail with non-digits', -> 41 | topic = Payment.fns.validateCardNumber '4242424e42424241' 42 | assert.equal topic, false 43 | 44 | describe 'validating card types', -> 45 | it 'should validate amex card types', -> 46 | assert(Payment.fns.validateCardNumber('378282246310005'), 'amex') 47 | assert(Payment.fns.validateCardNumber('371449635398431'), 'amex') 48 | assert(Payment.fns.validateCardNumber('378734493671000'), 'amex') 49 | it 'should validate dankort card types', -> 50 | assert(Payment.fns.validateCardNumber('5019123456789013'), 'dankort') 51 | it 'should validate dinersclub card types', -> 52 | assert(Payment.fns.validateCardNumber('30569309025904'), 'dinersclub') 53 | assert(Payment.fns.validateCardNumber('38520000023237'), 'dinersclub') 54 | it 'should validate discover card types', -> 55 | assert(Payment.fns.validateCardNumber('6011111111111117'), 'discover') 56 | assert(Payment.fns.validateCardNumber('6011000990139424'), 'discover') 57 | it 'should validate jcb card types', -> 58 | assert(Payment.fns.validateCardNumber('3530111333300000'), 'jcb') 59 | assert(Payment.fns.validateCardNumber('3566002020360505'), 'jcb') 60 | assert(Payment.fns.validateCardNumber('3518914107195001'), 'jcb') 61 | it 'should validate mastercard card types', -> 62 | assert(Payment.fns.validateCardNumber('5555555555554444'), 'mastercard') 63 | assert(Payment.fns.validateCardNumber('2221000010000015'), 'mastercard') 64 | it 'should validate visa card types', -> 65 | assert(Payment.fns.validateCardNumber('4111111111111111'), 'visa') 66 | assert(Payment.fns.validateCardNumber('4012888888881881'), 'visa') 67 | assert(Payment.fns.validateCardNumber('4222222222222'), 'visa') 68 | it 'should validate visaelectron card types', -> 69 | assert(Payment.fns.validateCardNumber('4917300800000000'), 'visaelectron') 70 | it 'should validate unionpay card types', -> 71 | assert(Payment.fns.validateCardNumber('6271136264806203568'), 'unionpay') 72 | assert(Payment.fns.validateCardNumber('6236265930072952775'), 'unionpay') 73 | assert(Payment.fns.validateCardNumber('6204679475679144515'), 'unionpay') 74 | assert(Payment.fns.validateCardNumber('6216657720782466507'), 'unionpay') 75 | it 'should validate maestro card types', -> 76 | assert(Payment.fns.validateCardNumber('6759649826438453'), 'maestro') 77 | assert(Payment.fns.validateCardNumber('6759 4111 0000 0008'), 'maestro') 78 | assert(Payment.fns.validateCardNumber('6759 6498 2643 8453'), 'maestro') 79 | assert(Payment.fns.validateCardNumber('5854 4424 5645 0444'), 'maestro') 80 | it 'should validate hipercard card types', -> 81 | assert(Payment.fns.validateCardNumber('6062821086773091'), 'hipercard') 82 | assert(Payment.fns.validateCardNumber('6375683647504601'), 'hipercard') 83 | assert(Payment.fns.validateCardNumber('6370957513839696'), 'hipercard') 84 | assert(Payment.fns.validateCardNumber('6375688248373892'), 'hipercard') 85 | assert(Payment.fns.validateCardNumber('6012135281693108'), 'hipercard') 86 | assert(Payment.fns.validateCardNumber('38410036464094'), 'hipercard') 87 | assert(Payment.fns.validateCardNumber('38414050328938'), 'hipercard') 88 | it 'should validate troy card types', -> 89 | assert(Payment.fns.validateCardNumber('9792817609140009'), 'troy') 90 | assert(Payment.fns.validateCardNumber('9792817609140132'), 'troy') 91 | 92 | describe 'Validating a CVC', -> 93 | it 'should fail if is empty', -> 94 | topic = Payment.fns.validateCardCVC '' 95 | assert.equal topic, false 96 | 97 | it 'should pass if is valid', -> 98 | topic = Payment.fns.validateCardCVC '123' 99 | assert.equal topic, true 100 | 101 | it 'should fail with non-digits', -> 102 | topic = Payment.fns.validateCardNumber '12e' 103 | assert.equal topic, false 104 | 105 | it 'should fail with less than 3 digits', -> 106 | topic = Payment.fns.validateCardNumber '12' 107 | assert.equal topic, false 108 | 109 | it 'should fail with more than 4 digits', -> 110 | topic = Payment.fns.validateCardNumber '12345' 111 | assert.equal topic, false 112 | 113 | describe 'Validating an expiration date', -> 114 | it 'should fail expires is before the current year', -> 115 | currentTime = new Date() 116 | topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() - 1 117 | assert.equal topic, false 118 | 119 | it 'that expires in the current year but before current month', -> 120 | currentTime = new Date() 121 | topic = Payment.fns.validateCardExpiry currentTime.getMonth(), currentTime.getFullYear() 122 | assert.equal topic, false 123 | 124 | it 'that has an invalid month', -> 125 | currentTime = new Date() 126 | topic = Payment.fns.validateCardExpiry 0, currentTime.getFullYear() 127 | assert.equal topic, false 128 | 129 | it 'that has an invalid month', -> 130 | currentTime = new Date() 131 | topic = Payment.fns.validateCardExpiry 13, currentTime.getFullYear() 132 | assert.equal topic, false 133 | 134 | it 'that is this year and month', -> 135 | currentTime = new Date() 136 | topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 137 | assert.equal topic, true 138 | 139 | it 'that is just after this month', -> 140 | # Remember - months start with 0 in JavaScript! 141 | currentTime = new Date() 142 | topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() 143 | assert.equal topic, true 144 | 145 | it 'that is after this year', -> 146 | currentTime = new Date() 147 | topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() + 1 148 | assert.equal topic, true 149 | 150 | it 'that has string numbers', -> 151 | currentTime = new Date() 152 | currentTime.setFullYear(currentTime.getFullYear() + 1, currentTime.getMonth() + 2) 153 | topic = Payment.fns.validateCardExpiry currentTime.getMonth() + '', currentTime.getFullYear() + '' 154 | assert.equal topic, true 155 | 156 | it 'should validate a string with two digits month and year delimited by slash', -> 157 | topic = Payment.fns.validateCardExpiry '03 / 25' 158 | assert.equal topic, true 159 | 160 | it 'should validate a string with two digits month and four digits year delimited by slash', -> 161 | topic = Payment.fns.validateCardExpiry '03 / 2025' 162 | assert.equal topic, true 163 | 164 | it 'should fail if expires is string mm/yyyy where the year is before the current year', -> 165 | topic = Payment.fns.validateCardExpiry '03 / 205' 166 | assert.equal topic, false 167 | 168 | it 'that has non-numbers', -> 169 | topic = Payment.fns.validateCardExpiry 'h12', '3300' 170 | assert.equal topic, false 171 | 172 | it 'should fail if year or month is NaN', -> 173 | topic = Payment.fns.validateCardExpiry '12', NaN 174 | assert.equal topic, false 175 | 176 | it 'should support year shorthand', -> 177 | assert.equal Payment.fns.validateCardExpiry('05', '25'), true 178 | 179 | describe 'Validating a CVC number', -> 180 | it 'should validate a three digit number with no card type', -> 181 | topic = Payment.fns.validateCardCVC('123') 182 | assert.equal topic, true 183 | 184 | it 'should not validate a three digit number with card type amex', -> 185 | topic = Payment.fns.validateCardCVC('123', 'amex') 186 | assert.equal topic, false 187 | 188 | it 'should validate a three digit number with card type other than amex', -> 189 | topic = Payment.fns.validateCardCVC('123', 'visa') 190 | assert.equal topic, true 191 | 192 | it 'should not validate a four digit number with a card type other than amex', -> 193 | topic = Payment.fns.validateCardCVC('1234', 'visa') 194 | assert.equal topic, false 195 | 196 | it 'should validate a four digit number with card type amex', -> 197 | topic = Payment.fns.validateCardCVC('1234', 'amex') 198 | assert.equal topic, true 199 | 200 | it 'should not validate a number larger than 4 digits', -> 201 | topic = Payment.fns.validateCardCVC('12344') 202 | assert.equal topic, false 203 | 204 | describe 'Parsing an expiry value', -> 205 | it 'should parse string expiry', -> 206 | topic = Payment.fns.cardExpiryVal('03 / 2025') 207 | assert.deepEqual topic, month: 3, year: 2025 208 | 209 | it 'should support shorthand year', -> 210 | topic = Payment.fns.cardExpiryVal('05/04') 211 | assert.deepEqual topic, month: 5, year: 2004 212 | 213 | it 'should return NaN when it cannot parse', -> 214 | topic = Payment.fns.cardExpiryVal('05/dd') 215 | assert isNaN(topic.year) 216 | 217 | describe 'Getting a card type', -> 218 | it 'should return Visa that begins with 40', -> 219 | topic = Payment.fns.cardType '4012121212121212' 220 | assert.equal topic, 'visa' 221 | 222 | it 'that begins with 5 should return MasterCard', -> 223 | topic = Payment.fns.cardType '5555555555554444' 224 | assert.equal topic, 'mastercard' 225 | 226 | it 'that begins with 2 should return MasterCard', -> 227 | topic = Payment.fns.cardType '2221000010000015' 228 | assert.equal topic, 'mastercard' 229 | 230 | it 'that begins with 34 should return American Express', -> 231 | topic = Payment.fns.cardType '3412121212121212' 232 | assert.equal topic, 'amex' 233 | 234 | it 'that begins with 457393 should return Elo', -> 235 | topic = Payment.fns.cardType '4573931212121212' 236 | assert.equal topic, 'elo' 237 | 238 | it 'should not miscategorize cards with 5067 in them as elo', -> 239 | topic = Payment.fns.cardType '4012506712121212' 240 | assert.equal topic, 'visa' 241 | 242 | it 'that begins with 9792 should return Troy', -> 243 | topic = Payment.fns.cardType '9792123456789012' 244 | assert.equal topic, 'troy' 245 | 246 | it 'should return hipercard type', -> 247 | assert.equal (Payment.fns.cardType '384100'), 'hipercard' 248 | assert.equal (Payment.fns.cardType '384140'), 'hipercard' 249 | assert.equal (Payment.fns.cardType '384160'), 'hipercard' 250 | assert.equal (Payment.fns.cardType '6062'), 'hipercard' 251 | assert.equal (Payment.fns.cardType '6012'), 'hipercard' 252 | 253 | it 'should not return hipercard type', -> 254 | topic = Payment.fns.cardType '6011' 255 | assert.equal topic, 'discover' 256 | 257 | it 'that is not numbers should return null', -> 258 | topic = Payment.fns.cardType 'aoeu' 259 | assert.equal topic, null 260 | 261 | it 'that has unrecognized beginning numbers should return null', -> 262 | topic = Payment.fns.cardType 'aoeu' 263 | assert.equal topic, null 264 | 265 | describe 'formatCardNumber', -> 266 | it 'should restrict non-number characters', -> 267 | number = document.createElement('input') 268 | number.type = 'text' 269 | QJ.val number, '4242' 270 | 271 | Payment.formatCardNumber(number) 272 | 273 | ev = document.createEvent "HTMLEvents" 274 | ev.initEvent "keypress", true, true 275 | ev.eventName = "keypress" 276 | ev.which = "a".charCodeAt(0) 277 | 278 | number.dispatchEvent(ev) 279 | 280 | assert.equal QJ.val(number), '4242' 281 | 282 | it 'should restrict characters after the length', -> 283 | number = document.createElement('input') 284 | number.type = 'text' 285 | QJ.val number, '4242 4242 4242 4242 424' 286 | 287 | Payment.formatCardNumber(number) 288 | 289 | ev = document.createEvent "HTMLEvents" 290 | ev.initEvent "keypress", true, true 291 | ev.eventName = "keypress" 292 | ev.which = "4".charCodeAt(0) 293 | 294 | number.dispatchEvent(ev) 295 | 296 | assert.equal QJ.val(number), '4242 4242 4242 4242 424' 297 | it 'should restrict characters when a generic card number is 16 characters', -> 298 | number = document.createElement('input') 299 | number.type = 'text' 300 | QJ.val(number, '0000 0000 0000 0000') 301 | 302 | Payment.formatCardNumber(number) 303 | 304 | ev = document.createEvent "HTMLEvents" 305 | ev.initEvent "keypress", true, true 306 | ev.eventName = "keypress" 307 | ev.which = "0".charCodeAt(0) 308 | 309 | number.dispatchEvent(ev) 310 | 311 | assert.equal QJ.val(number), '0000 0000 0000 0000' 312 | it 'should restrict characters when a visa card number is greater than 16 characters', -> 313 | number = document.createElement('input') 314 | number.type = 'text' 315 | QJ.val(number, '4000 0000 0000 0000 030') 316 | 317 | Payment.formatCardNumber(number) 318 | 319 | ev = document.createEvent "HTMLEvents" 320 | ev.initEvent "keypress", true, true 321 | ev.eventName = "keypress" 322 | ev.which = "0".charCodeAt(0) 323 | 324 | number.dispatchEvent(ev) 325 | 326 | assert.equal QJ.val(number), '4000 0000 0000 0000 030' 327 | 328 | it 'should restrict characters for visa when a `maxLength` parameter is set despite card length can be 16', -> 329 | number = document.createElement('input') 330 | number.type = 'text' 331 | QJ.val(number, '4242 4242 4242 4242') 332 | 333 | Payment.formatCardNumber(number, 16) 334 | 335 | ev = document.createEvent "HTMLEvents" 336 | ev.initEvent "keypress", true, true 337 | ev.eventName = "keypress" 338 | ev.which = "0".charCodeAt(0) 339 | 340 | number.dispatchEvent(ev) 341 | 342 | assert.equal QJ.val(number), '4242 4242 4242 4242' 343 | it 'should restrict characters correctly if `maxLength` exceeds max length for card', -> 344 | number = document.createElement('input') 345 | number.type = 'text' 346 | QJ.val(number, '3472 486270 35790') 347 | 348 | Payment.formatCardNumber(number, 16) 349 | 350 | ev = document.createEvent "HTMLEvents" 351 | ev.initEvent "keypress", true, true 352 | ev.eventName = "keypress" 353 | ev.which = "0".charCodeAt(0) 354 | 355 | number.dispatchEvent(ev) 356 | 357 | assert.equal QJ.val(number), '3472 486270 35790' 358 | it 'should format cc number correctly', -> 359 | number = document.createElement('input') 360 | number.type = 'text' 361 | QJ.val(number, '4242') 362 | 363 | Payment.formatCardNumber(number) 364 | 365 | ev = document.createEvent "HTMLEvents" 366 | ev.initEvent "keypress", true, true 367 | ev.eventName = "keypress" 368 | ev.which = "4".charCodeAt(0) 369 | 370 | number.dispatchEvent(ev) 371 | 372 | assert.equal QJ.val(number), '4242 4' 373 | 374 | it 'should remove a trailing space before a number on backspace ', -> 375 | number = document.createElement('input') 376 | number.type = 'text' 377 | QJ.val(number, '4242 ') 378 | number.selectionStart = 5 379 | number.selectionEnd = 5 380 | 381 | Payment.formatCardNumber(number) 382 | 383 | ev = document.createEvent "HTMLEvents" 384 | ev.initEvent "keydown", true, true 385 | ev.eventName = "keydown" 386 | ev.which = 8 387 | 388 | number.dispatchEvent(ev) 389 | 390 | assert.equal QJ.val(number), '424' 391 | it 'should remove the space after a number being deleted on a backspace', -> 392 | number = document.createElement('input') 393 | number.type = 'text' 394 | QJ.val(number, '4242 5') 395 | number.selectionStart = 6 396 | number.selectionEnd = 6 397 | 398 | Payment.formatCardNumber(number) 399 | 400 | ev = document.createEvent "HTMLEvents" 401 | ev.initEvent "keydown", true, true 402 | ev.eventName = "keydown" 403 | ev.which = 8 404 | 405 | number.dispatchEvent(ev) 406 | 407 | assert.equal QJ.val(number), '4242' 408 | it 'should set the card type correctly', -> 409 | number = document.createElement('input') 410 | number.type = 'text' 411 | QJ.val(number, '4') 412 | 413 | Payment.formatCardNumber(number) 414 | 415 | ev = document.createEvent "HTMLEvents" 416 | ev.initEvent "keyup", true, true 417 | ev.eventName = "keyup" 418 | number.dispatchEvent(ev) 419 | 420 | assert QJ.hasClass(number, 'visa') 421 | assert QJ.hasClass(number, 'identified') 422 | 423 | QJ.val(number, '') 424 | 425 | ev = document.createEvent "HTMLEvents" 426 | ev.initEvent "keyup", true, true 427 | ev.eventName = "keyup" 428 | number.dispatchEvent(ev) 429 | 430 | # JSDom doesn't support custom events right now, so this testing 431 | # doesn't work :( 432 | # 433 | # eventCalled = false 434 | # number.addEventListener 'payment.cardType', -> 435 | # console.log 'hiya' 436 | # eventCalled = true 437 | # assert eventCalled 438 | 439 | assert QJ.hasClass(number, 'unknown') 440 | assert !QJ.hasClass(number, 'identified') 441 | it 'should format correctly on paste', -> 442 | number = document.createElement('input') 443 | number.type = 'text' 444 | Payment.formatCardNumber(number) 445 | 446 | QJ.val(number, '42424') 447 | 448 | ev = document.createEvent "HTMLEvents" 449 | ev.initEvent "paste", true, true 450 | ev.eventName = "paste" 451 | 452 | number.dispatchEvent(ev) 453 | 454 | # must setTimeout because paste event handling is 455 | # done in a setTimeout 456 | setTimeout -> 457 | assert.equal QJ.val(number), '4242 4' 458 | 459 | describe 'formatCardExpiry', -> 460 | it 'should add a slash after two numbers', -> 461 | expiry = document.createElement('input') 462 | expiry.type = "text" 463 | QJ.val(expiry, '1') 464 | 465 | Payment.formatCardExpiry(expiry) 466 | 467 | ev = document.createEvent "HTMLEvents" 468 | ev.initEvent "keypress", true, true 469 | ev.eventName = "keypress" 470 | ev.which = "1".charCodeAt(0) 471 | 472 | expiry.dispatchEvent(ev) 473 | 474 | assert.equal QJ.val(expiry), '11 / ' 475 | it 'should format add a 0 and slash to a number > 1 correctly', -> 476 | expiry = document.createElement('input') 477 | expiry.type = "text" 478 | 479 | Payment.formatCardExpiry(expiry) 480 | 481 | ev = document.createEvent "HTMLEvents" 482 | ev.initEvent "keypress", true, true 483 | ev.eventName = "keypress" 484 | ev.which = "4".charCodeAt(0) 485 | 486 | expiry.dispatchEvent(ev) 487 | 488 | assert.equal QJ.val(expiry), '04 / ' 489 | it 'should format add a 0 to a number > 1 in the month input correctly', -> 490 | month = document.createElement('input') 491 | month.type = "text" 492 | year = document.createElement('input') 493 | year.type = "text" 494 | 495 | Payment.formatCardExpiry([month, year]) 496 | 497 | ev = document.createEvent "HTMLEvents" 498 | ev.initEvent "keypress", true, true 499 | ev.eventName = "keypress" 500 | ev.which = "4".charCodeAt(0) 501 | 502 | month.dispatchEvent(ev) 503 | 504 | assert.equal QJ.val(month), '04' 505 | it 'should format forward slash shorthand correctly', -> 506 | expiry = document.createElement('input') 507 | expiry.type = "text" 508 | QJ.val(expiry, '1') 509 | 510 | Payment.formatCardExpiry(expiry) 511 | 512 | ev = document.createEvent "HTMLEvents" 513 | ev.initEvent "keypress", true, true 514 | ev.eventName = "keypress" 515 | ev.which = "/".charCodeAt(0) 516 | 517 | expiry.dispatchEvent(ev) 518 | 519 | assert.equal QJ.val(expiry), '01 / ' 520 | 521 | it 'should only allow numbers', -> 522 | expiry = document.createElement('input') 523 | expiry.type = "text" 524 | QJ.val(expiry, '1') 525 | 526 | Payment.formatCardExpiry(expiry) 527 | 528 | ev = document.createEvent "HTMLEvents" 529 | ev.initEvent "keypress", true, true 530 | ev.eventName = "keypress" 531 | ev.which = "d".charCodeAt(0) 532 | 533 | expiry.dispatchEvent(ev) 534 | 535 | assert.equal QJ.val(expiry), '1' 536 | it 'should remove spaces trailing space and / after removing a number', -> 537 | expiry = document.createElement('input') 538 | expiry.type = "text" 539 | QJ.val(expiry, '12 / 1') 540 | 541 | Payment.formatCardExpiry(expiry) 542 | 543 | ev = document.createEvent "HTMLEvents" 544 | ev.initEvent "keydown", true, true 545 | ev.eventName = "keydown" 546 | ev.which = 8 # backspace 547 | 548 | expiry.dispatchEvent(ev) 549 | 550 | assert.equal QJ.val(expiry), '12' 551 | 552 | it 'should a number after removing a space and a /', -> 553 | expiry = document.createElement('input') 554 | expiry.type = "text" 555 | QJ.val(expiry, '12 / ') 556 | 557 | Payment.formatCardExpiry(expiry) 558 | 559 | ev = document.createEvent "HTMLEvents" 560 | ev.initEvent "keydown", true, true 561 | ev.eventName = "keydown" 562 | ev.which = 8 # backspace 563 | 564 | expiry.dispatchEvent(ev) 565 | 566 | assert.equal QJ.val(expiry), '1' 567 | it 'should restrict combined expiry fields to length <= 6 digits', -> 568 | expiry = document.createElement('input') 569 | expiry.type = "text" 570 | QJ.val(expiry, '12 / 2018') 571 | 572 | Payment.formatCardExpiry(expiry) 573 | 574 | ev = document.createEvent "HTMLEvents" 575 | ev.initEvent "keypress", true, true 576 | ev.eventName = "keypress" 577 | ev.which = "1".charCodeAt(0) 578 | 579 | expiry.dispatchEvent(ev) 580 | 581 | assert.equal QJ.val(expiry), '12 / 2018' 582 | it 'should restrict month expiry fields to length <= 2 digits', -> 583 | month = document.createElement('input') 584 | month.type = "text" 585 | QJ.val(month, '12') 586 | 587 | year = document.createElement('input') 588 | year.type = "text" 589 | QJ.val(year, '2018') 590 | 591 | Payment.formatCardExpiryMultiple(month, year) 592 | 593 | ev = document.createEvent "HTMLEvents" 594 | ev.initEvent "keypress", true, true 595 | ev.eventName = "keypress" 596 | ev.which = "1".charCodeAt(0) 597 | 598 | month.dispatchEvent(ev) 599 | 600 | assert.equal QJ.val(month), '12' 601 | it 'should restrict year expiry fields to length <= 4 digits', -> 602 | month = document.createElement('input') 603 | month.type = "text" 604 | QJ.val(month, '12') 605 | 606 | year = document.createElement('input') 607 | year.type = "text" 608 | QJ.val(year, '2018') 609 | 610 | Payment.formatCardExpiryMultiple(month, year) 611 | 612 | ev = document.createEvent "HTMLEvents" 613 | ev.initEvent "keypress", true, true 614 | ev.eventName = "keypress" 615 | ev.which = "1".charCodeAt(0) 616 | 617 | year.dispatchEvent(ev) 618 | 619 | assert.equal QJ.val(year), '2018' 620 | describe 'formatCVC', -> 621 | it 'should allow only numbers', -> 622 | cvc = document.createElement('input') 623 | cvc.type = "text" 624 | QJ.val(cvc, '1') 625 | 626 | Payment.formatCardCVC(cvc) 627 | 628 | ev = document.createEvent "HTMLEvents" 629 | ev.initEvent "keypress", true, true 630 | ev.eventName = "keypress" 631 | ev.which = "d".charCodeAt(0) 632 | 633 | cvc.dispatchEvent(ev) 634 | 635 | assert.equal QJ.val(cvc), '1' 636 | it 'should restrict to length <= 4', -> 637 | cvc = document.createElement('input') 638 | cvc.type = "text" 639 | QJ.val(cvc, '1234') 640 | 641 | Payment.formatCardCVC(cvc) 642 | 643 | ev = document.createEvent "HTMLEvents" 644 | ev.initEvent "keypress", true, true 645 | ev.eventName = "keypress" 646 | ev.which = "1".charCodeAt(0) 647 | 648 | cvc.dispatchEvent(ev) 649 | 650 | assert.equal QJ.val(cvc), '1234' 651 | 652 | describe 'Adding and removing cards', -> 653 | cards = undefined 654 | 655 | # Because Payment uses a global card array, we need to 656 | # ensure it gets reset after each test 657 | beforeEach -> 658 | cards = Payment.getCardArray().slice() 659 | afterEach -> 660 | Payment.setCardArray(cards) 661 | 662 | it 'should remove card by type', -> 663 | Payment.removeFromCardArray('amex') 664 | assert.equal 14, Payment.getCardArray().length 665 | 666 | it 'should add custom card', -> 667 | Payment.addToCardArray({ 668 | type: 'sorocred' 669 | pattern: /^6278/ 670 | format: /(\d{1,4})/g 671 | length: [16] 672 | cvcLength: [3] 673 | luhn: true 674 | }) 675 | assert.equal 16, Payment.getCardArray().length 676 | -------------------------------------------------------------------------------- /dist/payment.js: -------------------------------------------------------------------------------- 1 | var payment = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = ""; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 0); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */ 90 | /***/ (function(module, exports, __webpack_require__) { 91 | 92 | var Payment, QJ, cardFromNumber, cardFromType, cards, cursorSafeAssignValue, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, formatMonthExpiry, globalThis, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictCombinedExpiry, restrictExpiry, restrictMonthExpiry, restrictNumeric, restrictYearExpiry, setCardType, 93 | 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; }; 94 | 95 | globalThis = __webpack_require__(1)(); 96 | 97 | QJ = __webpack_require__(4); 98 | 99 | defaultFormat = /(\d{1,4})/g; 100 | 101 | cards = [ 102 | { 103 | type: 'amex', 104 | pattern: /^3[47]/, 105 | format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/, 106 | length: [15], 107 | cvcLength: [4], 108 | luhn: true 109 | }, { 110 | type: 'dankort', 111 | pattern: /^5019/, 112 | format: defaultFormat, 113 | length: [16], 114 | cvcLength: [3], 115 | luhn: true 116 | }, { 117 | type: 'dinersclub', 118 | pattern: /^(36|38|30[0-5])/, 119 | format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/, 120 | length: [14], 121 | cvcLength: [3], 122 | luhn: true 123 | }, { 124 | type: 'discover', 125 | pattern: /^(6011|65|64[4-9]|622)/, 126 | format: defaultFormat, 127 | length: [16], 128 | cvcLength: [3], 129 | luhn: true 130 | }, { 131 | type: 'elo', 132 | pattern: /^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175|^627780|^636297|^636369|^636368|^(506699|5067[0-6]\d|50677[0-8])|^(50900\d|5090[1-9]\d|509[1-9]\d{2})|^65003[1-3]|^(65003[5-9]|65004\d|65005[0-1])|^(65040[5-9]|6504[1-3]\d)|^(65048[5-9]|65049\d|6505[0-2]\d|65053[0-8])|^(65054[1-9]|6505[5-8]\d|65059[0-8])|^(65070\d|65071[0-8])|^65072[0-7]|^(65090[1-9]|65091\d|650920)|^(65165[2-9]|6516[6-7]\d)|^(65500\d|65501\d)|^(65502[1-9]|6550[3-4]\d|65505[0-8])|^(65092[1-9]|65097[0-8])/, 133 | format: defaultFormat, 134 | length: [16], 135 | cvcLength: [3], 136 | luhn: true 137 | }, { 138 | type: 'hipercard', 139 | pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/, 140 | format: defaultFormat, 141 | length: [14, 15, 16, 17, 18, 19], 142 | cvcLength: [3], 143 | luhn: true 144 | }, { 145 | type: 'jcb', 146 | pattern: /^(308[8-9]|309[0-3]|3094[0]{4}|309[6-9]|310[0-2]|311[2-9]|3120|315[8-9]|333[7-9]|334[0-9]|35)/, 147 | format: defaultFormat, 148 | length: [16, 19], 149 | cvcLength: [3], 150 | luhn: true 151 | }, { 152 | type: 'laser', 153 | pattern: /^(6706|6771|6709)/, 154 | format: defaultFormat, 155 | length: [16, 17, 18, 19], 156 | cvcLength: [3], 157 | luhn: true 158 | }, { 159 | type: 'maestro', 160 | pattern: /^(50|5[6-9]|6007|6220|6304|6703|6708|6759|676[1-3])/, 161 | format: defaultFormat, 162 | length: [12, 13, 14, 15, 16, 17, 18, 19], 163 | cvcLength: [3], 164 | luhn: true 165 | }, { 166 | type: 'mastercard', 167 | pattern: /^(5[1-5]|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)/, 168 | format: defaultFormat, 169 | length: [16], 170 | cvcLength: [3], 171 | luhn: true 172 | }, { 173 | type: 'mir', 174 | pattern: /^220[0-4][0-9][0-9]\d{10}$/, 175 | format: defaultFormat, 176 | length: [16], 177 | cvcLength: [3], 178 | luhn: true 179 | }, { 180 | type: 'troy', 181 | pattern: /^9792/, 182 | format: defaultFormat, 183 | length: [16], 184 | cvcLength: [3], 185 | luhn: true 186 | }, { 187 | type: 'unionpay', 188 | pattern: /^62/, 189 | format: defaultFormat, 190 | length: [16, 17, 18, 19], 191 | cvcLength: [3], 192 | luhn: false 193 | }, { 194 | type: 'visaelectron', 195 | pattern: /^4(026|17500|405|508|844|91[37])/, 196 | format: defaultFormat, 197 | length: [16], 198 | cvcLength: [3], 199 | luhn: true 200 | }, { 201 | type: 'visa', 202 | pattern: /^4/, 203 | format: defaultFormat, 204 | length: [13, 16], 205 | cvcLength: [3], 206 | luhn: true 207 | } 208 | ]; 209 | 210 | cardFromNumber = function(num) { 211 | var card, foundCard, j, len, match; 212 | num = (num + '').replace(/\D/g, ''); 213 | foundCard = void 0; 214 | for (j = 0, len = cards.length; j < len; j++) { 215 | card = cards[j]; 216 | if (match = num.match(card.pattern)) { 217 | if (!foundCard || match[0].length > foundCard[1][0].length) { 218 | foundCard = [card, match]; 219 | } 220 | } 221 | } 222 | return foundCard && foundCard[0]; 223 | }; 224 | 225 | cardFromType = function(type) { 226 | var card, j, len; 227 | for (j = 0, len = cards.length; j < len; j++) { 228 | card = cards[j]; 229 | if (card.type === type) { 230 | return card; 231 | } 232 | } 233 | }; 234 | 235 | luhnCheck = function(num) { 236 | var digit, digits, j, len, odd, sum; 237 | odd = true; 238 | sum = 0; 239 | digits = (num + '').split('').reverse(); 240 | for (j = 0, len = digits.length; j < len; j++) { 241 | digit = digits[j]; 242 | digit = parseInt(digit, 10); 243 | if ((odd = !odd)) { 244 | digit *= 2; 245 | } 246 | if (digit > 9) { 247 | digit -= 9; 248 | } 249 | sum += digit; 250 | } 251 | return sum % 10 === 0; 252 | }; 253 | 254 | hasTextSelected = function(target) { 255 | var e, ref; 256 | try { 257 | if ((target.selectionStart != null) && target.selectionStart !== target.selectionEnd) { 258 | return true; 259 | } 260 | if ((typeof document !== "undefined" && document !== null ? (ref = document.selection) != null ? ref.createRange : void 0 : void 0) != null) { 261 | if (document.selection.createRange().text) { 262 | return true; 263 | } 264 | } 265 | } catch (error) { 266 | e = error; 267 | } 268 | return false; 269 | }; 270 | 271 | reFormatCardNumber = function(e) { 272 | return setTimeout((function(_this) { 273 | return function() { 274 | var target, value; 275 | target = e.target; 276 | value = QJ.val(target); 277 | value = Payment.fns.formatCardNumber(value); 278 | cursorSafeAssignValue(target, value); 279 | return QJ.trigger(target, 'change'); 280 | }; 281 | })(this)); 282 | }; 283 | 284 | formatCardNumber = function(maxLength) { 285 | return function(e) { 286 | var card, digit, i, j, len, length, re, target, upperLength, upperLengths, value; 287 | if (e.which > 0) { 288 | digit = String.fromCharCode(e.which); 289 | value = QJ.val(e.target) + digit; 290 | } else { 291 | digit = e.data; 292 | value = QJ.val(e.target); 293 | } 294 | if (!/^\d+$/.test(digit)) { 295 | return; 296 | } 297 | target = e.target; 298 | card = cardFromNumber(value); 299 | length = (value.replace(/\D/g, '')).length; 300 | upperLengths = [16]; 301 | if (card) { 302 | upperLengths = card.length; 303 | } 304 | if (maxLength) { 305 | upperLengths = upperLengths.filter(function(x) { 306 | return x <= maxLength; 307 | }); 308 | } 309 | for (i = j = 0, len = upperLengths.length; j < len; i = ++j) { 310 | upperLength = upperLengths[i]; 311 | if (length >= upperLength && upperLengths[i + 1]) { 312 | continue; 313 | } 314 | if (length >= upperLength) { 315 | return; 316 | } 317 | } 318 | if (hasTextSelected(target)) { 319 | return; 320 | } 321 | if (card && card.type === 'amex') { 322 | re = /^(\d{4}|\d{4}\s\d{6})$/; 323 | } else { 324 | re = /(?:^|\s)(\d{4})$/; 325 | } 326 | value = value.substring(0, value.length - 1); 327 | if (re.test(value)) { 328 | e.preventDefault(); 329 | QJ.val(target, value + ' ' + digit); 330 | return QJ.trigger(target, 'change'); 331 | } 332 | }; 333 | }; 334 | 335 | formatBackCardNumber = function(e) { 336 | var target, value; 337 | target = e.target; 338 | value = QJ.val(target); 339 | if (e.meta) { 340 | return; 341 | } 342 | if (e.which !== 8) { 343 | return; 344 | } 345 | if (hasTextSelected(target)) { 346 | return; 347 | } 348 | if (/\d\s$/.test(value)) { 349 | e.preventDefault(); 350 | QJ.val(target, value.replace(/\d\s$/, '')); 351 | return QJ.trigger(target, 'change'); 352 | } else if (/\s\d?$/.test(value)) { 353 | e.preventDefault(); 354 | QJ.val(target, value.replace(/\s\d?$/, '')); 355 | return QJ.trigger(target, 'change'); 356 | } 357 | }; 358 | 359 | formatExpiry = function(e) { 360 | var digit, target, val; 361 | target = e.target; 362 | if (e.which > 0) { 363 | digit = String.fromCharCode(e.which); 364 | val = QJ.val(target) + digit; 365 | } else { 366 | digit = e.data; 367 | val = QJ.val(target); 368 | } 369 | if (!/^\d+$/.test(digit)) { 370 | return; 371 | } 372 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 373 | e.preventDefault(); 374 | QJ.val(target, "0" + val + " / "); 375 | return QJ.trigger(target, 'change'); 376 | } else if (/^\d\d$/.test(val)) { 377 | e.preventDefault(); 378 | QJ.val(target, val + " / "); 379 | return QJ.trigger(target, 'change'); 380 | } 381 | }; 382 | 383 | formatMonthExpiry = function(e) { 384 | var digit, target, val; 385 | digit = String.fromCharCode(e.which); 386 | if (!/^\d+$/.test(digit)) { 387 | return; 388 | } 389 | target = e.target; 390 | val = QJ.val(target) + digit; 391 | if (/^\d$/.test(val) && (val !== '0' && val !== '1')) { 392 | e.preventDefault(); 393 | QJ.val(target, "0" + val); 394 | return QJ.trigger(target, 'change'); 395 | } else if (/^\d\d$/.test(val)) { 396 | e.preventDefault(); 397 | QJ.val(target, "" + val); 398 | return QJ.trigger(target, 'change'); 399 | } 400 | }; 401 | 402 | formatForwardExpiry = function(e) { 403 | var digit, target, val; 404 | digit = String.fromCharCode(e.which); 405 | if (!/^\d+$/.test(digit)) { 406 | return; 407 | } 408 | target = e.target; 409 | val = QJ.val(target); 410 | if (/^\d\d$/.test(val)) { 411 | QJ.val(target, val + " / "); 412 | return QJ.trigger(target, 'change'); 413 | } 414 | }; 415 | 416 | formatForwardSlash = function(e) { 417 | var slash, target, val; 418 | slash = String.fromCharCode(e.which); 419 | if (slash !== '/') { 420 | return; 421 | } 422 | target = e.target; 423 | val = QJ.val(target); 424 | if (/^\d$/.test(val) && val !== '0') { 425 | QJ.val(target, "0" + val + " / "); 426 | return QJ.trigger(target, 'change'); 427 | } 428 | }; 429 | 430 | formatBackExpiry = function(e) { 431 | var target, value; 432 | if (e.metaKey) { 433 | return; 434 | } 435 | target = e.target; 436 | value = QJ.val(target); 437 | if (e.which !== 8) { 438 | return; 439 | } 440 | if (hasTextSelected(target)) { 441 | return; 442 | } 443 | if (/\d(\s|\/)+$/.test(value)) { 444 | e.preventDefault(); 445 | QJ.val(target, value.replace(/\d(\s|\/)*$/, '')); 446 | return QJ.trigger(target, 'change'); 447 | } else if (/\s\/\s?\d?$/.test(value)) { 448 | e.preventDefault(); 449 | QJ.val(target, value.replace(/\s\/\s?\d?$/, '')); 450 | return QJ.trigger(target, 'change'); 451 | } 452 | }; 453 | 454 | restrictNumeric = function(e) { 455 | var input; 456 | if (e.metaKey || e.ctrlKey) { 457 | return true; 458 | } 459 | if (e.which === 32) { 460 | return e.preventDefault(); 461 | } 462 | if (e.which === 0) { 463 | return true; 464 | } 465 | if (e.which < 33) { 466 | return true; 467 | } 468 | input = String.fromCharCode(e.which); 469 | if (!/[\d\s]/.test(input)) { 470 | return e.preventDefault(); 471 | } 472 | }; 473 | 474 | restrictCardNumber = function(maxLength) { 475 | return function(e) { 476 | var card, digit, length, target, value; 477 | target = e.target; 478 | digit = String.fromCharCode(e.which); 479 | if (!/^\d+$/.test(digit)) { 480 | return; 481 | } 482 | if (hasTextSelected(target)) { 483 | return; 484 | } 485 | value = (QJ.val(target) + digit).replace(/\D/g, ''); 486 | card = cardFromNumber(value); 487 | length = 16; 488 | if (card) { 489 | length = card.length[card.length.length - 1]; 490 | } 491 | if (maxLength) { 492 | length = Math.min(length, maxLength); 493 | } 494 | if (!(value.length <= length)) { 495 | return e.preventDefault(); 496 | } 497 | }; 498 | }; 499 | 500 | restrictExpiry = function(e, length) { 501 | var digit, target, value; 502 | target = e.target; 503 | digit = String.fromCharCode(e.which); 504 | if (!/^\d+$/.test(digit)) { 505 | return; 506 | } 507 | if (hasTextSelected(target)) { 508 | return; 509 | } 510 | value = QJ.val(target) + digit; 511 | value = value.replace(/\D/g, ''); 512 | if (value.length > length) { 513 | return e.preventDefault(); 514 | } 515 | }; 516 | 517 | restrictCombinedExpiry = function(e) { 518 | return restrictExpiry(e, 6); 519 | }; 520 | 521 | restrictMonthExpiry = function(e) { 522 | return restrictExpiry(e, 2); 523 | }; 524 | 525 | restrictYearExpiry = function(e) { 526 | return restrictExpiry(e, 4); 527 | }; 528 | 529 | restrictCVC = function(e) { 530 | var digit, target, val; 531 | target = e.target; 532 | digit = String.fromCharCode(e.which); 533 | if (!/^\d+$/.test(digit)) { 534 | return; 535 | } 536 | if (hasTextSelected(target)) { 537 | return; 538 | } 539 | val = QJ.val(target) + digit; 540 | if (!(val.length <= 4)) { 541 | return e.preventDefault(); 542 | } 543 | }; 544 | 545 | setCardType = function(e) { 546 | var allTypes, card, cardType, target, val; 547 | target = e.target; 548 | val = QJ.val(target); 549 | cardType = Payment.fns.cardType(val) || 'unknown'; 550 | if (!QJ.hasClass(target, cardType)) { 551 | allTypes = (function() { 552 | var j, len, results; 553 | results = []; 554 | for (j = 0, len = cards.length; j < len; j++) { 555 | card = cards[j]; 556 | results.push(card.type); 557 | } 558 | return results; 559 | })(); 560 | QJ.removeClass(target, 'unknown'); 561 | QJ.removeClass(target, allTypes.join(' ')); 562 | QJ.addClass(target, cardType); 563 | QJ.toggleClass(target, 'identified', cardType !== 'unknown'); 564 | return QJ.trigger(target, 'payment.cardType', cardType); 565 | } 566 | }; 567 | 568 | cursorSafeAssignValue = function(target, value) { 569 | var selectionEnd; 570 | selectionEnd = target.selectionEnd; 571 | QJ.val(target, value); 572 | if (selectionEnd) { 573 | return target.selectionEnd = selectionEnd; 574 | } 575 | }; 576 | 577 | Payment = (function() { 578 | function Payment() {} 579 | 580 | Payment.J = QJ; 581 | 582 | Payment.fns = { 583 | cardExpiryVal: function(value) { 584 | var month, prefix, ref, year; 585 | value = value.replace(/\s/g, ''); 586 | ref = value.split('/', 2), month = ref[0], year = ref[1]; 587 | if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) { 588 | prefix = (new Date).getFullYear(); 589 | prefix = prefix.toString().slice(0, 2); 590 | year = prefix + year; 591 | } 592 | month = parseInt(month, 10); 593 | year = parseInt(year, 10); 594 | return { 595 | month: month, 596 | year: year 597 | }; 598 | }, 599 | validateCardNumber: function(num) { 600 | var card, ref; 601 | num = (num + '').replace(/\s+|-/g, ''); 602 | if (!/^\d+$/.test(num)) { 603 | return false; 604 | } 605 | card = cardFromNumber(num); 606 | if (!card) { 607 | return false; 608 | } 609 | return (ref = num.length, indexOf.call(card.length, ref) >= 0) && (card.luhn === false || luhnCheck(num)); 610 | }, 611 | validateCardExpiry: function(month, year) { 612 | var currentTime, expiry, prefix, ref, ref1; 613 | if (typeof month === 'object' && 'month' in month) { 614 | ref = month, month = ref.month, year = ref.year; 615 | } else if (typeof month === 'string' && indexOf.call(month, '/') >= 0) { 616 | ref1 = Payment.fns.cardExpiryVal(month), month = ref1.month, year = ref1.year; 617 | } 618 | if (!(month && year)) { 619 | return false; 620 | } 621 | month = QJ.trim(month); 622 | year = QJ.trim(year); 623 | if (!/^\d+$/.test(month)) { 624 | return false; 625 | } 626 | if (!/^\d+$/.test(year)) { 627 | return false; 628 | } 629 | month = parseInt(month, 10); 630 | if (!(month && month <= 12)) { 631 | return false; 632 | } 633 | if (year.length === 2) { 634 | prefix = (new Date).getFullYear(); 635 | prefix = prefix.toString().slice(0, 2); 636 | year = prefix + year; 637 | } 638 | expiry = new Date(year, month); 639 | currentTime = new Date; 640 | expiry.setMonth(expiry.getMonth() - 1); 641 | expiry.setMonth(expiry.getMonth() + 1, 1); 642 | return expiry > currentTime; 643 | }, 644 | validateCardCVC: function(cvc, type) { 645 | var ref, ref1; 646 | cvc = QJ.trim(cvc); 647 | if (!/^\d+$/.test(cvc)) { 648 | return false; 649 | } 650 | if (type && cardFromType(type)) { 651 | return ref = cvc.length, indexOf.call((ref1 = cardFromType(type)) != null ? ref1.cvcLength : void 0, ref) >= 0; 652 | } else { 653 | return cvc.length >= 3 && cvc.length <= 4; 654 | } 655 | }, 656 | cardType: function(num) { 657 | var ref; 658 | if (!num) { 659 | return null; 660 | } 661 | return ((ref = cardFromNumber(num)) != null ? ref.type : void 0) || null; 662 | }, 663 | formatCardNumber: function(num) { 664 | var card, groups, ref, upperLength; 665 | card = cardFromNumber(num); 666 | if (!card) { 667 | return num; 668 | } 669 | upperLength = card.length[card.length.length - 1]; 670 | num = num.replace(/\D/g, ''); 671 | num = num.slice(0, upperLength); 672 | if (card.format.global) { 673 | return (ref = num.match(card.format)) != null ? ref.join(' ') : void 0; 674 | } else { 675 | groups = card.format.exec(num); 676 | if (groups == null) { 677 | return; 678 | } 679 | groups.shift(); 680 | groups = groups.filter(function(n) { 681 | return n; 682 | }); 683 | return groups.join(' '); 684 | } 685 | } 686 | }; 687 | 688 | Payment.restrictNumeric = function(el) { 689 | QJ.on(el, 'keypress', restrictNumeric); 690 | return QJ.on(el, 'input', restrictNumeric); 691 | }; 692 | 693 | Payment.cardExpiryVal = function(el) { 694 | return Payment.fns.cardExpiryVal(QJ.val(el)); 695 | }; 696 | 697 | Payment.formatCardCVC = function(el) { 698 | Payment.restrictNumeric(el); 699 | QJ.on(el, 'keypress', restrictCVC); 700 | QJ.on(el, 'input', restrictCVC); 701 | return el; 702 | }; 703 | 704 | Payment.formatCardExpiry = function(el) { 705 | var month, year; 706 | Payment.restrictNumeric(el); 707 | if (el.length && el.length === 2) { 708 | month = el[0], year = el[1]; 709 | this.formatCardExpiryMultiple(month, year); 710 | } else { 711 | QJ.on(el, 'keypress', restrictCombinedExpiry); 712 | QJ.on(el, 'keypress', formatExpiry); 713 | QJ.on(el, 'keypress', formatForwardSlash); 714 | QJ.on(el, 'keypress', formatForwardExpiry); 715 | QJ.on(el, 'keydown', formatBackExpiry); 716 | QJ.on(el, 'input', formatExpiry); 717 | } 718 | return el; 719 | }; 720 | 721 | Payment.formatCardExpiryMultiple = function(month, year) { 722 | QJ.on(month, 'keypress', restrictMonthExpiry); 723 | QJ.on(month, 'keypress', formatMonthExpiry); 724 | QJ.on(month, 'input', formatMonthExpiry); 725 | QJ.on(year, 'keypress', restrictYearExpiry); 726 | return QJ.on(year, 'input', restrictYearExpiry); 727 | }; 728 | 729 | Payment.formatCardNumber = function(el, maxLength) { 730 | Payment.restrictNumeric(el); 731 | QJ.on(el, 'keypress', restrictCardNumber(maxLength)); 732 | QJ.on(el, 'keypress', formatCardNumber(maxLength)); 733 | QJ.on(el, 'keydown', formatBackCardNumber); 734 | QJ.on(el, 'keyup blur', setCardType); 735 | QJ.on(el, 'blur', reFormatCardNumber); 736 | QJ.on(el, 'paste', reFormatCardNumber); 737 | QJ.on(el, 'input', formatCardNumber(maxLength)); 738 | return el; 739 | }; 740 | 741 | Payment.getCardArray = function() { 742 | return cards; 743 | }; 744 | 745 | Payment.setCardArray = function(cardArray) { 746 | cards = cardArray; 747 | return true; 748 | }; 749 | 750 | Payment.addToCardArray = function(cardObject) { 751 | return cards.push(cardObject); 752 | }; 753 | 754 | Payment.removeFromCardArray = function(type) { 755 | var key, value; 756 | for (key in cards) { 757 | value = cards[key]; 758 | if (value.type === type) { 759 | cards.splice(key, 1); 760 | } 761 | } 762 | return true; 763 | }; 764 | 765 | return Payment; 766 | 767 | })(); 768 | 769 | module.exports = Payment; 770 | 771 | globalThis.Payment = Payment; 772 | 773 | 774 | /***/ }), 775 | /* 1 */ 776 | /***/ (function(module, exports, __webpack_require__) { 777 | 778 | "use strict"; 779 | /* WEBPACK VAR INJECTION */(function(global) { 780 | 781 | var implementation = __webpack_require__(3); 782 | 783 | module.exports = function getPolyfill() { 784 | if (typeof global !== 'object' || !global || global.Math !== Math || global.Array !== Array) { 785 | return implementation; 786 | } 787 | return global; 788 | }; 789 | 790 | /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(2))) 791 | 792 | /***/ }), 793 | /* 2 */ 794 | /***/ (function(module, exports) { 795 | 796 | var g; 797 | 798 | // This works in non-strict mode 799 | g = (function() { 800 | return this; 801 | })(); 802 | 803 | try { 804 | // This works if eval is allowed (see CSP) 805 | g = g || new Function("return this")(); 806 | } catch (e) { 807 | // This works if the window reference is available 808 | if (typeof window === "object") g = window; 809 | } 810 | 811 | // g can still be undefined, but nothing to do about it... 812 | // We return undefined, instead of nothing here, so it's 813 | // easier to handle this case. if(!global) { ...} 814 | 815 | module.exports = g; 816 | 817 | 818 | /***/ }), 819 | /* 3 */ 820 | /***/ (function(module, exports, __webpack_require__) { 821 | 822 | "use strict"; 823 | /* eslint no-negated-condition: 0, no-new-func: 0 */ 824 | 825 | 826 | 827 | if (typeof self !== 'undefined') { 828 | module.exports = self; 829 | } else if (typeof window !== 'undefined') { 830 | module.exports = window; 831 | } else { 832 | module.exports = Function('return this')(); 833 | } 834 | 835 | 836 | /***/ }), 837 | /* 4 */ 838 | /***/ (function(module, exports) { 839 | 840 | // Generated by CoffeeScript 1.10.0 841 | (function() { 842 | var QJ, rreturn, rtrim; 843 | 844 | QJ = function(selector) { 845 | if (QJ.isDOMElement(selector)) { 846 | return selector; 847 | } 848 | return document.querySelectorAll(selector); 849 | }; 850 | 851 | QJ.isDOMElement = function(el) { 852 | return el && (el.nodeName != null); 853 | }; 854 | 855 | rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; 856 | 857 | QJ.trim = function(text) { 858 | if (text === null) { 859 | return ""; 860 | } else { 861 | return (text + "").replace(rtrim, ""); 862 | } 863 | }; 864 | 865 | rreturn = /\r/g; 866 | 867 | QJ.val = function(el, val) { 868 | var ret; 869 | if (arguments.length > 1) { 870 | return el.value = val; 871 | } else { 872 | ret = el.value; 873 | if (typeof ret === "string") { 874 | return ret.replace(rreturn, ""); 875 | } else { 876 | if (ret === null) { 877 | return ""; 878 | } else { 879 | return ret; 880 | } 881 | } 882 | } 883 | }; 884 | 885 | QJ.preventDefault = function(eventObject) { 886 | if (typeof eventObject.preventDefault === "function") { 887 | eventObject.preventDefault(); 888 | return; 889 | } 890 | eventObject.returnValue = false; 891 | return false; 892 | }; 893 | 894 | QJ.normalizeEvent = function(e) { 895 | var original; 896 | original = e; 897 | e = { 898 | which: original.which != null ? original.which : void 0, 899 | target: original.target || original.srcElement, 900 | preventDefault: function() { 901 | return QJ.preventDefault(original); 902 | }, 903 | originalEvent: original, 904 | data: original.data || original.detail 905 | }; 906 | if (e.which == null) { 907 | e.which = original.charCode != null ? original.charCode : original.keyCode; 908 | } 909 | return e; 910 | }; 911 | 912 | QJ.on = function(element, eventName, callback) { 913 | var el, i, j, len, len1, multEventName, originalCallback, ref; 914 | if (element.length) { 915 | for (i = 0, len = element.length; i < len; i++) { 916 | el = element[i]; 917 | QJ.on(el, eventName, callback); 918 | } 919 | return; 920 | } 921 | if (eventName.match(" ")) { 922 | ref = eventName.split(" "); 923 | for (j = 0, len1 = ref.length; j < len1; j++) { 924 | multEventName = ref[j]; 925 | QJ.on(element, multEventName, callback); 926 | } 927 | return; 928 | } 929 | originalCallback = callback; 930 | callback = function(e) { 931 | e = QJ.normalizeEvent(e); 932 | return originalCallback(e); 933 | }; 934 | if (element.addEventListener) { 935 | return element.addEventListener(eventName, callback, false); 936 | } 937 | if (element.attachEvent) { 938 | eventName = "on" + eventName; 939 | return element.attachEvent(eventName, callback); 940 | } 941 | element['on' + eventName] = callback; 942 | }; 943 | 944 | QJ.addClass = function(el, className) { 945 | var e; 946 | if (el.length) { 947 | return (function() { 948 | var i, len, results; 949 | results = []; 950 | for (i = 0, len = el.length; i < len; i++) { 951 | e = el[i]; 952 | results.push(QJ.addClass(e, className)); 953 | } 954 | return results; 955 | })(); 956 | } 957 | if (el.classList) { 958 | return el.classList.add(className); 959 | } else { 960 | return el.className += ' ' + className; 961 | } 962 | }; 963 | 964 | QJ.hasClass = function(el, className) { 965 | var e, hasClass, i, len; 966 | if (el.length) { 967 | hasClass = true; 968 | for (i = 0, len = el.length; i < len; i++) { 969 | e = el[i]; 970 | hasClass = hasClass && QJ.hasClass(e, className); 971 | } 972 | return hasClass; 973 | } 974 | if (el.classList) { 975 | return el.classList.contains(className); 976 | } else { 977 | return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className); 978 | } 979 | }; 980 | 981 | QJ.removeClass = function(el, className) { 982 | var cls, e, i, len, ref, results; 983 | if (el.length) { 984 | return (function() { 985 | var i, len, results; 986 | results = []; 987 | for (i = 0, len = el.length; i < len; i++) { 988 | e = el[i]; 989 | results.push(QJ.removeClass(e, className)); 990 | } 991 | return results; 992 | })(); 993 | } 994 | if (el.classList) { 995 | ref = className.split(' '); 996 | results = []; 997 | for (i = 0, len = ref.length; i < len; i++) { 998 | cls = ref[i]; 999 | results.push(el.classList.remove(cls)); 1000 | } 1001 | return results; 1002 | } else { 1003 | return el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); 1004 | } 1005 | }; 1006 | 1007 | QJ.toggleClass = function(el, className, bool) { 1008 | var e; 1009 | if (el.length) { 1010 | return (function() { 1011 | var i, len, results; 1012 | results = []; 1013 | for (i = 0, len = el.length; i < len; i++) { 1014 | e = el[i]; 1015 | results.push(QJ.toggleClass(e, className, bool)); 1016 | } 1017 | return results; 1018 | })(); 1019 | } 1020 | if (bool) { 1021 | if (!QJ.hasClass(el, className)) { 1022 | return QJ.addClass(el, className); 1023 | } 1024 | } else { 1025 | return QJ.removeClass(el, className); 1026 | } 1027 | }; 1028 | 1029 | QJ.append = function(el, toAppend) { 1030 | var e; 1031 | if (el.length) { 1032 | return (function() { 1033 | var i, len, results; 1034 | results = []; 1035 | for (i = 0, len = el.length; i < len; i++) { 1036 | e = el[i]; 1037 | results.push(QJ.append(e, toAppend)); 1038 | } 1039 | return results; 1040 | })(); 1041 | } 1042 | return el.insertAdjacentHTML('beforeend', toAppend); 1043 | }; 1044 | 1045 | QJ.find = function(el, selector) { 1046 | if (el instanceof NodeList || el instanceof Array) { 1047 | el = el[0]; 1048 | } 1049 | return el.querySelectorAll(selector); 1050 | }; 1051 | 1052 | QJ.trigger = function(el, name, data) { 1053 | var e, error, ev; 1054 | try { 1055 | ev = new CustomEvent(name, { 1056 | detail: data 1057 | }); 1058 | } catch (error) { 1059 | e = error; 1060 | ev = document.createEvent('CustomEvent'); 1061 | if (ev.initCustomEvent) { 1062 | ev.initCustomEvent(name, true, true, data); 1063 | } else { 1064 | ev.initEvent(name, true, true, data); 1065 | } 1066 | } 1067 | return el.dispatchEvent(ev); 1068 | }; 1069 | 1070 | module.exports = QJ; 1071 | 1072 | }).call(this); 1073 | 1074 | 1075 | /***/ }) 1076 | /******/ ]); --------------------------------------------------------------------------------