require '../scss/card.scss'
QJ = require 'qj'
payment = require 'payment'
extend = require 'node.extend'
class Card
initializedDataAttr: "data-jp-card-initialized"
cardTemplate: '' +
'
' +
'
' +
'
' +
'
' +
'
Visa
' +
'
' +
'
Mastercard
' +
'
Maestro
' +
'
' +
'
discover
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
{{cvc}}
' +
'
{{number}}
' +
'
{{name}}
' +
'
{{expiry}}
' +
'
' +
'
' +
'
' +
'
' +
'
{{cvc}}
' +
'
' +
'
' +
'
' +
'
'
template: (tpl, data) ->
tpl.replace /\{\{(.*?)\}\}/g, (match, key, str) ->
data[key]
cardTypes: [
'jp-card-amex',
'jp-card-dankort',
'jp-card-dinersclub',
'jp-card-discover',
'jp-card-jcb',
'jp-card-laser',
'jp-card-maestro',
'jp-card-mastercard',
'jp-card-unionpay',
'jp-card-visa',
'jp-card-visaelectron',
'jp-card-elo'
]
defaults:
formatting: true
formSelectors:
numberInput: 'input[name="number"]'
expiryInput: 'input[name="expiry"]'
cvcInput: 'input[name="cvc"]'
nameInput: 'input[name="name"]'
cardSelectors:
cardContainer: '.jp-card-container'
card: '.jp-card'
numberDisplay: '.jp-card-number'
expiryDisplay: '.jp-card-expiry'
cvcDisplay: '.jp-card-cvc'
nameDisplay: '.jp-card-name'
messages:
validDate: 'valid\nthru'
monthYear: 'month/year'
placeholders:
number: '•••• •••• •••• ••••'
cvc: '•••'
expiry: '••/••'
name: 'Full Name'
masks:
cardNumber: false
classes:
valid: 'jp-card-valid'
invalid: 'jp-card-invalid'
debug: false
constructor: (opts) ->
@options = extend(true, @defaults, opts)
unless @options.form
console.log "Please provide a form"
return
@$el = QJ(@options.form)
unless @options.container
console.log "Please provide a container"
return
@$container = QJ(@options.container)
# set a data attribute to ensure that card is only ever initialized
# once on a given container
toInitialize = if QJ.isDOMElement(@$container) then @$container else @$container[0]
return if toInitialize.getAttribute(@initializedDataAttr)
toInitialize.setAttribute(@initializedDataAttr, true)
@render()
@attachHandlers()
@handleInitialPlaceholders()
render: ->
QJ.append(@$container, @template(
@cardTemplate,
extend({}, @options.messages, @options.placeholders)
))
for name, selector of @options.cardSelectors
this["$#{name}"] = QJ.find(@$container, selector)
for name, selector of @options.formSelectors
selector = if @options[name] then @options[name] else selector
obj = QJ.find(@$el, selector)
console.error "Card can't find a #{name} in your form." if !obj.length and @options.debug
this["$#{name}"] = obj
if @options.formatting
Payment.formatCardNumber(@$numberInput)
Payment.formatCardCVC(@$cvcInput)
Payment.formatCardExpiry(@$expiryInput)
if @options.width
$cardContainer = QJ(@options.cardSelectors.cardContainer)[0]
baseWidth = parseInt($cardContainer.clientWidth || window.getComputedStyle($cardContainer).width)
$cardContainer.style.transform = "scale(#{@options.width / baseWidth})"
# safari can't handle transparent radial gradient right now
if navigator?.userAgent
ua = navigator.userAgent.toLowerCase()
if ua.indexOf('safari') != -1 and ua.indexOf('chrome') == -1
QJ.addClass @$card, 'jp-card-safari'
if (/MSIE 10\./i.test(navigator.userAgent))
QJ.addClass @$card, 'jp-card-ie-10'
# ie 11 does not support conditional compilation, use user agent instead
if (/rv:11.0/i.test(navigator.userAgent))
QJ.addClass @$card, 'jp-card-ie-11'
attachHandlers: ->
numberInputFilters = [@validToggler('cardNumber')]
numberInputFilters.push(@maskCardNumber) if @options.masks.cardNumber
bindVal @$numberInput, @$numberDisplay,
fill: false,
filters: numberInputFilters
QJ.on @$numberInput, 'payment.cardType', @handle('setCardType')
expiryFilters = [(val) -> val.replace /(\s+)/g, '']
expiryFilters.push @validToggler('cardExpiry')
bindVal @$expiryInput, @$expiryDisplay,
join: (text) ->
if text[0].length == 2 or text[1] then "/" else ""
filters: expiryFilters
bindVal @$cvcInput, @$cvcDisplay, filters: @validToggler('cardCVC')
QJ.on @$cvcInput, 'focus', @handle('flipCard')
QJ.on @$cvcInput, 'blur', @handle('unflipCard')
bindVal @$nameInput, @$nameDisplay,
fill: false
filters: @validToggler('cardHolderName')
join: ' '
handleInitialPlaceholders: ->
for name, selector of @options.formSelectors
el = this["$#{name}"]
if QJ.val(el)
# if the input has a value, we want to trigger a refresh
QJ.trigger el, 'paste'
# set a timeout because `jquery.payment` does the reset of the val
# in a timeout
setTimeout -> QJ.trigger el, 'keyup'
handle: (fn) ->
(e) =>
args = Array.prototype.slice.call arguments
args.unshift e.target
@handlers[fn].apply this, args
validToggler: (validatorName) ->
if validatorName == "cardExpiry"
isValid = (val) ->
objVal = Payment.fns.cardExpiryVal val
Payment.fns.validateCardExpiry objVal.month, objVal.year
else if validatorName == "cardCVC"
isValid = (val) => Payment.fns.validateCardCVC val, @cardType
else if validatorName == "cardNumber"
isValid = (val) -> Payment.fns.validateCardNumber val
else if validatorName == "cardHolderName"
isValid = (val) -> val != ""
(val, $in, $out) =>
result = isValid val
@toggleValidClass $in, result
@toggleValidClass $out, result
val
toggleValidClass: (el, test) ->
QJ.toggleClass el, @options.classes.valid, test
QJ.toggleClass el, @options.classes.invalid, !test
maskCardNumber: (val, el, out) =>
mask = @options.masks.cardNumber
numbers = val.split(' ')
if numbers.length >= 3
numbers.forEach (item, idx) ->
numbers[idx] = numbers[idx].replace(/\d/g, mask) unless idx == numbers.length - 1
numbers.join(' ')
else
val.replace /\d/g, mask
handlers:
setCardType: ($el, e) ->
cardType = e.data
unless QJ.hasClass @$card, cardType
QJ.removeClass @$card, 'jp-card-unknown'
QJ.removeClass @$card, @cardTypes.join(' ')
QJ.addClass @$card, "jp-card-#{cardType}"
QJ.toggleClass @$card, 'jp-card-identified', (cardType isnt 'unknown')
@cardType = cardType
flipCard: ->
QJ.addClass @$card, 'jp-card-flipped'
unflipCard: ->
QJ.removeClass @$card, 'jp-card-flipped'
bindVal = (el, out, opts={}) ->
opts.fill = opts.fill || false
opts.filters = opts.filters || []
opts.filters = [opts.filters] unless opts.filters instanceof Array
opts.join = opts.join || ""
if !(typeof(opts.join) == "function")
joiner = opts.join
opts.join = () -> joiner
outDefaults = (o.textContent for o in out)
QJ.on el, 'focus', ->
QJ.addClass out, 'jp-card-focused'
QJ.on el, 'blur', ->
QJ.removeClass out, 'jp-card-focused'
QJ.on el, 'keyup change paste', (e) ->
val = (QJ.val(elem) for elem in el)
join = opts.join(val)
val = val.join(join)
val = "" if val == join
for filter in opts.filters
val = filter(val, el, out)
for outEl, i in out
if opts.fill
outVal = val + outDefaults[i].substring(val.length)
else
outVal = val or outDefaults[i]
outEl.textContent = outVal
el
module.exports = Card
global.Card = Card