tolaria 1.1.2 → 1.2.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.
- checksums.yaml +4 -4
- data/README.md +8 -1
- data/app/assets/fonts/admin/fontawesome.eot +0 -0
- data/app/assets/fonts/admin/fontawesome.svg +39 -24
- data/app/assets/fonts/admin/fontawesome.ttf +0 -0
- data/app/assets/fonts/admin/fontawesome.woff +0 -0
- data/app/assets/fonts/admin/fontawesome.woff2 +0 -0
- data/app/assets/javascripts/admin/base.js +1 -0
- data/app/assets/javascripts/admin/lib/autosize.js +262 -0
- data/app/assets/javascripts/admin/lib/backbone.js +242 -216
- data/app/assets/javascripts/admin/lib/jquery.chosen.js +1 -1
- data/app/assets/javascripts/admin/lib/jquery.js +1 -1
- data/app/assets/javascripts/admin/lib/jquery.selection.js +1 -1
- data/app/assets/javascripts/admin/lib/moment.js +301 -189
- data/app/assets/javascripts/admin/views/fields/attachment_field.js +1 -0
- data/app/assets/javascripts/admin/views/fields/textarea.js +9 -0
- data/app/assets/stylesheets/admin/components/_buttons.scss +7 -1
- data/app/assets/stylesheets/admin/components/_navigation.scss +7 -12
- data/app/assets/stylesheets/admin/settings/_icons.scss +81 -1
- data/app/controllers/tolaria/resource_controller.rb +3 -3
- data/app/helpers/admin/table_helper.rb +5 -5
- data/app/views/admin/administrators/_show.html.erb +2 -2
- data/app/views/admin/shared/forms/_markdown_composer.html.erb +3 -1
- data/app/views/admin/tolaria_resource/_form_buttons.html.erb +12 -3
- data/app/views/admin/tolaria_resource/_search_form.html.erb +1 -1
- data/app/views/admin/tolaria_resource/edit.html.erb +7 -1
- data/app/views/admin/tolaria_resource/index.html.erb +11 -4
- data/app/views/admin/tolaria_resource/show.html.erb +13 -4
- data/lib/tasks/admin.rake +16 -7
- data/lib/tolaria/default_config.rb +1 -0
- data/lib/tolaria/form_buildable.rb +3 -2
- data/lib/tolaria/version.rb +1 -1
- data/test/integration/{interface_test.rb → crud_test.rb} +19 -3
- data/test/integration/filter_preservation_test.rb +61 -0
- data/tolaria.gemspec +1 -0
- metadata +9 -6
@@ -1,4 +1,4 @@
|
|
1
|
-
// Backbone 1.2.
|
1
|
+
// Backbone 1.2.3
|
2
2
|
// https://github.com/jashkenas/backbone
|
3
3
|
//
|
4
4
|
// Copyright (c) Jeremy Ashkenas, DocumentCloud, and
|
@@ -60,12 +60,11 @@
|
|
60
60
|
// restored later on, if `noConflict` is used.
|
61
61
|
var previousBackbone = root.Backbone;
|
62
62
|
|
63
|
-
// Create local
|
64
|
-
var
|
65
|
-
var slice = array.slice;
|
63
|
+
// Create a local reference to a common array method we'll want to use later.
|
64
|
+
var slice = Array.prototype.slice;
|
66
65
|
|
67
66
|
// Current version of the library. Keep in sync with `package.json`.
|
68
|
-
Backbone.VERSION = '1.2.
|
67
|
+
Backbone.VERSION = '1.2.3';
|
69
68
|
|
70
69
|
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
|
71
70
|
// the `$` variable.
|
@@ -89,12 +88,60 @@
|
|
89
88
|
// form param named `model`.
|
90
89
|
Backbone.emulateJSON = false;
|
91
90
|
|
91
|
+
// Proxy Backbone class methods to Underscore functions, wrapping the model's
|
92
|
+
// `attributes` object or collection's `models` array behind the scenes.
|
93
|
+
//
|
94
|
+
// collection.filter(function(model) { return model.get('age') > 10 });
|
95
|
+
// collection.each(this.addView);
|
96
|
+
//
|
97
|
+
// `Function#apply` can be slow so we use the method's arg count, if we know it.
|
98
|
+
var addMethod = function(length, method, attribute) {
|
99
|
+
switch (length) {
|
100
|
+
case 1: return function() {
|
101
|
+
return _[method](this[attribute]);
|
102
|
+
};
|
103
|
+
case 2: return function(value) {
|
104
|
+
return _[method](this[attribute], value);
|
105
|
+
};
|
106
|
+
case 3: return function(iteratee, context) {
|
107
|
+
return _[method](this[attribute], cb(iteratee, this), context);
|
108
|
+
};
|
109
|
+
case 4: return function(iteratee, defaultVal, context) {
|
110
|
+
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
|
111
|
+
};
|
112
|
+
default: return function() {
|
113
|
+
var args = slice.call(arguments);
|
114
|
+
args.unshift(this[attribute]);
|
115
|
+
return _[method].apply(_, args);
|
116
|
+
};
|
117
|
+
}
|
118
|
+
};
|
119
|
+
var addUnderscoreMethods = function(Class, methods, attribute) {
|
120
|
+
_.each(methods, function(length, method) {
|
121
|
+
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
|
122
|
+
});
|
123
|
+
};
|
124
|
+
|
125
|
+
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
|
126
|
+
var cb = function(iteratee, instance) {
|
127
|
+
if (_.isFunction(iteratee)) return iteratee;
|
128
|
+
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
|
129
|
+
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
|
130
|
+
return iteratee;
|
131
|
+
};
|
132
|
+
var modelMatcher = function(attrs) {
|
133
|
+
var matcher = _.matches(attrs);
|
134
|
+
return function(model) {
|
135
|
+
return matcher(model.attributes);
|
136
|
+
};
|
137
|
+
};
|
138
|
+
|
92
139
|
// Backbone.Events
|
93
140
|
// ---------------
|
94
141
|
|
95
142
|
// A module that can be mixed in to *any object* in order to provide it with
|
96
|
-
// custom
|
97
|
-
//
|
143
|
+
// a custom event channel. You may bind a callback to an event with `on` or
|
144
|
+
// remove with `off`; `trigger`-ing an event fires all callbacks in
|
98
145
|
// succession.
|
99
146
|
//
|
100
147
|
// var object = {};
|
@@ -109,25 +156,25 @@
|
|
109
156
|
|
110
157
|
// Iterates over the standard `event, callback` (as well as the fancy multiple
|
111
158
|
// space-separated events `"change blur", callback` and jQuery-style event
|
112
|
-
// maps `{event: callback}`)
|
113
|
-
|
114
|
-
// optional `opts`.
|
115
|
-
var eventsApi = function(iteratee, memo, name, callback, opts) {
|
159
|
+
// maps `{event: callback}`).
|
160
|
+
var eventsApi = function(iteratee, events, name, callback, opts) {
|
116
161
|
var i = 0, names;
|
117
162
|
if (name && typeof name === 'object') {
|
118
163
|
// Handle event maps.
|
164
|
+
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
|
119
165
|
for (names = _.keys(name); i < names.length ; i++) {
|
120
|
-
|
166
|
+
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
|
121
167
|
}
|
122
168
|
} else if (name && eventSplitter.test(name)) {
|
123
|
-
// Handle space separated event names.
|
169
|
+
// Handle space separated event names by delegating them individually.
|
124
170
|
for (names = name.split(eventSplitter); i < names.length; i++) {
|
125
|
-
|
171
|
+
events = iteratee(events, names[i], callback, opts);
|
126
172
|
}
|
127
173
|
} else {
|
128
|
-
|
174
|
+
// Finally, standard events.
|
175
|
+
events = iteratee(events, name, callback, opts);
|
129
176
|
}
|
130
|
-
return
|
177
|
+
return events;
|
131
178
|
};
|
132
179
|
|
133
180
|
// Bind an event to a `callback` function. Passing `"all"` will bind
|
@@ -136,8 +183,7 @@
|
|
136
183
|
return internalOn(this, name, callback, context);
|
137
184
|
};
|
138
185
|
|
139
|
-
//
|
140
|
-
// the public API.
|
186
|
+
// Guard the `listening` argument from the public API.
|
141
187
|
var internalOn = function(obj, name, callback, context, listening) {
|
142
188
|
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
|
143
189
|
context: context,
|
@@ -154,7 +200,8 @@
|
|
154
200
|
};
|
155
201
|
|
156
202
|
// Inversion-of-control versions of `on`. Tell *this* object to listen to
|
157
|
-
// an event in another object... keeping track of what it's listening to
|
203
|
+
// an event in another object... keeping track of what it's listening to
|
204
|
+
// for easier unbinding later.
|
158
205
|
Events.listenTo = function(obj, name, callback) {
|
159
206
|
if (!obj) return this;
|
160
207
|
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
|
@@ -222,10 +269,9 @@
|
|
222
269
|
|
223
270
|
// The reducing API that removes a callback from the `events` object.
|
224
271
|
var offApi = function(events, name, callback, options) {
|
225
|
-
// No events to consider.
|
226
272
|
if (!events) return;
|
227
273
|
|
228
|
-
var i = 0,
|
274
|
+
var i = 0, listening;
|
229
275
|
var context = options.context, listeners = options.listeners;
|
230
276
|
|
231
277
|
// Delete all events listeners and "drop" events.
|
@@ -277,9 +323,9 @@
|
|
277
323
|
};
|
278
324
|
|
279
325
|
// Bind an event to only be triggered a single time. After the first time
|
280
|
-
// the callback is invoked,
|
281
|
-
// passed in using the space-separated syntax, the
|
282
|
-
//
|
326
|
+
// the callback is invoked, its listener will be removed. If multiple events
|
327
|
+
// are passed in using the space-separated syntax, the handler will fire
|
328
|
+
// once for each event, not once for a combination of all events.
|
283
329
|
Events.once = function(name, callback, context) {
|
284
330
|
// Map the event into a `{event: once}` object.
|
285
331
|
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
|
@@ -294,7 +340,7 @@
|
|
294
340
|
};
|
295
341
|
|
296
342
|
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
|
297
|
-
// `offer` unbinds the `onceWrapper` after it
|
343
|
+
// `offer` unbinds the `onceWrapper` after it has been called.
|
298
344
|
var onceMap = function(map, name, callback, offer) {
|
299
345
|
if (callback) {
|
300
346
|
var once = map[name] = _.once(function() {
|
@@ -347,35 +393,6 @@
|
|
347
393
|
}
|
348
394
|
};
|
349
395
|
|
350
|
-
// Proxy Underscore methods to a Backbone class' prototype using a
|
351
|
-
// particular attribute as the data argument
|
352
|
-
var addMethod = function(length, method, attribute) {
|
353
|
-
switch (length) {
|
354
|
-
case 1: return function() {
|
355
|
-
return _[method](this[attribute]);
|
356
|
-
};
|
357
|
-
case 2: return function(value) {
|
358
|
-
return _[method](this[attribute], value);
|
359
|
-
};
|
360
|
-
case 3: return function(iteratee, context) {
|
361
|
-
return _[method](this[attribute], iteratee, context);
|
362
|
-
};
|
363
|
-
case 4: return function(iteratee, defaultVal, context) {
|
364
|
-
return _[method](this[attribute], iteratee, defaultVal, context);
|
365
|
-
};
|
366
|
-
default: return function() {
|
367
|
-
var args = slice.call(arguments);
|
368
|
-
args.unshift(this[attribute]);
|
369
|
-
return _[method].apply(_, args);
|
370
|
-
};
|
371
|
-
}
|
372
|
-
};
|
373
|
-
var addUnderscoreMethods = function(Class, methods, attribute) {
|
374
|
-
_.each(methods, function(length, method) {
|
375
|
-
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
|
376
|
-
});
|
377
|
-
};
|
378
|
-
|
379
396
|
// Aliases for backwards compatibility.
|
380
397
|
Events.bind = Events.on;
|
381
398
|
Events.unbind = Events.off;
|
@@ -464,10 +481,10 @@
|
|
464
481
|
// the core primitive operation of a model, updating the data and notifying
|
465
482
|
// anyone who needs to know about the change in state. The heart of the beast.
|
466
483
|
set: function(key, val, options) {
|
467
|
-
var attr, attrs, unset, changes, silent, changing, prev, current;
|
468
484
|
if (key == null) return this;
|
469
485
|
|
470
486
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
487
|
+
var attrs;
|
471
488
|
if (typeof key === 'object') {
|
472
489
|
attrs = key;
|
473
490
|
options = val;
|
@@ -481,33 +498,36 @@
|
|
481
498
|
if (!this._validate(attrs, options)) return false;
|
482
499
|
|
483
500
|
// Extract attributes and options.
|
484
|
-
unset
|
485
|
-
silent
|
486
|
-
changes
|
487
|
-
changing
|
488
|
-
this._changing
|
501
|
+
var unset = options.unset;
|
502
|
+
var silent = options.silent;
|
503
|
+
var changes = [];
|
504
|
+
var changing = this._changing;
|
505
|
+
this._changing = true;
|
489
506
|
|
490
507
|
if (!changing) {
|
491
508
|
this._previousAttributes = _.clone(this.attributes);
|
492
509
|
this.changed = {};
|
493
510
|
}
|
494
|
-
current = this.attributes, prev = this._previousAttributes;
|
495
511
|
|
496
|
-
|
497
|
-
|
512
|
+
var current = this.attributes;
|
513
|
+
var changed = this.changed;
|
514
|
+
var prev = this._previousAttributes;
|
498
515
|
|
499
516
|
// For each `set` attribute, update or delete the current value.
|
500
|
-
for (attr in attrs) {
|
517
|
+
for (var attr in attrs) {
|
501
518
|
val = attrs[attr];
|
502
519
|
if (!_.isEqual(current[attr], val)) changes.push(attr);
|
503
520
|
if (!_.isEqual(prev[attr], val)) {
|
504
|
-
|
521
|
+
changed[attr] = val;
|
505
522
|
} else {
|
506
|
-
delete
|
523
|
+
delete changed[attr];
|
507
524
|
}
|
508
525
|
unset ? delete current[attr] : current[attr] = val;
|
509
526
|
}
|
510
527
|
|
528
|
+
// Update the `id`.
|
529
|
+
this.id = this.get(this.idAttribute);
|
530
|
+
|
511
531
|
// Trigger all relevant attribute changes.
|
512
532
|
if (!silent) {
|
513
533
|
if (changes.length) this._pending = options;
|
@@ -559,13 +579,14 @@
|
|
559
579
|
// determining if there *would be* a change.
|
560
580
|
changedAttributes: function(diff) {
|
561
581
|
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
562
|
-
var val, changed = false;
|
563
582
|
var old = this._changing ? this._previousAttributes : this.attributes;
|
583
|
+
var changed = {};
|
564
584
|
for (var attr in diff) {
|
565
|
-
|
566
|
-
|
585
|
+
var val = diff[attr];
|
586
|
+
if (_.isEqual(old[attr], val)) continue;
|
587
|
+
changed[attr] = val;
|
567
588
|
}
|
568
|
-
return changed;
|
589
|
+
return _.size(changed) ? changed : false;
|
569
590
|
},
|
570
591
|
|
571
592
|
// Get the previous value of an attribute, recorded at the time the last
|
@@ -584,12 +605,12 @@
|
|
584
605
|
// Fetch the model from the server, merging the response with the model's
|
585
606
|
// local attributes. Any changed attributes will trigger a "change" event.
|
586
607
|
fetch: function(options) {
|
587
|
-
options =
|
588
|
-
if (options.parse === void 0) options.parse = true;
|
608
|
+
options = _.extend({parse: true}, options);
|
589
609
|
var model = this;
|
590
610
|
var success = options.success;
|
591
611
|
options.success = function(resp) {
|
592
|
-
|
612
|
+
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
613
|
+
if (!model.set(serverAttrs, options)) return false;
|
593
614
|
if (success) success.call(options.context, model, resp, options);
|
594
615
|
model.trigger('sync', model, resp, options);
|
595
616
|
};
|
@@ -601,9 +622,8 @@
|
|
601
622
|
// If the server returns an attributes hash that differs, the model's
|
602
623
|
// state will be `set` again.
|
603
624
|
save: function(key, val, options) {
|
604
|
-
var attrs, method, xhr, attributes = this.attributes, wait;
|
605
|
-
|
606
625
|
// Handle both `"key", value` and `{key: value}` -style arguments.
|
626
|
+
var attrs;
|
607
627
|
if (key == null || typeof key === 'object') {
|
608
628
|
attrs = key;
|
609
629
|
options = val;
|
@@ -611,8 +631,8 @@
|
|
611
631
|
(attrs = {})[key] = val;
|
612
632
|
}
|
613
633
|
|
614
|
-
options = _.extend({validate: true}, options);
|
615
|
-
wait = options.wait;
|
634
|
+
options = _.extend({validate: true, parse: true}, options);
|
635
|
+
var wait = options.wait;
|
616
636
|
|
617
637
|
// If we're not waiting and attributes exist, save acts as
|
618
638
|
// `set(attr).save(null, opts)` with validation. Otherwise, check if
|
@@ -623,35 +643,31 @@
|
|
623
643
|
if (!this._validate(attrs, options)) return false;
|
624
644
|
}
|
625
645
|
|
626
|
-
// Set temporary attributes if `{wait: true}`.
|
627
|
-
if (attrs && wait) {
|
628
|
-
this.attributes = _.extend({}, attributes, attrs);
|
629
|
-
}
|
630
|
-
|
631
646
|
// After a successful server-side save, the client is (optionally)
|
632
647
|
// updated with the server-side state.
|
633
|
-
if (options.parse === void 0) options.parse = true;
|
634
648
|
var model = this;
|
635
649
|
var success = options.success;
|
650
|
+
var attributes = this.attributes;
|
636
651
|
options.success = function(resp) {
|
637
652
|
// Ensure attributes are restored during synchronous saves.
|
638
653
|
model.attributes = attributes;
|
639
654
|
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
640
|
-
if (wait) serverAttrs = _.extend(
|
641
|
-
if (
|
642
|
-
return false;
|
643
|
-
}
|
655
|
+
if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
|
656
|
+
if (serverAttrs && !model.set(serverAttrs, options)) return false;
|
644
657
|
if (success) success.call(options.context, model, resp, options);
|
645
658
|
model.trigger('sync', model, resp, options);
|
646
659
|
};
|
647
660
|
wrapError(this, options);
|
648
661
|
|
649
|
-
|
662
|
+
// Set temporary attributes if `{wait: true}` to properly find new ids.
|
663
|
+
if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
|
664
|
+
|
665
|
+
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
|
650
666
|
if (method === 'patch' && !options.attrs) options.attrs = attrs;
|
651
|
-
xhr = this.sync(method, this, options);
|
667
|
+
var xhr = this.sync(method, this, options);
|
652
668
|
|
653
669
|
// Restore attributes.
|
654
|
-
|
670
|
+
this.attributes = attributes;
|
655
671
|
|
656
672
|
return xhr;
|
657
673
|
},
|
@@ -696,8 +712,8 @@
|
|
696
712
|
_.result(this.collection, 'url') ||
|
697
713
|
urlError();
|
698
714
|
if (this.isNew()) return base;
|
699
|
-
var id = this.
|
700
|
-
return base.replace(/
|
715
|
+
var id = this.get(this.idAttribute);
|
716
|
+
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
|
701
717
|
},
|
702
718
|
|
703
719
|
// **parse** converts a response into the hash of attributes to be `set` on
|
@@ -718,7 +734,7 @@
|
|
718
734
|
|
719
735
|
// Check if the model is currently in a valid state.
|
720
736
|
isValid: function(options) {
|
721
|
-
return this._validate({}, _.
|
737
|
+
return this._validate({}, _.defaults({validate: true}, options));
|
722
738
|
},
|
723
739
|
|
724
740
|
// Run validation against the next complete set of model attributes,
|
@@ -734,7 +750,8 @@
|
|
734
750
|
|
735
751
|
});
|
736
752
|
|
737
|
-
// Underscore methods that we want to implement on the Model
|
753
|
+
// Underscore methods that we want to implement on the Model, mapped to the
|
754
|
+
// number of arguments they take.
|
738
755
|
var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
|
739
756
|
omit: 0, chain: 1, isEmpty: 1 };
|
740
757
|
|
@@ -767,6 +784,16 @@
|
|
767
784
|
var setOptions = {add: true, remove: true, merge: true};
|
768
785
|
var addOptions = {add: true, remove: false};
|
769
786
|
|
787
|
+
// Splices `insert` into `array` at index `at`.
|
788
|
+
var splice = function(array, insert, at) {
|
789
|
+
at = Math.min(Math.max(at, 0), array.length);
|
790
|
+
var tail = Array(array.length - at);
|
791
|
+
var length = insert.length;
|
792
|
+
for (var i = 0; i < tail.length; i++) tail[i] = array[i + at];
|
793
|
+
for (i = 0; i < length; i++) array[i + at] = insert[i];
|
794
|
+
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
|
795
|
+
};
|
796
|
+
|
770
797
|
// Define the Collection's inheritable methods.
|
771
798
|
_.extend(Collection.prototype, Events, {
|
772
799
|
|
@@ -781,7 +808,7 @@
|
|
781
808
|
// The JSON representation of a Collection is an array of the
|
782
809
|
// models' attributes.
|
783
810
|
toJSON: function(options) {
|
784
|
-
return this.map(function(model){ return model.toJSON(options); });
|
811
|
+
return this.map(function(model) { return model.toJSON(options); });
|
785
812
|
},
|
786
813
|
|
787
814
|
// Proxy `Backbone.sync` by default.
|
@@ -789,19 +816,21 @@
|
|
789
816
|
return Backbone.sync.apply(this, arguments);
|
790
817
|
},
|
791
818
|
|
792
|
-
// Add a model, or list of models to the set.
|
819
|
+
// Add a model, or list of models to the set. `models` may be Backbone
|
820
|
+
// Models or raw JavaScript objects to be converted to Models, or any
|
821
|
+
// combination of the two.
|
793
822
|
add: function(models, options) {
|
794
823
|
return this.set(models, _.extend({merge: false}, options, addOptions));
|
795
824
|
},
|
796
825
|
|
797
826
|
// Remove a model, or a list of models from the set.
|
798
827
|
remove: function(models, options) {
|
799
|
-
|
828
|
+
options = _.extend({}, options);
|
829
|
+
var singular = !_.isArray(models);
|
800
830
|
models = singular ? [models] : _.clone(models);
|
801
|
-
|
802
|
-
removed = this._removeModels(models, options);
|
831
|
+
var removed = this._removeModels(models, options);
|
803
832
|
if (!options.silent && removed) this.trigger('update', this, options);
|
804
|
-
return singular ?
|
833
|
+
return singular ? removed[0] : removed;
|
805
834
|
},
|
806
835
|
|
807
836
|
// Update a collection by `set`-ing a new list of models, adding new ones,
|
@@ -809,83 +838,88 @@
|
|
809
838
|
// already exist in the collection, as necessary. Similar to **Model#set**,
|
810
839
|
// the core operation for updating the data contained by the collection.
|
811
840
|
set: function(models, options) {
|
841
|
+
if (models == null) return;
|
842
|
+
|
812
843
|
options = _.defaults({}, options, setOptions);
|
813
|
-
if (options.parse) models = this.parse(models, options);
|
844
|
+
if (options.parse && !this._isModel(models)) models = this.parse(models, options);
|
845
|
+
|
814
846
|
var singular = !_.isArray(models);
|
815
|
-
models = singular ?
|
816
|
-
|
847
|
+
models = singular ? [models] : models.slice();
|
848
|
+
|
817
849
|
var at = options.at;
|
818
850
|
if (at != null) at = +at;
|
819
851
|
if (at < 0) at += this.length + 1;
|
852
|
+
|
853
|
+
var set = [];
|
854
|
+
var toAdd = [];
|
855
|
+
var toRemove = [];
|
856
|
+
var modelMap = {};
|
857
|
+
|
858
|
+
var add = options.add;
|
859
|
+
var merge = options.merge;
|
860
|
+
var remove = options.remove;
|
861
|
+
|
862
|
+
var sort = false;
|
820
863
|
var sortable = this.comparator && (at == null) && options.sort !== false;
|
821
864
|
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
822
|
-
var toAdd = [], toRemove = [], modelMap = {};
|
823
|
-
var add = options.add, merge = options.merge, remove = options.remove;
|
824
|
-
var order = !sortable && add && remove ? [] : false;
|
825
|
-
var orderChanged = false;
|
826
865
|
|
827
866
|
// Turn bare objects into model references, and prevent invalid models
|
828
867
|
// from being added.
|
868
|
+
var model;
|
829
869
|
for (var i = 0; i < models.length; i++) {
|
830
|
-
|
870
|
+
model = models[i];
|
831
871
|
|
832
872
|
// If a duplicate is found, prevent it from being added and
|
833
873
|
// optionally merge it into the existing model.
|
834
|
-
|
835
|
-
|
836
|
-
if (merge &&
|
837
|
-
attrs = this._isModel(
|
874
|
+
var existing = this.get(model);
|
875
|
+
if (existing) {
|
876
|
+
if (merge && model !== existing) {
|
877
|
+
var attrs = this._isModel(model) ? model.attributes : model;
|
838
878
|
if (options.parse) attrs = existing.parse(attrs, options);
|
839
879
|
existing.set(attrs, options);
|
840
|
-
if (sortable && !sort
|
880
|
+
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
|
881
|
+
}
|
882
|
+
if (!modelMap[existing.cid]) {
|
883
|
+
modelMap[existing.cid] = true;
|
884
|
+
set.push(existing);
|
841
885
|
}
|
842
886
|
models[i] = existing;
|
843
887
|
|
844
888
|
// If this is a new, valid model, push it to the `toAdd` list.
|
845
889
|
} else if (add) {
|
846
|
-
model = models[i] = this._prepareModel(
|
847
|
-
if (
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
model = existing || model;
|
854
|
-
if (!model) continue;
|
855
|
-
id = this.modelId(model.attributes);
|
856
|
-
if (order && (model.isNew() || !modelMap[id])) {
|
857
|
-
order.push(model);
|
858
|
-
|
859
|
-
// Check to see if this is actually a new model at this index.
|
860
|
-
orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid;
|
890
|
+
model = models[i] = this._prepareModel(model, options);
|
891
|
+
if (model) {
|
892
|
+
toAdd.push(model);
|
893
|
+
this._addReference(model, options);
|
894
|
+
modelMap[model.cid] = true;
|
895
|
+
set.push(model);
|
896
|
+
}
|
861
897
|
}
|
862
|
-
|
863
|
-
modelMap[id] = true;
|
864
898
|
}
|
865
899
|
|
866
|
-
// Remove
|
900
|
+
// Remove stale models.
|
867
901
|
if (remove) {
|
868
|
-
for (
|
869
|
-
|
902
|
+
for (i = 0; i < this.length; i++) {
|
903
|
+
model = this.models[i];
|
904
|
+
if (!modelMap[model.cid]) toRemove.push(model);
|
870
905
|
}
|
871
906
|
if (toRemove.length) this._removeModels(toRemove, options);
|
872
907
|
}
|
873
908
|
|
874
909
|
// See if sorting is needed, update `length` and splice in new models.
|
875
|
-
|
910
|
+
var orderChanged = false;
|
911
|
+
var replace = !sortable && add && remove;
|
912
|
+
if (set.length && replace) {
|
913
|
+
orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
|
914
|
+
return model !== set[index];
|
915
|
+
});
|
916
|
+
this.models.length = 0;
|
917
|
+
splice(this.models, set, 0);
|
918
|
+
this.length = this.models.length;
|
919
|
+
} else if (toAdd.length) {
|
876
920
|
if (sortable) sort = true;
|
877
|
-
this.
|
878
|
-
|
879
|
-
for (var i = 0; i < toAdd.length; i++) {
|
880
|
-
this.models.splice(at + i, 0, toAdd[i]);
|
881
|
-
}
|
882
|
-
} else {
|
883
|
-
if (order) this.models.length = 0;
|
884
|
-
var orderedModels = order || toAdd;
|
885
|
-
for (var i = 0; i < orderedModels.length; i++) {
|
886
|
-
this.models.push(orderedModels[i]);
|
887
|
-
}
|
888
|
-
}
|
921
|
+
splice(this.models, toAdd, at == null ? this.length : at);
|
922
|
+
this.length = this.models.length;
|
889
923
|
}
|
890
924
|
|
891
925
|
// Silently sort the collection if appropriate.
|
@@ -893,10 +927,10 @@
|
|
893
927
|
|
894
928
|
// Unless silenced, it's time to fire all appropriate add/sort events.
|
895
929
|
if (!options.silent) {
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
930
|
+
for (i = 0; i < toAdd.length; i++) {
|
931
|
+
if (at != null) options.index = at + i;
|
932
|
+
model = toAdd[i];
|
933
|
+
model.trigger('add', model, this, options);
|
900
934
|
}
|
901
935
|
if (sort || orderChanged) this.trigger('sort', this, options);
|
902
936
|
if (toAdd.length || toRemove.length) this.trigger('update', this, options);
|
@@ -930,8 +964,7 @@
|
|
930
964
|
// Remove a model from the end of the collection.
|
931
965
|
pop: function(options) {
|
932
966
|
var model = this.at(this.length - 1);
|
933
|
-
this.remove(model, options);
|
934
|
-
return model;
|
967
|
+
return this.remove(model, options);
|
935
968
|
},
|
936
969
|
|
937
970
|
// Add a model to the beginning of the collection.
|
@@ -942,8 +975,7 @@
|
|
942
975
|
// Remove a model from the beginning of the collection.
|
943
976
|
shift: function(options) {
|
944
977
|
var model = this.at(0);
|
945
|
-
this.remove(model, options);
|
946
|
-
return model;
|
978
|
+
return this.remove(model, options);
|
947
979
|
},
|
948
980
|
|
949
981
|
// Slice out a sub-array of models from the collection.
|
@@ -967,10 +999,7 @@
|
|
967
999
|
// Return models with matching attributes. Useful for simple cases of
|
968
1000
|
// `filter`.
|
969
1001
|
where: function(attrs, first) {
|
970
|
-
|
971
|
-
return this[first ? 'find' : 'filter'](function(model) {
|
972
|
-
return matches(model.attributes);
|
973
|
-
});
|
1002
|
+
return this[first ? 'find' : 'filter'](attrs);
|
974
1003
|
},
|
975
1004
|
|
976
1005
|
// Return the first model with matching attributes. Useful for simple cases
|
@@ -983,16 +1012,19 @@
|
|
983
1012
|
// normal circumstances, as the set will maintain sort order as each item
|
984
1013
|
// is added.
|
985
1014
|
sort: function(options) {
|
986
|
-
|
1015
|
+
var comparator = this.comparator;
|
1016
|
+
if (!comparator) throw new Error('Cannot sort a set without a comparator');
|
987
1017
|
options || (options = {});
|
988
1018
|
|
1019
|
+
var length = comparator.length;
|
1020
|
+
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
|
1021
|
+
|
989
1022
|
// Run sort based on type of `comparator`.
|
990
|
-
if (_.isString(
|
991
|
-
this.models = this.sortBy(
|
1023
|
+
if (length === 1 || _.isString(comparator)) {
|
1024
|
+
this.models = this.sortBy(comparator);
|
992
1025
|
} else {
|
993
|
-
this.models.sort(
|
1026
|
+
this.models.sort(comparator);
|
994
1027
|
}
|
995
|
-
|
996
1028
|
if (!options.silent) this.trigger('sort', this, options);
|
997
1029
|
return this;
|
998
1030
|
},
|
@@ -1006,8 +1038,7 @@
|
|
1006
1038
|
// collection when they arrive. If `reset: true` is passed, the response
|
1007
1039
|
// data will be passed through the `reset` method instead of `set`.
|
1008
1040
|
fetch: function(options) {
|
1009
|
-
options =
|
1010
|
-
if (options.parse === void 0) options.parse = true;
|
1041
|
+
options = _.extend({parse: true}, options);
|
1011
1042
|
var success = options.success;
|
1012
1043
|
var collection = this;
|
1013
1044
|
options.success = function(resp) {
|
@@ -1026,7 +1057,8 @@
|
|
1026
1057
|
create: function(model, options) {
|
1027
1058
|
options = options ? _.clone(options) : {};
|
1028
1059
|
var wait = options.wait;
|
1029
|
-
|
1060
|
+
model = this._prepareModel(model, options);
|
1061
|
+
if (!model) return false;
|
1030
1062
|
if (!wait) this.add(model, options);
|
1031
1063
|
var collection = this;
|
1032
1064
|
var success = options.success;
|
@@ -1080,31 +1112,26 @@
|
|
1080
1112
|
return false;
|
1081
1113
|
},
|
1082
1114
|
|
1083
|
-
// Internal method called by both remove and set.
|
1084
|
-
// additional events. Returns true if anything was actually removed.
|
1115
|
+
// Internal method called by both remove and set.
|
1085
1116
|
_removeModels: function(models, options) {
|
1086
|
-
var
|
1087
|
-
for (var i = 0
|
1088
|
-
var model =
|
1117
|
+
var removed = [];
|
1118
|
+
for (var i = 0; i < models.length; i++) {
|
1119
|
+
var model = this.get(models[i]);
|
1089
1120
|
if (!model) continue;
|
1090
|
-
|
1091
|
-
if (id != null) delete this._byId[id];
|
1092
|
-
delete this._byId[model.cid];
|
1121
|
+
|
1093
1122
|
var index = this.indexOf(model);
|
1094
1123
|
this.models.splice(index, 1);
|
1095
1124
|
this.length--;
|
1125
|
+
|
1096
1126
|
if (!options.silent) {
|
1097
1127
|
options.index = index;
|
1098
1128
|
model.trigger('remove', model, this, options);
|
1099
1129
|
}
|
1100
|
-
|
1130
|
+
|
1131
|
+
removed.push(model);
|
1101
1132
|
this._removeReference(model, options);
|
1102
|
-
removed = true;
|
1103
1133
|
}
|
1104
|
-
|
1105
|
-
// caused by some models not actually getting removed.
|
1106
|
-
if (models.length !== j) models = models.slice(0, j);
|
1107
|
-
return removed;
|
1134
|
+
return removed.length ? removed : false;
|
1108
1135
|
},
|
1109
1136
|
|
1110
1137
|
// Method for checking whether an object should be considered a model for
|
@@ -1123,6 +1150,9 @@
|
|
1123
1150
|
|
1124
1151
|
// Internal method to sever a model's ties to a collection.
|
1125
1152
|
_removeReference: function(model, options) {
|
1153
|
+
delete this._byId[model.cid];
|
1154
|
+
var id = this.modelId(model.attributes);
|
1155
|
+
if (id != null) delete this._byId[id];
|
1126
1156
|
if (this === model.collection) delete model.collection;
|
1127
1157
|
model.off('all', this._onModelEvent, this);
|
1128
1158
|
},
|
@@ -1152,29 +1182,16 @@
|
|
1152
1182
|
// right here:
|
1153
1183
|
var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
|
1154
1184
|
foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
|
1155
|
-
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include:
|
1156
|
-
contains:
|
1185
|
+
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
|
1186
|
+
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
|
1157
1187
|
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
|
1158
1188
|
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
|
1159
|
-
isEmpty: 1, chain: 1, sample: 3, partition: 3
|
1189
|
+
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
|
1190
|
+
sortBy: 3, indexBy: 3};
|
1160
1191
|
|
1161
1192
|
// Mix in each Underscore method as a proxy to `Collection#models`.
|
1162
1193
|
addUnderscoreMethods(Collection, collectionMethods, 'models');
|
1163
1194
|
|
1164
|
-
// Underscore methods that take a property name as an argument.
|
1165
|
-
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
|
1166
|
-
|
1167
|
-
// Use attributes instead of properties.
|
1168
|
-
_.each(attributeMethods, function(method) {
|
1169
|
-
if (!_[method]) return;
|
1170
|
-
Collection.prototype[method] = function(value, context) {
|
1171
|
-
var iterator = _.isFunction(value) ? value : function(model) {
|
1172
|
-
return model.get(value);
|
1173
|
-
};
|
1174
|
-
return _[method](this.models, iterator, context);
|
1175
|
-
};
|
1176
|
-
});
|
1177
|
-
|
1178
1195
|
// Backbone.View
|
1179
1196
|
// -------------
|
1180
1197
|
|
@@ -1190,7 +1207,6 @@
|
|
1190
1207
|
// if an existing element is not provided...
|
1191
1208
|
var View = Backbone.View = function(options) {
|
1192
1209
|
this.cid = _.uniqueId('view');
|
1193
|
-
options || (options = {});
|
1194
1210
|
_.extend(this, _.pick(options, viewOptions));
|
1195
1211
|
this._ensureElement();
|
1196
1212
|
this.initialize.apply(this, arguments);
|
@@ -1199,7 +1215,7 @@
|
|
1199
1215
|
// Cached regex to split keys for `delegate`.
|
1200
1216
|
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1201
1217
|
|
1202
|
-
// List of view options to be
|
1218
|
+
// List of view options to be set as properties.
|
1203
1219
|
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
1204
1220
|
|
1205
1221
|
// Set up all inheritable **Backbone.View** properties and methods.
|
@@ -1273,11 +1289,12 @@
|
|
1273
1289
|
// Uses event delegation for efficiency.
|
1274
1290
|
// Omitting the selector binds the event to `this.el`.
|
1275
1291
|
delegateEvents: function(events) {
|
1276
|
-
|
1292
|
+
events || (events = _.result(this, 'events'));
|
1293
|
+
if (!events) return this;
|
1277
1294
|
this.undelegateEvents();
|
1278
1295
|
for (var key in events) {
|
1279
1296
|
var method = events[key];
|
1280
|
-
if (!_.isFunction(method)) method = this[
|
1297
|
+
if (!_.isFunction(method)) method = this[method];
|
1281
1298
|
if (!method) continue;
|
1282
1299
|
var match = key.match(delegateEventSplitter);
|
1283
1300
|
this.delegate(match[1], match[2], _.bind(method, this));
|
@@ -1290,6 +1307,7 @@
|
|
1290
1307
|
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
|
1291
1308
|
delegate: function(eventName, selector, listener) {
|
1292
1309
|
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1310
|
+
return this;
|
1293
1311
|
},
|
1294
1312
|
|
1295
1313
|
// Clears all callbacks previously bound to the view by `delegateEvents`.
|
@@ -1304,6 +1322,7 @@
|
|
1304
1322
|
// `selector` and `listener` are both optional.
|
1305
1323
|
undelegate: function(eventName, selector, listener) {
|
1306
1324
|
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1325
|
+
return this;
|
1307
1326
|
},
|
1308
1327
|
|
1309
1328
|
// Produces a DOM element to be assigned to your view. Exposed for
|
@@ -1540,7 +1559,7 @@
|
|
1540
1559
|
// falls back to polling.
|
1541
1560
|
var History = Backbone.History = function() {
|
1542
1561
|
this.handlers = [];
|
1543
|
-
_.
|
1562
|
+
this.checkUrl = _.bind(this.checkUrl, this);
|
1544
1563
|
|
1545
1564
|
// Ensure that `History` can be used outside of the browser.
|
1546
1565
|
if (typeof window !== 'undefined') {
|
@@ -1633,7 +1652,7 @@
|
|
1633
1652
|
this.options = _.extend({root: '/'}, this.options, options);
|
1634
1653
|
this.root = this.options.root;
|
1635
1654
|
this._wantsHashChange = this.options.hashChange !== false;
|
1636
|
-
this._hasHashChange = 'onhashchange' in window;
|
1655
|
+
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
|
1637
1656
|
this._useHashChange = this._wantsHashChange && this._hasHashChange;
|
1638
1657
|
this._wantsPushState = !!this.options.pushState;
|
1639
1658
|
this._hasPushState = !!(this.history && this.history.pushState);
|
@@ -1667,15 +1686,16 @@
|
|
1667
1686
|
// support the `hashchange` event, HTML5 history, or the user wants
|
1668
1687
|
// `hashChange` but not `pushState`.
|
1669
1688
|
if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
|
1670
|
-
|
1671
|
-
iframe.src = 'javascript:0';
|
1672
|
-
iframe.style.display = 'none';
|
1673
|
-
iframe.tabIndex = -1;
|
1689
|
+
this.iframe = document.createElement('iframe');
|
1690
|
+
this.iframe.src = 'javascript:0';
|
1691
|
+
this.iframe.style.display = 'none';
|
1692
|
+
this.iframe.tabIndex = -1;
|
1674
1693
|
var body = document.body;
|
1675
1694
|
// Using `appendChild` will throw on IE < 9 if the document is not ready.
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1695
|
+
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
|
1696
|
+
iWindow.document.open();
|
1697
|
+
iWindow.document.close();
|
1698
|
+
iWindow.location.hash = '#' + this.fragment;
|
1679
1699
|
}
|
1680
1700
|
|
1681
1701
|
// Add a cross-platform `addEventListener` shim for older browsers.
|
@@ -1713,7 +1733,7 @@
|
|
1713
1733
|
|
1714
1734
|
// Clean up the iframe if necessary.
|
1715
1735
|
if (this.iframe) {
|
1716
|
-
document.body.removeChild(this.iframe
|
1736
|
+
document.body.removeChild(this.iframe);
|
1717
1737
|
this.iframe = null;
|
1718
1738
|
}
|
1719
1739
|
|
@@ -1736,7 +1756,7 @@
|
|
1736
1756
|
// If the user pressed the back button, the iframe's hash will have
|
1737
1757
|
// changed and we should use that for comparison.
|
1738
1758
|
if (current === this.fragment && this.iframe) {
|
1739
|
-
current = this.getHash(this.iframe);
|
1759
|
+
current = this.getHash(this.iframe.contentWindow);
|
1740
1760
|
}
|
1741
1761
|
|
1742
1762
|
if (current === this.fragment) return false;
|
@@ -1751,7 +1771,7 @@
|
|
1751
1771
|
// If the root doesn't match, no routes can match either.
|
1752
1772
|
if (!this.matchRoot()) return false;
|
1753
1773
|
fragment = this.fragment = this.getFragment(fragment);
|
1754
|
-
return _.
|
1774
|
+
return _.some(this.handlers, function(handler) {
|
1755
1775
|
if (handler.route.test(fragment)) {
|
1756
1776
|
handler.callback(fragment);
|
1757
1777
|
return true;
|
@@ -1794,12 +1814,18 @@
|
|
1794
1814
|
// fragment to store history.
|
1795
1815
|
} else if (this._wantsHashChange) {
|
1796
1816
|
this._updateHash(this.location, fragment, options.replace);
|
1797
|
-
if (this.iframe && (fragment !== this.getHash(this.iframe))) {
|
1817
|
+
if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
|
1818
|
+
var iWindow = this.iframe.contentWindow;
|
1819
|
+
|
1798
1820
|
// Opening and closing the iframe tricks IE7 and earlier to push a
|
1799
1821
|
// history entry on hash-tag change. When replace is true, we don't
|
1800
1822
|
// want this.
|
1801
|
-
if (!options.replace)
|
1802
|
-
|
1823
|
+
if (!options.replace) {
|
1824
|
+
iWindow.document.open();
|
1825
|
+
iWindow.document.close();
|
1826
|
+
}
|
1827
|
+
|
1828
|
+
this._updateHash(iWindow.location, fragment, options.replace);
|
1803
1829
|
}
|
1804
1830
|
|
1805
1831
|
// If you've told us that you explicitly don't want fallback hashchange-
|