skeuocard-rails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/lib/skeuocard-rails.rb +3 -0
- data/lib/skeuocard-rails/engine.rb +6 -0
- data/vendor/assets/fonts/ocra-webfont.eot +0 -0
- data/vendor/assets/fonts/ocra-webfont.svg +138 -0
- data/vendor/assets/fonts/ocra-webfont.ttf +0 -0
- data/vendor/assets/fonts/ocra-webfont.woff +0 -0
- data/vendor/assets/images/card-flip-arrow.png +0 -0
- data/vendor/assets/images/card-invalid-indicator.png +0 -0
- data/vendor/assets/images/card-valid-anim.gif +0 -0
- data/vendor/assets/images/card-valid-indicator.png +0 -0
- data/vendor/assets/images/issuers/amex-blackcard-front.png +0 -0
- data/vendor/assets/images/issuers/visa-chase-sapphire.png +0 -0
- data/vendor/assets/images/issuers/visa-simple-front.png +0 -0
- data/vendor/assets/images/products/amex-front.png +0 -0
- data/vendor/assets/images/products/dinersclubintl-front.png +0 -0
- data/vendor/assets/images/products/discover-front.png +0 -0
- data/vendor/assets/images/products/generic-back.png +0 -0
- data/vendor/assets/images/products/generic-front.png +0 -0
- data/vendor/assets/images/products/mastercard-front.png +0 -0
- data/vendor/assets/images/products/visa-back.png +0 -0
- data/vendor/assets/images/products/visa-front.png +0 -0
- data/vendor/assets/javascripts/skeuocard.js +1432 -0
- data/vendor/assets/javascripts/src/skeuocard.coffee +1072 -0
- data/vendor/assets/javascripts/vendor/css_browser_selector.js +154 -0
- data/vendor/assets/javascripts/vendor/demo.fix.js +17 -0
- data/vendor/assets/javascripts/vendor/jquery-2.0.3.min.js +5 -0
- data/vendor/assets/stylesheets/src/_browser_hacks.scss +32 -0
- data/vendor/assets/stylesheets/src/_cards.scss +318 -0
- data/vendor/assets/stylesheets/src/_util.scss +15 -0
- data/vendor/assets/stylesheets/src/demo.scss +265 -0
- data/vendor/assets/stylesheets/src/skeuocard.reset.scss +52 -0
- data/vendor/assets/stylesheets/src/skeuocard.scss +168 -0
- metadata +92 -0
@@ -0,0 +1,1072 @@
|
|
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'
|