require 'jquery.payment' $ = jQuery $.card = {} $.card.fn = {} $.fn.card = (opts) -> $.card.fn.construct.apply(this, opts) class Card cardTemplate: """ <div class="card-container"> <div class="card"> <div class="front"> <div class="card-logo visa">visa</div> <div class="card-logo mastercard">MasterCard</div> <div class="card-logo amex"></div> <div class="card-logo discover">discover</div> <div class="lower"> <div class="shiny"></div> <div class="cvc display">{{cvc}}</div> <div class="number display">{{number}}</div> <div class="name display">{{name}}</div> <div class="expiry display" data-before="{{monthYear}}" data-after="{{validDate}}">{{expiry}}</div> </div> </div> <div class="back"> <div class="bar"></div> <div class="cvc display">{{cvc}}</div> <div class="shiny"></div> </div> </div> </div> """ 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' values: number: '•••• •••• •••• ••••' cvc: '•••' expiry: '••/••' name: 'Full Name' classes: valid: 'card-valid' invalid: 'card-invalid' constructor: (el, opts) -> @options = $.extend(true, {}, @defaults, opts) $.extend @options.messages, $.card.messages $.extend @options.values, $.card.values @$el = $(el) unless @options.container console.log "Please provide a container" return @$container = $(@options.container) @render() @attachHandlers() @handleInitialValues() render: -> @$container.append(@template( @cardTemplate, $.extend({}, @options.messages, @options.values) )) $.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 'safari' if (new Function("/*@cc_on return @_jscript_version; @*/")()) @$card.addClass 'ie-10' attachHandlers: -> @$numberInput .bindVal @$numberDisplay, fill: false, filters: @validToggler('cardNumber') .on 'payment.cardType', @handle('setCardType') expiryFilters = [(val) -> val.replace /(\s+)/g, ''] if @$expiryInput.length == 1 expiryFilters.push @validToggler('cardExpiry') @$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, filters: @validToggler('cardCVC') .on('focus', @handle('flipCard')) .on('blur', @handle('flipCard')) @$nameInput .bindVal @$nameDisplay, fill: false filters: @validToggler('cardHolderName') join: ' ' .on 'keydown', @handle('captureName') 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 validToggler: (validatorName) -> if validatorName == "cardExpiry" isValid = (val) -> objVal = $.payment.cardExpiryVal val $.payment.validateCardExpiry objVal.month, objVal.year else if validatorName == "cardCVC" isValid = (val) => $.payment.validateCardCVC val, @cardType else if validatorName == "cardNumber" isValid = (val) -> $.payment.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) -> el.toggleClass @options.classes.valid, test el.toggleClass @options.classes.invalid, !test 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') @cardType = cardType 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) captureName: ($el, e) -> banKeyCodes = [48,49,50,51,52,53,54,55,56,57,106,107,109,110,111,186,187,188,189,190,191,192,219,220,221,222] e.preventDefault() if banKeyCodes.indexOf(e.which or e.keyCode) != -1 $.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 $.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)