├── .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 | [](https://travis-ci.org/jondavidjohn/payform)
17 | 
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 |
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 | Number
11 |
12 |
13 |
14 |
15 |
20 |
21 | Expiry
22 |
23 |
24 |
25 |
26 | CVC
27 |
28 |
29 |
30 |
31 | Numeric
32 |
33 |
34 |
35 |
36 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/jquery.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Payform Demo
5 |
6 |
7 |
8 |
9 | Number
10 |
11 |
12 |
13 |
14 | Expiry
15 |
16 |
17 |
18 |
19 | CVC
20 |
21 |
22 |
23 |
24 | Numeric
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 |
--------------------------------------------------------------------------------