Commit eb5d0caa authored by Jesse Pollak's avatar Jesse Pollak
Browse files

initial commit

Showing with 2059 additions and 0 deletions
+2059 -0
node_modules
language: node_js
node_js:
- "0.10"
Copyright (c) 2014 Jesse Pollak
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Payment [![Build Status](https://travis-ci.org/jessepollak/payment.svg?branch=master)](https://travis-ci.org/jessepollak/payment)
A jQuery-free general purpose library for building credit card forms, validating inputs and formatting numbers. Heavily, heavily based on [@stripe's jquery.payment library](http://github.com/stripe/jquery.payment), but without the jQuery.
For example, you can make a input act like a credit card field (with number formatting and length restriction):
``` javascript
Payment.formatCardNumber(document.querySelector('input.cc-num'));
```
Then, when the payment form is submitted, you can validate the card number on the client-side:
``` javascript
var valid = Payment.fns.validateCardNumber(document.querySelector('input.cc-num').value);
if (!valid) {
alert('Your card is not valid!');
return false;
}
```
You can find a full [demo here](http://jessepollak.github.io/payment/example).
Supported card types are:
* Visa
* MasterCard
* American Express
* Discover
* JCB
* Diners Club
* Maestro
* Laser
* UnionPay
## API
### Payment.formatCardNumber
Formats card numbers:
* Includes a space between every 4 digits
* Restricts input to numbers
* Limits to 16 numbers
* Supports American Express formatting
* Adds a class of the card type (e.g. 'visa') to the input
Example:
``` javascript
Payment.formatCardNumber(document.querySelector('input.cc-num'));
```
### Payment.formatCardExpiry
Formats card expiry:
* Includes a `/` between the month and year
* Restricts input to numbers
* Restricts length
Example:
``` javascript
Payment.formatCardExpiry(document.querySelector('input.cc-exp'));
```
### Payment.formatCardCVC
Formats card CVC:
* Restricts length to 4 numbers
* Restricts input to numbers
Example:
``` javascript
Payment.formatCardCVC(document.querySelector('input.cc-cvc'));
```
### Payment.restrictNumeric
General numeric input restriction.
Example:
``` javascript
Payment.restrictNumeric(document.querySelector('[data-numeric]'));
```
### Payment.fns.validateCardNumber(number)
Validates a card number:
* Validates numbers
* Validates Luhn algorithm
* Validates length
Example:
``` javascript
Payment.fns.validateCardNumber('4242 4242 4242 4242'); //=> true
```
### Payment.fns.validateCardExpiry(month, year)
Validates a card expiry:
* Validates numbers
* Validates in the future
* Supports year shorthand
Example:
``` javascript
Payment.fns.validateCardExpiry('05', '20'); //=> true
Payment.fns.validateCardExpiry('05', '2015'); //=> true
Payment.fns.validateCardExpiry('05', '05'); //=> false
```
### Payment.fns.validateCardCVC(cvc, type)
Validates a card CVC:
* Validates number
* Validates length to 4
Example:
``` javascript
Payment.fns.validateCardCVC('123'); //=> true
Payment.fns.validateCardCVC('123', 'amex'); //=> true
Payment.fns.validateCardCVC('1234', 'amex'); //=> true
Payment.fns.validateCardCVC('12344'); //=> false
```
### Payment.fns.cardType(number)
Returns a card type. Either:
* `visa`
* `mastercard`
* `discover`
* `amex`
* `jcb`
* `dinersclub`
* `maestro`
* `laser`
* `unionpay`
The function will return `null` if the card type can't be determined.
Example:
``` javascript
Payment.fns.cardType('4242 4242 4242 4242'); //=> 'visa'
```
### Payment.fns.cardExpiryVal(string) and Payment.cardExpiryVal(el)
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`).
``` javascript
Payment.fns.cardExpiryVal('03 / 2025'); //=> {month: 3: year: 2025}
Payment.fns.cardExpiryVal('05 / 04'); //=> {month: 5, year: 2004}
Payment.fns.cardExpiryVal(document.querySelector('input.cc-exp')) //=> {month: 4, year: 2020}
```
This function doesn't perform any validation of the month or year; use `Payment.fns.validateCardExpiry(month, year)` for that.
## Example
Look in [`./example/index.html`](example/index.html)
## Building
Run `gulp build`
## Running tests
Run `gulp test`
## Autocomplete recommendations
We recommend you turn autocomplete on for credit card forms, except for the CVC field. You can do this by setting the `autocomplete` attribute:
``` html
<form autocomplete="on">
<input class="cc-number">
<input class="cc-cvc" autocomplete="off">
</form>
```
You should also mark up your fields using the [Autocomplete Types spec](http://wiki.whatwg.org/wiki/Autocomplete_Types). These are respected by a number of browsers, including Chrome.
``` html
<input type="text" class="cc-number" pattern="\d*" autocompletetype="cc-number" placeholder="Card number" required>
```
Set `autocompletetype` to `cc-number` for credit card numbers, `cc-exp` for credit card expiry and `cc-csc` for the CVC (security code).
## Mobile recommendations
We recommend you set the `pattern` attribute which will cause the numeric keyboard to be displayed on mobiles:
``` html
<input class="cc-number" pattern="\d*">
```
You may have to turn off HTML5 validation (using the `novalidate` form attribute) when using this `pattern`, as it won't match space formatting.
{
"name": "payment",
"version": "1.0.3",
"main": "lib/payment.js",
}
<!DOCTYPE html>
<html>
<head>
<script src="../lib/payment.js"></script>
<style type="text/css" media="screen">
input.invalid {
border: 2px solid red;
}
.validation.failed:after {
color: red;
content: 'Validation failed';
}
.validation.passed:after {
color: green;
content: 'Validation passed';
}
</style>
</head>
<body>
<form novalidate autocomplete="on">
<h2>Card number formatting</h2>
<input type="text" class="cc-number" pattern="\d*" x-autocompletetype="cc-number" placeholder="Card number" required>
<h2>Expiry formatting</h2>
<input type="text" class="cc-exp" pattern="\d*" x-autocompletetype="cc-exp" placeholder="Expires MM/YY" required maxlength="9">
<h2>CVC formatting</h2>
<input type="text" class="cc-cvc" pattern="\d*" x-autocompletetype="cc-csc" placeholder="Security code" required autocomplete="off">
<h2>Restrict Numeric</h2>
<input type="text" data-numeric>
<h2 class="validation"></h2>
<button type="submit">Submit</button>
</form>
<script>
var J = Payment.J,
numeric = document.querySelector('[data-numeric]'),
number = document.querySelector('.cc-number'),
exp = document.querySelector('.cc-exp'),
cvc = document.querySelector('.cc-cvc'),
validation = document.querySelector('.validation');
Payment.restrictNumeric(numeric);
Payment.formatCardNumber(number);
Payment.formatCardExpiry(exp);
Payment.formatCardCVC(cvc);
document.querySelector('form').onsubmit = function(e) {
e.preventDefault();
J.toggleClass(document.querySelectorAll('input'), 'invalid');
J.removeClass(validation, 'passed failed');
var cardType = Payment.fns.cardType(J.val(number));
J.toggleClass(number, 'invalid', !Payment.fns.validateCardNumber(J.val(number)));
J.toggleClass(exp, 'invalid', !Payment.fns.validateCardExpiry(Payment.cardExpiryVal(exp)));
J.toggleClass(cvc, 'invalid', !Payment.fns.validateCardCVC(J.val(cvc), cardType));
if (document.querySelectorAll('.invalid').length) {
J.addClass(validation, 'failed');
} else {
J.addClass(validation, 'passed');
}
}
</script>
</body>
</html>
gulp = require 'gulp'
browserify = require 'gulp-browserify'
scss = require 'gulp-sass'
prefix = require 'gulp-autoprefixer'
spawn = require('child_process').spawn
server = require('tiny-lr')()
livereload = require('gulp-livereload')
rename = require 'gulp-rename'
rimraf = require 'gulp-rimraf'
connect = require 'gulp-connect'
open = require 'gulp-open'
mocha = require 'gulp-mocha'
runs = require 'run-sequence'
development = process.env.NODE_ENV == 'development'
gulp.task 'browserify', ->
gulp.src './src/payment.coffee', read: false
.pipe browserify
insertGlobals: false
debug: development
transform: ['coffeeify']
extensions: ['.coffee']
.pipe livereload(server)
.pipe rename({ extname: '.js' })
.pipe gulp.dest('./lib/')
gulp.task 'watch', ['browserify', 'connect'], ->
server.listen 35729, ->
gulp.watch './src/**/*.coffee', ['browserify']
gulp.src('example/index.html')
.pipe open("", url: "http://localhost:8080/example")
gulp.task 'connect', ->
connect.server()
gulp.task 'clean', ->
gulp.src 'lib'
.pipe rimraf()
gulp.task 'test', ->
gulp.src('./test')
.pipe(mocha({ report: 'nyan', compilers: 'coffee:coffee-script/register' }))
gulp.task 'build', (cb) ->
runs(
'test',
'clean',
'browserify',
cb
)
gulp.task 'default', ['watch']
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (global){
var J, Payment, cardFromNumber, cardFromType, cards, defaultFormat, formatBackCardNumber, formatBackExpiry, formatCardNumber, formatExpiry, formatForwardExpiry, formatForwardSlash, hasTextSelected, luhnCheck, reFormatCardNumber, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric, setCardType,
__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; };
J = require('./utils');
defaultFormat = /(\d{1,4})/g;
cards = [
{
type: 'maestro',
pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
format: defaultFormat,
length: [12, 13, 14, 15, 16, 17, 18, 19],
cvcLength: [3],
luhn: true
}, {
type: 'dinersclub',
pattern: /^(36|38|30[0-5])/,
format: defaultFormat,
length: [14],
cvcLength: [3],
luhn: true
}, {
type: 'laser',
pattern: /^(6706|6771|6709)/,
format: defaultFormat,
length: [16, 17, 18, 19],
cvcLength: [3],
luhn: true
}, {
type: 'jcb',
pattern: /^35/,
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'unionpay',
pattern: /^62/,
format: defaultFormat,
length: [16, 17, 18, 19],
cvcLength: [3],
luhn: false
}, {
type: 'discover',
pattern: /^(6011|65|64[4-9]|622)/,
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'mastercard',
pattern: /^5[1-5]/,
format: defaultFormat,
length: [16],
cvcLength: [3],
luhn: true
}, {
type: 'amex',
pattern: /^3[47]/,
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
length: [15],
cvcLength: [3, 4],
luhn: true
}, {
type: 'visa',
pattern: /^4/,
format: defaultFormat,
length: [13, 14, 15, 16],
cvcLength: [3],
luhn: true
}
];
cardFromNumber = function(num) {
var card, _i, _len;
num = (num + '').replace(/\D/g, '');
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
if (card.pattern.test(num)) {
return card;
}
}
};
cardFromType = function(type) {
var card, _i, _len;
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
if (card.type === type) {
return card;
}
}
};
luhnCheck = function(num) {
var digit, digits, odd, sum, _i, _len;
odd = true;
sum = 0;
digits = (num + '').split('').reverse();
for (_i = 0, _len = digits.length; _i < _len; _i++) {
digit = digits[_i];
digit = parseInt(digit, 10);
if ((odd = !odd)) {
digit *= 2;
}
if (digit > 9) {
digit -= 9;
}
sum += digit;
}
return sum % 10 === 0;
};
hasTextSelected = function(target) {
var _ref;
if ((target.selectionStart != null) && target.selectionStart !== target.selectionEnd) {
return true;
}
if (typeof document !== "undefined" && document !== null ? (_ref = document.selection) != null ? typeof _ref.createRange === "function" ? _ref.createRange().text : void 0 : void 0 : void 0) {
return true;
}
return false;
};
reFormatCardNumber = function(e) {
return setTimeout((function(_this) {
return function() {
var target, value;
target = e.currentTarget;
value = J.val(target);
value = Payment.fns.formatCardNumber(value);
return J.val(target, value);
};
})(this));
};
formatCardNumber = function(e) {
var card, digit, length, re, target, upperLength, value;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
target = e.currentTarget;
value = J.val(target);
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;
}
if ((target.selectionStart != null) && target.selectionStart !== 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 J.val(target, value + ' ' + digit);
} else if (re.test(value + digit)) {
e.preventDefault();
return J.val(target, value + digit + ' ');
}
};
formatBackCardNumber = function(e) {
var target, value;
target = e.currentTarget;
value = J.val(target);
if (e.meta) {
return;
}
if (e.which !== 8) {
return;
}
if ((target.selectionStart != null) && target.selectionStart !== value.length) {
return;
}
if (/\d\s$/.test(value)) {
e.preventDefault();
return J.val(target, value.replace(/\d\s$/, ''));
} else if (/\s\d?$/.test(value)) {
e.preventDefault();
return J.val(target, value.replace(/\s\d?$/, ''));
}
};
formatExpiry = function(e) {
var digit, target, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
target = e.currentTarget;
val = J.val(target) + digit;
if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
e.preventDefault();
return J.val(target, "0" + val + " / ");
} else if (/^\d\d$/.test(val)) {
e.preventDefault();
return J.val(target, "" + val + " / ");
}
};
formatForwardExpiry = function(e) {
var digit, target, val;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
target = e.currentTarget;
val = J.val(target);
if (/^\d\d$/.test(val)) {
return J.val(target, "" + val + " / ");
}
};
formatForwardSlash = function(e) {
var slash, target, val;
slash = String.fromCharCode(e.which);
if (slash !== '/') {
return;
}
target = e.currentTarget;
val = J.val(target);
if (/^\d$/.test(val) && val !== '0') {
return J.val(target, "0" + val + " / ");
}
};
formatBackExpiry = function(e) {
var target, value;
if (e.metaKey) {
return;
}
target = e.currentTarget;
value = J.val(target);
if (e.which !== 8) {
return;
}
if ((target.selectionStart != null) && target.selectionStart !== value.length) {
return;
}
if (/\d(\s|\/)+$/.test(value)) {
e.preventDefault();
return J.val(target, value.replace(/\d(\s|\/)*$/, ''));
} else if (/\s\/\s?\d?$/.test(value)) {
e.preventDefault();
return J.val(target, value.replace(/\s\/\s?\d?$/, ''));
}
};
restrictNumeric = function(e) {
var input;
if (e.metaKey || e.ctrlKey) {
return true;
}
if (e.which === 32) {
return e.preventDefault();
}
if (e.which === 0) {
return true;
}
if (e.which < 33) {
return true;
}
input = String.fromCharCode(e.which);
if (!/[\d\s]/.test(input)) {
return e.preventDefault();
}
};
restrictCardNumber = function(e) {
var card, digit, target, value;
target = e.currentTarget;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected(target)) {
return;
}
value = (J.val(target) + digit).replace(/\D/g, '');
card = cardFromNumber(value);
if (card) {
if (!(value.length <= card.length[card.length.length - 1])) {
return e.preventDefault();
}
} else {
if (!(value.length <= 16)) {
return e.preventDefault();
}
}
};
restrictExpiry = function(e) {
var digit, target, value;
target = e.currentTarget;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
if (hasTextSelected(target)) {
return;
}
value = J.val(target) + digit;
value = value.replace(/\D/g, '');
if (value.length > 6) {
return e.preventDefault();
}
};
restrictCVC = function(e) {
var digit, target, val;
target = e.currentTarget;
digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) {
return;
}
val = J.val(target) + digit;
if (!(val.length <= 4)) {
return e.preventDefault();
}
};
setCardType = function(e) {
var allTypes, card, cardType, target, val;
target = e.currentTarget;
val = J.val(target);
cardType = Payment.fns.cardType(val) || 'unknown';
if (!J.hasClass(target, cardType)) {
allTypes = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = cards.length; _i < _len; _i++) {
card = cards[_i];
_results.push(card.type);
}
return _results;
})();
J.removeClass(target, 'unknown');
J.removeClass(target, allTypes.join(' '));
J.addClass(target, cardType);
J.toggleClass(target, 'identified', cardType !== 'unknown');
return J.trigger(target, 'payment.cardType', cardType);
}
};
Payment = (function() {
function Payment() {}
Payment.fns = {
cardExpiryVal: function(value) {
var month, prefix, year, _ref;
value = value.replace(/\s/g, '');
_ref = value.split('/', 2), month = _ref[0], year = _ref[1];
if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
prefix = (new Date).getFullYear();
prefix = prefix.toString().slice(0, 2);
year = prefix + year;
}
month = parseInt(month, 10);
year = parseInt(year, 10);
return {
month: month,
year: year
};
},
validateCardNumber: function(num) {
var card, _ref;
num = (num + '').replace(/\s+|-/g, '');
if (!/^\d+$/.test(num)) {
return false;
}
card = cardFromNumber(num);
if (!card) {
return false;
}
return (_ref = num.length, __indexOf.call(card.length, _ref) >= 0) && (card.luhn === false || luhnCheck(num));
},
validateCardExpiry: function(month, year) {
var currentTime, expiry, prefix, _ref;
if (typeof month === 'object' && 'month' in month) {
_ref = month, month = _ref.month, year = _ref.year;
}
if (!(month && year)) {
return false;
}
month = J.trim(month);
year = J.trim(year);
if (!/^\d+$/.test(month)) {
return false;
}
if (!/^\d+$/.test(year)) {
return false;
}
if (!(parseInt(month, 10) <= 12)) {
return false;
}
if (year.length === 2) {
prefix = (new Date).getFullYear();
prefix = prefix.toString().slice(0, 2);
year = prefix + year;
}
expiry = new Date(year, month);
currentTime = new Date;
expiry.setMonth(expiry.getMonth() - 1);
expiry.setMonth(expiry.getMonth() + 1, 1);
return expiry > currentTime;
},
validateCardCVC: function(cvc, type) {
var _ref, _ref1;
cvc = J.trim(cvc);
if (!/^\d+$/.test(cvc)) {
return false;
}
if (type) {
return _ref = cvc.length, __indexOf.call((_ref1 = cardFromType(type)) != null ? _ref1.cvcLength : void 0, _ref) >= 0;
} else {
return cvc.length >= 3 && cvc.length <= 4;
}
},
cardType: function(num) {
var _ref;
if (!num) {
return null;
}
return ((_ref = cardFromNumber(num)) != null ? _ref.type : void 0) || null;
},
formatCardNumber: function(num) {
var card, groups, upperLength, _ref;
card = cardFromNumber(num);
if (!card) {
return num;
}
upperLength = card.length[card.length.length - 1];
num = num.replace(/\D/g, '');
num = num.slice(0, +upperLength + 1 || 9e9);
if (card.format.global) {
return (_ref = num.match(card.format)) != null ? _ref.join(' ') : void 0;
} else {
groups = card.format.exec(num);
if (groups != null) {
groups.shift();
}
return groups != null ? groups.join(' ') : void 0;
}
}
};
Payment.restrictNumeric = function(el) {
return J.on(el, 'keypress', restrictNumeric);
};
Payment.cardExpiryVal = function(el) {
return Payment.fns.cardExpiryVal(J.val(el));
};
Payment.formatCardCVC = function(el) {
Payment.restrictNumeric(el);
J.on(el, 'keypress', restrictCVC);
return el;
};
Payment.formatCardExpiry = function(el) {
Payment.restrictNumeric(el);
J.on(el, 'keypress', restrictExpiry);
J.on(el, 'keypress', formatExpiry);
J.on(el, 'keypress', formatForwardSlash);
J.on(el, 'keypress', formatForwardExpiry);
J.on(el, 'keydown', formatBackExpiry);
return el;
};
Payment.formatCardNumber = function(el) {
Payment.restrictNumeric(el);
J.on(el, 'keypress', restrictCardNumber);
J.on(el, 'keypress', formatCardNumber);
J.on(el, 'keydown', formatBackCardNumber);
J.on(el, 'keyup', setCardType);
J.on(el, 'paste', reFormatCardNumber);
return el;
};
return Payment;
})();
Payment.J = J;
module.exports = Payment;
global.Payment = Payment;
}).call(this,typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./utils":2}],2:[function(require,module,exports){
var J, rreturn, rtrim;
J = function(selector) {
return document.querySelectorAll(selector);
};
rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;
J.trim = function(text) {
if (text === null) {
return "";
} else {
return (text + "").replace(rtrim, "");
}
};
rreturn = /\r/g;
J.val = function(el, val) {
var ret;
if (arguments.length > 1) {
return el.value = val;
} else {
ret = el.value;
if (typeof ret === "string") {
return ret.replace(rreturn, "");
} else {
if (ret === null) {
return "";
} else {
return ret;
}
}
}
};
J.on = function(el, ev, fn) {
el.addEventListener(ev, fn);
return el;
};
J.addClass = function(el, className) {
var e;
if (el.length) {
return (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = el.length; _i < _len; _i++) {
e = el[_i];
_results.push(J.addClass(e, className));
}
return _results;
})();
}
if (el.classList) {
return el.classList.add(className);
} else {
return el.className += ' ' + className;
}
};
J.hasClass = function(el, className) {
if (el.classList) {
return el.classList.contains(className);
} else {
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
}
};
J.removeClass = function(el, className) {
var cls, e, _i, _len, _ref, _results;
if (el.length) {
return (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = el.length; _i < _len; _i++) {
e = el[_i];
_results.push(J.removeClass(e, className));
}
return _results;
})();
}
if (el.classList) {
_ref = className.split(' ');
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
cls = _ref[_i];
_results.push(el.classList.remove(cls));
}
return _results;
} else {
return el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
}
};
J.toggleClass = function(el, className, bool) {
var e;
if (el.length) {
return (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = el.length; _i < _len; _i++) {
e = el[_i];
_results.push(J.toggleClass(e, className, bool));
}
return _results;
})();
}
if (bool) {
if (!J.hasClass(el, className)) {
return J.addClass(el, className);
}
} else {
return J.removeClass(el, className);
}
};
J.trigger = function(el, name, data) {
var ev;
if (window.CustomEvent) {
ev = new CustomEvent(name, {
detail: data
});
} else {
ev = document.createEvent('CustomEvent');
if (ev.initCustomEvent) {
ev.initCustomEvent(name, true, true, data);
} else {
ev.initEvent(name, true, true, data);
}
}
return el.dispatchEvent(ev);
};
module.exports = J;
},{}]},{},[1])
\ No newline at end of file
{
"name": "payment.js",
"version": "0.0.1",
"description": "A general purpose library for building credit card forms, validating inputs and formatting numbers. Base on jquery.payment by @stripe, but without the jQuery.",
"keywords": [
"payment",
"cc",
"card"
],
"author": "Jesse Pollak",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/jessepollak/payment.git"
},
"main": "lib/payment.js",
"scripts": {
"test": "cake test"
},
"devDependencies": {
"coffee-script": "~1.7",
"jsdom": "~0.10",
"mocha": "~1.18",
"gulp": "~3.8.5",
"gulp-browserify": "~0.5.0",
"gulp-sass": "~0.7.1",
"gulp-autoprefixer": "0.0.7",
"tiny-lr": "0.0.7",
"gulp-livereload": "~1.5.0",
"nodemon": "~1.1.1",
"coffeeify": "~0.6.0",
"gulp-rename": "~1.2.0",
"gulp-changed": "~0.3.0",
"gulp-rimraf": "~0.1.0",
"gulp-connect": "~2.0.5",
"gulp-open": "~0.2.8",
"run-sequence": "~0.3.6",
"gulp-mocha": "~0.5.1"
}
}
{
"name": "payment",
"version": "0.0.1",
"title": "payment",
"description": "A general purpose library for building credit card forms, validating inputs and formatting numbers. Base on jquery.payment by @stripe, but without the jQuery.",
"keywords": [
"payment",
"cc",
"card"
],
"author": {
"name": "Jesse Pollak",
"url": "https://twitter.com/jessepollak",
"email": "contact@jessepollak.me"
},
"licenses": [
{
"type": "MIT",
"url": "https://github.com/jessepollak/payment/blob/master/LICENSE"
}
],
"homepage": "https://github.com/jessepollak/payment",
"docs": "https://github.com/jessepollak/payment",
"bugs": "https://github.com/jessepollak/payment/issues",
"demo": "http://jessepollak.github.io/payment/example",
}
J = require './utils'
# Utils
defaultFormat = /(\d{1,4})/g
cards = [
{
type: 'maestro'
pattern: /^(5018|5020|5038|6304|6759|676[1-3])/
format: defaultFormat
length: [12..19]
cvcLength: [3]
luhn: true
}
{
type: 'dinersclub'
pattern: /^(36|38|30[0-5])/
format: defaultFormat
length: [14]
cvcLength: [3]
luhn: true
}
{
type: 'laser'
pattern: /^(6706|6771|6709)/
format: defaultFormat
length: [16..19]
cvcLength: [3]
luhn: true
}
{
type: 'jcb'
pattern: /^35/
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'unionpay'
pattern: /^62/
format: defaultFormat
length: [16..19]
cvcLength: [3]
luhn: false
}
{
type: 'discover'
pattern: /^(6011|65|64[4-9]|622)/
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'mastercard'
pattern: /^5[1-5]/
format: defaultFormat
length: [16]
cvcLength: [3]
luhn: true
}
{
type: 'amex'
pattern: /^3[47]/
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
length: [15]
cvcLength: [3..4]
luhn: true
}
{
type: 'visa'
pattern: /^4/
format: defaultFormat
length: [13..16]
cvcLength: [3]
luhn: true
}
]
cardFromNumber = (num) ->
num = (num + '').replace(/\D/g, '')
return card for card in cards when card.pattern.test(num)
cardFromType = (type) ->
return card for card in cards when card.type is type
luhnCheck = (num) ->
odd = true
sum = 0
digits = (num + '').split('').reverse()
for digit in digits
digit = parseInt(digit, 10)
digit *= 2 if (odd = !odd)
digit -= 9 if digit > 9
sum += digit
sum % 10 == 0
hasTextSelected = (target) ->
# If some text is selected
return true if target.selectionStart? and
target.selectionStart isnt target.selectionEnd
# If some text is selected in IE
return true if document?.selection?.createRange?().text
false
# Private
# Format Card Number
reFormatCardNumber = (e) ->
setTimeout =>
target = e.currentTarget
value = J.val(target)
value = Payment.fns.formatCardNumber(value)
J.val(target, value)
formatCardNumber = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
target = e.currentTarget
value = J.val(target)
card = cardFromNumber(value + digit)
length = (value.replace(/\D/g, '') + digit).length
upperLength = 16
upperLength = card.length[card.length.length - 1] if card
return if length >= upperLength
# Return if focus isn't at the end of the text
return if target.selectionStart? and
target.selectionStart isnt value.length
if card && card.type is 'amex'
# Amex cards are formatted differently
re = /^(\d{4}|\d{4}\s\d{6})$/
else
re = /(?:^|\s)(\d{4})$/
# If '4242' + 4
if re.test(value)
e.preventDefault()
J.val(target, value + ' ' + digit)
# If '424' + 2
else if re.test(value + digit)
e.preventDefault()
J.val(target, value + digit + ' ')
formatBackCardNumber = (e) ->
target = e.currentTarget
value = J.val(target)
return if e.meta
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if target.selectionStart? and
target.selectionStart isnt value.length
# Remove the trailing space
if /\d\s$/.test(value)
e.preventDefault()
J.val(target, value.replace(/\d\s$/, ''))
else if /\s\d?$/.test(value)
e.preventDefault()
J.val(target, value.replace(/\s\d?$/, ''))
# Format Expiry
formatExpiry = (e) ->
# Only format if input is a number
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
target = e.currentTarget
val = J.val(target) + digit
if /^\d$/.test(val) and val not in ['0', '1']
e.preventDefault()
J.val(target, "0#{val} / ")
else if /^\d\d$/.test(val)
e.preventDefault()
J.val(target, "#{val} / ")
formatForwardExpiry = (e) ->
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
target = e.currentTarget
val = J.val(target)
if /^\d\d$/.test(val)
J.val(target, "#{val} / ")
formatForwardSlash = (e) ->
slash = String.fromCharCode(e.which)
return unless slash is '/'
target = e.currentTarget
val = J.val(target)
if /^\d$/.test(val) and val isnt '0'
J.val(target, "0#{val} / ")
formatBackExpiry = (e) ->
# If shift+backspace is pressed
return if e.metaKey
target = e.currentTarget
value = J.val(target)
# Return unless backspacing
return unless e.which is 8
# Return if focus isn't at the end of the text
return if target.selectionStart? and
target.selectionStart isnt value.length
# Remove the trailing space
if /\d(\s|\/)+$/.test(value)
e.preventDefault()
J.val(target, value.replace(/\d(\s|\/)*$/, ''))
else if /\s\/\s?\d?$/.test(value)
e.preventDefault()
J.val(target, value.replace(/\s\/\s?\d?$/, ''))
# Restrictions
restrictNumeric = (e) ->
# Key event is for a browser shortcut
return true if e.metaKey or e.ctrlKey
# If keycode is a space
return e.preventDefault() if e.which is 32
# If keycode is a special char (WebKit)
return true if e.which is 0
# If char is a special char (Firefox)
return true if e.which < 33
input = String.fromCharCode(e.which)
# Char is a number or a space
return e.preventDefault() if !/[\d\s]/.test(input)
restrictCardNumber = (e) ->
target = e.currentTarget
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected(target)
# Restrict number of digits
value = (J.val(target) + digit).replace(/\D/g, '')
card = cardFromNumber(value)
if card
e.preventDefault() unless value.length <= card.length[card.length.length - 1]
else
# All other cards are 16 digits long
e.preventDefault() unless value.length <= 16
restrictExpiry = (e) ->
target = e.currentTarget
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
return if hasTextSelected(target)
value = J.val(target) + digit
value = value.replace(/\D/g, '')
return e.preventDefault() if value.length > 6
restrictCVC = (e) ->
target = e.currentTarget
digit = String.fromCharCode(e.which)
return unless /^\d+$/.test(digit)
val = J.val(target) + digit
return e.preventDefault() unless val.length <= 4
setCardType = (e) ->
target = e.currentTarget
val = J.val(target)
cardType = Payment.fns.cardType(val) or 'unknown'
unless J.hasClass(target, cardType)
allTypes = (card.type for card in cards)
J.removeClass target, 'unknown'
J.removeClass target, allTypes.join(' ')
J.addClass target, cardType
J.toggleClass target, 'identified', cardType isnt 'unknown'
J.trigger target, 'payment.cardType', cardType
# Public
class Payment
@fns:
cardExpiryVal: (value) ->
value = value.replace(/\s/g, '')
[month, year] = value.split('/', 2)
# Allow for year shortcut
if year?.length is 2 and /^\d+$/.test(year)
prefix = (new Date).getFullYear()
prefix = prefix.toString()[0..1]
year = prefix + year
month = parseInt(month, 10)
year = parseInt(year, 10)
month: month, year: year
validateCardNumber: (num) ->
num = (num + '').replace(/\s+|-/g, '')
return false unless /^\d+$/.test(num)
card = cardFromNumber(num)
return false unless card
num.length in card.length and
(card.luhn is false or luhnCheck(num))
validateCardExpiry: (month, year) ->
# Allow passing an object
if typeof month is 'object' and 'month' of month
{month, year} = month
return false unless month and year
month = J.trim(month)
year = J.trim(year)
return false unless /^\d+$/.test(month)
return false unless /^\d+$/.test(year)
return false unless parseInt(month, 10) <= 12
if year.length is 2
prefix = (new Date).getFullYear()
prefix = prefix.toString()[0..1]
year = prefix + year
expiry = new Date(year, month)
currentTime = new Date
# Months start from 0 in JavaScript
expiry.setMonth(expiry.getMonth() - 1)
# The cc expires at the end of the month,
# so we need to make the expiry the first day
# of the month after
expiry.setMonth(expiry.getMonth() + 1, 1)
expiry > currentTime
validateCardCVC: (cvc, type) ->
cvc = J.trim(cvc)
return false unless /^\d+$/.test(cvc)
if type
# Check against a explicit card type
cvc.length in cardFromType(type)?.cvcLength
else
# Check against all types
cvc.length >= 3 and cvc.length <= 4
cardType: (num) ->
return null unless num
cardFromNumber(num)?.type or null
formatCardNumber: (num) ->
card = cardFromNumber(num)
return num unless card
upperLength = card.length[card.length.length - 1]
num = num.replace(/\D/g, '')
num = num[0..upperLength]
if card.format.global
num.match(card.format)?.join(' ')
else
groups = card.format.exec(num)
groups?.shift()
groups?.join(' ')
@restrictNumeric: (el) ->
J.on el, 'keypress', restrictNumeric
@cardExpiryVal: (el) ->
Payment.fns.cardExpiryVal(J.val(el))
@formatCardCVC: (el) ->
Payment.restrictNumeric el
J.on el, 'keypress', restrictCVC
el
@formatCardExpiry: (el) ->
Payment.restrictNumeric el
J.on el, 'keypress', restrictExpiry
J.on el, 'keypress', formatExpiry
J.on el, 'keypress', formatForwardSlash
J.on el, 'keypress', formatForwardExpiry
J.on el, 'keydown', formatBackExpiry
el
@formatCardNumber: (el) ->
Payment.restrictNumeric el
J.on el, 'keypress', restrictCardNumber
J.on el, 'keypress', formatCardNumber
J.on el, 'keydown', formatBackCardNumber
J.on el, 'keyup', setCardType
J.on el, 'paste', reFormatCardNumber
el
Payment.J = J
module.exports = Payment
global.Payment = Payment
\ No newline at end of file
J = (selector) ->
document.querySelectorAll selector
rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g
J.trim = (text) ->
if text == null then "" else (text + "").replace rtrim, ""
rreturn = /\r/g
J.val = (el, val) ->
if arguments.length > 1
el.value = val
else
ret = el.value
if typeof(ret) == "string"
ret.replace(rreturn, "")
else
if ret == null then "" else ret
J.on = (el, ev, fn) ->
el.addEventListener ev, fn
el
J.addClass = (el, className) ->
return (J.addClass(e, className) for e in el) if el.length
if (el.classList)
el.classList.add(className)
else
el.className += ' ' + className
J.hasClass = (el, className) ->
if (el.classList)
el.classList.contains(className)
else
new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className)
J.removeClass = (el, className) ->
return (J.removeClass(e, className) for e in el) if el.length
if (el.classList)
for cls in className.split(' ')
el.classList.remove(cls)
else
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ')
J.toggleClass = (el, className, bool) ->
return (J.toggleClass(e, className, bool) for e in el) if el.length
if bool
J.addClass(el, className) unless J.hasClass(el, className)
else
J.removeClass el, className
J.trigger = (el, name, data) ->
if (window.CustomEvent)
ev = new CustomEvent(name, {detail: data})
else
ev = document.createEvent('CustomEvent')
# jsdom doesn't have initCustomEvent, so we need this check for
# testing
if ev.initCustomEvent
ev.initCustomEvent name, true, true, data
else
ev.initEvent name, true, true, data
el.dispatchEvent(ev)
module.exports = J
\ No newline at end of file
assert = require('assert')
window = require('jsdom').jsdom().createWindow()
global.window = window
global.document = window.document
Payment = require('../src/payment')
J = require('../src/utils')
describe 'jquery.payment', ->
describe 'Validating a card number', ->
it 'should fail if empty', ->
topic = Payment.fns.validateCardNumber ''
assert.equal topic, false
it 'should fail if is a bunch of spaces', ->
topic = Payment.fns.validateCardNumber ' '
assert.equal topic, false
it 'should success if is valid', ->
topic = Payment.fns.validateCardNumber '4242424242424242'
assert.equal topic, true
it 'that has dashes in it but is valid', ->
topic = Payment.fns.validateCardNumber '4242-4242-4242-4242'
assert.equal topic, true
it 'should succeed if it has spaces in it but is valid', ->
topic = Payment.fns.validateCardNumber '4242 4242 4242 4242'
assert.equal topic, true
it 'that does not pass the luhn checker', ->
topic = Payment.fns.validateCardNumber '4242424242424241'
assert.equal topic, false
it 'should fail if is more than 16 digits', ->
topic = Payment.fns.validateCardNumber '42424242424242424'
assert.equal topic, false
it 'should fail if is less than 10 digits', ->
topic = Payment.fns.validateCardNumber '424242424'
assert.equal topic, false
it 'should fail with non-digits', ->
topic = Payment.fns.validateCardNumber '4242424e42424241'
assert.equal topic, false
it 'should validate for all card types', ->
assert(Payment.fns.validateCardNumber('378282246310005'), 'amex')
assert(Payment.fns.validateCardNumber('371449635398431'), 'amex')
assert(Payment.fns.validateCardNumber('378734493671000'), 'amex')
assert(Payment.fns.validateCardNumber('30569309025904'), 'dinersclub')
assert(Payment.fns.validateCardNumber('38520000023237'), 'dinersclub')
assert(Payment.fns.validateCardNumber('6011111111111117'), 'discover')
assert(Payment.fns.validateCardNumber('6011000990139424'), 'discover')
assert(Payment.fns.validateCardNumber('3530111333300000'), 'jcb')
assert(Payment.fns.validateCardNumber('3566002020360505'), 'jcb')
assert(Payment.fns.validateCardNumber('5555555555554444'), 'mastercard')
assert(Payment.fns.validateCardNumber('4111111111111111'), 'visa')
assert(Payment.fns.validateCardNumber('4012888888881881'), 'visa')
assert(Payment.fns.validateCardNumber('4222222222222'), 'visa')
assert(Payment.fns.validateCardNumber('6759649826438453'), 'maestro')
assert(Payment.fns.validateCardNumber('6271136264806203568'), 'unionpay')
assert(Payment.fns.validateCardNumber('6236265930072952775'), 'unionpay')
assert(Payment.fns.validateCardNumber('6204679475679144515'), 'unionpay')
assert(Payment.fns.validateCardNumber('6216657720782466507'), 'unionpay')
describe 'Validating a CVC', ->
it 'should fail if is empty', ->
topic = Payment.fns.validateCardCVC ''
assert.equal topic, false
it 'should pass if is valid', ->
topic = Payment.fns.validateCardCVC '123'
assert.equal topic, true
it 'should fail with non-digits', ->
topic = Payment.fns.validateCardNumber '12e'
assert.equal topic, false
it 'should fail with less than 3 digits', ->
topic = Payment.fns.validateCardNumber '12'
assert.equal topic, false
it 'should fail with more than 4 digits', ->
topic = Payment.fns.validateCardNumber '12345'
assert.equal topic, false
describe 'Validating an expiration date', ->
it 'should fail expires is before the current year', ->
currentTime = new Date()
topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() - 1
assert.equal topic, false
it 'that expires in the current year but before current month', ->
currentTime = new Date()
topic = Payment.fns.validateCardExpiry currentTime.getMonth(), currentTime.getFullYear()
assert.equal topic, false
it 'that has an invalid month', ->
currentTime = new Date()
topic = Payment.fns.validateCardExpiry 13, currentTime.getFullYear()
assert.equal topic, false
it 'that is this year and month', ->
currentTime = new Date()
topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear()
assert.equal topic, true
it 'that is just after this month', ->
# Remember - months start with 0 in JavaScript!
currentTime = new Date()
topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear()
assert.equal topic, true
it 'that is after this year', ->
currentTime = new Date()
topic = Payment.fns.validateCardExpiry currentTime.getMonth() + 1, currentTime.getFullYear() + 1
assert.equal topic, true
it 'that has string numbers', ->
currentTime = new Date()
currentTime.setFullYear(currentTime.getFullYear() + 1, currentTime.getMonth() + 2)
topic = Payment.fns.validateCardExpiry currentTime.getMonth() + '', currentTime.getFullYear() + ''
assert.equal topic, true
it 'that has non-numbers', ->
topic = Payment.fns.validateCardExpiry 'h12', '3300'
assert.equal topic, false
it 'should fail if year or month is NaN', ->
topic = Payment.fns.validateCardExpiry '12', NaN
assert.equal topic, false
it 'should support year shorthand', ->
assert.equal Payment.fns.validateCardExpiry('05', '20'), true
describe 'Validating a CVC number', ->
it 'should validate a three digit number with no card type', ->
topic = Payment.fns.validateCardCVC('123')
assert.equal topic, true
it 'should validate a three digit number with card type amex', ->
topic = Payment.fns.validateCardCVC('123', 'amex')
assert.equal topic, true
it 'should validate a three digit number with card type other than amex', ->
topic = Payment.fns.validateCardCVC('123', 'visa')
assert.equal topic, true
it 'should not validate a four digit number with a card type other than amex', ->
topic = Payment.fns.validateCardCVC('1234', 'visa')
assert.equal topic, false
it 'should validate a four digit number with card type amex', ->
topic = Payment.fns.validateCardCVC('1234', 'amex')
assert.equal topic, true
it 'should not validate a number larger than 4 digits', ->
topic = Payment.fns.validateCardCVC('12344')
assert.equal topic, false
describe 'Parsing an expiry value', ->
it 'should parse string expiry', ->
topic = Payment.fns.cardExpiryVal('03 / 2025')
assert.deepEqual topic, month: 3, year: 2025
it 'should support shorthand year', ->
topic = Payment.fns.cardExpiryVal('05/04')
assert.deepEqual topic, month: 5, year: 2004
it 'should return NaN when it cannot parse', ->
topic = Payment.fns.cardExpiryVal('05/dd')
assert isNaN(topic.year)
describe 'Getting a card type', ->
it 'should return Visa that begins with 40', ->
topic = Payment.fns.cardType '4012121212121212'
assert.equal topic, 'visa'
it 'that begins with 5 should return MasterCard', ->
topic = Payment.fns.cardType '5555555555554444'
assert.equal topic, 'mastercard'
it 'that begins with 34 should return American Express', ->
topic = Payment.fns.cardType '3412121212121212'
assert.equal topic, 'amex'
it 'that is not numbers should return null', ->
topic = Payment.fns.cardType 'aoeu'
assert.equal topic, null
it 'that has unrecognized beginning numbers should return null', ->
topic = Payment.fns.cardType 'aoeu'
assert.equal topic, null
it 'should return correct type for all test numbers', ->
assert.equal(Payment.fns.cardType('378282246310005'), 'amex')
assert.equal(Payment.fns.cardType('371449635398431'), 'amex')
assert.equal(Payment.fns.cardType('378734493671000'), 'amex')
assert.equal(Payment.fns.cardType('30569309025904'), 'dinersclub')
assert.equal(Payment.fns.cardType('38520000023237'), 'dinersclub')
assert.equal(Payment.fns.cardType('6011111111111117'), 'discover')
assert.equal(Payment.fns.cardType('6011000990139424'), 'discover')
assert.equal(Payment.fns.cardType('3530111333300000'), 'jcb')
assert.equal(Payment.fns.cardType('3566002020360505'), 'jcb')
assert.equal(Payment.fns.cardType('5555555555554444'), 'mastercard')
assert.equal(Payment.fns.cardType('4111111111111111'), 'visa')
assert.equal(Payment.fns.cardType('4012888888881881'), 'visa')
assert.equal(Payment.fns.cardType('4222222222222'), 'visa')
assert.equal(Payment.fns.cardType('6759649826438453'), 'maestro')
assert.equal(Payment.fns.cardType('6271136264806203568'), 'unionpay')
assert.equal(Payment.fns.cardType('6236265930072952775'), 'unionpay')
assert.equal(Payment.fns.cardType('6204679475679144515'), 'unionpay')
assert.equal(Payment.fns.cardType('6216657720782466507'), 'unionpay')
describe 'formatCardNumber', ->
it 'should restrict non-number characters', ->
number = document.createElement('input')
number.type = 'text'
J.val number, '4242'
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "a".charCodeAt(0)
number.dispatchEvent(ev)
assert.equal J.val(number), '4242'
it 'should restrict characters when the card is number 16 characters', ->
number = document.createElement('input')
number.type = 'text'
J.val(number, '4242 4242 4242 4242')
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "4".charCodeAt(0)
number.dispatchEvent(ev)
assert.equal J.val(number), '4242 4242 4242 4242'
it 'should format cc number correctly', ->
number = document.createElement('input')
number.type = 'text'
J.val(number, '4242')
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = 52
number.dispatchEvent(ev)
assert.equal J.val(number), '4242 4'
it 'should remove a trailing space before a number on backspace ', ->
number = document.createElement('input')
number.type = 'text'
J.val(number, '4242 ')
number.selectionStart = 5
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keydown", true, true
ev.eventName = "keydown"
ev.which = 8
number.dispatchEvent(ev)
assert.equal J.val(number), '424'
it 'should remove the space after a number being deleted on a backspace', ->
number = document.createElement('input')
number.type = 'text'
J.val(number, '4242 5')
number.selectionStart = 6
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keydown", true, true
ev.eventName = "keydown"
ev.which = 8
number.dispatchEvent(ev)
assert.equal J.val(number), '4242'
it 'should set the card type correctly', ->
number = document.createElement('input')
number.type = 'text'
J.val(number, '4')
Payment.formatCardNumber(number)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keyup", true, true
ev.eventName = "keyup"
number.dispatchEvent(ev)
assert J.hasClass(number, 'visa')
assert J.hasClass(number, 'identified')
J.val(number, '')
ev = document.createEvent "HTMLEvents"
ev.initEvent "keyup", true, true
ev.eventName = "keyup"
number.dispatchEvent(ev)
# JSDom doesn't support custom events right now, so this testing
# doesn't work :(
#
# eventCalled = false
# number.addEventListener 'payment.cardType', ->
# console.log 'hiya'
# eventCalled = true
# assert eventCalled
assert J.hasClass(number, 'unknown')
assert !J.hasClass(number, 'identified')
it 'should format correctly on paste', ->
number = document.createElement('input')
number.type = 'text'
Payment.formatCardNumber(number)
J.val(number, '42424')
ev = document.createEvent "HTMLEvents"
ev.initEvent "paste", true, true
ev.eventName = "paste"
number.dispatchEvent(ev)
# must setTimeout because paste event handling is
# done in a setTimeout
setTimeout ->
assert.equal J.val(number), '4242 4'
describe 'formatCardExpiry', ->
it 'should add a slash after two numbers', ->
expiry = document.createElement('input')
expiry.type = "text"
J.val(expiry, '1')
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "1".charCodeAt(0)
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '11 / '
it 'should format add a 0 and slash to a number > 1 correctly', ->
expiry = document.createElement('input')
expiry.type = "text"
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "4".charCodeAt(0)
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '04 / '
it 'should format forward slash shorthand correctly', ->
expiry = document.createElement('input')
expiry.type = "text"
J.val(expiry, '1')
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "/".charCodeAt(0)
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '01 / '
it 'should only allow numbers', ->
expiry = document.createElement('input')
expiry.type = "text"
J.val(expiry, '1')
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "d".charCodeAt(0)
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '1'
it 'should remove spaces trailing space and / after removing a number', ->
expiry = document.createElement('input')
expiry.type = "text"
J.val(expiry, '12 / 1')
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keydown", true, true
ev.eventName = "keydown"
ev.which = 8 # backspace
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '12'
it 'should a number after removing a space and a /', ->
expiry = document.createElement('input')
expiry.type = "text"
J.val(expiry, '12 / ')
Payment.formatCardExpiry(expiry)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keydown", true, true
ev.eventName = "keydown"
ev.which = 8 # backspace
expiry.dispatchEvent(ev)
assert.equal J.val(expiry), '1'
describe 'formatCVC', ->
it 'should allow only numbers', ->
cvc = document.createElement('input')
cvc.type = "text"
J.val(cvc, '1')
Payment.formatCardCVC(cvc)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "d".charCodeAt(0)
cvc.dispatchEvent(ev)
assert.equal J.val(cvc), '1'
it 'should restrict to length <= 4', ->
cvc = document.createElement('input')
cvc.type = "text"
J.val(cvc, '1234')
Payment.formatCardCVC(cvc)
ev = document.createEvent "HTMLEvents"
ev.initEvent "keypress", true, true
ev.eventName = "keypress"
ev.which = "1".charCodeAt(0)
cvc.dispatchEvent(ev)
assert.equal J.val(cvc), '1234'
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment