require 'jquery.payment'
$ = jQuery
$.card = {}
$.card.fn = {}
$.fn.card = (opts) ->
$.card.fn.construct.apply(this, opts)
class Card
cardTemplate: """
visa
MasterCard
discover
••••
•••• •••• •••• ••••
{{fullName}}
••/••
"""
template: (tpl, data) ->
tpl.replace /\{\{(.*?)\}\}/g, (match, key, str) ->
data[key]
cardTypes: [
'maestro',
'dinersclub',
'laser',
'jcb',
'unionpay',
'discover',
'mastercard',
'amex',
'visa'
]
defaults:
formatting: true
formSelectors:
numberInput: 'input[name="number"]'
expiryInput: 'input[name="expiry"]'
cvcInput: 'input[name="cvc"]'
nameInput: 'input[name="name"]'
cardSelectors:
cardContainer: '.card-container'
card: '.card'
numberDisplay: '.number'
expiryDisplay: '.expiry'
cvcDisplay: '.cvc'
nameDisplay: '.name'
messages:
validDate: 'valid\nthru'
monthYear: 'month/year'
fullName: 'Full Name'
constructor: (el, opts) ->
@options = $.extend({}, @defaults, opts)
$.extend @options.messages, $.card.messages
@$el = $(el)
unless @options.container
console.log "Please provide a container"
return
@$container = $(@options.container)
@render()
@attachHandlers()
@handleInitialValues()
render: ->
@$container.append @template(@cardTemplate, @options.messages)
$.each @options.cardSelectors, (name, selector) =>
this["$#{name}"] = @$container.find(selector)
$.each @options.formSelectors, (name, selector) =>
if @options[name]
obj = $(@options[name])
else
obj = @$el.find(selector)
console.error "Card can't find a #{name} in your form." if !obj.length
this["$#{name}"] = obj
if @options.formatting
@$numberInput.payment('formatCardNumber')
@$cvcInput.payment('formatCardCVC')
# we can only format if there's only one expiry input
if @$expiryInput.length == 1
@$expiryInput.payment('formatCardExpiry')
if @options.width
baseWidth = parseInt @$cardContainer.css('width')
@$cardContainer.css "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
@$card.addClass 'no-radial-gradient'
attachHandlers: ->
@$numberInput
.bindVal @$numberDisplay,
fill: false,
filters: validToggler('validateCardNumber')
.on 'payment.cardType', @handle('setCardType')
expiryFilters = [(val) -> val.replace /(\s+)/g, '']
if @$expiryInput.length == 1
expiryFilters.push validToggler('validateCardExpiry')
@$expiryInput.on 'keydown', @handle('captureTab')
@$expiryInput
.bindVal @$expiryDisplay,
join: (text) ->
if text[0].length == 2 or text[1] then "/" else ""
filters: expiryFilters
@$cvcInput
.bindVal(@$cvcDisplay, validToggler 'validateCardCVC' )
.on('focus', @handle('flipCard'))
.on('blur', @handle('flipCard'))
@$nameInput
.bindVal @$nameDisplay,
fill: false
join: ' '
handleInitialValues: ->
$.each @options.formSelectors, (name, selector) =>
el = this["$#{name}"]
if el.val()
# if the input has a value, we want to trigger a refresh
el.trigger 'paste'
# set a timeout because `jquery.payment` does the reset of the val
# in a timeout
setTimeout -> el.trigger 'keyup'
handle: (fn) ->
(e) =>
$el = $(e.currentTarget)
args = Array.prototype.slice.call arguments
args.unshift $el
@handlers[fn].apply this, args
handlers:
setCardType: ($el, e, cardType) ->
unless @$card.hasClass(cardType)
@$card.removeClass('unknown')
@$card.removeClass(@cardTypes.join(' '))
@$card.addClass(cardType)
@$card.toggleClass('identified', cardType isnt 'unknown')
flipCard: ($el, e) ->
@$card.toggleClass('flipped')
captureTab: ($el, e) ->
keyCode = e.keyCode or e.which
return if keyCode != 9 or e.shiftKey
val = $el.payment('cardExpiryVal')
return unless val.month or val.year
e.preventDefault() if !$.payment.validateCardExpiry(val.month, val.year)
$.fn.bindVal = (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
$el = $(this)
outDefaults = (out.eq(i).text() for o, i in out)
$el.on 'focus', ->
out.addClass 'focused'
$el.on 'blur', ->
out.removeClass 'focused'
$el.on 'keyup change paste', (e) ->
val = $el.map(-> $(this).val()).get()
join = opts.join(val)
val = val.join(join)
val = "" if val == join
for filter in opts.filters
val = filter(val, $el, out)
for o, i in out
if opts.fill
outVal = val + outDefaults[i].substring(val.length)
else
outVal = val or outDefaults[i]
out.eq(i).text(outVal)
$el
validToggler = (validatorName) ->
if validatorName == "validateCardExpiry"
return (val, $in, $out) ->
val = $in.payment('cardExpiryVal')
$in.toggleClass('valid', $.payment.validateCardExpiry(val.month , val.year))
val
else if validatorName == "validateCardNumber"
return (val, $in, $out) ->
$in.toggleClass('valid', $.payment.validateCardNumber(val))
val
else
return (val, $in, $out) ->
$out.toggleClass('valid', $.payment[validatorName](val))
val
$.fn.extend card: (option, args...) ->
@each ->
$this = $(this)
data = $this.data('card')
if !data
$this.data 'card', (data = new Card(this, option))
if typeof option == 'string'
data[option].apply(data, args)