vue-credit-card-validation
Version:
A Vue plugin for formatting and validating credit card forms to offer the same functionality as Stripe's jquery.payment library (and more!).
398 lines (318 loc) • 12.8 kB
JavaScript
import { default as cards } from './cards.js';
import { default as validation } from './validation.js';
const cardFormatUtils = {
cardFromNumber : function (num) {
num = (num + '').replace(/\D/g, '');
for (let i in cards) {
for (let j in cards[i].patterns) {
let p = cards[i].patterns[j] + '';
if (num.substr(0, p.length) === p) { return cards[i]; }
}
}
},
cardFromType: function (type) {
for (let i in cards) { if (cards[i].type === type) { return cards[i]; } }
},
luhnCheck: function (num) {
let odd = true;
let sum = 0;
let digits = (num + '').split('').reverse();
for (let i in digits) {
let digit = parseInt(digits[i], 10);
if (odd = !odd) { digit *= 2; }
if (digit > 9) { digit -= 9; }
sum += digit;
}
return (sum % 10) === 0;
},
hasTextSelected: function (target) {
// If some text is selected
if ((target.selectionStart != null) &&
(target.selectionStart !== target.selectionEnd)) { return true; }
// If some text is selected in IE
if (cardFormatUtils.__guard__(typeof document !== 'undefined' && document !== null ? document.selection : undefined, x => x.createRange) != null) {
if (document.selection.createRange().text) { return true; }
}
return false;
},
// Private
// Safe Val
safeVal: function (value, target, e) {
if (e.inputType === 'deleteContentBackward') {
return;
}
let cursor;
try {
cursor = target.selectionStart;
} catch (error) {
cursor = null;
}
let last = target.value;
target.value = value;
value = target.value;
if ((cursor !== null) && document.activeElement == target) {
if (cursor === last.length) { cursor = target.value.length; }
// This hack looks for scenarios where we are changing an input's value such
// that "X| " is replaced with " |X" (where "|" is the cursor). In those
// scenarios, we want " X|".
//
// For example:
// 1. Input field has value "4444| "
// 2. User types "1"
// 3. Input field has value "44441| "
// 4. Reformatter changes it to "4444 |1"
// 5. By incrementing the cursor, we make it "4444 1|"
//
// This is awful, and ideally doesn't go here, but given the current design
// of the system there does not appear to be a better solution.
//
// Note that we can't just detect when the cursor-1 is " ", because that
// would incorrectly increment the cursor when backspacing, e.g. pressing
// backspace in this scenario: "4444 1|234 5".
if (last !== value) {
let prevPair = last.slice(cursor - 1, +cursor + 1 || undefined);
let currPair = target.value.slice(cursor - 1, +cursor + 1 || undefined);
let digit = value[cursor];
if (/\d/.test(digit) &&
(prevPair === `${digit} `) && (currPair === ` ${digit}`)) { cursor = cursor + 1; }
}
target.selectionStart = cursor;
return target.selectionEnd = cursor;
}
},
// Replace Full-Width Chars
replaceFullWidthChars: function (str) {
if (str == null) { str = ''; }
let fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
let halfWidth = '0123456789';
let value = '';
let chars = str.split('');
// Avoid using reserved word `char`
for (let i in chars) {
let idx = fullWidth.indexOf(chars[i]);
if (idx > -1) {
chars[i] = halfWidth[idx];
}
value += chars[i];
}
return value;
},
// Format Numeric
reFormatNumeric: function (e) {
let target = e.currentTarget;
return setTimeout(function () {
let value = target.value;
value = cardFormatUtils.replaceFullWidthChars(value);
value = value.replace(/\D/g, '');
return cardFormatUtils.safeVal(value, target, e);
});
},
// Format Card Number
reFormatCardNumber: function (e) {
let target = e.currentTarget;
return setTimeout(() => {
let value = target.value;
value = cardFormatUtils.replaceFullWidthChars(value);
value = validation.formatCardNumber(value);
return cardFormatUtils.safeVal(value, target, e);
});
},
formatCardNumber: function (e) {
// Only format if input is a number
let re;
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
let target = e.currentTarget;
let value = target.value;
let card = cardFormatUtils.cardFromNumber(value + digit);
let length = (value.replace(/\D/g, '') + digit);
let upperLength = 16;
if (card) { upperLength = card.length[card.length.length - 1]; }
if (length >= upperLength) { return; }
// Return if focus isn't at the end of the text
if ((target.selectionStart != null) &&
(target.selectionStart !== value.length)) { return; }
if (card && (card.type === '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 + digit)) {
e.preventDefault();
return setTimeout(() => target.value = value + ' ' + digit);
// If '424' + 2
} else if (re.test(value + digit)) {
e.preventDefault();
return setTimeout(() => target.value = value + digit + ' ');
}
},
formatBackCardNumber: function (e) {
let target = e.currentTarget;
let value = target.value;
// Return unless backspacing
if (e.which !== 8) { return; }
// Return if focus isn't at the end of the text
if ((target.selectionStart != null) &&
(target.selectionStart !== value.length)) { return; }
// Remove the digit + trailing space
if (/\d\s$/.test(value)) {
e.preventDefault();
return setTimeout(() => target.value = value.replace(/\d\s$/, ''));
// Remove digit if ends in space + digit
} else if (/\s\d?$/.test(value)) {
e.preventDefault();
return setTimeout(() => target.value = value.replace(/\d$/, ''));
}
},
// Format Expiry
reFormatExpiry: function (e) {
let target = e.currentTarget;
return setTimeout(function () {
let value = target.value;
value = cardFormatUtils.replaceFullWidthChars(value);
value = validation.formatExpiry(value);
return cardFormatUtils.safeVal(value, target, e);
});
},
formatExpiry: function (e) {
// Only format if input is a number
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
let target = e.currentTarget;
let val = target.value + digit;
if (/^\d$/.test(val) && !['0', '1'].includes(val)) {
e.preventDefault();
return setTimeout(() => target.value = (`0${val} / `));
} else if (/^\d\d$/.test(val)) {
e.preventDefault();
return setTimeout(function () {
// Split for months where we have the second digit > 2 (past 12) and turn
// that into (m1)(m2) => 0(m1) / (m2)
let m1 = parseInt(val[0], 10);
let m2 = parseInt(val[1], 10);
if ((m2 > 2) && (m1 !== 0)) {
return target.value = (`0${m1} / ${m2}`);
} else {
return target.value = (`${val} / `);
}
});
}
},
formatForwardExpiry: function (e) {
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
let target = e.currentTarget;
let val = target.value;
if (/^\d\d$/.test(val)) {
return target.value = (`${val} / `);
}
},
formatForwardSlashAndSpace: function (e) {
let which = String.fromCharCode(e.which);
if ((which !== '/') && (which !== ' ')) { return; }
let target = e.currentTarget;
let val = target.value;
if (/^\d$/.test(val) && (val !== '0')) {
return target.value = (`0${val} / `);
}
},
formatBackExpiry: function (e) {
let target = e.currentTarget;
let value = target.value;
// Return unless backspacing
if (e.which !== 8) { return; }
// Return if focus isn't at the end of the text
if ((target.selectionStart != null) &&
(target.selectionStart !== value.length)) { return; }
// Remove the trailing space + last digit
if (/\d\s\/\s$/.test(value)) {
e.preventDefault();
return setTimeout(() => target.value = value.replace(/\d\s\/\s$/, ''));
}
},
// Adds maxlength to Expiry field
handleExpiryAttributes: function(e){
e.setAttribute('maxlength', 9);
},
// Format CVC
reFormatCVC: function (e) {
let target = e.currentTarget;
return setTimeout(function () {
let value = target.value;
value = cardFormatUtils.replaceFullWidthChars(value);
value = value.replace(/\D/g, '').slice(0, 4);
return cardFormatUtils.safeVal(value, target, e);
});
},
// Restrictions
restrictNumeric: function (e) {
// Key event is for a browser shortcut
if (e.metaKey || e.ctrlKey) { return true; }
// If keycode is a space
if (e.which === 32) { return false; }
// If keycode is a special char (WebKit)
if (e.which === 0) { return true; }
// If char is a special char (Firefox)
if (e.which < 33) { return true; }
let input = String.fromCharCode(e.which);
// Char is a number or a space
return (!!/[\d\s]/.test(input)) ? true : e.preventDefault();
},
restrictCardNumber: function (e) {
let target = e.currentTarget;
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
if (cardFormatUtils.hasTextSelected(target)) { return; }
// Restrict number of digits
let value = (target.value + digit).replace(/\D/g, '');
let card = cardFormatUtils.cardFromNumber(value);
if (card) {
return value.length <= card.length[card.length.length - 1];
} else {
// All other cards are 16 digits long
return value.length <= 16;
}
},
restrictExpiry: function (e) {
let target = e.currentTarget;
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
if (cardFormatUtils.hasTextSelected(target)) { return; }
let value = target.value + digit;
value = value.replace(/\D/g, '');
if (value.length > 6) { return false; }
},
restrictCVC: function (e) {
let target = e.currentTarget;
let digit = String.fromCharCode(e.which);
if (!/^\d+$/.test(digit)) { return; }
if (cardFormatUtils.hasTextSelected(target)) { return; }
let val = target.value + digit;
return val.length <= 4;
},
setCardType: function (e) {
let target = e.currentTarget;
let val = target.value;
let cardType = validation.cardType(val) || 'unknown';
if (target.className.indexOf(cardType) === -1) {
let allTypes = [];
for(let i in cards){
allTypes.push(cards[i].type);
}
target.classList.remove('unknown');
target.classList.remove('identified');
target.classList.remove(... allTypes);
target.classList.add(cardType);
target.dataset.cardBrand = cardType;
if(cardType !== 'unknown'){
target.classList.add('identified');
}
}
},
__guard__: function (value, transform) {
return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
}
};
export default cardFormatUtils;