skeuocard-rails 0.1.0 → 1.0.0beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- // Generated by CoffeeScript 1.3.3
2
-
3
1
  /*
4
2
  "Skeuocard" -- A Skeuomorphic Credit-Card Input Enhancement
5
3
  @description Skeuocard is a skeuomorphic credit card input plugin, supporting
@@ -13,13 +11,14 @@
13
11
 
14
12
 
15
13
  (function() {
16
- var CCIssuers, CCProducts, Skeuocard,
17
- __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; },
14
+ var $, Skeuocard, visaProduct,
18
15
  __slice = [].slice,
19
- __hasProp = {}.hasOwnProperty,
20
- __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
16
+ __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
17
+
18
+ $ = jQuery;
21
19
 
22
20
  Skeuocard = (function() {
21
+ Skeuocard.currentDate = new Date();
23
22
 
24
23
  function Skeuocard(el, opts) {
25
24
  var optDefaults;
@@ -31,54 +30,69 @@
31
30
  underlyingFields: {}
32
31
  };
33
32
  this._inputViews = {};
33
+ this._inputViewsByFace = {
34
+ front: [],
35
+ back: []
36
+ };
34
37
  this._tabViews = {};
35
- this.product = void 0;
36
- this.productShortname = void 0;
37
- this.issuerShortname = void 0;
38
- this._cardProductNeedsLayout = true;
39
- this.acceptedCardProducts = {};
38
+ this._state = {};
39
+ this.product = null;
40
40
  this.visibleFace = 'front';
41
- this._initialValidationState = {};
42
- this._validationState = {
43
- number: false,
44
- exp: false,
45
- name: false,
46
- cvc: false
47
- };
48
- this._faceFillState = {
49
- front: false,
50
- back: false
51
- };
52
41
  optDefaults = {
53
42
  debug: false,
54
- acceptedCardProducts: [],
43
+ acceptedCardProducts: null,
55
44
  cardNumberPlaceholderChar: 'X',
56
45
  genericPlaceholder: "XXXX XXXX XXXX XXXX",
57
46
  typeInputSelector: '[name="cc_type"]',
58
47
  numberInputSelector: '[name="cc_number"]',
59
- expInputSelector: '[name="cc_exp"]',
48
+ expMonthInputSelector: '[name="cc_exp_month"]',
49
+ expYearInputSelector: '[name="cc_exp_year"]',
60
50
  nameInputSelector: '[name="cc_name"]',
61
51
  cvcInputSelector: '[name="cc_cvc"]',
62
- currentDate: new Date(),
63
52
  initialValues: {},
64
53
  validationState: {},
65
54
  strings: {
66
- hiddenFaceFillPrompt: "Click here to<br /> fill in the other side.",
55
+ hiddenFaceFillPrompt: "<strong>Click here</strong> to <br>fill in the other side.",
67
56
  hiddenFaceErrorWarning: "There's a problem on the other side.",
68
- hiddenFaceSwitchPrompt: "Back to the other side..."
57
+ hiddenFaceSwitchPrompt: "Forget something?<br> Flip the card over."
69
58
  }
70
59
  };
71
60
  this.options = $.extend(optDefaults, opts);
72
61
  this._conformDOM();
73
- this._setAcceptedCardProducts();
74
- this._createInputs();
75
- this._updateProductIfNeeded();
76
- this._flipToInvalidSide();
62
+ this._bindInputEvents();
63
+ this._importImplicitOptions();
64
+ this.render();
77
65
  }
78
66
 
67
+ Skeuocard.prototype._log = function() {
68
+ var msg;
69
+ msg = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
70
+ if ((typeof console !== "undefined" && console !== null ? console.log : void 0) && !!this.options.debug) {
71
+ if (this.options.debug != null) {
72
+ return console.log.apply(console, ["[skeuocard]"].concat(__slice.call(msg)));
73
+ }
74
+ }
75
+ };
76
+
77
+ Skeuocard.prototype.trigger = function() {
78
+ var args, _ref;
79
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
80
+ return (_ref = this.el.container).trigger.apply(_ref, args);
81
+ };
82
+
83
+ Skeuocard.prototype.bind = function() {
84
+ var args, _ref;
85
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
86
+ return (_ref = this.el.container).bind.apply(_ref, args);
87
+ };
88
+
89
+ /*
90
+ Transform the elements within the container, conforming the DOM so that it
91
+ becomes styleable, and that the underlying inputs are hidden.
92
+ */
93
+
94
+
79
95
  Skeuocard.prototype._conformDOM = function() {
80
- var el, fieldName, fieldValue, _ref, _ref1, _ref2,
81
- _this = this;
82
96
  this.el.container.removeClass('no-js');
83
97
  this.el.container.addClass("skeuocard js");
84
98
  this.el.container.find("> :not(input,select,textarea)").remove();
@@ -86,419 +100,367 @@
86
100
  this.el.underlyingFields = {
87
101
  type: this.el.container.find(this.options.typeInputSelector),
88
102
  number: this.el.container.find(this.options.numberInputSelector),
89
- exp: this.el.container.find(this.options.expInputSelector),
103
+ expMonth: this.el.container.find(this.options.expMonthInputSelector),
104
+ expYear: this.el.container.find(this.options.expYearInputSelector),
90
105
  name: this.el.container.find(this.options.nameInputSelector),
91
106
  cvc: this.el.container.find(this.options.cvcInputSelector)
92
107
  };
93
- _ref = this.options.initialValues;
94
- for (fieldName in _ref) {
95
- fieldValue = _ref[fieldName];
96
- this.el.underlyingFields[fieldName].val(fieldValue);
97
- }
98
- _ref1 = this.el.underlyingFields;
99
- for (fieldName in _ref1) {
100
- el = _ref1[fieldName];
101
- this.options.initialValues[fieldName] = el.val();
102
- }
103
- _ref2 = this.el.underlyingFields;
104
- for (fieldName in _ref2) {
105
- el = _ref2[fieldName];
106
- if (this.options.validationState[fieldName] === false || el.hasClass('invalid')) {
107
- this._initialValidationState[fieldName] = false;
108
- if (!el.hasClass('invalid')) {
109
- el.addClass('invalid');
110
- }
111
- }
112
- }
113
- this.el.underlyingFields.number.bind("change", function(e) {
114
- _this._inputViews.number.setValue(_this._getUnderlyingValue('number'));
115
- return _this.render();
116
- });
117
- this.el.underlyingFields.exp.bind("change", function(e) {
118
- _this._inputViews.exp.setValue(_this._getUnderlyingValue('exp'));
119
- return _this.render();
120
- });
121
- this.el.underlyingFields.name.bind("change", function(e) {
122
- _this._inputViews.exp.setValue(_this._getUnderlyingValue('name'));
123
- return _this.render();
124
- });
125
- this.el.underlyingFields.cvc.bind("change", function(e) {
126
- _this._inputViews.exp.setValue(_this._getUnderlyingValue('cvc'));
127
- return _this.render();
128
- });
129
- this.el.surfaceFront = $("<div>").attr({
108
+ this.el.front = $("<div>").attr({
130
109
  "class": "face front"
131
110
  });
132
- this.el.surfaceBack = $("<div>").attr({
111
+ this.el.back = $("<div>").attr({
133
112
  "class": "face back"
134
113
  });
135
114
  this.el.cardBody = $("<div>").attr({
136
115
  "class": "card-body"
137
116
  });
138
- this.el.surfaceFront.appendTo(this.el.cardBody);
139
- this.el.surfaceBack.appendTo(this.el.cardBody);
117
+ this.el.front.appendTo(this.el.cardBody);
118
+ this.el.back.appendTo(this.el.cardBody);
140
119
  this.el.cardBody.appendTo(this.el.container);
141
- this._tabViews.front = new this.FlipTabView('front');
142
- this._tabViews.back = new this.FlipTabView('back');
143
- this.el.surfaceFront.prepend(this._tabViews.front.el);
144
- this.el.surfaceBack.prepend(this._tabViews.back.el);
145
- this._tabViews.front.hide();
146
- this._tabViews.back.hide();
147
- this._tabViews.front.el.click(function() {
148
- return _this.flip();
120
+ this._tabViews.front = new Skeuocard.prototype.FlipTabView(this, 'front', {
121
+ strings: this.options.strings
149
122
  });
150
- this._tabViews.back.el.click(function() {
151
- return _this.flip();
123
+ this._tabViews.back = new Skeuocard.prototype.FlipTabView(this, 'back', {
124
+ strings: this.options.strings
152
125
  });
126
+ this.el.front.prepend(this._tabViews.front.el);
127
+ this.el.back.prepend(this._tabViews.back.el);
128
+ this._tabViews.front.hide();
129
+ this._tabViews.back.hide();
130
+ this._inputViews = {
131
+ number: new this.SegmentedCardNumberInputView(),
132
+ exp: new this.ExpirationInputView({
133
+ currentDate: this.options.currentDate
134
+ }),
135
+ name: new this.TextInputView({
136
+ "class": "cc-name",
137
+ placeholder: "YOUR NAME"
138
+ }),
139
+ cvc: new this.TextInputView({
140
+ "class": "cc-cvc",
141
+ placeholder: "XXX",
142
+ requireMaxLength: true
143
+ })
144
+ };
145
+ this._inputViews.number.el.addClass('cc-number');
146
+ this._inputViews.number.el.appendTo(this.el.front);
147
+ this._inputViews.name.el.appendTo(this.el.front);
148
+ this._inputViews.exp.el.addClass('cc-exp');
149
+ this._inputViews.exp.el.appendTo(this.el.front);
150
+ this._inputViews.cvc.el.appendTo(this.el.back);
153
151
  return this.el.container;
154
152
  };
155
153
 
156
- Skeuocard.prototype._setAcceptedCardProducts = function() {
157
- var matcher, product, _ref,
154
+ /*
155
+ Import implicit initialization options from the DOM. Brings in things like
156
+ the accepted card type, initial validation state, existing values, etc.
157
+ */
158
+
159
+
160
+ Skeuocard.prototype._importImplicitOptions = function() {
161
+ var fieldEl, fieldName, _initialExp, _ref,
158
162
  _this = this;
159
- if (this.options.acceptedCardProducts.length === 0) {
163
+ _ref = this.el.underlyingFields;
164
+ for (fieldName in _ref) {
165
+ fieldEl = _ref[fieldName];
166
+ if (this.options.initialValues[fieldName] == null) {
167
+ this.options.initialValues[fieldName] = fieldEl.val();
168
+ } else {
169
+ this.options.initialValues[fieldName] = this.options.initialValues[fieldName].toString();
170
+ this._setUnderlyingValue(fieldName, this.options.initialValues[fieldName]);
171
+ }
172
+ if (this.options.initialValues[fieldName].length > 0) {
173
+ this._state['initiallyFilled'] = true;
174
+ }
175
+ if (this.options.validationState[fieldName] == null) {
176
+ this.options.validationState[fieldName] = !fieldEl.hasClass('invalid');
177
+ }
178
+ }
179
+ if (this.options.acceptedCardProducts == null) {
180
+ this.options.acceptedCardProducts = [];
160
181
  this.el.underlyingFields.type.find('option').each(function(i, _el) {
161
- var cardProductShortname, el;
182
+ var el, shortname;
162
183
  el = $(_el);
163
- cardProductShortname = el.attr('data-card-product-shortname') || el.attr('value');
164
- return _this.options.acceptedCardProducts.push(cardProductShortname);
184
+ shortname = el.attr('data-sc-type') || el.attr('value');
185
+ return _this.options.acceptedCardProducts.push(shortname);
165
186
  });
166
187
  }
167
- for (matcher in CCProducts) {
168
- product = CCProducts[matcher];
169
- if (_ref = product.companyShortname, __indexOf.call(this.options.acceptedCardProducts, _ref) >= 0) {
170
- this.acceptedCardProducts[matcher] = product;
171
- }
188
+ if (this.options.initialValues.number.length > 0) {
189
+ this.set('number', this.options.initialValues.number);
172
190
  }
173
- return this.acceptedCardProducts;
191
+ if (this.options.initialValues.name.length > 0) {
192
+ this.set('name', this.options.initialValues.name);
193
+ }
194
+ if (this.options.initialValues.cvc.length > 0) {
195
+ this.set('cvc', this.options.initialValues.cvc);
196
+ }
197
+ if (this.options.initialValues.expYear.length > 0 && this.options.initialValues.expMonth.length > 0) {
198
+ _initialExp = new Date(parseInt(this.options.initialValues.expYear), parseInt(this.options.initialValues.expMonth) - 1, 1);
199
+ this.set('exp', _initialExp);
200
+ }
201
+ this._updateValidationForFace('front');
202
+ return this._updateValidationForFace('back');
174
203
  };
175
204
 
176
- Skeuocard.prototype._updateProductIfNeeded = function() {
177
- var matchedIssuerIdentifier, matchedProduct, matchedProductIdentifier, number;
178
- number = this._getUnderlyingValue('number');
179
- matchedProduct = this.getProductForNumber(number);
180
- matchedProductIdentifier = (matchedProduct != null ? matchedProduct.companyShortname : void 0) || '';
181
- matchedIssuerIdentifier = (matchedProduct != null ? matchedProduct.issuerShortname : void 0) || '';
182
- if ((this.productShortname !== matchedProductIdentifier) || (this.issuerShortname !== matchedIssuerIdentifier)) {
183
- this.productShortname = matchedProductIdentifier;
184
- this.issuerShortname = matchedIssuerIdentifier;
185
- this.product = matchedProduct;
186
- this._cardProductNeedsLayout = true;
187
- this.trigger('productWillChange.skeuocard', [this, this.productShortname, matchedProductIdentifier]);
188
- this._log("Triggering render because product changed.");
189
- this.render();
190
- return this.trigger('productDidChange.skeuocard', [this, this.productShortname, matchedProductIdentifier]);
191
- }
205
+ Skeuocard.prototype.set = function(field, newValue) {
206
+ this._inputViews[field].setValue(newValue);
207
+ return this._inputViews[field].trigger('valueChanged', this._inputViews[field]);
192
208
  };
193
209
 
194
- Skeuocard.prototype._createInputs = function() {
195
- var _this = this;
196
- this._inputViews.number = new this.SegmentedCardNumberInputView();
197
- this._inputViews.exp = new this.ExpirationInputView({
198
- currentDate: this.options.currentDate
210
+ /*
211
+ Bind interaction events to their appropriate handlers.
212
+ */
213
+
214
+
215
+ Skeuocard.prototype._bindInputEvents = function() {
216
+ var _expirationChange,
217
+ _this = this;
218
+ this.el.underlyingFields.number.bind("change", function(e) {
219
+ _this._inputViews.number.setValue(_this._getUnderlyingValue('number'));
220
+ return _this.render();
199
221
  });
200
- this._inputViews.name = new this.TextInputView({
201
- "class": "cc-name",
202
- placeholder: "YOUR NAME"
222
+ _expirationChange = function(e) {
223
+ var month, year;
224
+ month = parseInt(_this._getUnderlyingValue('expMonth'));
225
+ year = parseInt(_this._getUnderlyingValue('expYear'));
226
+ _this._inputViews.exp.setValue(new Date(year, month - 1));
227
+ return _this.render();
228
+ };
229
+ this.el.underlyingFields.expMonth.bind("change", _expirationChange);
230
+ this.el.underlyingFields.expYear.bind("change", _expirationChange);
231
+ this.el.underlyingFields.name.bind("change", function(e) {
232
+ _this._inputViews.exp.setValue(_this._getUnderlyingValue('name'));
233
+ return _this.render();
203
234
  });
204
- this._inputViews.cvc = new this.TextInputView({
205
- "class": "cc-cvc",
206
- placeholder: "XXX",
207
- requireMaxLength: true
235
+ this.el.underlyingFields.cvc.bind("change", function(e) {
236
+ _this._inputViews.exp.setValue(_this._getUnderlyingValue('cvc'));
237
+ return _this.render();
208
238
  });
209
- this._inputViews.number.el.addClass('cc-number');
210
- this._inputViews.number.el.appendTo(this.el.surfaceFront);
211
- this._inputViews.name.el.appendTo(this.el.surfaceFront);
212
- this._inputViews.exp.el.addClass('cc-exp');
213
- this._inputViews.exp.el.appendTo(this.el.surfaceFront);
214
- this._inputViews.cvc.el.appendTo(this.el.surfaceBack);
215
- this._inputViews.number.bind("change", function(e, input) {
216
- _this._setUnderlyingValue('number', input.getValue());
217
- _this._updateValidationStateForInputView('number');
218
- return _this._updateProductIfNeeded();
239
+ this._inputViews.number.bind("change valueChanged", function(e, input) {
240
+ var cardNumber, matchedProduct, number, previousProduct, _ref, _ref1;
241
+ cardNumber = input.getValue();
242
+ _this._setUnderlyingValue('number', cardNumber);
243
+ _this._updateValidation('number', cardNumber);
244
+ number = _this._getUnderlyingValue('number');
245
+ matchedProduct = Skeuocard.prototype.CardProduct.firstMatchingNumber(number);
246
+ if (!((_ref = _this.product) != null ? _ref.eql(matchedProduct) : void 0)) {
247
+ _this._log("Product will change:", _this.product, "=>", matchedProduct);
248
+ if (_ref1 = matchedProduct != null ? matchedProduct.attrs.companyShortname : void 0, __indexOf.call(_this.options.acceptedCardProducts, _ref1) >= 0) {
249
+ _this.trigger('productWillChange.skeuocard', [_this, _this.product, matchedProduct]);
250
+ previousProduct = _this.product;
251
+ _this.el.container.removeClass('unaccepted');
252
+ _this._renderProduct(matchedProduct);
253
+ _this.product = matchedProduct;
254
+ } else if (matchedProduct != null) {
255
+ _this.trigger('productWillChange.skeuocard', [_this, _this.product, null]);
256
+ _this.el.container.addClass('unaccepted');
257
+ _this._renderProduct(null);
258
+ _this.product = null;
259
+ } else {
260
+ _this.trigger('productWillChange.skeuocard', [_this, _this.product, null]);
261
+ _this.el.container.removeClass('unaccepted');
262
+ _this._renderProduct(null);
263
+ _this.product = null;
264
+ }
265
+ return _this.trigger('productDidChange.skeuocard', [_this, previousProduct, _this.product]);
266
+ }
219
267
  });
220
- this._inputViews.exp.bind("keyup", function(e, input) {
221
- _this._setUnderlyingValue('exp', input.value);
222
- return _this._updateValidationStateForInputView('exp');
268
+ this._inputViews.exp.bind("keyup valueChanged", function(e, input) {
269
+ var newDate;
270
+ newDate = input.getValue();
271
+ _this._updateValidation('exp', newDate);
272
+ if (newDate != null) {
273
+ _this._setUnderlyingValue('expMonth', newDate.getMonth() + 1);
274
+ return _this._setUnderlyingValue('expYear', newDate.getFullYear());
275
+ }
223
276
  });
224
- this._inputViews.name.bind("keyup", function(e) {
225
- _this._setUnderlyingValue('name', $(e.target).val());
226
- return _this._updateValidationStateForInputView('name');
277
+ this._inputViews.name.bind("keyup valueChanged", function(e, input) {
278
+ var value;
279
+ value = input.getValue();
280
+ _this._setUnderlyingValue('name', value);
281
+ return _this._updateValidation('name', value);
227
282
  });
228
- this._inputViews.cvc.bind("keyup", function(e) {
229
- _this._setUnderlyingValue('cvc', $(e.target).val());
230
- return _this._updateValidationStateForInputView('cvc');
283
+ this._inputViews.cvc.bind("keyup valueChanged", function(e, input) {
284
+ var value;
285
+ value = input.getValue();
286
+ _this._setUnderlyingValue('cvc', value);
287
+ return _this._updateValidation('cvc', value);
288
+ });
289
+ this.el.container.delegate("input", "keyup keydown", this._handleFieldTab.bind(this));
290
+ this._tabViews.front.el.click(function() {
291
+ return _this.flip();
292
+ });
293
+ return this._tabViews.back.el.click(function() {
294
+ return _this.flip();
231
295
  });
232
- this._inputViews.number.setValue(this._getUnderlyingValue('number'));
233
- this._inputViews.exp.setValue(this._getUnderlyingValue('exp'));
234
- this._inputViews.name.el.val(this._getUnderlyingValue('name'));
235
- return this._inputViews.cvc.el.val(this._getUnderlyingValue('cvc'));
236
- };
237
-
238
- Skeuocard.prototype._log = function() {
239
- var msg;
240
- msg = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
241
- if ((typeof console !== "undefined" && console !== null ? console.log : void 0) && !!this.options.debug) {
242
- if (this.options.debug != null) {
243
- return console.log.apply(console, ["[skeuocard]"].concat(__slice.call(msg)));
244
- }
245
- }
246
296
  };
247
297
 
248
- Skeuocard.prototype._flipToInvalidSide = function() {
249
- var fieldName, state, _errorCounts, _oppositeFace, _ref, _ref1;
250
- if (Object.keys(this._initialValidationState).length > 0) {
298
+ Skeuocard.prototype._handleFieldTab = function(e) {
299
+ var backFieldEls, currentFieldEl, frontFieldEls, _currentFace, _oppositeFace;
300
+ if (e.which === 9) {
301
+ currentFieldEl = $(e.currentTarget);
251
302
  _oppositeFace = this.visibleFace === 'front' ? 'back' : 'front';
252
- _errorCounts = {
253
- front: 0,
254
- back: 0
255
- };
256
- _ref = this._initialValidationState;
257
- for (fieldName in _ref) {
258
- state = _ref[fieldName];
259
- _errorCounts[(_ref1 = this.product) != null ? _ref1.layout[fieldName] : void 0]++;
303
+ _currentFace = this.visibleFace === 'front' ? 'front' : 'back';
304
+ backFieldEls = this.el[_oppositeFace].find('input');
305
+ frontFieldEls = this.el[_currentFace].find('input');
306
+ if (this.visibleFace === 'front' && this.el.front.hasClass('filled') && backFieldEls.length > 0 && frontFieldEls.index(currentFieldEl) === frontFieldEls.length - 1 && !e.shiftKey) {
307
+ this.flip();
308
+ backFieldEls.first().focus();
309
+ e.preventDefault();
260
310
  }
261
- if (_errorCounts[this.visibleFace] === 0 && _errorCounts[_oppositeFace] > 0) {
262
- return this.flip();
311
+ if (this.visibleFace === 'back' && e.shiftKey) {
312
+ this.flip();
313
+ backFieldEls.last().focus();
314
+ e.preventDefault();
263
315
  }
264
316
  }
317
+ return true;
265
318
  };
266
319
 
267
- Skeuocard.prototype.render = function() {
268
- var container, el, fieldName, inputEl, sel, surfaceName, _hiddenFaceFilled, _hiddenFaceValid, _oppositeFace, _ref, _visibleFaceFilled, _visibleFaceValid,
269
- _this = this;
270
- this._log("*** start rendering ***");
271
- if (this._cardProductNeedsLayout === true) {
272
- if (this.product !== void 0) {
273
- this._log("[render]", "Activating product", this.product);
274
- this.el.container.removeClass(function(index, css) {
275
- return (css.match(/\b(product|issuer)-\S+/g) || []).join(' ');
276
- });
277
- this.el.container.addClass("product-" + this.product.companyShortname);
278
- if (this.product.issuerShortname != null) {
279
- this.el.container.addClass("issuer-" + this.product.issuerShortname);
280
- }
281
- this._setUnderlyingCardType(this.product.companyShortname);
282
- this._inputViews.number.setGroupings(this.product.cardNumberGrouping);
283
- this._inputViews.exp.show();
284
- this._inputViews.name.show();
285
- this._inputViews.exp.reconfigure({
286
- pattern: this.product.expirationFormat
287
- });
288
- this._inputViews.cvc.show();
289
- this._inputViews.cvc.attr({
290
- maxlength: this.product.cvcLength,
291
- placeholder: new Array(this.product.cvcLength + 1).join(this.options.cardNumberPlaceholderChar)
292
- });
293
- _ref = this.product.layout;
294
- for (fieldName in _ref) {
295
- surfaceName = _ref[fieldName];
296
- sel = surfaceName === 'front' ? 'surfaceFront' : 'surfaceBack';
297
- container = this.el[sel];
298
- inputEl = this._inputViews[fieldName].el;
299
- if (!(container.has(inputEl).length > 0)) {
300
- this._log("Moving", inputEl, "=>", container);
301
- el = this._inputViews[fieldName].el.detach();
302
- $(el).appendTo(this.el[sel]);
303
- }
304
- }
305
- } else {
306
- this._log("[render]", "Becoming generic.");
307
- this._inputViews.exp.clear();
308
- this._inputViews.cvc.clear();
309
- this._inputViews.exp.hide();
310
- this._inputViews.name.hide();
311
- this._inputViews.cvc.hide();
312
- this._inputViews.number.setGroupings([this.options.genericPlaceholder.length]);
313
- /*
314
- @_inputViews.number.reconfigure
315
- groupings: [@options.genericPlaceholder.length],
316
- placeholder: @options.genericPlaceholder
317
- */
318
-
319
- this.el.container.removeClass(function(index, css) {
320
- return (css.match(/\bproduct-\S+/g) || []).join(' ');
321
- });
322
- this.el.container.removeClass(function(index, css) {
323
- return (css.match(/\bissuer-\S+/g) || []).join(' ');
324
- });
325
- }
326
- this._cardProductNeedsLayout = false;
327
- }
328
- this._log("Validation state:", this._validationState);
329
- this.showInitialValidationErrors();
330
- _oppositeFace = this.visibleFace === 'front' ? 'back' : 'front';
331
- _visibleFaceFilled = this._faceFillState[this.visibleFace];
332
- _visibleFaceValid = this.isFaceValid(this.visibleFace);
333
- _hiddenFaceFilled = this._faceFillState[_oppositeFace];
334
- _hiddenFaceValid = this.isFaceValid(_oppositeFace);
335
- if (_visibleFaceFilled && !_visibleFaceValid) {
336
- this._log("Visible face is filled, but invalid; showing validation errors.");
337
- this.showValidationErrors();
338
- } else if (!_visibleFaceFilled) {
339
- this._log("Visible face hasn't been filled; hiding validation errors.");
340
- this.hideValidationErrors();
341
- } else {
342
- this._log("Visible face has been filled, and is valid.");
343
- this.hideValidationErrors();
344
- }
345
- if (this.visibleFace === 'front' && this.fieldsForFace('back').length > 0) {
346
- if (_visibleFaceFilled && _visibleFaceValid && !_hiddenFaceFilled) {
347
- this._tabViews.front.prompt(this.options.strings.hiddenFaceFillPrompt, true);
348
- } else if (_hiddenFaceFilled && !_hiddenFaceValid) {
349
- this._tabViews.front.warn(this.options.strings.hiddenFaceErrorWarning, true);
350
- } else if (_hiddenFaceFilled && _hiddenFaceValid) {
351
- this._tabViews.front.prompt(this.options.strings.hiddenFaceSwitchPrompt, true);
352
- } else {
353
- this._tabViews.front.hide();
320
+ Skeuocard.prototype._updateValidation = function(fieldName, newValue) {
321
+ var fillStateChanged, isFilled, isFixed, isValid, needsFix, validationStateChanged;
322
+ if (this.product == null) {
323
+ return false;
324
+ }
325
+ isFilled = this.product[fieldName].isFilled(newValue);
326
+ needsFix = (this.options.validationState[fieldName] != null) === false;
327
+ isFixed = (this.options.initialValues[fieldName] != null) && newValue !== this.options.initialValues[fieldName];
328
+ isValid = this.product[fieldName].isValid(newValue) && ((needsFix && isFixed) || true);
329
+ fillStateChanged = this._state["" + fieldName + "Filled"] !== isFilled;
330
+ validationStateChanged = this._state["" + fieldName + "Valid"] !== isValid;
331
+ if (fillStateChanged) {
332
+ this.trigger("fieldFillStateWillChange.skeuocard", [this, fieldName, isFilled]);
333
+ this._inputViews[fieldName].el.toggleClass('filled', isFilled);
334
+ this._state["" + fieldName + "Filled"] = isFilled;
335
+ this.trigger("fieldFillStateDidChange.skeuocard", [this, fieldName, isFilled]);
336
+ }
337
+ if (validationStateChanged) {
338
+ this.trigger("fieldValidationStateWillChange.skeuocard", [this, fieldName, isFilled]);
339
+ this._inputViews[fieldName].el.toggleClass('valid', isValid);
340
+ this._inputViews[fieldName].el.toggleClass('invalid', !isValid);
341
+ this._state["" + fieldName + "Valid"] = isValid;
342
+ this.trigger("fieldValidationStateDidChange.skeuocard", [this, fieldName, isFilled]);
343
+ }
344
+ return this._updateValidationForFace(this.visibleFace);
345
+ };
346
+
347
+ Skeuocard.prototype._updateValidationForFace = function(face) {
348
+ var fieldsFilled, fieldsValid, fillStateChanged, isFilled, isValid, iv, validationStateChanged;
349
+ fieldsFilled = ((function() {
350
+ var _i, _len, _ref, _results;
351
+ _ref = this._inputViewsByFace[face];
352
+ _results = [];
353
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
354
+ iv = _ref[_i];
355
+ _results.push(iv.el.hasClass('filled'));
354
356
  }
355
- } else {
356
- if (_hiddenFaceValid) {
357
- this._tabViews.back.prompt(this.options.strings.hiddenFaceSwitchPrompt, true);
358
- } else {
359
- this._tabViews.back.warn(this.options.strings.hiddenFaceErrorWarning, true);
357
+ return _results;
358
+ }).call(this)).every(Boolean);
359
+ fieldsValid = ((function() {
360
+ var _i, _len, _ref, _results;
361
+ _ref = this._inputViewsByFace[face];
362
+ _results = [];
363
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
364
+ iv = _ref[_i];
365
+ _results.push(iv.el.hasClass('valid'));
360
366
  }
367
+ return _results;
368
+ }).call(this)).every(Boolean);
369
+ isFilled = (fieldsFilled && (this.product != null)) || (this._state['initiallyFilled'] || false);
370
+ isValid = fieldsValid && (this.product != null);
371
+ fillStateChanged = this._state["" + face + "Filled"] !== isFilled;
372
+ validationStateChanged = this._state["" + face + "Valid"] !== isValid;
373
+ if (fillStateChanged) {
374
+ this.trigger("faceFillStateWillChange.skeuocard", [this, face, isFilled]);
375
+ this.el[face].toggleClass('filled', isFilled);
376
+ this._state["" + face + "Filled"] = isFilled;
377
+ this.trigger("faceFillStateDidChange.skeuocard", [this, face, isFilled]);
378
+ }
379
+ if (validationStateChanged) {
380
+ this.trigger("faceValidationStateWillChange.skeuocard", [this, face, isValid]);
381
+ this.el[face].toggleClass('valid', isValid);
382
+ this.el[face].toggleClass('invalid', !isValid);
383
+ this._state["" + face + "Valid"] = isValid;
384
+ return this.trigger("faceValidationStateDidChange.skeuocard", [this, face, isValid]);
385
+ }
386
+ };
387
+
388
+ /*
389
+ Assert rendering changes necessary for the current product. Passing a null
390
+ value instead of a product will revert the card to a generic state.
391
+ */
392
+
393
+
394
+ Skeuocard.prototype._renderProduct = function(product) {
395
+ var destFace, fieldName, focused, view, viewEl, _ref, _ref1,
396
+ _this = this;
397
+ this._log("[_renderProduct]", "Rendering product:", product);
398
+ this.el.container.removeClass(function(index, css) {
399
+ return (css.match(/\b(product|issuer)-\S+/g) || []).join(' ');
400
+ });
401
+ if ((product != null ? product.attrs.companyShortname : void 0) != null) {
402
+ this.el.container.addClass("product-" + product.attrs.companyShortname);
361
403
  }
362
- if (!this.isValid()) {
363
- this.el.container.removeClass('valid');
364
- this.el.container.addClass('invalid');
365
- } else {
366
- this.el.container.addClass('valid');
367
- this.el.container.removeClass('invalid');
404
+ if ((product != null ? product.attrs.issuerShortname : void 0) != null) {
405
+ this.el.container.addClass("issuer-" + product.attrs.issuerShortname);
368
406
  }
369
- return this._log("*** rendering complete ***");
370
- };
371
-
372
- Skeuocard.prototype.showInitialValidationErrors = function() {
373
- var fieldName, state, _ref, _results;
374
- _ref = this._initialValidationState;
375
- _results = [];
376
- for (fieldName in _ref) {
377
- state = _ref[fieldName];
378
- if (state === false && this._validationState[fieldName] === false) {
379
- _results.push(this._inputViews[fieldName].addClass('invalid'));
380
- } else {
381
- _results.push(this._inputViews[fieldName].removeClass('invalid'));
407
+ this._setUnderlyingValue('type', (product != null ? product.attrs.companyShortname : void 0) || null);
408
+ this._inputViews.number.setGroupings((product != null ? product.attrs.cardNumberGrouping : void 0) || [this.options.genericPlaceholder.length]);
409
+ if (product != null) {
410
+ this._inputViews.exp.reconfigure({
411
+ pattern: (product != null ? product.attrs.expirationFormat : void 0) || "MM/YY"
412
+ });
413
+ this._inputViews.cvc.attr({
414
+ maxlength: product.attrs.cvcLength,
415
+ placeholder: new Array(product.attrs.cvcLength + 1).join(this.options.cardNumberPlaceholderChar)
416
+ });
417
+ this._inputViewsByFace = {
418
+ front: [],
419
+ back: []
420
+ };
421
+ focused = $('*:focus');
422
+ _ref = product.attrs.layout;
423
+ for (fieldName in _ref) {
424
+ destFace = _ref[fieldName];
425
+ this._log("Moving", fieldName, "to", destFace);
426
+ viewEl = this._inputViews[fieldName].el.detach();
427
+ viewEl.appendTo(this.el[destFace]);
428
+ this._inputViewsByFace[destFace].push(this._inputViews[fieldName]);
429
+ this._inputViews[fieldName].show();
382
430
  }
383
- }
384
- return _results;
385
- };
386
-
387
- Skeuocard.prototype.showValidationErrors = function() {
388
- var fieldName, state, _ref, _results;
389
- _ref = this._validationState;
390
- _results = [];
391
- for (fieldName in _ref) {
392
- state = _ref[fieldName];
393
- if (state === true) {
394
- _results.push(this._inputViews[fieldName].removeClass('invalid'));
395
- } else {
396
- _results.push(this._inputViews[fieldName].addClass('invalid'));
431
+ setTimeout(function() {
432
+ var fieldEl, fieldLength;
433
+ fieldEl = focused.first();
434
+ fieldLength = fieldEl[0].maxLength;
435
+ fieldEl.focus();
436
+ return fieldEl[0].setSelectionRange(fieldLength, fieldLength);
437
+ }, 10);
438
+ } else {
439
+ _ref1 = this._inputViews;
440
+ for (fieldName in _ref1) {
441
+ view = _ref1[fieldName];
442
+ if (fieldName !== 'number') {
443
+ view.hide();
444
+ }
397
445
  }
398
446
  }
399
- return _results;
447
+ return product;
400
448
  };
401
449
 
402
- Skeuocard.prototype.hideValidationErrors = function() {
403
- var fieldName, state, _ref, _results;
404
- _ref = this._validationState;
450
+ Skeuocard.prototype._renderValidation = function() {
451
+ var fieldName, fieldView, _ref, _results;
452
+ _ref = this._inputViews;
405
453
  _results = [];
406
454
  for (fieldName in _ref) {
407
- state = _ref[fieldName];
408
- if ((this._initialValidationState[fieldName] === false && state === true) || (!(this._initialValidationState[fieldName] != null))) {
409
- _results.push(this._inputViews[fieldName].el.removeClass('invalid'));
410
- } else {
411
- _results.push(void 0);
412
- }
455
+ fieldView = _ref[fieldName];
456
+ _results.push(this._updateValidation(fieldName, fieldView.getValue()));
413
457
  }
414
458
  return _results;
415
459
  };
416
460
 
417
- Skeuocard.prototype.setFieldValidationState = function(fieldName, valid) {
418
- if (valid) {
419
- this.el.underlyingFields[fieldName].removeClass('invalid');
420
- } else {
421
- this.el.underlyingFields[fieldName].addClass('invalid');
422
- }
423
- return this._validationState[fieldName] = valid;
424
- };
425
-
426
- Skeuocard.prototype.isFaceFilled = function(faceName) {
427
- var fields, filled, name;
428
- fields = this.fieldsForFace(faceName);
429
- filled = (function() {
430
- var _i, _len, _results;
431
- _results = [];
432
- for (_i = 0, _len = fields.length; _i < _len; _i++) {
433
- name = fields[_i];
434
- if (this._inputViews[name].isFilled()) {
435
- _results.push(name);
436
- }
437
- }
438
- return _results;
439
- }).call(this);
440
- if (fields.length > 0) {
441
- return filled.length === fields.length;
442
- } else {
443
- return false;
444
- }
445
- };
446
-
447
- Skeuocard.prototype.fieldsForFace = function(faceName) {
448
- var face, fn, _ref;
449
- if ((_ref = this.product) != null ? _ref.layout : void 0) {
450
- return (function() {
451
- var _ref1, _results;
452
- _ref1 = this.product.layout;
453
- _results = [];
454
- for (fn in _ref1) {
455
- face = _ref1[fn];
456
- if (face === faceName) {
457
- _results.push(fn);
458
- }
459
- }
460
- return _results;
461
- }).call(this);
462
- }
463
- return [];
464
- };
465
-
466
- Skeuocard.prototype._updateValidationStateForInputView = function(fieldName) {
467
- var field, fieldValid;
468
- field = this._inputViews[fieldName];
469
- fieldValid = field.isValid() && !(this._initialValidationState[fieldName] === false && field.getValue() === this.options.initialValues[fieldName]);
470
- if (fieldValid !== this._validationState[fieldName]) {
471
- this.setFieldValidationState(fieldName, fieldValid);
472
- this._faceFillState.front = this.isFaceFilled('front');
473
- this._faceFillState.back = this.isFaceFilled('back');
474
- this.trigger('validationStateDidChange.skeuocard', [this, this._validationState]);
475
- this._log("Change in validation for " + fieldName + " triggers re-render.");
476
- return this.render();
477
- }
478
- };
479
-
480
- Skeuocard.prototype.isFaceValid = function(faceName) {
481
- var fieldName, valid, _i, _len, _ref;
482
- valid = true;
483
- _ref = this.fieldsForFace(faceName);
484
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
485
- fieldName = _ref[_i];
486
- valid &= this._validationState[fieldName];
487
- }
488
- return !!valid;
489
- };
490
-
491
- Skeuocard.prototype.isValid = function() {
492
- return this._validationState.number && this._validationState.exp && this._validationState.name && this._validationState.cvc;
493
- };
494
-
495
- Skeuocard.prototype._getUnderlyingValue = function(field) {
496
- return this.el.underlyingFields[field].val();
497
- };
498
-
499
- Skeuocard.prototype._setUnderlyingValue = function(field, newValue) {
500
- this.trigger('change.skeuocard', [this]);
501
- return this.el.underlyingFields[field].val(newValue);
461
+ Skeuocard.prototype.render = function() {
462
+ this._renderProduct(this.product);
463
+ return this._renderValidation();
502
464
  };
503
465
 
504
466
  Skeuocard.prototype.flip = function() {
@@ -506,122 +468,123 @@
506
468
  targetFace = this.visibleFace === 'front' ? 'back' : 'front';
507
469
  this.trigger('faceWillBecomeVisible.skeuocard', [this, targetFace]);
508
470
  this.visibleFace = targetFace;
509
- this.render();
510
471
  this.el.cardBody.toggleClass('flip');
511
- surfaceName = this.visibleFace === 'front' ? 'surfaceFront' : 'surfaceBack';
512
- this.el[surfaceName].find('input').first().focus();
472
+ surfaceName = this.visibleFace === 'front' ? 'front' : 'back';
473
+ this.el[surfaceName].find('.cc-field').not('.filled').find('input').first().focus();
513
474
  return this.trigger('faceDidBecomeVisible.skeuocard', [this, targetFace]);
514
475
  };
515
476
 
516
- Skeuocard.prototype.getProductForNumber = function(num) {
517
- var d, issuer, m, matcher, parts, _ref;
518
- _ref = this.acceptedCardProducts;
519
- for (m in _ref) {
520
- d = _ref[m];
521
- parts = m.split('/');
522
- matcher = new RegExp(parts[1], parts[2]);
523
- if (matcher.test(num)) {
524
- issuer = this.getIssuerForNumber(num) || {};
525
- return $.extend({}, d, issuer);
526
- }
477
+ Skeuocard.prototype._setUnderlyingValue = function(field, newValue) {
478
+ var fieldEl, remapAttrKey, _newValue,
479
+ _this = this;
480
+ fieldEl = this.el.underlyingFields[field];
481
+ _newValue = (newValue || "").toString();
482
+ if (fieldEl == null) {
483
+ throw "Set underlying value of unknown field: " + field + ".";
527
484
  }
528
- return void 0;
529
- };
530
-
531
- Skeuocard.prototype.getIssuerForNumber = function(num) {
532
- var d, m, matcher, parts;
533
- for (m in CCIssuers) {
534
- d = CCIssuers[m];
535
- parts = m.split('/');
536
- matcher = new RegExp(parts[1], parts[2]);
537
- if (matcher.test(num)) {
538
- return d;
539
- }
485
+ this.trigger('change.skeuocard', [this]);
486
+ if (!fieldEl.is('select')) {
487
+ return this.el.underlyingFields[field].val(_newValue);
488
+ } else {
489
+ remapAttrKey = "data-sc-" + field.toLowerCase();
490
+ return fieldEl.find('option').each(function(i, _el) {
491
+ var optionEl;
492
+ optionEl = $(_el);
493
+ if (_newValue === (optionEl.attr(remapAttrKey) || optionEl.attr('value'))) {
494
+ return _this.el.underlyingFields[field].val(optionEl.attr('value'));
495
+ }
496
+ });
540
497
  }
541
- return void 0;
542
498
  };
543
499
 
544
- Skeuocard.prototype._setUnderlyingCardType = function(shortname) {
545
- var _this = this;
546
- return this.el.underlyingFields.type.find('option').each(function(i, _el) {
547
- var el;
548
- el = $(_el);
549
- if (shortname === (el.attr('data-card-product-shortname') || el.attr('value'))) {
550
- return el.val(el.attr('value'));
551
- }
552
- });
553
- };
554
-
555
- Skeuocard.prototype.trigger = function() {
556
- var args, _ref;
557
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
558
- return (_ref = this.el.container).trigger.apply(_ref, args);
500
+ Skeuocard.prototype._getUnderlyingValue = function(field) {
501
+ var _ref;
502
+ return (_ref = this.el.underlyingFields[field]) != null ? _ref.val() : void 0;
559
503
  };
560
504
 
561
- Skeuocard.prototype.bind = function() {
562
- var args, _ref;
563
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
564
- return (_ref = this.el.container).trigger.apply(_ref, args);
505
+ Skeuocard.prototype.isValid = function() {
506
+ return !this.el.front.hasClass('invalid') && !this.el.back.hasClass('invalid');
565
507
  };
566
508
 
567
509
  return Skeuocard;
568
510
 
569
511
  })();
570
512
 
513
+ window.Skeuocard = Skeuocard;
514
+
571
515
  /*
572
516
  Skeuocard::FlipTabView
573
517
  Handles rendering of the "flip button" control and its various warning and
574
518
  prompt states.
575
-
576
- TODO: Rebuild this so that it observes events and contains its own logic.
577
519
  */
578
520
 
579
521
 
580
522
  Skeuocard.prototype.FlipTabView = (function() {
581
-
582
- function FlipTabView(face, opts) {
523
+ function FlipTabView(sc, face, opts) {
524
+ var _this = this;
583
525
  if (opts == null) {
584
526
  opts = {};
585
527
  }
528
+ this.card = sc;
529
+ this.face = face;
586
530
  this.el = $("<div class=\"flip-tab " + face + "\"><p></p></div>");
587
531
  this.options = opts;
532
+ this._state = {};
533
+ this.card.bind('faceFillStateWillChange.skeuocard', this._faceStateChanged.bind(this));
534
+ this.card.bind('faceValidationStateWillChange.skeuocard', this._faceValidationChanged.bind(this));
535
+ this.card.bind('productWillChange.skeuocard', function(e, card, prevProduct, newProduct) {
536
+ if (newProduct == null) {
537
+ return _this.hide();
538
+ }
539
+ });
588
540
  }
589
541
 
590
- FlipTabView.prototype._setText = function(text) {
591
- return this.el.find('p').html(text);
542
+ FlipTabView.prototype._faceStateChanged = function(e, card, face, isFilled) {
543
+ var oppositeFace;
544
+ oppositeFace = face === 'front' ? 'back' : 'front';
545
+ if (isFilled === true && this.card._inputViewsByFace[oppositeFace].length > 0) {
546
+ this.show();
547
+ }
548
+ if (face !== this.face) {
549
+ this._state.opposingFaceFilled = isFilled;
550
+ }
551
+ if (this._state.opposingFaceFilled !== true) {
552
+ return this.warn(this.options.strings.hiddenFaceFillPrompt, true);
553
+ }
592
554
  };
593
555
 
594
- FlipTabView.prototype.warn = function(message, withAnimation) {
595
- if (withAnimation == null) {
596
- withAnimation = false;
556
+ FlipTabView.prototype._faceValidationChanged = function(e, card, face, isValid) {
557
+ if (face !== this.face) {
558
+ this._state.opposingFaceValid = isValid;
597
559
  }
560
+ if (this._state.opposingFaceValid) {
561
+ return this.prompt(this.options.strings.hiddenFaceSwitchPrompt);
562
+ } else {
563
+ if (this._state.opposingFaceFilled) {
564
+ return this.warn(this.options.strings.hiddenFaceErrorWarning);
565
+ } else {
566
+ return this.warn(this.options.strings.hiddenFaceFillPrompt);
567
+ }
568
+ }
569
+ };
570
+
571
+ FlipTabView.prototype._setText = function(text) {
572
+ return this.el.find('p').first().html(text);
573
+ };
574
+
575
+ FlipTabView.prototype.warn = function(message) {
598
576
  this._resetClasses();
599
- this.el.addClass('warn');
600
577
  this._setText(message);
601
- this.show();
602
- if (withAnimation) {
603
- this.el.removeClass('warn-anim');
604
- return this.el.addClass('warn-anim');
605
- }
578
+ return this.el.addClass('warn');
606
579
  };
607
580
 
608
- FlipTabView.prototype.prompt = function(message, withAnimation) {
609
- if (withAnimation == null) {
610
- withAnimation = false;
611
- }
581
+ FlipTabView.prototype.prompt = function(message) {
612
582
  this._resetClasses();
613
- this.el.addClass('prompt');
614
583
  this._setText(message);
615
- this.show();
616
- if (withAnimation) {
617
- this.el.removeClass('valid-anim');
618
- return this.el.addClass('valid-anim');
619
- }
584
+ return this.el.addClass('prompt');
620
585
  };
621
586
 
622
587
  FlipTabView.prototype._resetClasses = function() {
623
- this.el.removeClass('valid-anim');
624
- this.el.removeClass('warn-anim');
625
588
  this.el.removeClass('warn');
626
589
  return this.el.removeClass('prompt');
627
590
  };
@@ -639,95 +602,29 @@
639
602
  })();
640
603
 
641
604
  /*
642
- Skeuocard::TextInputView
605
+ # Skeuocard::SegmentedCardNumberInputView
606
+ # Provides a reconfigurable segmented input view for credit card numbers.
643
607
  */
644
608
 
645
609
 
646
- Skeuocard.prototype.TextInputView = (function() {
647
-
648
- function TextInputView() {}
649
-
650
- TextInputView.prototype.bind = function() {
651
- var args, _ref;
652
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
653
- return (_ref = this.el).bind.apply(_ref, args);
654
- };
655
-
656
- TextInputView.prototype.trigger = function() {
657
- var args, _ref;
658
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
659
- return (_ref = this.el).trigger.apply(_ref, args);
660
- };
661
-
662
- TextInputView.prototype._getFieldCaretPosition = function(el) {
663
- var input, sel, selLength;
664
- input = el.get(0);
665
- if (input.selectionEnd != null) {
666
- return input.selectionEnd;
667
- } else if (document.selection) {
668
- input.focus();
669
- sel = document.selection.createRange();
670
- selLength = document.selection.createRange().text.length;
671
- sel.moveStart('character', -input.value.length);
672
- return selLength;
673
- }
674
- };
675
-
676
- TextInputView.prototype._setFieldCaretPosition = function(el, pos) {
677
- var input, range;
678
- input = el.get(0);
679
- if (input.createTextRange != null) {
680
- range = input.createTextRange();
681
- range.move("character", pos);
682
- return range.select();
683
- } else if (input.selectionStart != null) {
684
- input.focus();
685
- return input.setSelectionRange(pos, pos);
686
- }
687
- };
688
-
689
- TextInputView.prototype.show = function() {
690
- return this.el.show();
691
- };
692
-
693
- TextInputView.prototype.hide = function() {
694
- return this.el.hide();
695
- };
696
-
697
- TextInputView.prototype.addClass = function() {
698
- var args, _ref;
699
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
700
- return (_ref = this.el).addClass.apply(_ref, args);
701
- };
702
-
703
- TextInputView.prototype.removeClass = function() {
704
- var args, _ref;
705
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
706
- return (_ref = this.el).removeClass.apply(_ref, args);
707
- };
708
-
709
- TextInputView.prototype._zeroPadNumber = function(num, places) {
710
- var zero;
711
- zero = places - num.toString().length + 1;
712
- return Array(zero).join("0") + num;
713
- };
714
-
715
- return TextInputView;
716
-
717
- })();
718
-
719
610
  Skeuocard.prototype.SegmentedCardNumberInputView = (function() {
720
-
721
611
  SegmentedCardNumberInputView.prototype._digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
722
612
 
723
- SegmentedCardNumberInputView.prototype._arrowKeys = {
724
- left: 37,
725
- up: 38,
726
- right: 39,
727
- down: 40
613
+ SegmentedCardNumberInputView.prototype._keys = {
614
+ backspace: 8,
615
+ tab: 9,
616
+ enter: 13,
617
+ del: 46,
618
+ arrowLeft: 37,
619
+ arrowUp: 38,
620
+ arrowRight: 39,
621
+ arrowDown: 40,
622
+ arrows: [37, 38, 39, 40],
623
+ command: 16,
624
+ alt: 17
728
625
  };
729
626
 
730
- SegmentedCardNumberInputView.prototype._specialKeys = [8, 9, 16, 17, 18, 19, 20, 27, 33, 34, 35, 36, 37, 38, 39, 40, 45, 46, 91, 93, 144, 145, 224];
627
+ SegmentedCardNumberInputView.prototype._specialKeys = [8, 9, 13, 46, 37, 38, 39, 40, 16, 17];
731
628
 
732
629
  function SegmentedCardNumberInputView(opts) {
733
630
  if (opts == null) {
@@ -747,93 +644,124 @@
747
644
  }
748
645
 
749
646
  SegmentedCardNumberInputView.prototype._buildDOM = function() {
647
+ var _this = this;
750
648
  this.el = $('<fieldset>');
649
+ this.el.addClass('cc-field');
751
650
  this.el.delegate("input", "keypress", this._handleGroupKeyPress.bind(this));
752
651
  this.el.delegate("input", "keydown", this._handleGroupKeyDown.bind(this));
753
652
  this.el.delegate("input", "keyup", this._handleGroupKeyUp.bind(this));
754
653
  this.el.delegate("input", "paste", this._handleGroupPaste.bind(this));
755
- return this.el.delegate("input", "change", this._handleGroupChange.bind(this));
654
+ this.el.delegate("input", "change", this._handleGroupChange.bind(this));
655
+ this.el.delegate("input", "focus", function(e) {
656
+ return _this.el.addClass('focus');
657
+ });
658
+ return this.el.delegate("input", "blur", function(e) {
659
+ return _this.el.removeClass('focus');
660
+ });
756
661
  };
757
662
 
758
663
  SegmentedCardNumberInputView.prototype._handleGroupKeyDown = function(e) {
759
- var currentTarget, inputGroupEl, inputMaxLength, nextInputEl, prevInputEl, selectionEnd;
664
+ var currentTarget, cursorEnd, cursorStart, inputGroupEl, inputMaxLength, nextInputEl, prevInputEl, _ref;
760
665
  if (e.ctrlKey || e.metaKey) {
761
666
  return this._handleModifiedKeyDown(e);
762
667
  }
763
668
  inputGroupEl = $(e.currentTarget);
764
669
  currentTarget = e.currentTarget;
765
- selectionEnd = currentTarget.selectionEnd;
670
+ cursorStart = currentTarget.selectionStart;
671
+ cursorEnd = currentTarget.selectionEnd;
766
672
  inputMaxLength = currentTarget.maxLength;
767
673
  prevInputEl = inputGroupEl.prevAll('input');
768
674
  nextInputEl = inputGroupEl.nextAll('input');
769
- if (e.which === 8 && prevInputEl.length > 0) {
770
- if (selectionEnd === 0) {
771
- this._focusField(prevInputEl.first(), 'end');
772
- }
773
- }
774
- return true;
775
- };
776
-
777
- SegmentedCardNumberInputView.prototype._handleGroupKeyUp = function(e) {
778
- var currentTarget, inputGroupEl, inputMaxLength, nextInputEl, selectionEnd, _ref;
779
- inputGroupEl = $(e.currentTarget);
780
- currentTarget = e.currentTarget;
781
- selectionEnd = currentTarget.selectionEnd;
782
- inputMaxLength = currentTarget.maxLength;
783
- nextInputEl = inputGroupEl.nextAll('input');
784
- if (e.ctrlKey || e.metaKey) {
785
- return false;
786
- }
787
- if ((_ref = e.which) === 37 || _ref === 38 || _ref === 39 || _ref === 40) {
788
- if (this._state.selectingAll) {
789
- this._endSelectAll();
790
- }
791
- }
792
675
  switch (e.which) {
793
- case this._arrowKeys.left:
794
- if (selectionEnd === 0) {
676
+ case this._keys.backspace:
677
+ if (prevInputEl.length > 0 && cursorEnd === 0) {
678
+ this._focusField(prevInputEl.first(), 'end');
679
+ }
680
+ break;
681
+ case this._keys.arrowUp:
682
+ if (cursorEnd === inputMaxLength) {
683
+ this._focusField(inputGroupEl, 'start');
684
+ } else {
795
685
  this._focusField(inputGroupEl.prev(), 'end');
796
686
  }
687
+ e.preventDefault();
797
688
  break;
798
- case this._arrowKeys.right:
799
- if (selectionEnd === inputMaxLength) {
689
+ case this._keys.arrowDown:
690
+ if (cursorEnd === inputMaxLength) {
800
691
  this._focusField(inputGroupEl.next(), 'start');
692
+ } else {
693
+ this._focusField(inputGroupEl, 'end');
801
694
  }
802
- break;
803
- case this._arrowKeys.up:
804
- this._focusField(inputGroupEl.next(), 'start');
805
695
  e.preventDefault();
806
696
  break;
807
- case this._arrowKeys.down:
808
- this._focusField(inputGroupEl.prev(), 'start');
809
- e.preventDefault();
697
+ case this._keys.arrowLeft:
698
+ if (cursorEnd === 0) {
699
+ this._focusField(inputGroupEl.prev(), 'end');
700
+ e.preventDefault();
701
+ }
702
+ break;
703
+ case this._keys.arrowRight:
704
+ if (cursorEnd === inputMaxLength) {
705
+ this._focusField(inputGroupEl.next(), 'start');
706
+ e.preventDefault();
707
+ }
810
708
  break;
811
709
  default:
812
- if (selectionEnd === inputMaxLength) {
813
- if (nextInputEl.length !== 0) {
814
- this._focusField(nextInputEl.first(), 'start');
815
- } else {
816
- e.preventDefault();
817
- }
710
+ if (!(_ref = e.which, __indexOf.call(this._specialKeys, _ref) >= 0) && (cursorStart === inputMaxLength && cursorEnd === inputMaxLength) && nextInputEl.length !== 0) {
711
+ this._focusField(nextInputEl.first(), 'start');
818
712
  }
819
713
  }
820
- this.trigger('change', [this]);
821
714
  return true;
822
715
  };
823
716
 
824
717
  SegmentedCardNumberInputView.prototype._handleGroupKeyPress = function(e) {
825
- var currentTarget, inputGroupEl, inputMaxLength, isDigit, nextInputEl, selectionEnd, _ref, _ref1;
718
+ var inputGroupEl, isDigit, _ref, _ref1;
719
+ inputGroupEl = $(e.currentTarget);
720
+ isDigit = (_ref = String.fromCharCode(e.which), __indexOf.call(this._digits, _ref) >= 0);
721
+ if (e.ctrlKey || e.metaKey) {
722
+ return true;
723
+ }
724
+ if (e.which === 0) {
725
+ return true;
726
+ }
727
+ if ((!e.shiftKey && (_ref1 = e.which, __indexOf.call(this._specialKeys, _ref1) >= 0)) || isDigit) {
728
+ return true;
729
+ }
730
+ e.preventDefault();
731
+ return false;
732
+ };
733
+
734
+ SegmentedCardNumberInputView.prototype._handleGroupKeyUp = function(e) {
735
+ var currentTarget, cursorEnd, cursorStart, inputGroupEl, inputMaxLength, nextInputEl, _ref, _ref1, _ref2;
826
736
  inputGroupEl = $(e.currentTarget);
827
737
  currentTarget = e.currentTarget;
828
- selectionEnd = currentTarget.selectionEnd;
829
738
  inputMaxLength = currentTarget.maxLength;
830
- isDigit = (_ref = String.fromCharCode(e.which), __indexOf.call(this._digits, _ref) >= 0);
739
+ cursorStart = currentTarget.selectionStart;
740
+ cursorEnd = currentTarget.selectionEnd;
831
741
  nextInputEl = inputGroupEl.nextAll('input');
832
- if (e.ctrlKey || e.metaKey || (_ref1 = e.which, __indexOf.call(this._specialKeys, _ref1) >= 0) || isDigit) {
742
+ if (e.ctrlKey || e.metaKey) {
833
743
  return true;
834
- } else {
835
- e.preventDefault();
836
- return false;
744
+ }
745
+ if (this._state.selectingAll && (_ref = e.which, __indexOf.call(this._specialKeys, _ref) >= 0) && e.which !== this._keys.command && e.which !== this._keys.alt) {
746
+ this._endSelectAll();
747
+ }
748
+ if (!(_ref1 = e.which, __indexOf.call(this._specialKeys, _ref1) >= 0) && !(e.shiftKey && e.which === this._keys.tab) && (cursorStart === inputMaxLength && cursorEnd === inputMaxLength) && nextInputEl.length !== 0) {
749
+ this._focusField(nextInputEl.first(), 'start');
750
+ }
751
+ if (!(e.shiftKey && (_ref2 = e.which, __indexOf.call(this._specialKeys, _ref2) >= 0))) {
752
+ this.trigger('change', [this]);
753
+ }
754
+ return true;
755
+ };
756
+
757
+ SegmentedCardNumberInputView.prototype._handleModifiedKeyDown = function(e) {
758
+ var char;
759
+ char = String.fromCharCode(e.which);
760
+ switch (char) {
761
+ case 'a':
762
+ case 'A':
763
+ this._beginSelectAll();
764
+ return e.preventDefault();
837
765
  }
838
766
  };
839
767
 
@@ -850,16 +778,6 @@
850
778
  }, 50);
851
779
  };
852
780
 
853
- SegmentedCardNumberInputView.prototype._handleModifiedKeyDown = function(e) {
854
- var char;
855
- char = String.fromCharCode(e.which);
856
- switch (char) {
857
- case 'A':
858
- this._beginSelectAll();
859
- return e.preventDefault();
860
- }
861
- };
862
-
863
781
  SegmentedCardNumberInputView.prototype._handleGroupChange = function(e) {
864
782
  return e.stopPropagation();
865
783
  };
@@ -870,14 +788,14 @@
870
788
 
871
789
  SegmentedCardNumberInputView.prototype._beginSelectAll = function() {
872
790
  var fieldEl;
873
- if (this._state.selectingAll === false) {
874
- this._state.selectingAll = true;
791
+ if (!this.el.hasClass('selecting-all')) {
875
792
  this._state.lastGrouping = this.options.groupings;
876
- this._state.lastValue = this.getValue();
793
+ this._state.lastLength = this.getValue().length;
877
794
  this.setGroupings(this.optDefaults.groupings);
878
795
  this.el.addClass('selecting-all');
879
796
  fieldEl = this.el.find("input");
880
- return fieldEl[0].setSelectionRange(0, fieldEl.val().length);
797
+ fieldEl[0].setSelectionRange(0, fieldEl.val().length);
798
+ return this._state.selectingAll = true;
881
799
  } else {
882
800
  fieldEl = this.el.find("input");
883
801
  return fieldEl[0].setSelectionRange(0, fieldEl.val().length);
@@ -885,14 +803,12 @@
885
803
  };
886
804
 
887
805
  SegmentedCardNumberInputView.prototype._endSelectAll = function() {
888
- if (this._state.selectingAll) {
889
- if (this._state.lastValue === this.getValue()) {
806
+ if (this.el.hasClass('selecting-all')) {
807
+ this._state.selectingAll = false;
808
+ if (this._state.lastLength === this.getValue().length) {
890
809
  this.setGroupings(this._state.lastGrouping);
891
- } else {
892
- this._focusField(this.el.find('input').last(), 'end');
893
810
  }
894
- this.el.removeClass('selecting-all');
895
- return this._state.selectingAll = false;
811
+ return this.el.removeClass('selecting-all');
896
812
  }
897
813
  };
898
814
 
@@ -949,8 +865,8 @@
949
865
  field = this.el.find('input').last();
950
866
  this._focusField(field, place);
951
867
  } else {
952
- field = void 0;
953
- fieldOffset = void 0;
868
+ field = null;
869
+ fieldOffset = null;
954
870
  _lastStartPos = 0;
955
871
  _ref = this.options.groupings;
956
872
  for (groupIndex = _i = 0, _len = _ref.length; _i < _len; groupIndex = ++_i) {
@@ -1019,35 +935,6 @@
1019
935
  });
1020
936
  };
1021
937
 
1022
- SegmentedCardNumberInputView.prototype.isFilled = function() {
1023
- return this.getValue().length === this.maxLength();
1024
- };
1025
-
1026
- SegmentedCardNumberInputView.prototype.isValid = function() {
1027
- return this.isFilled() && this.isValidLuhn(this.getValue());
1028
- };
1029
-
1030
- SegmentedCardNumberInputView.prototype.isValidLuhn = function(identifier) {
1031
- var alt, i, num, sum, _i, _ref;
1032
- sum = 0;
1033
- alt = false;
1034
- for (i = _i = _ref = identifier.length - 1; _i >= 0; i = _i += -1) {
1035
- num = parseInt(identifier.charAt(i), 10);
1036
- if (isNaN(num)) {
1037
- return false;
1038
- }
1039
- if (alt) {
1040
- num *= 2;
1041
- if (num > 9) {
1042
- num = (num % 10) + 1;
1043
- }
1044
- }
1045
- alt = !alt;
1046
- sum += num;
1047
- }
1048
- return sum % 10 === 0;
1049
- };
1050
-
1051
938
  SegmentedCardNumberInputView.prototype.bind = function() {
1052
939
  var args, _ref;
1053
940
  args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
@@ -1089,37 +976,43 @@
1089
976
  */
1090
977
 
1091
978
 
1092
- Skeuocard.prototype.ExpirationInputView = (function(_super) {
1093
-
1094
- __extends(ExpirationInputView, _super);
1095
-
979
+ Skeuocard.prototype.ExpirationInputView = (function() {
1096
980
  function ExpirationInputView(opts) {
1097
981
  var _this = this;
1098
982
  if (opts == null) {
1099
983
  opts = {};
1100
984
  }
1101
- opts.dateFormatter || (opts.dateFormatter = function(date) {
1102
- return date.getDate() + "-" + (date.getMonth() + 1) + "-" + date.getFullYear();
1103
- });
1104
- opts.dateParser || (opts.dateParser = function(value) {
1105
- var dateParts;
1106
- dateParts = value.split('-');
1107
- return new Date(dateParts[2], dateParts[1] - 1, dateParts[0]);
1108
- });
1109
- opts.currentDate || (opts.currentDate = new Date());
1110
985
  opts.pattern || (opts.pattern = "MM/YY");
1111
986
  this.options = opts;
1112
- this.date = void 0;
1113
- this.value = void 0;
987
+ this.date = null;
1114
988
  this.el = $("<fieldset>");
989
+ this.el.addClass('cc-field');
1115
990
  this.el.delegate("input", "keydown", function(e) {
1116
991
  return _this._onKeyDown(e);
1117
992
  });
1118
993
  this.el.delegate("input", "keyup", function(e) {
1119
994
  return _this._onKeyUp(e);
1120
995
  });
996
+ this.el.delegate("input", "focus", function(e) {
997
+ return _this.el.addClass('focus');
998
+ });
999
+ this.el.delegate("input", "blur", function(e) {
1000
+ return _this.el.removeClass('focus');
1001
+ });
1121
1002
  }
1122
1003
 
1004
+ ExpirationInputView.prototype.bind = function() {
1005
+ var args, _ref;
1006
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
1007
+ return (_ref = this.el).bind.apply(_ref, args);
1008
+ };
1009
+
1010
+ ExpirationInputView.prototype.trigger = function() {
1011
+ var args, _ref;
1012
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
1013
+ return (_ref = this.el).trigger.apply(_ref, args);
1014
+ };
1015
+
1123
1016
  ExpirationInputView.prototype._getFieldCaretPosition = function(el) {
1124
1017
  var input, sel, selLength;
1125
1018
  input = el.get(0);
@@ -1197,6 +1090,12 @@
1197
1090
  }
1198
1091
  };
1199
1092
 
1093
+ ExpirationInputView.prototype._zeroPadNumber = function(num, places) {
1094
+ var zero;
1095
+ zero = places - num.toString().length + 1;
1096
+ return Array(zero).join("0") + num;
1097
+ };
1098
+
1200
1099
  ExpirationInputView.prototype._updateFieldValues = function() {
1201
1100
  var currentDate,
1202
1101
  _this = this;
@@ -1228,24 +1127,13 @@
1228
1127
  });
1229
1128
  };
1230
1129
 
1231
- ExpirationInputView.prototype.setDate = function(newDate) {
1130
+ ExpirationInputView.prototype.setValue = function(newDate) {
1232
1131
  this.date = newDate;
1233
- this.value = this.options.dateFormatter(newDate);
1234
- return this._updateFieldValues();
1235
- };
1236
-
1237
- ExpirationInputView.prototype.setValue = function(newValue) {
1238
- this.value = newValue;
1239
- this.date = this.options.dateParser(newValue);
1240
1132
  return this._updateFieldValues();
1241
1133
  };
1242
1134
 
1243
- ExpirationInputView.prototype.getDate = function() {
1244
- return this.date;
1245
- };
1246
-
1247
1135
  ExpirationInputView.prototype.getValue = function() {
1248
- return this.value;
1136
+ return this.date;
1249
1137
  };
1250
1138
 
1251
1139
  ExpirationInputView.prototype.reconfigure = function(opts) {
@@ -1294,6 +1182,10 @@
1294
1182
  }
1295
1183
  };
1296
1184
 
1185
+ ExpirationInputView.prototype.getRawValue = function(fieldType) {
1186
+ return parseInt(this.el.find(".cc-exp-field-" + fieldType).val());
1187
+ };
1188
+
1297
1189
  ExpirationInputView.prototype._onKeyUp = function(e) {
1298
1190
  var arrowKeys, dateObj, day, groupCaretPos, groupEl, groupMaxLength, groupValLength, month, nextInputEl, pattern, specialKeys, year, _ref, _ref1;
1299
1191
  e.stopPropagation();
@@ -1316,18 +1208,16 @@
1316
1208
  if ((_ref1 = e.which, __indexOf.call(specialKeys, _ref1) < 0) && groupEl.val().length === groupMaxLength && !$.isEmptyObject(nextInputEl) && this._getFieldCaretPosition(groupEl) === groupMaxLength) {
1317
1209
  nextInputEl.focus();
1318
1210
  }
1319
- day = parseInt(this.el.find('.cc-exp-field-d').val()) || 1;
1320
- month = parseInt(this.el.find('.cc-exp-field-m').val());
1321
- year = parseInt(this.el.find('.cc-exp-field-y').val());
1211
+ day = this.getRawValue('d') || 1;
1212
+ month = this.getRawValue('m');
1213
+ year = this.getRawValue('y');
1322
1214
  if (month === 0 || year === 0) {
1323
- this.value = "";
1324
1215
  this.date = null;
1325
1216
  } else {
1326
1217
  if (year < 2000) {
1327
1218
  year += 2000;
1328
1219
  }
1329
1220
  dateObj = new Date(year, month - 1, day);
1330
- this.value = this.options.dateFormatter(dateObj);
1331
1221
  this.date = dateObj;
1332
1222
  }
1333
1223
  this.trigger("keyup", [this]);
@@ -1338,126 +1228,352 @@
1338
1228
  return this.el.find("input");
1339
1229
  };
1340
1230
 
1341
- ExpirationInputView.prototype.isFilled = function() {
1342
- var el, inputEl, _i, _len, _ref;
1343
- _ref = this.groupEls;
1344
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
1345
- inputEl = _ref[_i];
1346
- el = $(inputEl);
1347
- if (el.val().length !== parseInt(el.attr('maxlength'))) {
1348
- return false;
1349
- }
1350
- }
1351
- return true;
1231
+ ExpirationInputView.prototype.show = function() {
1232
+ return this.el.show();
1352
1233
  };
1353
1234
 
1354
- ExpirationInputView.prototype.isValid = function() {
1355
- return this.isFilled() && ((this.date.getFullYear() === this.options.currentDate.getFullYear() && this.date.getMonth() >= this.options.currentDate.getMonth()) || this.date.getFullYear() > this.options.currentDate.getFullYear());
1235
+ ExpirationInputView.prototype.hide = function() {
1236
+ return this.el.hide();
1356
1237
  };
1357
1238
 
1358
1239
  return ExpirationInputView;
1359
1240
 
1360
- })(Skeuocard.prototype.TextInputView);
1241
+ })();
1361
1242
 
1362
- Skeuocard.prototype.TextInputView = (function(_super) {
1243
+ /*
1244
+ Skeuocard::TextInputView
1245
+ */
1363
1246
 
1364
- __extends(TextInputView, _super);
1365
1247
 
1248
+ Skeuocard.prototype.TextInputView = (function() {
1366
1249
  function TextInputView(opts) {
1367
- this.el = $("<input>").attr({
1250
+ var _this = this;
1251
+ this.el = $('<div>');
1252
+ this.inputEl = $("<input>").attr({
1368
1253
  type: 'text',
1369
1254
  placeholder: opts.placeholder,
1370
1255
  "class": opts["class"]
1371
1256
  });
1257
+ this.el.append(this.inputEl);
1258
+ this.el.addClass('cc-field');
1372
1259
  this.options = opts;
1260
+ this.el.delegate("input", "focus", function(e) {
1261
+ return _this.el.addClass('focus');
1262
+ });
1263
+ this.el.delegate("input", "blur", function(e) {
1264
+ return _this.el.removeClass('focus');
1265
+ });
1266
+ this.el.delegate("input", "keyup", function(e) {
1267
+ e.stopPropagation();
1268
+ return _this.trigger('keyup', [_this]);
1269
+ });
1373
1270
  }
1374
1271
 
1375
1272
  TextInputView.prototype.clear = function() {
1376
- return this.el.val("");
1273
+ return this.inputEl.val("");
1377
1274
  };
1378
1275
 
1379
1276
  TextInputView.prototype.attr = function() {
1380
1277
  var args, _ref;
1381
1278
  args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
1382
- return (_ref = this.el).attr.apply(_ref, args);
1279
+ return (_ref = this.inputEl).attr.apply(_ref, args);
1383
1280
  };
1384
1281
 
1385
- TextInputView.prototype.isFilled = function() {
1386
- return this.el.val().length > 0;
1282
+ TextInputView.prototype.setValue = function(newValue) {
1283
+ return this.inputEl.val(newValue);
1387
1284
  };
1388
1285
 
1389
- TextInputView.prototype.isValid = function() {
1390
- if (this.options.requireMaxLength) {
1391
- return this.el.val().length === parseInt(this.el.attr('maxlength'));
1392
- } else {
1393
- return this.isFilled();
1394
- }
1286
+ TextInputView.prototype.getValue = function() {
1287
+ return this.inputEl.val();
1395
1288
  };
1396
1289
 
1397
- TextInputView.prototype.getValue = function() {
1398
- return this.el.val();
1290
+ TextInputView.prototype.bind = function() {
1291
+ var args, _ref;
1292
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
1293
+ return (_ref = this.el).bind.apply(_ref, args);
1399
1294
  };
1400
1295
 
1401
- return TextInputView;
1296
+ TextInputView.prototype.trigger = function() {
1297
+ var args, _ref;
1298
+ args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
1299
+ return (_ref = this.el).trigger.apply(_ref, args);
1300
+ };
1402
1301
 
1403
- })(Skeuocard.prototype.TextInputView);
1302
+ TextInputView.prototype.show = function() {
1303
+ return this.el.show();
1304
+ };
1404
1305
 
1405
- window.Skeuocard = Skeuocard;
1306
+ TextInputView.prototype.hide = function() {
1307
+ return this.el.hide();
1308
+ };
1309
+
1310
+ return TextInputView;
1311
+
1312
+ })();
1406
1313
 
1407
1314
  /*
1408
- # Card Definitions
1315
+ Skeuocard::CardProduct
1409
1316
  */
1410
1317
 
1411
1318
 
1412
- CCProducts = {};
1319
+ Skeuocard.prototype.CardProduct = (function() {
1320
+ CardProduct._registry = [];
1321
+
1322
+ CardProduct.create = function(opts) {
1323
+ return this._registry.push(new Skeuocard.prototype.CardProduct(opts));
1324
+ };
1325
+
1326
+ CardProduct.firstMatchingShortname = function(shortname) {
1327
+ var card, _i, _len, _ref;
1328
+ _ref = this._registry;
1329
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
1330
+ card = _ref[_i];
1331
+ if (card.attrs.companyShortname === shortname) {
1332
+ return card;
1333
+ }
1334
+ }
1335
+ return null;
1336
+ };
1337
+
1338
+ CardProduct.firstMatchingNumber = function(number) {
1339
+ var card, combinedOptions, variation, _i, _len, _ref;
1340
+ _ref = this._registry;
1341
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
1342
+ card = _ref[_i];
1343
+ if (card.pattern.test(number)) {
1344
+ if ((variation = card.firstVariationMatchingNumber(number))) {
1345
+ combinedOptions = $.extend({}, card.attrs, variation);
1346
+ return new Skeuocard.prototype.CardProduct(combinedOptions);
1347
+ }
1348
+ return new Skeuocard.prototype.CardProduct(card.attrs);
1349
+ }
1350
+ }
1351
+ return null;
1352
+ };
1353
+
1354
+ function CardProduct(attrs) {
1355
+ this.attrs = $.extend({}, attrs);
1356
+ this.pattern = this.attrs.pattern;
1357
+ this._variances = [];
1358
+ this.name = {
1359
+ isFilled: this._isCardNameFilled.bind(this),
1360
+ isValid: this._isCardNameValid.bind(this)
1361
+ };
1362
+ this.number = {
1363
+ isFilled: this._isCardNumberFilled.bind(this),
1364
+ isValid: this._isCardNumberValid.bind(this)
1365
+ };
1366
+ this.exp = {
1367
+ isFilled: this._isCardExpirationFilled.bind(this),
1368
+ isValid: this._isCardExpirationValid.bind(this)
1369
+ };
1370
+ this.cvc = {
1371
+ isFilled: this._isCardCVCFilled.bind(this),
1372
+ isValid: this._isCardCVCValid.bind(this)
1373
+ };
1374
+ }
1375
+
1376
+ CardProduct.prototype.createVariation = function(attrs) {
1377
+ return this._variances.push(attrs);
1378
+ };
1379
+
1380
+ CardProduct.prototype.firstVariationMatchingNumber = function(number) {
1381
+ var variance, _i, _len, _ref;
1382
+ _ref = this._variances;
1383
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
1384
+ variance = _ref[_i];
1385
+ if (variance.pattern.test(number)) {
1386
+ return variance;
1387
+ }
1388
+ }
1389
+ return null;
1390
+ };
1391
+
1392
+ CardProduct.prototype.fieldsForLayoutFace = function(faceName) {
1393
+ var face, fieldName, _ref, _results;
1394
+ _ref = this.attrs.layout;
1395
+ _results = [];
1396
+ for (fieldName in _ref) {
1397
+ face = _ref[fieldName];
1398
+ if (face === faceName) {
1399
+ _results.push(fieldName);
1400
+ }
1401
+ }
1402
+ return _results;
1403
+ };
1404
+
1405
+ CardProduct.prototype._id = function() {
1406
+ var ident;
1407
+ ident = this.attrs.companyShortname;
1408
+ if (this.attrs.issuerShortname != null) {
1409
+ ident += this.attrs.issuerShortname;
1410
+ }
1411
+ return ident;
1412
+ };
1413
+
1414
+ CardProduct.prototype.eql = function(otherCardProduct) {
1415
+ return (otherCardProduct != null ? otherCardProduct._id() : void 0) === this._id();
1416
+ };
1417
+
1418
+ CardProduct.prototype._daysInMonth = function(m, y) {
1419
+ switch (m) {
1420
+ case 1:
1421
+ if ((y % 4 === 0 && y % 100) || y % 400 === 0) {
1422
+ return 29;
1423
+ } else {
1424
+ return 28;
1425
+ }
1426
+ case 3:
1427
+ case 5:
1428
+ case 8:
1429
+ case 10:
1430
+ return 30;
1431
+ default:
1432
+ return 31;
1433
+ }
1434
+ };
1435
+
1436
+ CardProduct.prototype._isCardNumberFilled = function(number) {
1437
+ var _ref;
1438
+ if (this.attrs.cardNumberLength != null) {
1439
+ return (_ref = number.length, __indexOf.call(this.attrs.cardNumberLength, _ref) >= 0);
1440
+ }
1441
+ };
1442
+
1443
+ CardProduct.prototype._isCardExpirationFilled = function(exp) {
1444
+ var currentDate, day, month, year;
1445
+ currentDate = Skeuocard.currentDate;
1446
+ if (!((exp != null) && (exp.getMonth != null) && (exp.getFullYear != null))) {
1447
+ return false;
1448
+ }
1449
+ day = exp.getDate();
1450
+ month = exp.getMonth();
1451
+ year = exp.getFullYear();
1452
+ return (day > 0 && day <= this._daysInMonth(month, year)) && (month >= 0 && month <= 11) && (year >= 1900 && year <= currentDate.getFullYear() + 10);
1453
+ };
1454
+
1455
+ CardProduct.prototype._isCardCVCFilled = function(cvc) {
1456
+ return cvc.length === this.attrs.cvcLength;
1457
+ };
1458
+
1459
+ CardProduct.prototype._isCardNameFilled = function(name) {
1460
+ return name.length > 0;
1461
+ };
1462
+
1463
+ CardProduct.prototype._isCardNumberValid = function(number) {
1464
+ return /^\d+$/.test(number) && (this.attrs.validateLuhn === false || this._isValidLuhn(number)) && this._isCardNumberFilled(number);
1465
+ };
1466
+
1467
+ CardProduct.prototype._isCardExpirationValid = function(exp) {
1468
+ var currentDate, day, isDateInFuture, month, year;
1469
+ if (!((exp != null) && (exp.getMonth != null) && (exp.getFullYear != null))) {
1470
+ return false;
1471
+ }
1472
+ currentDate = Skeuocard.currentDate;
1473
+ day = exp.getDate();
1474
+ month = exp.getMonth();
1475
+ year = exp.getFullYear();
1476
+ isDateInFuture = (year === currentDate.getFullYear() && month >= currentDate.getMonth()) || year > currentDate.getFullYear();
1477
+ return isDateInFuture && this._isCardExpirationFilled(exp);
1478
+ };
1479
+
1480
+ CardProduct.prototype._isCardCVCValid = function(cvc) {
1481
+ return this._isCardCVCFilled(cvc);
1482
+ };
1483
+
1484
+ CardProduct.prototype._isCardNameValid = function(name) {
1485
+ return this._isCardNameFilled(name);
1486
+ };
1487
+
1488
+ CardProduct.prototype._isValidLuhn = function(number) {
1489
+ var alt, i, num, sum, _i, _ref;
1490
+ sum = 0;
1491
+ alt = false;
1492
+ for (i = _i = _ref = number.length - 1; _i >= 0; i = _i += -1) {
1493
+ num = parseInt(number.charAt(i), 10);
1494
+ if (isNaN(num)) {
1495
+ return false;
1496
+ }
1497
+ if (alt) {
1498
+ num *= 2;
1499
+ if (num > 9) {
1500
+ num = (num % 10) + 1;
1501
+ }
1502
+ }
1503
+ alt = !alt;
1504
+ sum += num;
1505
+ }
1506
+ return sum % 10 === 0;
1507
+ };
1508
+
1509
+ return CardProduct;
1510
+
1511
+ })();
1512
+
1513
+ /*
1514
+ # Seed CardProducts.
1515
+ */
1413
1516
 
1414
- CCProducts[/^30[0-5][0-9]/] = {
1517
+
1518
+ Skeuocard.prototype.CardProduct.create({
1519
+ pattern: /^(36|38|30[0-5])/,
1415
1520
  companyName: "Diners Club",
1416
1521
  companyShortname: "dinersclubintl",
1417
1522
  cardNumberGrouping: [4, 6, 4],
1523
+ cardNumberLength: [14],
1418
1524
  expirationFormat: "MM/YY",
1419
1525
  cvcLength: 3,
1526
+ validateLuhn: true,
1420
1527
  layout: {
1421
1528
  number: 'front',
1422
1529
  exp: 'front',
1423
1530
  name: 'front',
1424
1531
  cvc: 'back'
1425
1532
  }
1426
- };
1533
+ });
1427
1534
 
1428
- CCProducts[/^3095/] = {
1429
- companyName: "Diners Club International",
1430
- companyShortname: "dinersclubintl",
1431
- cardNumberGrouping: [4, 6, 4],
1432
- expirationFormat: "MM/YY",
1535
+ Skeuocard.prototype.CardProduct.create({
1536
+ pattern: /^35/,
1537
+ companyName: "JCB",
1538
+ companyShortname: "jcb",
1539
+ cardNumberGrouping: [4, 4, 4, 4],
1540
+ cardNumberLength: [16],
1541
+ expirationFormat: "MM/'YY",
1433
1542
  cvcLength: 3,
1543
+ validateLuhn: true,
1434
1544
  layout: {
1435
1545
  number: 'front',
1436
1546
  exp: 'front',
1437
1547
  name: 'front',
1438
1548
  cvc: 'back'
1439
1549
  }
1440
- };
1550
+ });
1441
1551
 
1442
- CCProducts[/^36\d{2}/] = {
1443
- companyName: "Diners Club International",
1444
- companyShortname: "dinersclubintl",
1445
- cardNumberGrouping: [4, 6, 4],
1552
+ Skeuocard.prototype.CardProduct.create({
1553
+ pattern: /^3[47]/,
1554
+ companyName: "American Express",
1555
+ companyShortname: "amex",
1556
+ cardNumberGrouping: [4, 6, 5],
1557
+ cardNumberLength: [15],
1446
1558
  expirationFormat: "MM/YY",
1447
- cvcLength: 3,
1559
+ cvcLength: 4,
1560
+ validateLuhn: true,
1448
1561
  layout: {
1449
1562
  number: 'front',
1450
1563
  exp: 'front',
1451
1564
  name: 'front',
1452
- cvc: 'back'
1565
+ cvc: 'front'
1453
1566
  }
1454
- };
1567
+ });
1455
1568
 
1456
- CCProducts[/^35\d{2}/] = {
1457
- companyName: "JCB",
1458
- companyShortname: "jcb",
1569
+ Skeuocard.prototype.CardProduct.create({
1570
+ pattern: /^(6706|6771|6709)/,
1571
+ companyName: "Laser Card Services Ltd.",
1572
+ companyShortname: "laser",
1459
1573
  cardNumberGrouping: [4, 4, 4, 4],
1574
+ cardNumberLength: [16, 17, 18, 19],
1460
1575
  expirationFormat: "MM/YY",
1576
+ validateLuhn: true,
1461
1577
  cvcLength: 3,
1462
1578
  layout: {
1463
1579
  number: 'front',
@@ -1465,27 +1581,33 @@
1465
1581
  name: 'front',
1466
1582
  cvc: 'back'
1467
1583
  }
1468
- };
1584
+ });
1469
1585
 
1470
- CCProducts[/^3[47]/] = {
1471
- companyName: "American Express",
1472
- companyShortname: "amex",
1473
- cardNumberGrouping: [4, 6, 5],
1586
+ Skeuocard.prototype.CardProduct.create({
1587
+ pattern: /^4/,
1588
+ companyName: "Visa",
1589
+ companyShortname: "visa",
1590
+ cardNumberGrouping: [4, 4, 4, 4],
1591
+ cardNumberLength: [13, 14, 15, 16],
1474
1592
  expirationFormat: "MM/YY",
1475
- cvcLength: 4,
1593
+ validateLuhn: true,
1594
+ cvcLength: 3,
1476
1595
  layout: {
1477
1596
  number: 'front',
1478
1597
  exp: 'front',
1479
1598
  name: 'front',
1480
- cvc: 'front'
1599
+ cvc: 'back'
1481
1600
  }
1482
- };
1483
-
1484
- CCProducts[/^38/] = {
1485
- companyName: "Hipercard",
1486
- companyShortname: "hipercard",
1487
- cardNumberGrouping: [4, 4, 4, 4],
1601
+ });
1602
+
1603
+ Skeuocard.prototype.CardProduct.create({
1604
+ pattern: /^(62|88)/,
1605
+ companyName: "China UnionPay",
1606
+ companyShortname: "unionpay",
1607
+ cardNumberGrouping: [19],
1608
+ cardNumberLength: [16, 17, 18, 19],
1488
1609
  expirationFormat: "MM/YY",
1610
+ validateLuhn: false,
1489
1611
  cvcLength: 3,
1490
1612
  layout: {
1491
1613
  number: 'front',
@@ -1493,13 +1615,16 @@
1493
1615
  name: 'front',
1494
1616
  cvc: 'back'
1495
1617
  }
1496
- };
1618
+ });
1497
1619
 
1498
- CCProducts[/^4[0-9]\d{2}/] = {
1499
- companyName: "Visa",
1500
- companyShortname: "visa",
1620
+ Skeuocard.prototype.CardProduct.create({
1621
+ pattern: /^5[1-5]/,
1622
+ companyName: "Mastercard",
1623
+ companyShortname: "mastercard",
1501
1624
  cardNumberGrouping: [4, 4, 4, 4],
1625
+ cardNumberLength: [16],
1502
1626
  expirationFormat: "MM/YY",
1627
+ validateLuhn: true,
1503
1628
  cvcLength: 3,
1504
1629
  layout: {
1505
1630
  number: 'front',
@@ -1507,13 +1632,16 @@
1507
1632
  name: 'front',
1508
1633
  cvc: 'back'
1509
1634
  }
1510
- };
1635
+ });
1511
1636
 
1512
- CCProducts[/^5[0-8]\d{2}/] = {
1513
- companyName: "Mastercard",
1514
- companyShortname: "mastercard",
1637
+ Skeuocard.prototype.CardProduct.create({
1638
+ pattern: /^(5018|5020|5038|6304|6759|676[1-3])/,
1639
+ companyName: "Maestro (MasterCard)",
1640
+ companyShortname: "maestro",
1515
1641
  cardNumberGrouping: [4, 4, 4, 4],
1642
+ cardNumberLength: [12, 13, 14, 15, 16, 17, 18, 19],
1516
1643
  expirationFormat: "MM/YY",
1644
+ validateLuhn: true,
1517
1645
  cvcLength: 3,
1518
1646
  layout: {
1519
1647
  number: 'front',
@@ -1521,13 +1649,16 @@
1521
1649
  name: 'front',
1522
1650
  cvc: 'back'
1523
1651
  }
1524
- };
1652
+ });
1525
1653
 
1526
- CCProducts[/^6011/] = {
1654
+ Skeuocard.prototype.CardProduct.create({
1655
+ pattern: /^(6011|65|64[4-9]|622)/,
1527
1656
  companyName: "Discover",
1528
1657
  companyShortname: "discover",
1529
1658
  cardNumberGrouping: [4, 4, 4, 4],
1659
+ cardNumberLength: [16],
1530
1660
  expirationFormat: "MM/YY",
1661
+ validateLuhn: true,
1531
1662
  cvcLength: 3,
1532
1663
  layout: {
1533
1664
  number: 'front',
@@ -1535,25 +1666,21 @@
1535
1666
  name: 'front',
1536
1667
  cvc: 'back'
1537
1668
  }
1538
- };
1539
-
1540
- CCIssuers = {};
1541
-
1542
- /*
1543
- Hack fixes the Chase Sapphire card's stupid (nice?) layout non-conformity.
1544
- */
1669
+ });
1545
1670
 
1671
+ visaProduct = Skeuocard.prototype.CardProduct.firstMatchingShortname('visa');
1546
1672
 
1547
- CCIssuers[/^414720/] = {
1673
+ visaProduct.createVariation({
1674
+ pattern: /^414720/,
1548
1675
  issuingAuthority: "Chase",
1549
1676
  issuerName: "Chase Sapphire Card",
1550
1677
  issuerShortname: "chase-sapphire",
1551
1678
  layout: {
1679
+ name: 'front',
1552
1680
  number: 'front',
1553
1681
  exp: 'front',
1554
- name: 'front',
1555
1682
  cvc: 'front'
1556
1683
  }
1557
- };
1684
+ });
1558
1685
 
1559
1686
  }).call(this);