skeuocard-rails 0.0.1 → 0.0.2.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/vendor/assets/images/card-flip-arrow.png +0 -0
  4. data/vendor/assets/images/card-invalid-indicator.png +0 -0
  5. data/vendor/assets/images/card-valid-anim.gif +0 -0
  6. data/vendor/assets/images/card-valid-indicator.png +0 -0
  7. data/vendor/assets/images/issuers/amex-blackcard-front.png +0 -0
  8. data/vendor/assets/images/issuers/visa-chase-sapphire.png +0 -0
  9. data/vendor/assets/images/issuers/visa-simple-front.png +0 -0
  10. data/vendor/assets/images/products/amex-front.png +0 -0
  11. data/vendor/assets/images/products/dinersclubintl-front.png +0 -0
  12. data/vendor/assets/images/products/discover-front.png +0 -0
  13. data/vendor/assets/images/products/generic-back.png +0 -0
  14. data/vendor/assets/images/products/generic-front.png +0 -0
  15. data/vendor/assets/images/products/mastercard-front.png +0 -0
  16. data/vendor/assets/images/products/visa-back.png +0 -0
  17. data/vendor/assets/images/products/visa-front.png +0 -0
  18. data/vendor/assets/javascripts/skeuocard.js +302 -177
  19. data/vendor/assets/stylesheets/src/_browser_hacks.scss +0 -0
  20. data/vendor/assets/stylesheets/src/_cards.scss +50 -22
  21. data/vendor/assets/stylesheets/src/_util.scss +0 -0
  22. data/vendor/assets/stylesheets/src/skeuocard.reset.scss +0 -0
  23. data/vendor/assets/stylesheets/src/skeuocard.scss +6 -0
  24. metadata +6 -10
  25. data/vendor/assets/javascripts/src/skeuocard.coffee +0 -1072
  26. data/vendor/assets/javascripts/vendor/css_browser_selector.js +0 -154
  27. data/vendor/assets/javascripts/vendor/demo.fix.js +0 -17
  28. data/vendor/assets/javascripts/vendor/jquery-2.0.3.min.js +0 -5
  29. data/vendor/assets/stylesheets/src/demo.scss +0 -265
@@ -1,1072 +0,0 @@
1
- ###
2
- "Skeuocard" -- A Skeuomorphic Credit-Card Input Enhancement
3
- @description Skeuocard is a skeuomorphic credit card input plugin, supporting
4
- progressive enhancement. It renders a credit-card input which
5
- behaves similarly to a physical credit card.
6
- @author Ken Keiter <ken@kenkeiter.com>
7
- @updated 2013-07-25
8
- @website http://kenkeiter.com/
9
- @exports [window.Skeuocard]
10
- ###
11
-
12
- class Skeuocard
13
-
14
- constructor: (el, opts = {})->
15
- @el = {container: $(el), underlyingFields: {}}
16
- @_inputViews = {}
17
- @_tabViews = {}
18
- @product = undefined
19
- @productShortname = undefined
20
- @issuerShortname = undefined
21
- @_cardProductNeedsLayout = true
22
- @acceptedCardProducts = {}
23
- @visibleFace = 'front'
24
- @_initialValidationState = {}
25
- @_validationState = {number: false, exp: false, name: false, cvc: false}
26
- @_faceFillState = {front: false, back: false}
27
-
28
- # configure default opts
29
- optDefaults =
30
- debug: false
31
- acceptedCardProducts: []
32
- cardNumberPlaceholderChar: 'X'
33
- genericPlaceholder: "XXXX XXXX XXXX XXXX"
34
- typeInputSelector: '[name="cc_type"]'
35
- numberInputSelector: '[name="cc_number"]'
36
- expInputSelector: '[name="cc_exp"]'
37
- nameInputSelector: '[name="cc_name"]'
38
- cvcInputSelector: '[name="cc_cvc"]'
39
- currentDate: new Date()
40
- initialValues: {}
41
- validationState: {}
42
- strings:
43
- hiddenFaceFillPrompt: "Click here to<br /> fill in the other side."
44
- hiddenFaceErrorWarning: "There's a problem on the other side."
45
- hiddenFaceSwitchPrompt: "Back to the other side..."
46
- @options = $.extend(optDefaults, opts)
47
-
48
- # initialize the card
49
- @_conformDOM() # conform the DOM to match our styling requirements
50
- @_setAcceptedCardProducts() # determine which card products to accept
51
- @_createInputs() # create reconfigurable input views
52
- @_updateProductIfNeeded()
53
- @_flipToInvalidSide()
54
-
55
-
56
- # Transform the elements within the container, conforming the DOM so that it
57
- # becomes styleable, and that the underlying inputs are hidden.
58
- _conformDOM: ->
59
- # for CSS determination that this is an enhanced input, add 'js' class to
60
- # the container
61
- @el.container.removeClass('no-js')
62
- @el.container.addClass("skeuocard js")
63
- # remove anything that's not an underlying form field
64
- @el.container.find("> :not(input,select,textarea)").remove()
65
- @el.container.find("> input,select,textarea").hide()
66
- # attach underlying form elements
67
- @el.underlyingFields =
68
- type: @el.container.find(@options.typeInputSelector)
69
- number: @el.container.find(@options.numberInputSelector)
70
- exp: @el.container.find(@options.expInputSelector)
71
- name: @el.container.find(@options.nameInputSelector)
72
- cvc: @el.container.find(@options.cvcInputSelector)
73
- # sync initial values, with constructor options taking precedence
74
- for fieldName, fieldValue of @options.initialValues
75
- @el.underlyingFields[fieldName].val(fieldValue)
76
- for fieldName, el of @el.underlyingFields
77
- @options.initialValues[fieldName] = el.val()
78
- # sync initial validation state, with constructor options taking precedence
79
- # we use the underlying form values to track state
80
- for fieldName, el of @el.underlyingFields
81
- if @options.validationState[fieldName] is false or el.hasClass('invalid')
82
- @_initialValidationState[fieldName] = false
83
- unless el.hasClass('invalid')
84
- el.addClass('invalid')
85
- # bind change handlers to render
86
- @el.underlyingFields.number.bind "change", (e)=>
87
- @_inputViews.number.setValue @_getUnderlyingValue('number')
88
- @render()
89
- @el.underlyingFields.exp.bind "change", (e)=>
90
- @_inputViews.exp.setValue @_getUnderlyingValue('exp')
91
- @render()
92
- @el.underlyingFields.name.bind "change", (e)=>
93
- @_inputViews.exp.setValue @_getUnderlyingValue('name')
94
- @render()
95
- @el.underlyingFields.cvc.bind "change", (e)=>
96
- @_inputViews.exp.setValue @_getUnderlyingValue('cvc')
97
- @render()
98
- # construct the necessary card elements
99
- @el.surfaceFront = $("<div>").attr(class: "face front")
100
- @el.surfaceBack = $("<div>").attr(class: "face back")
101
- @el.cardBody = $("<div>").attr(class: "card-body")
102
- # add elements to the DOM
103
- @el.surfaceFront.appendTo(@el.cardBody)
104
- @el.surfaceBack.appendTo(@el.cardBody)
105
- @el.cardBody.appendTo(@el.container)
106
- # create the validation indicator (flip tab), and attach them.
107
- @_tabViews.front = new @FlipTabView('front')
108
- @_tabViews.back = new @FlipTabView('back')
109
- @el.surfaceFront.prepend(@_tabViews.front.el)
110
- @el.surfaceBack.prepend(@_tabViews.back.el)
111
- @_tabViews.front.hide()
112
- @_tabViews.back.hide()
113
-
114
- @_tabViews.front.el.click =>
115
- @flip()
116
- @_tabViews.back.el.click =>
117
- @flip()
118
-
119
- return @el.container
120
-
121
- _setAcceptedCardProducts: ->
122
- # build the set of accepted card products
123
- if @options.acceptedCardProducts.length is 0
124
- @el.underlyingFields.type.find('option').each (i, _el)=>
125
- el = $(_el)
126
- cardProductShortname = el.attr('data-card-product-shortname') || el.attr('value')
127
- @options.acceptedCardProducts.push cardProductShortname
128
- # find all matching card products by shortname, and add them to the
129
- # list of @acceptedCardProducts
130
- for matcher, product of CCProducts
131
- if product.companyShortname in @options.acceptedCardProducts
132
- @acceptedCardProducts[matcher] = product
133
- return @acceptedCardProducts
134
-
135
- _updateProductIfNeeded: ->
136
- # determine if product changed; if so, change it globally, and
137
- # call render() to render the changes.
138
- number = @_getUnderlyingValue('number')
139
- matchedProduct = @getProductForNumber(number)
140
- matchedProductIdentifier = matchedProduct?.companyShortname || ''
141
- matchedIssuerIdentifier = matchedProduct?.issuerShortname || ''
142
-
143
- if (@productShortname isnt matchedProductIdentifier) or
144
- (@issuerShortname isnt matchedIssuerIdentifier)
145
- @productShortname = matchedProductIdentifier
146
- @issuerShortname = matchedIssuerIdentifier
147
- @product = matchedProduct
148
- @_cardProductNeedsLayout = true
149
- @trigger 'productWillChange.skeuocard',
150
- [@, @productShortname, matchedProductIdentifier]
151
- @_log("Triggering render because product changed.")
152
- @render()
153
- @trigger('productDidChange.skeuocard', [@, @productShortname, matchedProductIdentifier])
154
-
155
- # Create the new inputs, and attach them to their appropriate card face els.
156
- _createInputs: ->
157
- @_inputViews.number = new @SegmentedCardNumberInputView()
158
- @_inputViews.exp = new @ExpirationInputView(currentDate: @options.currentDate)
159
- @_inputViews.name = new @TextInputView(
160
- class: "cc-name", placeholder: "YOUR NAME")
161
- @_inputViews.cvc = new @TextInputView(
162
- class: "cc-cvc", placeholder: "XXX", requireMaxLength: true)
163
-
164
- # style and attach the number view to the DOM
165
- @_inputViews.number.el.addClass('cc-number')
166
- @_inputViews.number.el.appendTo(@el.surfaceFront)
167
- # attach name input
168
- @_inputViews.name.el.appendTo(@el.surfaceFront)
169
- # style and attach the exp view to the DOM
170
- @_inputViews.exp.el.addClass('cc-exp')
171
- @_inputViews.exp.el.appendTo(@el.surfaceFront)
172
- # attach cvc field to the DOM
173
- @_inputViews.cvc.el.appendTo(@el.surfaceBack)
174
-
175
- # bind change events to their underlying form elements
176
- @_inputViews.number.bind "keyup", (e, input)=>
177
- @_setUnderlyingValue('number', input.value)
178
- @_updateValidationStateForInputView('number')
179
- @_updateProductIfNeeded()
180
- @_inputViews.exp.bind "keyup", (e, input)=>
181
- @_setUnderlyingValue('exp', input.value)
182
- @_updateValidationStateForInputView('exp')
183
- @_inputViews.name.bind "keyup", (e)=>
184
- @_setUnderlyingValue('name', $(e.target).val())
185
- @_updateValidationStateForInputView('name')
186
- @_inputViews.cvc.bind "keyup", (e)=>
187
- @_setUnderlyingValue('cvc', $(e.target).val())
188
- @_updateValidationStateForInputView('cvc')
189
-
190
- # setup default values; when render is called, these will be picked up
191
- @_inputViews.number.setValue @_getUnderlyingValue('number')
192
- @_inputViews.exp.setValue @_getUnderlyingValue('exp')
193
- @_inputViews.name.el.val @_getUnderlyingValue('name')
194
- @_inputViews.cvc.el.val @_getUnderlyingValue('cvc')
195
-
196
- # Debugging helper; if debug is set to true at instantiation, messages will
197
- # be printed to the console.
198
- _log: (msg...)->
199
- if console?.log and !!@options.debug
200
- console.log("[skeuocard]", msg...) if @options.debug?
201
-
202
- _flipToInvalidSide: ->
203
- if Object.keys(@_initialValidationState).length > 0
204
- _oppositeFace = if @visibleFace is 'front' then 'back' else 'front'
205
- # if the back face has errors, and the front does not, flip there.
206
- _errorCounts = {front: 0, back: 0}
207
- for fieldName, state of @_initialValidationState
208
- _errorCounts[@product?.layout[fieldName]]++
209
- if _errorCounts[@visibleFace] == 0 and _errorCounts[_oppositeFace] > 0
210
- @flip()
211
-
212
- # Update the card's visual representation to reflect internal state.
213
- render: ->
214
- @_log("*** start rendering ***")
215
-
216
- # Render card product layout changes.
217
- if @_cardProductNeedsLayout is true
218
- # Update product-specific details
219
- if @product isnt undefined
220
- # change the design and layout of the card to match the matched prod.
221
- @_log("[render]", "Activating product", @product)
222
- @el.container.removeClass (index, css)=>
223
- (css.match(/\b(product|issuer)-\S+/g) || []).join(' ')
224
- @el.container.addClass("product-#{@product.companyShortname}")
225
- if @product.issuerShortname?
226
- @el.container.addClass("issuer-#{@product.issuerShortname}")
227
- # Adjust underlying card type to match detected type
228
- @_setUnderlyingCardType(@product.companyShortname)
229
- # Reconfigure input to match product
230
- @_inputViews.number.reconfigure
231
- groupings: @product.cardNumberGrouping
232
- placeholderChar: @options.cardNumberPlaceholderChar
233
- @_inputViews.exp.show()
234
- @_inputViews.name.show()
235
- @_inputViews.exp.reconfigure
236
- pattern: @product.expirationFormat
237
- @_inputViews.cvc.show()
238
- @_inputViews.cvc.attr
239
- maxlength: @product.cvcLength
240
- placeholder: new Array(@product.cvcLength + 1).join(@options.cardNumberPlaceholderChar)
241
- for fieldName, surfaceName of @product.layout
242
- sel = if surfaceName is 'front' then 'surfaceFront' else 'surfaceBack'
243
- container = @el[sel]
244
- inputEl = @_inputViews[fieldName].el
245
- unless container.has(inputEl).length > 0
246
- @_log("Moving", inputEl, "=>", container)
247
- el = @_inputViews[fieldName].el.detach()
248
- $(el).appendTo(@el[sel])
249
- else
250
- @_log("[render]", "Becoming generic.")
251
- # Reset to generic input
252
- @_inputViews.exp.clear()
253
- @_inputViews.cvc.clear()
254
- @_inputViews.exp.hide()
255
- @_inputViews.name.hide()
256
- @_inputViews.cvc.hide()
257
- @_inputViews.number.reconfigure
258
- groupings: [@options.genericPlaceholder.length],
259
- placeholder: @options.genericPlaceholder
260
- @el.container.removeClass (index, css)=>
261
- (css.match(/\bproduct-\S+/g) || []).join(' ')
262
- @el.container.removeClass (index, css)=>
263
- (css.match(/\bissuer-\S+/g) || []).join(' ')
264
- @_cardProductNeedsLayout = false
265
-
266
- @_log("Validation state:", @_validationState)
267
-
268
- # Render validation changes
269
- @showInitialValidationErrors()
270
-
271
- # If the current face is filled, and there are validation errors, show 'em
272
- _oppositeFace = if @visibleFace is 'front' then 'back' else 'front'
273
- _visibleFaceFilled = @_faceFillState[@visibleFace]
274
- _visibleFaceValid = @isFaceValid(@visibleFace)
275
- _hiddenFaceFilled = @_faceFillState[_oppositeFace]
276
- _hiddenFaceValid = @isFaceValid(_oppositeFace)
277
-
278
- if _visibleFaceFilled and not _visibleFaceValid
279
- @_log("Visible face is filled, but invalid; showing validation errors.")
280
- @showValidationErrors()
281
- else if not _visibleFaceFilled
282
- @_log("Visible face hasn't been filled; hiding validation errors.")
283
- @hideValidationErrors()
284
- else
285
- @_log("Visible face has been filled, and is valid.")
286
- @hideValidationErrors()
287
-
288
- if @visibleFace is 'front' and @fieldsForFace('back').length > 0
289
- if _visibleFaceFilled and _visibleFaceValid and not _hiddenFaceFilled
290
- @_tabViews.front.prompt(@options.strings.hiddenFaceFillPrompt, true)
291
- else if _hiddenFaceFilled and not _hiddenFaceValid
292
- @_tabViews.front.warn(@options.strings.hiddenFaceErrorWarning, true)
293
- else if _hiddenFaceFilled and _hiddenFaceValid
294
- @_tabViews.front.prompt(@options.strings.hiddenFaceSwitchPrompt, true)
295
- else
296
- @_tabViews.front.hide()
297
- else
298
- if _hiddenFaceValid
299
- @_tabViews.back.prompt(@options.strings.hiddenFaceSwitchPrompt, true)
300
- else
301
- @_tabViews.back.warn(@options.strings.hiddenFaceErrorWarning, true)
302
-
303
- # Update the validity indicator for the whole card body
304
- if not @isValid()
305
- @el.container.removeClass('valid')
306
- @el.container.addClass('invalid')
307
- else
308
- @el.container.addClass('valid')
309
- @el.container.removeClass('invalid')
310
-
311
- @_log("*** rendering complete ***")
312
-
313
- # We should *always* show initial validation errors; they shouldn't show and
314
- # hide with the rest of the errors unless their value has been changed.
315
- showInitialValidationErrors: ->
316
- for fieldName, state of @_initialValidationState
317
- if state is false and @_validationState[fieldName] is false
318
- # if the error hasn't been rectified
319
- @_inputViews[fieldName].addClass('invalid')
320
- else
321
- @_inputViews[fieldName].removeClass('invalid')
322
-
323
- showValidationErrors: ->
324
- for fieldName, state of @_validationState
325
- if state is true
326
- @_inputViews[fieldName].removeClass('invalid')
327
- else
328
- @_inputViews[fieldName].addClass('invalid')
329
-
330
- hideValidationErrors: ->
331
- for fieldName, state of @_validationState
332
- if (@_initialValidationState[fieldName] is false and state is true) or
333
- (not @_initialValidationState[fieldName]?)
334
- @_inputViews[fieldName].el.removeClass('invalid')
335
-
336
- setFieldValidationState: (fieldName, valid)->
337
- if valid
338
- @el.underlyingFields[fieldName].removeClass('invalid')
339
- else
340
- @el.underlyingFields[fieldName].addClass('invalid')
341
- @_validationState[fieldName] = valid
342
-
343
- isFaceFilled: (faceName)->
344
- fields = @fieldsForFace(faceName)
345
- filled = (name for name in fields when @_inputViews[name].isFilled())
346
- if fields.length > 0
347
- return filled.length is fields.length
348
- else
349
- return false
350
-
351
- fieldsForFace: (faceName)->
352
- if @product?.layout
353
- return (fn for fn, face of @product.layout when face is faceName)
354
- return []
355
-
356
- _updateValidationStateForInputView: (fieldName)->
357
- field = @_inputViews[fieldName]
358
- fieldValid = field.isValid() and
359
- not (@_initialValidationState[fieldName] is false and
360
- field.getValue() is @options.initialValues[fieldName])
361
- # trigger a change event if the field has changed
362
- if fieldValid isnt @_validationState[fieldName]
363
- @setFieldValidationState(fieldName, fieldValid)
364
- # Update the fill state
365
- @_faceFillState.front = @isFaceFilled('front')
366
- @_faceFillState.back = @isFaceFilled('back')
367
- @trigger('validationStateDidChange.skeuocard', [@, @_validationState])
368
- @_log("Change in validation for #{fieldName} triggers re-render.")
369
- @render()
370
-
371
- isFaceValid: (faceName)->
372
- valid = true
373
- for fieldName in @fieldsForFace(faceName)
374
- valid &= @_validationState[fieldName]
375
- return !!valid
376
-
377
- isValid: ->
378
- @_validationState.number and
379
- @_validationState.exp and
380
- @_validationState.name and
381
- @_validationState.cvc
382
-
383
- # Get a value from the underlying form.
384
- _getUnderlyingValue: (field)->
385
- @el.underlyingFields[field].val()
386
-
387
- # Set a value in the underlying form.
388
- _setUnderlyingValue: (field, newValue)->
389
- @trigger('change.skeuocard', [@]) # changing the underlying value triggers a change.
390
- @el.underlyingFields[field].val(newValue)
391
-
392
- # Flip the card over.
393
- flip: ->
394
- targetFace = if @visibleFace is 'front' then 'back' else 'front'
395
- @trigger('faceWillBecomeVisible.skeuocard', [@, targetFace])
396
- @visibleFace = targetFace
397
- @render()
398
- @el.cardBody.toggleClass('flip')
399
- @trigger('faceDidBecomeVisible.skeuocard', [@, targetFace])
400
-
401
- getProductForNumber: (num)->
402
- for m, d of @acceptedCardProducts
403
- parts = m.split('/')
404
- matcher = new RegExp(parts[1], parts[2])
405
- if matcher.test(num)
406
- issuer = @getIssuerForNumber(num) || {}
407
- return $.extend({}, d, issuer)
408
- return undefined
409
-
410
- getIssuerForNumber: (num)->
411
- for m, d of CCIssuers
412
- parts = m.split('/')
413
- matcher = new RegExp(parts[1], parts[2])
414
- if matcher.test(num)
415
- return d
416
- return undefined
417
-
418
- _setUnderlyingCardType: (shortname)->
419
- @el.underlyingFields.type.find('option').each (i, _el)=>
420
- el = $(_el)
421
- if shortname is (el.attr('data-card-product-shortname') || el.attr('value'))
422
- el.val(el.attr('value')) # change which option is selected
423
-
424
- trigger: (args...)->
425
- @el.container.trigger(args...)
426
-
427
- bind: (args...)->
428
- @el.container.trigger(args...)
429
-
430
- ###
431
- Skeuocard::FlipTabView
432
- Handles rendering of the "flip button" control and its various warning and
433
- prompt states.
434
- ###
435
- class Skeuocard::FlipTabView
436
- constructor: (face, opts = {})->
437
- @el = $("<div class=\"flip-tab #{face}\"><p></p></div>")
438
- @options = opts
439
-
440
- _setText: (text)->
441
- @el.find('p').html(text)
442
-
443
- warn: (message, withAnimation = false)->
444
- @_resetClasses()
445
- @el.addClass('warn')
446
- @_setText(message)
447
- @show()
448
- if withAnimation
449
- @el.removeClass('warn-anim')
450
- @el.addClass('warn-anim')
451
-
452
- prompt: (message, withAnimation = false)->
453
- @_resetClasses()
454
- @el.addClass('prompt')
455
- @_setText(message)
456
- @show()
457
- if withAnimation
458
- @el.removeClass('valid-anim')
459
- @el.addClass('valid-anim')
460
-
461
- _resetClasses: ->
462
- @el.removeClass('valid-anim')
463
- @el.removeClass('warn-anim')
464
- @el.removeClass('warn')
465
- @el.removeClass('prompt')
466
-
467
- show: ->
468
- @el.show()
469
-
470
- hide: ->
471
- @el.hide()
472
-
473
- ###
474
- Skeuocard::TextInputView
475
- ###
476
- class Skeuocard::TextInputView
477
-
478
- bind: (args...)->
479
- @el.bind(args...)
480
-
481
- trigger: (args...)->
482
- @el.trigger(args...)
483
-
484
- _getFieldCaretPosition: (el)->
485
- input = el.get(0)
486
- if input.selectionEnd?
487
- return input.selectionEnd
488
- else if document.selection
489
- input.focus()
490
- sel = document.selection.createRange()
491
- selLength = document.selection.createRange().text.length
492
- sel.moveStart('character', -input.value.length)
493
- return selLength
494
-
495
- _setFieldCaretPosition: (el, pos)->
496
- input = el.get(0)
497
- if input.createTextRange?
498
- range = input.createTextRange()
499
- range.move "character", pos
500
- range.select()
501
- else if input.selectionStart?
502
- input.focus()
503
- input.setSelectionRange(pos, pos)
504
-
505
- show: ->
506
- @el.show()
507
-
508
- hide: ->
509
- @el.hide()
510
-
511
- addClass: (args...)->
512
- @el.addClass(args...)
513
-
514
- removeClass: (args...)->
515
- @el.removeClass(args...)
516
-
517
- _zeroPadNumber: (num, places)->
518
- zero = places - num.toString().length + 1
519
- return Array(zero).join("0") + num
520
-
521
- class Skeuocard::SegmentedCardNumberInputView extends Skeuocard::TextInputView
522
- constructor: (opts = {})->
523
- # Setup option defaults
524
- opts.value ||= ""
525
- opts.groupings ||= [19]
526
- opts.placeholderChar ||= "X"
527
- @options = opts
528
- # everythIng else
529
- @value = @options.value
530
- @el = $("<fieldset>")
531
- @el.delegate "input", "keydown", (e)=> @_onGroupKeyDown(e)
532
- @el.delegate "input", "keyup", (e)=> @_onGroupKeyUp(e)
533
- @groupEls = $()
534
-
535
- _onGroupKeyDown: (e)->
536
- e.stopPropagation()
537
- groupEl = $(e.currentTarget)
538
-
539
- arrowKeys = [37, 38, 39, 40]
540
- groupEl = $(e.currentTarget)
541
- groupMaxLength = parseInt(groupEl.attr('maxlength'))
542
- groupCaretPos = @_getFieldCaretPosition(groupEl)
543
-
544
- if e.which is 8 and groupCaretPos is 0 and not $.isEmptyObject(groupEl.prev())
545
- groupEl.prev().focus()
546
-
547
- if e.which in arrowKeys
548
- switch e.which
549
- when 37 # left
550
- if groupCaretPos is 0 and not $.isEmptyObject(groupEl.prev())
551
- groupEl.prev().focus()
552
- when 39 # right
553
- if groupCaretPos is groupMaxLength and not $.isEmptyObject(groupEl.next())
554
- groupEl.next().focus()
555
- when 38 # up
556
- if not $.isEmptyObject(groupEl.prev())
557
- groupEl.prev().focus()
558
- when 40 # down
559
- if not $.isEmptyObject(groupEl.next())
560
- groupEl.next().focus()
561
-
562
- _onGroupKeyUp: (e)->
563
- e.stopPropagation() # prevent event from bubbling up
564
-
565
- specialKeys = [8, 9, 16, 17, 18, 19, 20, 27, 33, 34, 35, 36,
566
- 37, 38, 39, 40, 45, 46, 91, 93, 144, 145, 224]
567
- groupEl = $(e.currentTarget)
568
- groupMaxLength = parseInt(groupEl.attr('maxlength'))
569
- groupCaretPos = @_getFieldCaretPosition(groupEl)
570
-
571
- if e.which not in specialKeys
572
- # intercept bad chars, returning user to the right char pos if need be
573
- groupValLength = groupEl.val().length
574
- pattern = new RegExp('[^0-9]+', 'g')
575
- groupEl.val(groupEl.val().replace(pattern, ''))
576
- if groupEl.val().length < groupValLength # we caught bad char
577
- @_setFieldCaretPosition(groupEl, groupCaretPos - 1)
578
- else
579
- @_setFieldCaretPosition(groupEl, groupCaretPos)
580
-
581
- if e.which not in specialKeys and
582
- groupEl.val().length is groupMaxLength and
583
- not $.isEmptyObject(groupEl.next()) and
584
- @_getFieldCaretPosition(groupEl) is groupMaxLength
585
- groupEl.next().focus()
586
-
587
- # update the value
588
- newValue = ""
589
- @groupEls.each -> newValue += $(@).val()
590
- @value = newValue
591
- @trigger("keyup", [@])
592
- return false
593
-
594
- setGroupings: (groupings)->
595
- caretPos = @_caretPosition()
596
- @el.empty() # remove all inputs
597
- _startLength = 0
598
- for groupLength in groupings
599
- groupEl = $("<input>").attr
600
- type: 'text'
601
- size: groupLength
602
- maxlength: groupLength
603
- class: "group#{groupLength}"
604
- # restore value, if necessary
605
- if @value.length > _startLength
606
- groupEl.val(@value.substr(_startLength, groupLength))
607
- _startLength += groupLength
608
- @el.append(groupEl)
609
- @options.groupings = groupings
610
- @groupEls = @el.find("input")
611
- # restore to previous settings
612
- @_caretTo(caretPos)
613
- if @options.placeholderChar isnt undefined
614
- @setPlaceholderChar(@options.placeholderChar)
615
- if @options.placeholder isnt undefined
616
- @setPlaceholder(@options.placeholder)
617
-
618
- setPlaceholderChar: (ch)->
619
- @groupEls.each ->
620
- el = $(@)
621
- el.attr 'placeholder', new Array(parseInt(el.attr('maxlength'))+1).join(ch)
622
- @options.placeholder = undefined
623
- @options.placeholderChar = ch
624
-
625
- setPlaceholder: (str)->
626
- @groupEls.each ->
627
- $(@).attr 'placeholder', str
628
- @options.placeholderChar = undefined
629
- @options.placeholder = str
630
-
631
- setValue: (newValue)->
632
- lastPos = 0
633
- @groupEls.each ->
634
- el = $(@)
635
- len = parseInt(el.attr('maxlength'))
636
- el.val(newValue.substr(lastPos, len))
637
- lastPos += len
638
- @value = newValue
639
-
640
- getValue: ->
641
- @value
642
-
643
- reconfigure: (changes = {})->
644
- if changes.groupings?
645
- @setGroupings(changes.groupings)
646
- if changes.placeholderChar?
647
- @setPlaceholderChar(changes.placeholderChar)
648
- if changes.placeholder?
649
- @setPlaceholder(changes.placeholder)
650
- if changes.value?
651
- @setValue(changes.value)
652
-
653
- _caretTo: (index)->
654
- pos = 0
655
- inputEl = undefined
656
- inputElIndex = 0
657
- # figure out which group we're in
658
- @groupEls.each (i, e)=>
659
- el = $(e)
660
- elLength = parseInt(el.attr('maxlength'))
661
- if index <= elLength + pos and index >= pos
662
- inputEl = el
663
- inputElIndex = index - pos
664
- pos += elLength
665
- # move the caret there
666
- @_setFieldCaretPosition(inputEl, inputElIndex)
667
-
668
- _caretPosition: ->
669
- iPos = 0
670
- finalPos = 0
671
- @groupEls.each (i, e)=>
672
- el = $(e)
673
- if el.is(':focus')
674
- finalPos = iPos + @_getFieldCaretPosition(el)
675
- iPos += parseInt(el.attr('maxlength'))
676
- return finalPos
677
-
678
- maxLength: ->
679
- @options.groupings.reduce((a,b)->(a+b))
680
-
681
- isFilled: ->
682
- @value.length == @maxLength()
683
-
684
- isValid: ->
685
- @isFilled() and @isValidLuhn(@value)
686
-
687
- isValidLuhn: (identifier)->
688
- sum = 0
689
- alt = false
690
- for i in [identifier.length - 1..0] by -1
691
- num = parseInt identifier.charAt(i), 10
692
- return false if isNaN(num)
693
- if alt
694
- num *= 2
695
- num = (num % 10) + 1 if num > 9
696
- alt = !alt
697
- sum += num
698
- sum % 10 is 0
699
-
700
- ###
701
- Skeuocard::ExpirationInputView
702
- ###
703
- class Skeuocard::ExpirationInputView extends Skeuocard::TextInputView
704
- constructor: (opts = {})->
705
- # setup option defaults
706
- opts.dateFormatter ||= (date)->
707
- date.getDate() + "-" + (date.getMonth()+1) + "-" + date.getFullYear()
708
- opts.dateParser ||= (value)->
709
- dateParts = value.split('-')
710
- new Date(dateParts[2], dateParts[1]-1, dateParts[0])
711
- opts.currentDate ||= new Date()
712
- opts.pattern ||= "MM/YY"
713
-
714
- @options = opts
715
- # setup default values
716
- @date = undefined
717
- @value = undefined
718
- # create dom container
719
- @el = $("<fieldset>")
720
- @el.delegate "input", "keydown", (e)=> @_onKeyDown(e)
721
- @el.delegate "input", "keyup", (e)=> @_onKeyUp(e)
722
-
723
- _getFieldCaretPosition: (el)->
724
- input = el.get(0)
725
- if input.selectionEnd?
726
- return input.selectionEnd
727
- else if document.selection
728
- input.focus()
729
- sel = document.selection.createRange()
730
- selLength = document.selection.createRange().text.length
731
- sel.moveStart('character', -input.value.length)
732
- return selLength
733
-
734
- _setFieldCaretPosition: (el, pos)->
735
- input = el.get(0)
736
- if input.createTextRange?
737
- range = input.createTextRange()
738
- range.move "character", pos
739
- range.select()
740
- else if input.selectionStart?
741
- input.focus()
742
- input.setSelectionRange(pos, pos)
743
-
744
- setPattern: (pattern)->
745
- groupings = []
746
- patternParts = pattern.split('')
747
- _currentLength = 0
748
- for char, i in patternParts
749
- _currentLength++
750
- if patternParts[i+1] != char
751
- groupings.push([_currentLength, char])
752
- _currentLength = 0
753
- @options.groupings = groupings
754
- @_setGroupings(@options.groupings)
755
-
756
- _setGroupings: (groupings)->
757
- fieldChars = ['D', 'M', 'Y']
758
- @el.empty()
759
- _startLength = 0
760
- for group in groupings
761
- groupLength = group[0]
762
- groupChar = group[1]
763
- if groupChar in fieldChars # this group is a field
764
- input = $('<input>').attr
765
- type: 'text'
766
- placeholder: new Array(groupLength+1).join(groupChar)
767
- maxlength: groupLength
768
- class: 'cc-exp-field-' + groupChar.toLowerCase() +
769
- ' group' + groupLength
770
- input.data('fieldtype', groupChar)
771
- @el.append(input)
772
- else # this group is a separator
773
- sep = $('<span>').attr
774
- class: 'separator'
775
- sep.html(new Array(groupLength + 1).join(groupChar))
776
- @el.append(sep)
777
- @groupEls = @el.find('input')
778
- @_updateFieldValues() if @date?
779
-
780
- _updateFieldValues: ->
781
- currentDate = @date
782
- unless @groupEls # they need to be created
783
- return @setPattern(@options.pattern)
784
- @groupEls.each (i,_el)=>
785
- el = $(_el)
786
- groupLength = parseInt(el.attr('maxlength'))
787
- switch el.data('fieldtype')
788
- when 'M'
789
- el.val @_zeroPadNumber(currentDate.getMonth() + 1, groupLength)
790
- when 'D'
791
- el.val @_zeroPadNumber(currentDate.getDate(), groupLength)
792
- when 'Y'
793
- year = if groupLength >= 4 then currentDate.getFullYear() else
794
- currentDate.getFullYear().toString().substr(2,4)
795
- el.val(year)
796
-
797
- clear: ->
798
- @value = ""
799
- @date = null
800
- @groupEls.each ->
801
- $(@).val('')
802
-
803
- setDate: (newDate)->
804
- @date = newDate
805
- @value = @options.dateFormatter(newDate)
806
- @_updateFieldValues()
807
-
808
- setValue: (newValue)->
809
- @value = newValue
810
- @date = @options.dateParser(newValue)
811
- @_updateFieldValues()
812
-
813
- getDate: ->
814
- @date
815
-
816
- getValue: ->
817
- @value
818
-
819
- reconfigure: (opts)->
820
- if opts.pattern?
821
- @setPattern(opts.pattern)
822
- if opts.value?
823
- @setValue(opts.value)
824
-
825
- _onKeyDown: (e)->
826
- e.stopPropagation()
827
- groupEl = $(e.currentTarget)
828
-
829
- groupEl = $(e.currentTarget)
830
- groupMaxLength = parseInt(groupEl.attr('maxlength'))
831
- groupCaretPos = @_getFieldCaretPosition(groupEl)
832
-
833
- prevInputEl = groupEl.prevAll('input').first()
834
- nextInputEl = groupEl.nextAll('input').first()
835
-
836
- # Handle delete key
837
- if e.which is 8 and groupCaretPos is 0 and
838
- not $.isEmptyObject(prevInputEl)
839
- prevInputEl.focus()
840
-
841
- if e.which in [37, 38, 39, 40] # arrow keys
842
- switch e.which
843
- when 37 # left
844
- if groupCaretPos is 0 and not $.isEmptyObject(prevInputEl)
845
- prevInputEl.focus()
846
- when 39 # right
847
- if groupCaretPos is groupMaxLength and not $.isEmptyObject(nextInputEl)
848
- nextInputEl.focus()
849
- when 38 # up
850
- if not $.isEmptyObject(groupEl.prev('input'))
851
- prevInputEl.focus()
852
- when 40 # down
853
- if not $.isEmptyObject(groupEl.next('input'))
854
- nextInputEl.focus()
855
-
856
- _onKeyUp: (e)->
857
- e.stopPropagation()
858
-
859
- specialKeys = [8, 9, 16, 17, 18, 19, 20, 27, 33, 34, 35, 36,
860
- 37, 38, 39, 40, 45, 46, 91, 93, 144, 145, 224]
861
- arrowKeys = [37, 38, 39, 40]
862
- groupEl = $(e.currentTarget)
863
- groupMaxLength = parseInt(groupEl.attr('maxlength'))
864
- groupCaretPos = @_getFieldCaretPosition(groupEl)
865
-
866
- if e.which not in specialKeys
867
- # intercept bad chars, returning user to the right char pos if need be
868
- groupValLength = groupEl.val().length
869
- pattern = new RegExp('[^0-9]+', 'g')
870
- groupEl.val(groupEl.val().replace(pattern, ''))
871
- if groupEl.val().length < groupValLength # we caught bad char
872
- @_setFieldCaretPosition(groupEl, groupCaretPos - 1)
873
- else
874
- @_setFieldCaretPosition(groupEl, groupCaretPos)
875
-
876
- nextInputEl = groupEl.nextAll('input').first()
877
-
878
- if e.which not in specialKeys and
879
- groupEl.val().length is groupMaxLength and
880
- not $.isEmptyObject(nextInputEl) and
881
- @_getFieldCaretPosition(groupEl) is groupMaxLength
882
- nextInputEl.focus()
883
-
884
- # get a date object representing what's been entered
885
- day = parseInt(@el.find('.cc-exp-field-d').val()) || 1
886
- month = parseInt(@el.find('.cc-exp-field-m').val())
887
- year = parseInt(@el.find('.cc-exp-field-y').val())
888
- if month is 0 or year is 0
889
- @value = ""
890
- @date = null
891
- else
892
- year += 2000 if year < 2000
893
- dateObj = new Date(year, month-1, day)
894
- @value = @options.dateFormatter(dateObj)
895
- @date = dateObj
896
- @trigger("keyup", [@])
897
- return false
898
-
899
- _inputGroupEls: ->
900
- @el.find("input")
901
-
902
- isFilled: ->
903
- for inputEl in @groupEls
904
- el = $(inputEl)
905
- return false if el.val().length != parseInt(el.attr('maxlength'))
906
- return true
907
-
908
- isValid: ->
909
- @isFilled() and
910
- ((@date.getFullYear() == @options.currentDate.getFullYear() and
911
- @date.getMonth() >= @options.currentDate.getMonth()) or
912
- @date.getFullYear() > @options.currentDate.getFullYear())
913
-
914
-
915
- class Skeuocard::TextInputView extends Skeuocard::TextInputView
916
- constructor: (opts)->
917
- @el = $("<input>").attr
918
- type: 'text'
919
- placeholder: opts.placeholder
920
- class: opts.class
921
- @options = opts
922
-
923
- clear: ->
924
- @el.val("")
925
-
926
- attr: (args...)->
927
- @el.attr(args...)
928
-
929
- isFilled: ->
930
- return @el.val().length > 0
931
-
932
- isValid: ->
933
- if @options.requireMaxLength
934
- return @el.val().length is parseInt(@el.attr('maxlength'))
935
- else
936
- return @isFilled()
937
-
938
- getValue: ->
939
- @el.val()
940
-
941
- # Export the object.
942
- window.Skeuocard = Skeuocard
943
-
944
- ###
945
- # Card Definitions
946
- ###
947
-
948
- # List of credit card products by matching prefix.
949
- CCProducts = {}
950
-
951
- CCProducts[/^30[0-5][0-9]/] =
952
- companyName: "Diners Club"
953
- companyShortname: "dinersclubintl"
954
- cardNumberGrouping: [4,6,4]
955
- expirationFormat: "MM/YY"
956
- cvcLength: 3
957
- layout:
958
- number: 'front'
959
- exp: 'front'
960
- name: 'front'
961
- cvc: 'back'
962
-
963
- CCProducts[/^3095/] =
964
- companyName: "Diners Club International"
965
- companyShortname: "dinersclubintl"
966
- cardNumberGrouping: [4,6,4]
967
- expirationFormat: "MM/YY"
968
- cvcLength: 3
969
- layout:
970
- number: 'front'
971
- exp: 'front'
972
- name: 'front'
973
- cvc: 'back'
974
-
975
- CCProducts[/^36\d{2}/] =
976
- companyName: "Diners Club International"
977
- companyShortname: "dinersclubintl"
978
- cardNumberGrouping: [4,6,4]
979
- expirationFormat: "MM/YY"
980
- cvcLength: 3
981
- layout:
982
- number: 'front'
983
- exp: 'front'
984
- name: 'front'
985
- cvc: 'back'
986
-
987
- CCProducts[/^35\d{2}/] =
988
- companyName: "JCB"
989
- companyShortname: "jcb"
990
- cardNumberGrouping: [4,4,4,4]
991
- expirationFormat: "MM/YY"
992
- cvcLength: 3
993
- layout:
994
- number: 'front'
995
- exp: 'front'
996
- name: 'front'
997
- cvc: 'back'
998
-
999
- CCProducts[/^37/] =
1000
- companyName: "American Express"
1001
- companyShortname: "amex"
1002
- cardNumberGrouping: [4,6,5]
1003
- expirationFormat: "MM/YY"
1004
- cvcLength: 4
1005
- layout:
1006
- number: 'front'
1007
- exp: 'front'
1008
- name: 'front'
1009
- cvc: 'front'
1010
-
1011
- CCProducts[/^38/] =
1012
- companyName: "Hipercard"
1013
- companyShortname: "hipercard"
1014
- cardNumberGrouping: [4,4,4,4]
1015
- expirationFormat: "MM/YY"
1016
- cvcLength: 3
1017
- layout:
1018
- number: 'front'
1019
- exp: 'front'
1020
- name: 'front'
1021
- cvc: 'back'
1022
-
1023
- CCProducts[/^4[0-9]\d{2}/] =
1024
- companyName: "Visa"
1025
- companyShortname: "visa"
1026
- cardNumberGrouping: [4,4,4,4]
1027
- expirationFormat: "MM/YY"
1028
- cvcLength: 3
1029
- layout:
1030
- number: 'front'
1031
- exp: 'front'
1032
- name: 'front'
1033
- cvc: 'back'
1034
-
1035
- CCProducts[/^5[0-8]\d{2}/] =
1036
- companyName: "Mastercard"
1037
- companyShortname: "mastercard"
1038
- cardNumberGrouping: [4,4,4,4]
1039
- expirationFormat: "MM/YY"
1040
- cvcLength: 3
1041
- layout:
1042
- number: 'front'
1043
- exp: 'front'
1044
- name: 'front'
1045
- cvc: 'back'
1046
-
1047
- CCProducts[/^6011/] =
1048
- companyName: "Discover"
1049
- companyShortname: "discover"
1050
- cardNumberGrouping: [4,4,4,4]
1051
- expirationFormat: "MM/YY"
1052
- cvcLength: 3
1053
- layout:
1054
- number: 'front'
1055
- exp: 'front'
1056
- name: 'front'
1057
- cvc: 'back'
1058
-
1059
- CCIssuers = {}
1060
-
1061
- ###
1062
- Hack fixes the Chase Sapphire card's stupid (nice?) layout non-conformity.
1063
- ###
1064
- CCIssuers[/^414720/] =
1065
- issuingAuthority: "Chase"
1066
- issuerName: "Chase Sapphire Card"
1067
- issuerShortname: "chase-sapphire"
1068
- layout:
1069
- number: 'front'
1070
- exp: 'front'
1071
- name: 'front'
1072
- cvc: 'front'