judge 0.5.0 → 1.0.0
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.
- data/.gitignore +8 -0
- data/.travis.yml +2 -1
- data/Gemfile +1 -10
- data/Rakefile +9 -44
- data/judge.gemspec +17 -164
- data/lib/generators/judge/templates/json2.js +46 -42
- data/lib/generators/judge/templates/judge.js +42 -41
- data/lib/generators/judge/templates/underscore.js +219 -97
- data/lib/judge.rb +6 -4
- data/lib/judge/form_builder.rb +75 -0
- data/lib/judge/{utils.rb → message_collection.rb} +45 -28
- data/lib/judge/validator.rb +21 -0
- data/lib/judge/validator_collection.rb +28 -0
- data/lib/judge/version.rb +3 -0
- data/spec/javascripts/JudgeSpec.js +25 -23
- data/test/expected_elements.rb +235 -0
- data/test/factories.rb +23 -0
- data/test/setup.rb +70 -0
- data/test/test_form_builder.rb +69 -0
- data/test/test_helper.rb +8 -20
- data/test/test_message_collection.rb +84 -0
- data/test/test_validator.rb +37 -0
- data/test/test_validator_collection.rb +19 -0
- metadata +46 -137
- data/.document +0 -5
- data/Gemfile.lock +0 -116
- data/VERSION +0 -1
- data/docs/docco.css +0 -196
- data/docs/judge.html +0 -280
- data/lib/judge/form.rb +0 -59
- data/test/dummy/Gemfile +0 -3
- data/test/dummy/Gemfile.lock +0 -75
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/controllers/application_controller.rb +0 -3
- data/test/dummy/app/controllers/foos_controller.rb +0 -11
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/city.rb +0 -5
- data/test/dummy/app/models/continent.rb +0 -4
- data/test/dummy/app/models/country.rb +0 -6
- data/test/dummy/app/models/fake.rb +0 -2
- data/test/dummy/app/models/foo.rb +0 -19
- data/test/dummy/app/views/foos/new.html.erb +0 -71
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -45
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.yml +0 -22
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -26
- data/test/dummy/config/environments/production.rb +0 -49
- data/test/dummy/config/environments/test.rb +0 -35
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/inflections.rb +0 -10
- data/test/dummy/config/initializers/mime_types.rb +0 -5
- data/test/dummy/config/initializers/secret_token.rb +0 -7
- data/test/dummy/config/initializers/session_store.rb +0 -8
- data/test/dummy/config/locales/en.yml +0 -12
- data/test/dummy/config/routes.rb +0 -3
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/db/migrate/20110624115516_create_foos.rb +0 -26
- data/test/dummy/db/migrate/20110724201117_create_fake_collections.rb +0 -14
- data/test/dummy/db/migrate/20110724201548_rename_fake_collection_to_fake.rb +0 -9
- data/test/dummy/db/migrate/20110725082530_create_continent_country_and_city_tables.rb +0 -24
- data/test/dummy/db/schema.rb +0 -55
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/server.log +0 -0
- data/test/dummy/public/404.html +0 -26
- data/test/dummy/public/422.html +0 -26
- data/test/dummy/public/500.html +0 -26
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/public/javascripts/application.js +0 -2
- data/test/dummy/public/javascripts/controls.js +0 -965
- data/test/dummy/public/javascripts/dragdrop.js +0 -974
- data/test/dummy/public/javascripts/effects.js +0 -1123
- data/test/dummy/public/javascripts/prototype.js +0 -6001
- data/test/dummy/public/javascripts/rails.js +0 -175
- data/test/dummy/public/stylesheets/.gitkeep +0 -0
- data/test/dummy/script/rails +0 -6
- data/test/judge_test.rb +0 -186
@@ -2,6 +2,8 @@
|
|
2
2
|
// You can find a guide and some more traditional API documentation at <http://joecorcoran.github.com/judge/>.
|
3
3
|
// Hopefully this page will help you understand what's happening under the hood.
|
4
4
|
|
5
|
+
/* http://raw.github.com/joecorcoran/judge/master/LICENSE.txt */
|
6
|
+
|
5
7
|
/*jshint curly: true, evil: true, newcap: true, noarg: true, strict: false */
|
6
8
|
/*global _: false, JSON: false */
|
7
9
|
|
@@ -41,7 +43,7 @@ judge.Watcher = function (element) {
|
|
41
43
|
}
|
42
44
|
|
43
45
|
// Convenient access to this Watcher.
|
44
|
-
var
|
46
|
+
var watcher = this;
|
45
47
|
|
46
48
|
// Watcher instance properties.
|
47
49
|
this.element = element;
|
@@ -50,15 +52,15 @@ judge.Watcher = function (element) {
|
|
50
52
|
// The `validate` instance method returns the validity of the watched element,
|
51
53
|
// represented as an object containing the element itself and some validity information.
|
52
54
|
this.validate = function() {
|
53
|
-
|
54
|
-
var validators =
|
55
|
+
watcher.errorMessages = [];
|
56
|
+
var validators = watcher.validators,
|
55
57
|
validity = true,
|
56
58
|
messages = [];
|
57
59
|
_(validators).each(function(validator) {
|
58
60
|
var options = validator.options,
|
59
61
|
msgs = validator.messages;
|
60
|
-
if (
|
61
|
-
var result =
|
62
|
+
if (watcher.element.value.length || options.allow_blank !== true) {
|
63
|
+
var result = watcher.validates()[validator.kind](options, msgs);
|
62
64
|
if (!result.valid && result.hasOwnProperty('messages')) {
|
63
65
|
validity = false;
|
64
66
|
messages.push(result.messages);
|
@@ -68,7 +70,7 @@ judge.Watcher = function (element) {
|
|
68
70
|
return {
|
69
71
|
valid: validity,
|
70
72
|
messages: _(messages).flatten(),
|
71
|
-
element:
|
73
|
+
element: watcher.element
|
72
74
|
};
|
73
75
|
};
|
74
76
|
|
@@ -76,14 +78,14 @@ judge.Watcher = function (element) {
|
|
76
78
|
|
77
79
|
// Watcher prototype methods.
|
78
80
|
judge.Watcher.prototype.validates = function() {
|
79
|
-
var
|
81
|
+
var watcher = this,
|
80
82
|
extendedMethods = judge.customValidators,
|
81
83
|
methods = {
|
82
84
|
|
83
85
|
// Presence validator ported as closely as possible
|
84
86
|
// from [ActiveModel::Validations::PresenceValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/PresenceValidator.html).
|
85
87
|
presence: function(options, messages) {
|
86
|
-
if (
|
88
|
+
if (watcher.element.value.length) {
|
87
89
|
return { valid:true };
|
88
90
|
} else{
|
89
91
|
return { valid:false, messages:[messages.blank] };
|
@@ -94,7 +96,7 @@ judge.Watcher.prototype.validates = function() {
|
|
94
96
|
// from [ActiveModel::Validations::LengthValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/LengthValidator.html).
|
95
97
|
length: function(options, messages) {
|
96
98
|
var msgs = [],
|
97
|
-
length =
|
99
|
+
length = watcher.element.value.length,
|
98
100
|
types = {
|
99
101
|
minimum: { operator: '<', message: 'too_short' },
|
100
102
|
maximum: { operator: '>', message: 'too_long' },
|
@@ -113,7 +115,7 @@ judge.Watcher.prototype.validates = function() {
|
|
113
115
|
// from [ActiveModel::Validations::ExclusionValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/ExclusionValidator.html).
|
114
116
|
exclusion: function(options, messages) {
|
115
117
|
var stringIn = _(options['in']).map(function(o) { return o.toString(); });
|
116
|
-
if (_(stringIn).include(
|
118
|
+
if (_(stringIn).include(watcher.element.value)) {
|
117
119
|
return {
|
118
120
|
valid:false,
|
119
121
|
messages:[messages.exclusion]
|
@@ -127,7 +129,7 @@ judge.Watcher.prototype.validates = function() {
|
|
127
129
|
// from [ActiveModel::Validations::InclusionValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/InclusionValidator.html).
|
128
130
|
inclusion: function(options, messages) {
|
129
131
|
var stringIn = _(options['in']).map(function(o) { return o.toString(); });
|
130
|
-
if (!_(stringIn).include(
|
132
|
+
if (!_(stringIn).include(watcher.element.value)) {
|
131
133
|
return {
|
132
134
|
valid:false,
|
133
135
|
messages:[messages.inclusion]
|
@@ -148,7 +150,7 @@ judge.Watcher.prototype.validates = function() {
|
|
148
150
|
less_than_or_equal_to: '<='
|
149
151
|
},
|
150
152
|
msgs = [],
|
151
|
-
value =
|
153
|
+
value = watcher.element.value,
|
152
154
|
parsedValue = parseFloat(value, 10);
|
153
155
|
|
154
156
|
if (isNaN(Number(value))) {
|
@@ -170,14 +172,14 @@ judge.Watcher.prototype.validates = function() {
|
|
170
172
|
}
|
171
173
|
});
|
172
174
|
}
|
173
|
-
return msgs.length? { valid:false, messages:msgs } : { valid:true };
|
175
|
+
return msgs.length ? { valid:false, messages:msgs } : { valid:true };
|
174
176
|
},
|
175
177
|
|
176
178
|
// Format validator ported as closely as possible
|
177
179
|
// from [ActiveModel::Validations::FormatValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/FormatValidator.html).
|
178
180
|
format: function(options, messages) {
|
179
181
|
var msgs = [],
|
180
|
-
value =
|
182
|
+
value = watcher.element.value;
|
181
183
|
if (options.hasOwnProperty('with')) {
|
182
184
|
var withReg = judge.utils.convertRegExp(options['with']);
|
183
185
|
if (!withReg.test(value)) {
|
@@ -196,7 +198,7 @@ judge.Watcher.prototype.validates = function() {
|
|
196
198
|
// Acceptance validator ported as closely as possible
|
197
199
|
// from [ActiveModel::Validations::AcceptanceValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/AcceptanceValidator.html).
|
198
200
|
acceptance: function(options, messages) {
|
199
|
-
if (
|
201
|
+
if (watcher.element.checked === true) {
|
200
202
|
return { valid:true };
|
201
203
|
} else {
|
202
204
|
return {
|
@@ -209,10 +211,10 @@ judge.Watcher.prototype.validates = function() {
|
|
209
211
|
// Confirmation validator ported as closely as possible
|
210
212
|
// from [ActiveModel::Validations::ConfirmationValidator](http://api.rubyonrails.org/classes/ActiveModel/Validations/ConfirmationValidator.html).
|
211
213
|
confirmation: function(options, messages) {
|
212
|
-
var id =
|
214
|
+
var id = watcher.element.getAttribute('id'),
|
213
215
|
confId = id + '_confirmation',
|
214
216
|
confElem = document.getElementById(confId);
|
215
|
-
if (
|
217
|
+
if (watcher.element.value === confElem.value) {
|
216
218
|
return { valid:true };
|
217
219
|
} else {
|
218
220
|
return {
|
@@ -239,7 +241,7 @@ judge.store = (function() {
|
|
239
241
|
|
240
242
|
return {
|
241
243
|
|
242
|
-
// Stores
|
244
|
+
// Stores watcher(s) for element(s) against a user defined key.
|
243
245
|
save: function(key, element) {
|
244
246
|
var elements = judge.utils.isCollection(element) ? element : [element];
|
245
247
|
_(elements).each(function(element) {
|
@@ -253,25 +255,28 @@ judge.store = (function() {
|
|
253
255
|
return store;
|
254
256
|
},
|
255
257
|
|
256
|
-
// Removes
|
258
|
+
// Removes stored watcher(s).
|
257
259
|
remove: function(key, element) {
|
258
260
|
if (!store.hasOwnProperty(key)) {
|
259
261
|
return null;
|
260
262
|
}
|
261
263
|
var elements = judge.utils.isCollection(element) ? element : [element];
|
262
264
|
store[key] = _(store[key]).reject(function(j) { return _(elements).include(j.element); });
|
263
|
-
|
265
|
+
if (store[key].length === 0) {
|
266
|
+
delete store[key];
|
267
|
+
}
|
268
|
+
return store[key];
|
264
269
|
},
|
265
270
|
|
266
|
-
// Returns the entire store object, or an array of
|
271
|
+
// Returns the entire store object, or an array of watchers stored against
|
267
272
|
// the given key.
|
268
273
|
get: function(key) {
|
269
274
|
if (_(key).isUndefined()) { return store; }
|
270
275
|
return store.hasOwnProperty(key) ? store[key] : null;
|
271
276
|
},
|
272
277
|
|
273
|
-
// Returns the entire store object with
|
274
|
-
// or all DOM elements stored within all
|
278
|
+
// Returns the entire store object with watchers converted to elements,
|
279
|
+
// or all DOM elements stored within all watchers stored against the given key.
|
275
280
|
getDOM: function(key) {
|
276
281
|
if (_(key).isUndefined()) {
|
277
282
|
var convertedStore = {};
|
@@ -283,14 +288,21 @@ judge.store = (function() {
|
|
283
288
|
return store.hasOwnProperty(key) ? _(store[key]).pluck('element') : null;
|
284
289
|
},
|
285
290
|
|
286
|
-
//
|
291
|
+
// Shortcut for `judge.validate(judge.store.getDOM(key));`.
|
292
|
+
// Returns null if no stored elements are found for the given key.
|
293
|
+
validate: function(key) {
|
294
|
+
var elements = judge.store.getDOM(key);
|
295
|
+
return (key && !_(elements).isNull()) ? judge.validate(elements) : null;
|
296
|
+
},
|
297
|
+
|
298
|
+
// Wipes the entire store object, or wipes all watchers stored against
|
287
299
|
// the given key.
|
288
300
|
clear: function(key) {
|
289
301
|
if (_(key).isUndefined()) {
|
290
302
|
store = {};
|
291
303
|
} else {
|
292
304
|
if (!store.hasOwnProperty(key)) { return null; }
|
293
|
-
store[key]
|
305
|
+
delete store[key];
|
294
306
|
}
|
295
307
|
return store;
|
296
308
|
}
|
@@ -312,17 +324,6 @@ judge.validate = function(elements) {
|
|
312
324
|
|
313
325
|
// The obligatory utilities.
|
314
326
|
judge.utils = {
|
315
|
-
|
316
|
-
// Determines whether an object is a DOM element of the types that judge can work with.
|
317
|
-
isValidatable: function(object) {
|
318
|
-
var type = judge.utils.getObjectString(object),
|
319
|
-
types = [
|
320
|
-
'HTMLInputElement',
|
321
|
-
'HTMLTextAreaElement',
|
322
|
-
'HTMLSelectElement'
|
323
|
-
];
|
324
|
-
return _(types).include(type);
|
325
|
-
},
|
326
327
|
|
327
328
|
// A way of checking isArray, but including weird object types that are returned from collection queries.
|
328
329
|
isCollection: function(object) {
|
@@ -356,11 +357,13 @@ judge.utils = {
|
|
356
357
|
isEven: function(value) { return (value % 2 === 0) ? true : false; },
|
357
358
|
isOdd: function(value) { return !judge.utils.isEven(value); },
|
358
359
|
|
359
|
-
// Converts a
|
360
|
+
// Converts a Ruby regular expression, given as a string, into JavaScript.
|
360
361
|
// This is rudimentary at best, as there are many, many differences between Ruby
|
361
362
|
// and JavaScript when it comes to regexp-fu. Basically, if you validate your records
|
362
363
|
// using complex regular expressions AND you hope that this will "just work"
|
363
364
|
// on the client-side too… you are going to be pretty disappointed.
|
365
|
+
// If you know of a better way (or an existing library) to convert regular expressions
|
366
|
+
// from Ruby to JavaScript, I would really love to hear from you.
|
364
367
|
convertRegExp: function(string) {
|
365
368
|
var p = string.slice(1, -1).split(':'),
|
366
369
|
o = p.shift(),
|
@@ -368,8 +371,8 @@ judge.utils = {
|
|
368
371
|
return new RegExp(r, judge.utils.convertFlags(o));
|
369
372
|
},
|
370
373
|
convertFlags: function(string) {
|
371
|
-
var off
|
372
|
-
multi
|
374
|
+
var off = new RegExp('-'),
|
375
|
+
multi = new RegExp('m');
|
373
376
|
string = string.replace('?', '');
|
374
377
|
if (off.test(string) || !multi.test(string)) {
|
375
378
|
return '';
|
@@ -378,5 +381,3 @@ judge.utils = {
|
|
378
381
|
}
|
379
382
|
}
|
380
383
|
};
|
381
|
-
|
382
|
-
|
@@ -1,4 +1,4 @@
|
|
1
|
-
// Underscore.js 1.1
|
1
|
+
// Underscore.js 1.2.1
|
2
2
|
// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
|
3
3
|
// Underscore is freely distributable under the MIT license.
|
4
4
|
// Portions of Underscore are inspired or borrowed from Prototype,
|
@@ -48,19 +48,26 @@
|
|
48
48
|
// Create a safe reference to the Underscore object for use below.
|
49
49
|
var _ = function(obj) { return new wrapper(obj); };
|
50
50
|
|
51
|
-
// Export the Underscore object for **CommonJS**, with
|
52
|
-
// for the old `require()` API. If we're not in
|
53
|
-
// global object.
|
54
|
-
if (typeof
|
55
|
-
module.exports
|
56
|
-
|
51
|
+
// Export the Underscore object for **Node.js** and **"CommonJS"**, with
|
52
|
+
// backwards-compatibility for the old `require()` API. If we're not in
|
53
|
+
// CommonJS, add `_` to the global object.
|
54
|
+
if (typeof exports !== 'undefined') {
|
55
|
+
if (typeof module !== 'undefined' && module.exports) {
|
56
|
+
exports = module.exports = _;
|
57
|
+
}
|
58
|
+
exports._ = _;
|
59
|
+
} else if (typeof define === 'function' && define.amd) {
|
60
|
+
// Register as a named module with AMD.
|
61
|
+
define('underscore', function() {
|
62
|
+
return _;
|
63
|
+
});
|
57
64
|
} else {
|
58
65
|
// Exported as a string, for Closure Compiler "advanced" mode.
|
59
66
|
root['_'] = _;
|
60
67
|
}
|
61
68
|
|
62
69
|
// Current version.
|
63
|
-
_.VERSION = '1.1
|
70
|
+
_.VERSION = '1.2.1';
|
64
71
|
|
65
72
|
// Collection Functions
|
66
73
|
// --------------------
|
@@ -198,8 +205,8 @@
|
|
198
205
|
var found = false;
|
199
206
|
if (obj == null) return found;
|
200
207
|
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
201
|
-
any(obj, function(value) {
|
202
|
-
|
208
|
+
found = any(obj, function(value) {
|
209
|
+
return value === target;
|
203
210
|
});
|
204
211
|
return found;
|
205
212
|
};
|
@@ -220,6 +227,7 @@
|
|
220
227
|
// Return the maximum element or (element-based computation).
|
221
228
|
_.max = function(obj, iterator, context) {
|
222
229
|
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
|
230
|
+
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
223
231
|
var result = {computed : -Infinity};
|
224
232
|
each(obj, function(value, index, list) {
|
225
233
|
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
@@ -231,6 +239,7 @@
|
|
231
239
|
// Return the minimum element (or element-based computation).
|
232
240
|
_.min = function(obj, iterator, context) {
|
233
241
|
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
|
242
|
+
if (!iterator && _.isEmpty(obj)) return Infinity;
|
234
243
|
var result = {computed : Infinity};
|
235
244
|
each(obj, function(value, index, list) {
|
236
245
|
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
@@ -239,6 +248,21 @@
|
|
239
248
|
return result.value;
|
240
249
|
};
|
241
250
|
|
251
|
+
// Shuffle an array.
|
252
|
+
_.shuffle = function(obj) {
|
253
|
+
var shuffled = [], rand;
|
254
|
+
each(obj, function(value, index, list) {
|
255
|
+
if (index == 0) {
|
256
|
+
shuffled[0] = value;
|
257
|
+
} else {
|
258
|
+
rand = Math.floor(Math.random() * (index + 1));
|
259
|
+
shuffled[index] = shuffled[rand];
|
260
|
+
shuffled[rand] = value;
|
261
|
+
}
|
262
|
+
});
|
263
|
+
return shuffled;
|
264
|
+
};
|
265
|
+
|
242
266
|
// Sort the object's values by a criterion produced by an iterator.
|
243
267
|
_.sortBy = function(obj, iterator, context) {
|
244
268
|
return _.pluck(_.map(obj, function(value, index, list) {
|
@@ -252,9 +276,11 @@
|
|
252
276
|
}), 'value');
|
253
277
|
};
|
254
278
|
|
255
|
-
// Groups the object's values by a criterion
|
256
|
-
|
279
|
+
// Groups the object's values by a criterion. Pass either a string attribute
|
280
|
+
// to group by, or a function that returns the criterion.
|
281
|
+
_.groupBy = function(obj, val) {
|
257
282
|
var result = {};
|
283
|
+
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
|
258
284
|
each(obj, function(value, index) {
|
259
285
|
var key = iterator(value, index);
|
260
286
|
(result[key] || (result[key] = [])).push(value);
|
@@ -298,6 +324,20 @@
|
|
298
324
|
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
299
325
|
};
|
300
326
|
|
327
|
+
// Returns everything but the last entry of the array. Especcialy useful on
|
328
|
+
// the arguments object. Passing **n** will return all the values in
|
329
|
+
// the array, excluding the last N. The **guard** check allows it to work with
|
330
|
+
// `_.map`.
|
331
|
+
_.initial = function(array, n, guard) {
|
332
|
+
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
333
|
+
};
|
334
|
+
|
335
|
+
// Get the last element of an array. Passing **n** will return the last N
|
336
|
+
// values in the array. The **guard** check allows it to work with `_.map`.
|
337
|
+
_.last = function(array, n, guard) {
|
338
|
+
return (n != null) && !guard ? slice.call(array, array.length - n) : array[array.length - 1];
|
339
|
+
};
|
340
|
+
|
301
341
|
// Returns everything but the first entry of the array. Aliased as `tail`.
|
302
342
|
// Especially useful on the arguments object. Passing an **index** will return
|
303
343
|
// the rest of the values in the array from that index onward. The **guard**
|
@@ -306,20 +346,15 @@
|
|
306
346
|
return slice.call(array, (index == null) || guard ? 1 : index);
|
307
347
|
};
|
308
348
|
|
309
|
-
// Get the last element of an array.
|
310
|
-
_.last = function(array) {
|
311
|
-
return array[array.length - 1];
|
312
|
-
};
|
313
|
-
|
314
349
|
// Trim out all falsy values from an array.
|
315
350
|
_.compact = function(array) {
|
316
351
|
return _.filter(array, function(value){ return !!value; });
|
317
352
|
};
|
318
353
|
|
319
354
|
// Return a completely flattened version of an array.
|
320
|
-
_.flatten = function(array) {
|
355
|
+
_.flatten = function(array, shallow) {
|
321
356
|
return _.reduce(array, function(memo, value) {
|
322
|
-
if (_.isArray(value)) return memo.concat(_.flatten(value));
|
357
|
+
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
|
323
358
|
memo[memo.length] = value;
|
324
359
|
return memo;
|
325
360
|
}, []);
|
@@ -333,17 +368,23 @@
|
|
333
368
|
// Produce a duplicate-free version of the array. If the array has already
|
334
369
|
// been sorted, you have the option of using a faster algorithm.
|
335
370
|
// Aliased as `unique`.
|
336
|
-
_.uniq = _.unique = function(array, isSorted) {
|
337
|
-
|
338
|
-
|
371
|
+
_.uniq = _.unique = function(array, isSorted, iterator) {
|
372
|
+
var initial = iterator ? _.map(array, iterator) : array;
|
373
|
+
var result = [];
|
374
|
+
_.reduce(initial, function(memo, el, i) {
|
375
|
+
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
|
376
|
+
memo[memo.length] = el;
|
377
|
+
result[result.length] = array[i];
|
378
|
+
}
|
339
379
|
return memo;
|
340
380
|
}, []);
|
381
|
+
return result;
|
341
382
|
};
|
342
383
|
|
343
384
|
// Produce an array that contains the union: each distinct element from all of
|
344
385
|
// the passed-in arrays.
|
345
386
|
_.union = function() {
|
346
|
-
return _.uniq(_.flatten(arguments));
|
387
|
+
return _.uniq(_.flatten(arguments, true));
|
347
388
|
};
|
348
389
|
|
349
390
|
// Produce an array that contains every item shared between all the
|
@@ -391,7 +432,6 @@
|
|
391
432
|
return -1;
|
392
433
|
};
|
393
434
|
|
394
|
-
|
395
435
|
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
396
436
|
_.lastIndexOf = function(array, item) {
|
397
437
|
if (array == null) return -1;
|
@@ -426,15 +466,25 @@
|
|
426
466
|
// Function (ahem) Functions
|
427
467
|
// ------------------
|
428
468
|
|
469
|
+
// Reusable constructor function for prototype setting.
|
470
|
+
var ctor = function(){};
|
471
|
+
|
429
472
|
// Create a function bound to a given object (assigning `this`, and arguments,
|
430
473
|
// optionally). Binding with arguments is also known as `curry`.
|
431
474
|
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
432
475
|
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
433
|
-
_.bind = function(func,
|
476
|
+
_.bind = function bind(func, context) {
|
477
|
+
var bound, args;
|
434
478
|
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
435
|
-
|
436
|
-
|
437
|
-
|
479
|
+
if (!_.isFunction(func)) throw new TypeError;
|
480
|
+
args = slice.call(arguments, 2);
|
481
|
+
return bound = function() {
|
482
|
+
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
483
|
+
ctor.prototype = func.prototype;
|
484
|
+
var self = new ctor;
|
485
|
+
var result = func.apply(self, args.concat(slice.call(arguments)));
|
486
|
+
if (Object(result) === result) return result;
|
487
|
+
return self;
|
438
488
|
};
|
439
489
|
};
|
440
490
|
|
@@ -470,31 +520,43 @@
|
|
470
520
|
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
471
521
|
};
|
472
522
|
|
473
|
-
//
|
474
|
-
|
475
|
-
|
523
|
+
// Returns a function, that, when invoked, will only be triggered at most once
|
524
|
+
// during a given window of time.
|
525
|
+
_.throttle = function(func, wait) {
|
526
|
+
var context, args, timeout, throttling, more;
|
527
|
+
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
|
476
528
|
return function() {
|
477
|
-
|
478
|
-
var
|
529
|
+
context = this; args = arguments;
|
530
|
+
var later = function() {
|
479
531
|
timeout = null;
|
480
|
-
func.apply(context, args);
|
532
|
+
if (more) func.apply(context, args);
|
533
|
+
whenDone();
|
481
534
|
};
|
482
|
-
if (
|
483
|
-
if (
|
535
|
+
if (!timeout) timeout = setTimeout(later, wait);
|
536
|
+
if (throttling) {
|
537
|
+
more = true;
|
538
|
+
} else {
|
539
|
+
func.apply(context, args);
|
540
|
+
}
|
541
|
+
whenDone();
|
542
|
+
throttling = true;
|
484
543
|
};
|
485
544
|
};
|
486
545
|
|
487
|
-
// Returns a function, that, when invoked, will only be triggered at most once
|
488
|
-
// during a given window of time.
|
489
|
-
_.throttle = function(func, wait) {
|
490
|
-
return limit(func, wait, false);
|
491
|
-
};
|
492
|
-
|
493
546
|
// Returns a function, that, as long as it continues to be invoked, will not
|
494
547
|
// be triggered. The function will be called after it stops being called for
|
495
548
|
// N milliseconds.
|
496
549
|
_.debounce = function(func, wait) {
|
497
|
-
|
550
|
+
var timeout;
|
551
|
+
return function() {
|
552
|
+
var context = this, args = arguments;
|
553
|
+
var later = function() {
|
554
|
+
timeout = null;
|
555
|
+
func.apply(context, args);
|
556
|
+
};
|
557
|
+
clearTimeout(timeout);
|
558
|
+
timeout = setTimeout(later, wait);
|
559
|
+
};
|
498
560
|
};
|
499
561
|
|
500
562
|
// Returns a function that will be executed at most one time, no matter how
|
@@ -538,7 +600,6 @@
|
|
538
600
|
};
|
539
601
|
};
|
540
602
|
|
541
|
-
|
542
603
|
// Object Functions
|
543
604
|
// ----------------
|
544
605
|
|
@@ -588,6 +649,7 @@
|
|
588
649
|
|
589
650
|
// Create a (shallow-cloned) duplicate of an object.
|
590
651
|
_.clone = function(obj) {
|
652
|
+
if (!_.isObject(obj)) return obj;
|
591
653
|
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
592
654
|
};
|
593
655
|
|
@@ -599,47 +661,93 @@
|
|
599
661
|
return obj;
|
600
662
|
};
|
601
663
|
|
602
|
-
//
|
603
|
-
|
604
|
-
//
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
if (
|
609
|
-
// Basic equality test (watch out for coercions).
|
610
|
-
if (a == b) return true;
|
611
|
-
// One is falsy and the other truthy.
|
612
|
-
if ((!a && b) || (a && !b)) return false;
|
664
|
+
// Internal recursive comparison function.
|
665
|
+
function eq(a, b, stack) {
|
666
|
+
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
667
|
+
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
668
|
+
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
669
|
+
// A strict comparison is necessary because `null == undefined`.
|
670
|
+
if ((a == null) || (b == null)) return a === b;
|
613
671
|
// Unwrap any wrapped objects.
|
614
672
|
if (a._chain) a = a._wrapped;
|
615
673
|
if (b._chain) b = b._wrapped;
|
616
|
-
//
|
617
|
-
if (a.isEqual) return a.isEqual(b);
|
618
|
-
if (b.isEqual) return b.isEqual(a);
|
619
|
-
//
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
//
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
//
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
674
|
+
// Invoke a custom `isEqual` method if one is provided.
|
675
|
+
if (_.isFunction(a.isEqual)) return a.isEqual(b);
|
676
|
+
if (_.isFunction(b.isEqual)) return b.isEqual(a);
|
677
|
+
// Compare object types.
|
678
|
+
var typeA = typeof a;
|
679
|
+
if (typeA != typeof b) return false;
|
680
|
+
// Optimization; ensure that both values are truthy or falsy.
|
681
|
+
if (!a != !b) return false;
|
682
|
+
// `NaN` values are equal.
|
683
|
+
if (_.isNaN(a)) return _.isNaN(b);
|
684
|
+
// Compare string objects by value.
|
685
|
+
var isStringA = _.isString(a), isStringB = _.isString(b);
|
686
|
+
if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b);
|
687
|
+
// Compare number objects by value.
|
688
|
+
var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b);
|
689
|
+
if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b;
|
690
|
+
// Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0.
|
691
|
+
var isBooleanA = _.isBoolean(a), isBooleanB = _.isBoolean(b);
|
692
|
+
if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b;
|
693
|
+
// Compare dates by their millisecond values.
|
694
|
+
var isDateA = _.isDate(a), isDateB = _.isDate(b);
|
695
|
+
if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime();
|
696
|
+
// Compare RegExps by their source patterns and flags.
|
697
|
+
var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b);
|
698
|
+
if (isRegExpA || isRegExpB) {
|
699
|
+
// Ensure commutative equality for RegExps.
|
700
|
+
return isRegExpA && isRegExpB &&
|
701
|
+
a.source == b.source &&
|
702
|
+
a.global == b.global &&
|
703
|
+
a.multiline == b.multiline &&
|
704
|
+
a.ignoreCase == b.ignoreCase;
|
705
|
+
}
|
706
|
+
// Ensure that both values are objects.
|
707
|
+
if (typeA != 'object') return false;
|
708
|
+
// Arrays or Arraylikes with different lengths are not equal.
|
709
|
+
if (a.length !== b.length) return false;
|
710
|
+
// Objects with different constructors are not equal.
|
711
|
+
if (a.constructor !== b.constructor) return false;
|
712
|
+
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
713
|
+
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
714
|
+
var length = stack.length;
|
715
|
+
while (length--) {
|
716
|
+
// Linear search. Performance is inversely proportional to the number of
|
717
|
+
// unique nested structures.
|
718
|
+
if (stack[length] == a) return true;
|
719
|
+
}
|
720
|
+
// Add the first object to the stack of traversed objects.
|
721
|
+
stack.push(a);
|
722
|
+
var size = 0, result = true;
|
723
|
+
// Deep compare objects.
|
724
|
+
for (var key in a) {
|
725
|
+
if (hasOwnProperty.call(a, key)) {
|
726
|
+
// Count the expected number of properties.
|
727
|
+
size++;
|
728
|
+
// Deep compare each member.
|
729
|
+
if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break;
|
730
|
+
}
|
731
|
+
}
|
732
|
+
// Ensure that both objects contain the same number of properties.
|
733
|
+
if (result) {
|
734
|
+
for (key in b) {
|
735
|
+
if (hasOwnProperty.call(b, key) && !(size--)) break;
|
736
|
+
}
|
737
|
+
result = !size;
|
738
|
+
}
|
739
|
+
// Remove the first object from the stack of traversed objects.
|
740
|
+
stack.pop();
|
741
|
+
return result;
|
742
|
+
}
|
743
|
+
|
744
|
+
// Perform a deep comparison to check if two objects are equal.
|
745
|
+
_.isEqual = function(a, b) {
|
746
|
+
return eq(a, b, []);
|
640
747
|
};
|
641
748
|
|
642
|
-
// Is a given array or object empty?
|
749
|
+
// Is a given array, string, or object empty?
|
750
|
+
// An "empty" object has no enumerable own-properties.
|
643
751
|
_.isEmpty = function(obj) {
|
644
752
|
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
645
753
|
for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
|
@@ -654,7 +762,7 @@
|
|
654
762
|
// Is a given value an array?
|
655
763
|
// Delegates to ECMA5's native Array.isArray
|
656
764
|
_.isArray = nativeIsArray || function(obj) {
|
657
|
-
return toString.call(obj)
|
765
|
+
return toString.call(obj) == '[object Array]';
|
658
766
|
};
|
659
767
|
|
660
768
|
// Is a given variable an object?
|
@@ -663,44 +771,50 @@
|
|
663
771
|
};
|
664
772
|
|
665
773
|
// Is a given variable an arguments object?
|
666
|
-
|
667
|
-
|
668
|
-
|
774
|
+
if (toString.call(arguments) == '[object Arguments]') {
|
775
|
+
_.isArguments = function(obj) {
|
776
|
+
return toString.call(obj) == '[object Arguments]';
|
777
|
+
};
|
778
|
+
} else {
|
779
|
+
_.isArguments = function(obj) {
|
780
|
+
return !!(obj && hasOwnProperty.call(obj, 'callee'));
|
781
|
+
};
|
782
|
+
}
|
669
783
|
|
670
784
|
// Is a given value a function?
|
671
785
|
_.isFunction = function(obj) {
|
672
|
-
return
|
786
|
+
return toString.call(obj) == '[object Function]';
|
673
787
|
};
|
674
788
|
|
675
789
|
// Is a given value a string?
|
676
790
|
_.isString = function(obj) {
|
677
|
-
return
|
791
|
+
return toString.call(obj) == '[object String]';
|
678
792
|
};
|
679
793
|
|
680
794
|
// Is a given value a number?
|
681
795
|
_.isNumber = function(obj) {
|
682
|
-
return
|
796
|
+
return toString.call(obj) == '[object Number]';
|
683
797
|
};
|
684
798
|
|
685
|
-
// Is the given value `NaN`?
|
686
|
-
// that does not equal itself.
|
799
|
+
// Is the given value `NaN`?
|
687
800
|
_.isNaN = function(obj) {
|
801
|
+
// `NaN` is the only value for which `===` is not reflexive.
|
688
802
|
return obj !== obj;
|
689
803
|
};
|
690
804
|
|
691
805
|
// Is a given value a boolean?
|
692
806
|
_.isBoolean = function(obj) {
|
693
|
-
return obj === true || obj === false;
|
807
|
+
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
694
808
|
};
|
695
809
|
|
696
810
|
// Is a given value a date?
|
697
811
|
_.isDate = function(obj) {
|
698
|
-
return
|
812
|
+
return toString.call(obj) == '[object Date]';
|
699
813
|
};
|
700
814
|
|
701
815
|
// Is the given value a regular expression?
|
702
816
|
_.isRegExp = function(obj) {
|
703
|
-
return
|
817
|
+
return toString.call(obj) == '[object RegExp]';
|
704
818
|
};
|
705
819
|
|
706
820
|
// Is a given value equal to null?
|
@@ -733,6 +847,11 @@
|
|
733
847
|
for (var i = 0; i < n; i++) iterator.call(context, i);
|
734
848
|
};
|
735
849
|
|
850
|
+
// Escape a string for HTML interpolation.
|
851
|
+
_.escape = function(string) {
|
852
|
+
return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
853
|
+
};
|
854
|
+
|
736
855
|
// Add your own custom functions to the Underscore object, ensuring that
|
737
856
|
// they're correctly added to the OOP wrapper as well.
|
738
857
|
_.mixin = function(obj) {
|
@@ -753,7 +872,8 @@
|
|
753
872
|
// following template settings to use alternative delimiters.
|
754
873
|
_.templateSettings = {
|
755
874
|
evaluate : /<%([\s\S]+?)%>/g,
|
756
|
-
interpolate : /<%=([\s\S]+?)%>/g
|
875
|
+
interpolate : /<%=([\s\S]+?)%>/g,
|
876
|
+
escape : /<%-([\s\S]+?)%>/g
|
757
877
|
};
|
758
878
|
|
759
879
|
// JavaScript micro-templating, similar to John Resig's implementation.
|
@@ -765,6 +885,9 @@
|
|
765
885
|
'with(obj||{}){__p.push(\'' +
|
766
886
|
str.replace(/\\/g, '\\\\')
|
767
887
|
.replace(/'/g, "\\'")
|
888
|
+
.replace(c.escape, function(match, code) {
|
889
|
+
return "',_.escape(" + code.replace(/\\'/g, "'") + "),'";
|
890
|
+
})
|
768
891
|
.replace(c.interpolate, function(match, code) {
|
769
892
|
return "'," + code.replace(/\\'/g, "'") + ",'";
|
770
893
|
})
|
@@ -776,8 +899,8 @@
|
|
776
899
|
.replace(/\n/g, '\\n')
|
777
900
|
.replace(/\t/g, '\\t')
|
778
901
|
+ "');}return __p.join('');";
|
779
|
-
var func = new Function('obj', tmpl);
|
780
|
-
return data ? func(data) : func;
|
902
|
+
var func = new Function('obj', '_', tmpl);
|
903
|
+
return data ? func(data, _) : function(data) { return func(data, _) };
|
781
904
|
};
|
782
905
|
|
783
906
|
// The OOP Wrapper
|
@@ -836,5 +959,4 @@
|
|
836
959
|
return this._wrapped;
|
837
960
|
};
|
838
961
|
|
839
|
-
})();
|
840
|
-
|
962
|
+
}).call(this);
|