Commit 07d0b47c authored by Artem Denysov's avatar Artem Denysov Committed by Sam Saccone

Update marionette.js to v3 (#1676)

* update marionette.js to v3

* update readme

* update name of Mn on main page

* fix code style
parent b3d4c2a8
...@@ -19,3 +19,6 @@ node_modules/backbone ...@@ -19,3 +19,6 @@ node_modules/backbone
node_modules/backbone.localstorage node_modules/backbone.localstorage
!node_modules/backbone.localstorage/backbone.localStorage.js !node_modules/backbone.localstorage/backbone.localStorage.js
node_modules/backbone.radio
!node_modules/backbone.radio/build/backbone.radio.js
...@@ -51,10 +51,10 @@ ...@@ -51,10 +51,10 @@
</div> </div>
<input class="edit" value="<%- title %>"> <input class="edit" value="<%- title %>">
</script> </script>
<script type="text/html" id="template-todoListCompositeView"> <script type="text/html" id="template-todoListView">
<input id="toggle-all" type="checkbox"> <input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul> <ul></ul>
</script> </script>
<!-- vendor libraries --> <!-- vendor libraries -->
<script src="node_modules/todomvc-common/base.js"></script> <script src="node_modules/todomvc-common/base.js"></script>
...@@ -62,8 +62,8 @@ ...@@ -62,8 +62,8 @@
<script src="node_modules/underscore/underscore.js"></script> <script src="node_modules/underscore/underscore.js"></script>
<script src="node_modules/backbone/backbone.js"></script> <script src="node_modules/backbone/backbone.js"></script>
<script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script> <script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<script src="node_modules/backbone.marionette/lib/backbone.marionette.js"></script>
<script src="node_modules/backbone.radio/build/backbone.radio.js"></script> <script src="node_modules/backbone.radio/build/backbone.radio.js"></script>
<script src="node_modules/backbone.marionette/lib/backbone.marionette.js"></script>
<!-- application --> <!-- application -->
<script src="js/TodoMVC.Application.js"></script> <script src="js/TodoMVC.Application.js"></script>
<script src="js/TodoMVC.Todos.js"></script> <script src="js/TodoMVC.Todos.js"></script>
......
...@@ -5,7 +5,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -5,7 +5,7 @@ var TodoMVC = TodoMVC || {};
(function () { (function () {
'use strict'; 'use strict';
var TodoApp = Backbone.Marionette.Application.extend({ var TodoApp = Mn.Application.extend({
setRootLayout: function () { setRootLayout: function () {
this.root = new TodoMVC.RootLayout(); this.root = new TodoMVC.RootLayout();
} }
......
...@@ -7,7 +7,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -7,7 +7,7 @@ var TodoMVC = TodoMVC || {};
var filterChannel = Backbone.Radio.channel('filter'); var filterChannel = Backbone.Radio.channel('filter');
TodoMVC.RootLayout = Backbone.Marionette.LayoutView.extend({ TodoMVC.RootLayout = Mn.View.extend({
el: '#todoapp', el: '#todoapp',
...@@ -20,7 +20,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -20,7 +20,7 @@ var TodoMVC = TodoMVC || {};
// Layout Header View // Layout Header View
// ------------------ // ------------------
TodoMVC.HeaderLayout = Backbone.Marionette.ItemView.extend({ TodoMVC.HeaderLayout = Mn.View.extend({
template: '#template-header', template: '#template-header',
...@@ -60,7 +60,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -60,7 +60,7 @@ var TodoMVC = TodoMVC || {};
// Layout Footer View // Layout Footer View
// ------------------ // ------------------
TodoMVC.FooterLayout = Backbone.Marionette.ItemView.extend({ TodoMVC.FooterLayout = Mn.View.extend({
template: '#template-footer', template: '#template-footer',
// UI bindings create cached attributes that // UI bindings create cached attributes that
...@@ -82,7 +82,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -82,7 +82,7 @@ var TodoMVC = TodoMVC || {};
all: 'render' all: 'render'
}, },
templateHelpers: { templateContext: {
activeCountLabel: function () { activeCountLabel: function () {
return (this.activeCount === 1 ? 'item' : 'items') + ' left'; return (this.activeCount === 1 ? 'item' : 'items') + ' left';
} }
......
...@@ -12,7 +12,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -12,7 +12,7 @@ var TodoMVC = TodoMVC || {};
// //
// Handles a single dynamic route to show // Handles a single dynamic route to show
// the active vs complete todo items // the active vs complete todo items
TodoMVC.Router = Backbone.Marionette.AppRouter.extend({ TodoMVC.Router = Mn.AppRouter.extend({
appRoutes: { appRoutes: {
'*filter': 'filterItems' '*filter': 'filterItems'
} }
...@@ -23,7 +23,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -23,7 +23,7 @@ var TodoMVC = TodoMVC || {};
// //
// Control the workflow and logic that exists at the application // Control the workflow and logic that exists at the application
// level, above the implementation detail of views and models // level, above the implementation detail of views and models
TodoMVC.Controller = Backbone.Marionette.Object.extend({ TodoMVC.Controller = Mn.Object.extend({
initialize: function () { initialize: function () {
this.todoList = new TodoMVC.TodoList(); this.todoList = new TodoMVC.TodoList();
......
...@@ -12,7 +12,7 @@ var TodoMVC = TodoMVC || {}; ...@@ -12,7 +12,7 @@ var TodoMVC = TodoMVC || {};
// //
// Display an individual todo item, and respond to changes // Display an individual todo item, and respond to changes
// that are made to the item, including marking completed. // that are made to the item, including marking completed.
TodoMVC.TodoView = Backbone.Marionette.ItemView.extend({ TodoMVC.TodoView = Mn.View.extend({
tagName: 'li', tagName: 'li',
...@@ -81,18 +81,38 @@ var TodoMVC = TodoMVC || {}; ...@@ -81,18 +81,38 @@ var TodoMVC = TodoMVC || {};
} }
}); });
// Item List View // Item List View Body
// -------------- // --------------
// //
// Controls the rendering of the list of items, including the // Controls the rendering of the list of items, including the
// filtering of activs vs completed items for display. // filtering of items for display.
TodoMVC.ListView = Backbone.Marionette.CompositeView.extend({ TodoMVC.ListViewBody = Mn.CollectionView.extend({
tagName: 'ul',
template: '#template-todoListCompositeView', id: 'todo-list',
childView: TodoMVC.TodoView, childView: TodoMVC.TodoView,
childViewContainer: '#todo-list', filter: function (child) {
var filteredOn = filterChannel.request('filterState').get('filter');
return child.matchesFilter(filteredOn);
}
});
// Item List View
// --------------
//
// Manages List View
TodoMVC.ListView = Mn.View.extend({
template: '#template-todoListView',
regions: {
listBody: {
el: 'ul',
replaceElement: true
}
},
ui: { ui: {
toggle: '#toggle-all' toggle: '#toggle-all'
...@@ -111,11 +131,6 @@ var TodoMVC = TodoMVC || {}; ...@@ -111,11 +131,6 @@ var TodoMVC = TodoMVC || {};
this.listenTo(filterChannel.request('filterState'), 'change:filter', this.render, this); this.listenTo(filterChannel.request('filterState'), 'change:filter', this.render, this);
}, },
filter: function (child) {
var filteredOn = filterChannel.request('filterState').get('filter');
return child.matchesFilter(filteredOn);
},
setCheckAllState: function () { setCheckAllState: function () {
function reduceCompleted(left, right) { function reduceCompleted(left, right) {
return left && right.get('completed'); return left && right.get('completed');
...@@ -132,6 +147,12 @@ var TodoMVC = TodoMVC || {}; ...@@ -132,6 +147,12 @@ var TodoMVC = TodoMVC || {};
this.collection.each(function (todo) { this.collection.each(function (todo) {
todo.save({ completed: isChecked }); todo.save({ completed: isChecked });
}); });
},
onRender: function () {
this.showChildView('listBody', new TodoMVC.ListViewBody({
collection: this.collection
}));
} }
}); });
})(); })();
This source diff could not be displayed because it is too large. You can view the blob instead.
// Backbone.Radio v1.0.2 // Backbone.Radio v2.0.0
(function (global, factory) { (function (global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory(require("underscore"), require("backbone")) : typeof define === "function" && define.amd ? define(["underscore", "backbone"], factory) : global.Backbone.Radio = factory(global._, global.Backbone); typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('underscore'), require('backbone')) :
})(this, function (_, Backbone) { typeof define === 'function' && define.amd ? define(['underscore', 'backbone'], factory) :
"use strict"; (global.Backbone = global.Backbone || {}, global.Backbone.Radio = factory(global._,global.Backbone));
}(this, function (_,Backbone) { 'use strict';
_ = 'default' in _ ? _['default'] : _;
Backbone = 'default' in Backbone ? Backbone['default'] : Backbone;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;
};
var previousRadio = Backbone.Radio; var previousRadio = Backbone.Radio;
var Radio = Backbone.Radio = {}; var Radio = Backbone.Radio = {};
Radio.VERSION = "1.0.2"; Radio.VERSION = '2.0.0';
// This allows you to run multiple instances of Radio on the same // This allows you to run multiple instances of Radio on the same
// webapp. After loading the new version, call `noConflict()` to // webapp. After loading the new version, call `noConflict()` to
...@@ -25,7 +36,7 @@ ...@@ -25,7 +36,7 @@
// Format debug text. // Format debug text.
Radio._debugText = function (warning, eventName, channelName) { Radio._debugText = function (warning, eventName, channelName) {
return warning + (channelName ? " on the " + channelName + " channel" : "") + ": \"" + eventName + "\""; return warning + (channelName ? ' on the ' + channelName + ' channel' : '') + ': "' + eventName + '"';
}; };
// This is the method that's called when an unregistered event was called. // This is the method that's called when an unregistered event was called.
...@@ -52,7 +63,7 @@ ...@@ -52,7 +63,7 @@
var results = {}; var results = {};
// Handle event maps. // Handle event maps.
if (typeof name === "object") { if ((typeof name === 'undefined' ? 'undefined' : _typeof(name)) === 'object') {
for (var key in name) { for (var key in name) {
var result = obj[action].apply(obj, [key, name[key]].concat(rest)); var result = obj[action].apply(obj, [key, name[key]].concat(rest));
eventSplitter.test(key) ? _.extend(results, result) : results[key] = result; eventSplitter.test(key) ? _.extend(results, result) : results[key] = result;
...@@ -94,7 +105,7 @@ ...@@ -94,7 +105,7 @@
// A helper used by `off` methods to the handler from the store // A helper used by `off` methods to the handler from the store
function removeHandler(store, name, callback, context) { function removeHandler(store, name, callback, context) {
var event = store[name]; var event = store[name];
if ((!callback || (callback === event.callback || callback === event.callback._callback)) && (!context || context === event.context)) { if ((!callback || callback === event.callback || callback === event.callback._callback) && (!context || context === event.context)) {
delete store[name]; delete store[name];
return true; return true;
} }
...@@ -134,15 +145,18 @@ ...@@ -134,15 +145,18 @@
// This is to produce an identical function in both tuneIn and tuneOut, // This is to produce an identical function in both tuneIn and tuneOut,
// so that Backbone.Events unregisters it. // so that Backbone.Events unregisters it.
function _partial(channelName) { function _partial(channelName) {
return _logs[channelName] || (_logs[channelName] = _.partial(Radio.log, channelName)); return _logs[channelName] || (_logs[channelName] = _.bind(Radio.log, Radio, channelName));
} }
_.extend(Radio, { _.extend(Radio, {
// Log information about the channel and event // Log information about the channel and event
log: function log(channelName, eventName) { log: function log(channelName, eventName) {
var args = _.rest(arguments, 2); if (typeof console === 'undefined') {
console.log("[" + channelName + "] \"" + eventName + "\"", args); return;
}
var args = _.toArray(arguments).slice(2);
console.log('[' + channelName + '] "' + eventName + '"', args);
}, },
// Logs all events on this channel to the console. It sets an // Logs all events on this channel to the console. It sets an
...@@ -151,7 +165,7 @@ ...@@ -151,7 +165,7 @@
tuneIn: function tuneIn(channelName) { tuneIn: function tuneIn(channelName) {
var channel = Radio.channel(channelName); var channel = Radio.channel(channelName);
channel._tunedIn = true; channel._tunedIn = true;
channel.on("all", _partial(channelName)); channel.on('all', _partial(channelName));
return this; return this;
}, },
...@@ -159,7 +173,7 @@ ...@@ -159,7 +173,7 @@
tuneOut: function tuneOut(channelName) { tuneOut: function tuneOut(channelName) {
var channel = Radio.channel(channelName); var channel = Radio.channel(channelName);
channel._tunedIn = false; channel._tunedIn = false;
channel.off("all", _partial(channelName)); channel.off('all', _partial(channelName));
delete _logs[channelName]; delete _logs[channelName];
return this; return this;
} }
...@@ -182,8 +196,8 @@ ...@@ -182,8 +196,8 @@
// Make a request // Make a request
request: function request(name) { request: function request(name) {
var args = _.rest(arguments); var args = _.toArray(arguments).slice(1);
var results = Radio._eventsApi(this, "request", name, args); var results = Radio._eventsApi(this, 'request', name, args);
if (results) { if (results) {
return results; return results;
} }
...@@ -196,25 +210,25 @@ ...@@ -196,25 +210,25 @@
} }
// If the request isn't handled, log it in DEBUG mode and exit // If the request isn't handled, log it in DEBUG mode and exit
if (requests && (requests[name] || requests["default"])) { if (requests && (requests[name] || requests['default'])) {
var handler = requests[name] || requests["default"]; var handler = requests[name] || requests['default'];
args = requests[name] ? args : arguments; args = requests[name] ? args : arguments;
return Radio._callHandler(handler.callback, handler.context, args); return Radio._callHandler(handler.callback, handler.context, args);
} else { } else {
Radio.debugLog("An unhandled request was fired", name, channelName); Radio.debugLog('An unhandled request was fired', name, channelName);
} }
}, },
// Set up a handler for a request // Set up a handler for a request
reply: function reply(name, callback, context) { reply: function reply(name, callback, context) {
if (Radio._eventsApi(this, "reply", name, [callback, context])) { if (Radio._eventsApi(this, 'reply', name, [callback, context])) {
return this; return this;
} }
this._requests || (this._requests = {}); this._requests || (this._requests = {});
if (this._requests[name]) { if (this._requests[name]) {
Radio.debugLog("A request was overwritten", name, this.channelName); Radio.debugLog('A request was overwritten', name, this.channelName);
} }
this._requests[name] = { this._requests[name] = {
...@@ -227,7 +241,7 @@ ...@@ -227,7 +241,7 @@
// Set up a handler that can only be requested once // Set up a handler that can only be requested once
replyOnce: function replyOnce(name, callback, context) { replyOnce: function replyOnce(name, callback, context) {
if (Radio._eventsApi(this, "replyOnce", name, [callback, context])) { if (Radio._eventsApi(this, 'replyOnce', name, [callback, context])) {
return this; return this;
} }
...@@ -243,7 +257,7 @@ ...@@ -243,7 +257,7 @@
// Remove handler(s) // Remove handler(s)
stopReplying: function stopReplying(name, callback, context) { stopReplying: function stopReplying(name, callback, context) {
if (Radio._eventsApi(this, "stopReplying", name)) { if (Radio._eventsApi(this, 'stopReplying', name)) {
return this; return this;
} }
...@@ -251,7 +265,7 @@ ...@@ -251,7 +265,7 @@
if (!name && !callback && !context) { if (!name && !callback && !context) {
delete this._requests; delete this._requests;
} else if (!removeHandlers(this._requests, name, callback, context)) { } else if (!removeHandlers(this._requests, name, callback, context)) {
Radio.debugLog("Attempted to remove the unregistered request", name, this.channelName); Radio.debugLog('Attempted to remove the unregistered request', name, this.channelName);
} }
return this; return this;
...@@ -269,7 +283,7 @@ ...@@ -269,7 +283,7 @@
Radio.channel = function (channelName) { Radio.channel = function (channelName) {
if (!channelName) { if (!channelName) {
throw new Error("You must provide a name for the channel."); throw new Error('You must provide a name for the channel.');
} }
if (Radio._channels[channelName]) { if (Radio._channels[channelName]) {
...@@ -310,14 +324,13 @@ ...@@ -310,14 +324,13 @@
* *
*/ */
var channel, var channel;
args, var args;
systems = [Backbone.Events, Radio.Commands, Radio.Requests]; var systems = [Backbone.Events, Radio.Requests];
_.each(systems, function (system) { _.each(systems, function (system) {
_.each(system, function (method, methodName) { _.each(system, function (method, methodName) {
Radio[methodName] = function (channelName) { Radio[methodName] = function (channelName) {
args = _.rest(arguments); args = _.toArray(arguments).slice(1);
channel = this.channel(channelName); channel = this.channel(channelName);
return channel[methodName].apply(channel, args); return channel[methodName].apply(channel, args);
}; };
...@@ -326,11 +339,12 @@ ...@@ -326,11 +339,12 @@
Radio.reset = function (channelName) { Radio.reset = function (channelName) {
var channels = !channelName ? this._channels : [this._channels[channelName]]; var channels = !channelName ? this._channels : [this._channels[channelName]];
_.invoke(channels, "reset"); _.each(channels, function (channel) {
channel.reset();
});
}; };
var backbone_radio = Radio; return Radio;
return backbone_radio; }));
});
//# sourceMappingURL=./backbone.radio.js.map //# sourceMappingURL=./backbone.radio.js.map
\ No newline at end of file
// Backbone.js 1.1.2 // Backbone.js 1.3.3
// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
// For all details and documentation: // For all details and documentation:
// http://backbonejs.org // http://backbonejs.org
(function(root, factory) { (function(factory) {
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = (typeof self == 'object' && self.self === self && self) ||
(typeof global == 'object' && global.global === global && global);
// Set up Backbone appropriately for the environment. Start with AMD. // Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
...@@ -17,15 +22,16 @@ ...@@ -17,15 +22,16 @@
// Next for Node.js or CommonJS. jQuery may not be needed as a module. // Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') { } else if (typeof exports !== 'undefined') {
var _ = require('underscore'); var _ = require('underscore'), $;
factory(root, exports, _); try { $ = require('jquery'); } catch (e) {}
factory(root, exports, _, $);
// Finally, as a browser global. // Finally, as a browser global.
} else { } else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
} }
}(this, function(root, Backbone, _, $) { })(function(root, Backbone, _, $) {
// Initial Setup // Initial Setup
// ------------- // -------------
...@@ -34,14 +40,11 @@ ...@@ -34,14 +40,11 @@
// restored later on, if `noConflict` is used. // restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone; var previousBackbone = root.Backbone;
// Create local references to array methods we'll want to use later. // Create a local reference to a common array method we'll want to use later.
var array = []; var slice = Array.prototype.slice;
var push = array.push;
var slice = array.slice;
var splice = array.splice;
// Current version of the library. Keep in sync with `package.json`. // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.1.2'; Backbone.VERSION = '1.3.3';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable. // the `$` variable.
...@@ -60,17 +63,65 @@ ...@@ -60,17 +63,65 @@
Backbone.emulateHTTP = false; Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct // Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as // `application/json` requests ... this will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a // `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`. // form param named `model`.
Backbone.emulateJSON = false; Backbone.emulateJSON = false;
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
// Backbone.Events // Backbone.Events
// --------------- // ---------------
// A module that can be mixed in to *any object* in order to provide it with // A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback // a custom event channel. You may bind a callback to an event with `on` or
// functions to an event; `trigger`-ing an event fires all callbacks in // remove with `off`; `trigger`-ing an event fires all callbacks in
// succession. // succession.
// //
// var object = {}; // var object = {};
...@@ -78,123 +129,234 @@ ...@@ -78,123 +129,234 @@
// object.on('expand', function(){ alert('expanded'); }); // object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand'); // object.trigger('expand');
// //
var Events = Backbone.Events = { var Events = Backbone.Events = {};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for (names = _.keys(name); i < names.length ; i++) {
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space-separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return events;
};
// Bind an event to a `callback` function. Passing `"all"` will bind // Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired. // the callback to all events fired.
on: function(name, callback, context) { Events.on = function(name, callback, context) {
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; return internalOn(this, name, callback, context);
this._events || (this._events = {}); };
var events = this._events[name] || (this._events[name] = []);
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind an event to only be triggered a single time. After the first time // Guard the `listening` argument from the public API.
// the callback is invoked, it will be removed. var internalOn = function(obj, name, callback, context, listening) {
once: function(name, callback, context) { obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; context: context,
var self = this; ctx: obj,
var once = _.once(function() { listening: listening
self.off(name, once);
callback.apply(this, arguments);
}); });
once._callback = callback;
return this.on(name, once, context);
},
// Remove one or many callbacks. If `context` is null, removes all if (listening) {
// callbacks with that function. If `callback` is null, removes all var listeners = obj._listeners || (obj._listeners = {});
// callbacks for the event. If `name` is null, removes all bound listeners[listening.id] = listening;
// callbacks for all events.
off: function(name, callback, context) {
var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) {
this._events = void 0;
return this;
}
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (events = this._events[name]) {
this._events[name] = retain = [];
if (callback || context) {
for (j = 0, k = events.length; j < k; j++) {
ev = events[j];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
retain.push(ev);
}
}
}
if (!retain.length) delete this._events[name];
} }
return obj;
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
} }
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this; return this;
}, };
// Trigger one or many events, firing all bound callbacks. Callbacks are // The reducing API that adds a callback to the `events` object.
// passed the same arguments as `trigger` is, apart from the event name var onApi = function(events, name, callback, options) {
// (unless you're listening on `"all"`, which will cause your callback to if (callback) {
// receive the true name of the event as the first argument). var handlers = events[name] || (events[name] = []);
trigger: function(name) { var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
if (!this._events) return this; if (!this._events) return this;
var args = slice.call(arguments, 1); this._events = eventsApi(offApi, this._events, name, callback, {
if (!eventsApi(this, 'trigger', name, args)) return this; context: context,
var events = this._events[name]; listeners: this._listeners
var allEvents = this._events.all; });
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this; return this;
}, };
// Tell this object to stop listening to either specific events ... or // Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to. // to every object it's currently listening to.
stopListening: function(obj, name, callback) { Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo; var listeningTo = this._listeningTo;
if (!listeningTo) return this; if (!listeningTo) return this;
var remove = !name && !callback;
if (!callback && typeof name === 'object') callback = this; var ids = obj ? [obj._listenId] : _.keys(listeningTo);
if (obj) (listeningTo = {})[obj._listenId] = obj;
for (var id in listeningTo) { for (var i = 0; i < ids.length; i++) {
obj = listeningTo[id]; var listening = listeningTo[ids[i]];
obj.off(name, callback, this);
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; // If listening doesn't exist, this object is not currently
} // listening to obj. Break out early.
return this; if (!listening) break;
listening.obj.off(name, callback, this);
} }
return this;
}; };
// Regular expression used to split event strings. // The reducing API that removes a callback from the `events` object.
var eventSplitter = /\s+/; var offApi = function(events, name, callback, options) {
if (!events) return;
// Implement fancy features of the Events API such as multiple event var i = 0, listening;
// names `"change blur"` and jQuery-style event maps `{change: action}` var context = options.context, listeners = options.listeners;
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps. // Delete all events listeners and "drop" events.
if (typeof name === 'object') { if (!name && !callback && !context) {
for (var key in name) { var ids = _.keys(listeners);
obj[action].apply(obj, [key, name[key]].concat(rest)); for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
} }
return false; return;
} }
// Handle space separated event names. var names = name ? [name] : _.keys(events);
if (eventSplitter.test(name)) { for (; i < names.length; i++) {
var names = name.split(eventSplitter); name = names[i];
for (var i = 0, l = names.length; i < l; i++) { var handlers = events[name];
obj[action].apply(obj, [names[i]].concat(rest));
// Bail out if there are no events stored.
if (!handlers) break;
// Replace events if there are any remaining. Otherwise, clean up.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
} else {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
} }
return false;
} }
return true; // Update tail event if the list has any events. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
return events;
};
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
var onceMap = function(map, name, callback, offer) {
if (callback) {
var once = map[name] = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
}
return map;
};
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
var args = Array(length);
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
}; };
// A difficult-to-believe, but optimized internal dispatch function for // A difficult-to-believe, but optimized internal dispatch function for
...@@ -211,22 +373,6 @@ ...@@ -211,22 +373,6 @@
} }
}; };
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility. // Aliases for backwards compatibility.
Events.bind = Events.on; Events.bind = Events.on;
Events.unbind = Events.off; Events.unbind = Events.off;
...@@ -248,11 +394,12 @@ ...@@ -248,11 +394,12 @@
var Model = Backbone.Model = function(attributes, options) { var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {}; var attrs = attributes || {};
options || (options = {}); options || (options = {});
this.cid = _.uniqueId('c'); this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {}; this.attributes = {};
if (options.collection) this.collection = options.collection; if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {}; if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults')); var defaults = _.result(this, 'defaults');
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options); this.set(attrs, options);
this.changed = {}; this.changed = {};
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
...@@ -271,6 +418,10 @@ ...@@ -271,6 +418,10 @@
// CouchDB users may want to set this to `"_id"`. // CouchDB users may want to set this to `"_id"`.
idAttribute: 'id', idAttribute: 'id',
// The prefix is used to create the client id which is used to identify models locally.
// You may want to override this if you're experiencing name clashes with model ids.
cidPrefix: 'c',
// Initialize is an empty function by default. Override it with your own // Initialize is an empty function by default. Override it with your own
// initialization logic. // initialization logic.
initialize: function(){}, initialize: function(){},
...@@ -302,14 +453,19 @@ ...@@ -302,14 +453,19 @@
return this.get(attr) != null; return this.get(attr) != null;
}, },
// Special-cased proxy to underscore's `_.matches` method.
matches: function(attrs) {
return !!_.iteratee(attrs, this)(this.attributes);
},
// Set a hash of model attributes on the object, firing `"change"`. This is // Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying // the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast. // anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) { set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this; if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') { if (typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
...@@ -323,37 +479,40 @@ ...@@ -323,37 +479,40 @@
if (!this._validate(attrs, options)) return false; if (!this._validate(attrs, options)) return false;
// Extract attributes and options. // Extract attributes and options.
unset = options.unset; var unset = options.unset;
silent = options.silent; var silent = options.silent;
changes = []; var changes = [];
changing = this._changing; var changing = this._changing;
this._changing = true; this._changing = true;
if (!changing) { if (!changing) {
this._previousAttributes = _.clone(this.attributes); this._previousAttributes = _.clone(this.attributes);
this.changed = {}; this.changed = {};
} }
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`. var current = this.attributes;
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; var changed = this.changed;
var prev = this._previousAttributes;
// For each `set` attribute, update or delete the current value. // For each `set` attribute, update or delete the current value.
for (attr in attrs) { for (var attr in attrs) {
val = attrs[attr]; val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr); if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) { if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val; changed[attr] = val;
} else { } else {
delete this.changed[attr]; delete changed[attr];
} }
unset ? delete current[attr] : current[attr] = val; unset ? delete current[attr] : current[attr] = val;
} }
// Update the `id`.
if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes. // Trigger all relevant attribute changes.
if (!silent) { if (!silent) {
if (changes.length) this._pending = options; if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) { for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options); this.trigger('change:' + changes[i], this, current[changes[i]], options);
} }
} }
...@@ -401,13 +560,14 @@ ...@@ -401,13 +560,14 @@
// determining if there *would be* a change. // determining if there *would be* a change.
changedAttributes: function(diff) { changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes; var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
for (var attr in diff) { for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue; var val = diff[attr];
(changed || (changed = {}))[attr] = val; if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
} }
return changed; return _.size(changed) ? changed : false;
}, },
// Get the previous value of an attribute, recorded at the time the last // Get the previous value of an attribute, recorded at the time the last
...@@ -423,17 +583,16 @@ ...@@ -423,17 +583,16 @@
return _.clone(this._previousAttributes); return _.clone(this._previousAttributes);
}, },
// Fetch the model from the server. If the server's representation of the // Fetch the model from the server, merging the response with the model's
// model differs from its current attributes, they will be overridden, // local attributes. Any changed attributes will trigger a "change" event.
// triggering a `"change"` event.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = _.extend({parse: true}, options);
if (options.parse === void 0) options.parse = true;
var model = this; var model = this;
var success = options.success; var success = options.success;
options.success = function(resp) { options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false; var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (success) success(model, resp, options); if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
...@@ -444,9 +603,8 @@ ...@@ -444,9 +603,8 @@
// If the server returns an attributes hash that differs, the model's // If the server returns an attributes hash that differs, the model's
// state will be `set` again. // state will be `set` again.
save: function(key, val, options) { save: function(key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (key == null || typeof key === 'object') { if (key == null || typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
...@@ -454,46 +612,43 @@ ...@@ -454,46 +612,43 @@
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
options = _.extend({validate: true}, options); options = _.extend({validate: true, parse: true}, options);
var wait = options.wait;
// If we're not waiting and attributes exist, save acts as // If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if // `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set. // the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) { if (attrs && !wait) {
if (!this.set(attrs, options)) return false; if (!this.set(attrs, options)) return false;
} else { } else if (!this._validate(attrs, options)) {
if (!this._validate(attrs, options)) return false; return false;
}
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
} }
// After a successful server-side save, the client is (optionally) // After a successful server-side save, the client is (optionally)
// updated with the server-side state. // updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this; var model = this;
var success = options.success; var success = options.success;
var attributes = this.attributes;
options.success = function(resp) { options.success = function(resp) {
// Ensure attributes are restored during synchronous saves. // Ensure attributes are restored during synchronous saves.
model.attributes = attributes; model.attributes = attributes;
var serverAttrs = model.parse(resp, options); var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { if (serverAttrs && !model.set(serverAttrs, options)) return false;
return false; if (success) success.call(options.context, model, resp, options);
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); // Set temporary attributes if `{wait: true}` to properly find new ids.
if (method === 'patch') options.attrs = attrs; if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
xhr = this.sync(method, this, options);
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this.sync(method, this, options);
// Restore attributes. // Restore attributes.
if (attrs && options.wait) this.attributes = attributes; this.attributes = attributes;
return xhr; return xhr;
}, },
...@@ -505,25 +660,27 @@ ...@@ -505,25 +660,27 @@
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
var model = this; var model = this;
var success = options.success; var success = options.success;
var wait = options.wait;
var destroy = function() { var destroy = function() {
model.stopListening();
model.trigger('destroy', model, model.collection, options); model.trigger('destroy', model, model.collection, options);
}; };
options.success = function(resp) { options.success = function(resp) {
if (options.wait || model.isNew()) destroy(); if (wait) destroy();
if (success) success(model, resp, options); if (success) success.call(options.context, model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options); if (!model.isNew()) model.trigger('sync', model, resp, options);
}; };
var xhr = false;
if (this.isNew()) { if (this.isNew()) {
options.success(); _.defer(options.success);
return false; } else {
}
wrapError(this, options); wrapError(this, options);
xhr = this.sync('delete', this, options);
var xhr = this.sync('delete', this, options); }
if (!options.wait) destroy(); if (!wait) destroy();
return xhr; return xhr;
}, },
...@@ -536,7 +693,8 @@ ...@@ -536,7 +693,8 @@
_.result(this.collection, 'url') || _.result(this.collection, 'url') ||
urlError(); urlError();
if (this.isNew()) return base; if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); var id = this.get(this.idAttribute);
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
}, },
// **parse** converts a response into the hash of attributes to be `set` on // **parse** converts a response into the hash of attributes to be `set` on
...@@ -557,7 +715,7 @@ ...@@ -557,7 +715,7 @@
// Check if the model is currently in a valid state. // Check if the model is currently in a valid state.
isValid: function(options) { isValid: function(options) {
return this._validate({}, _.extend(options || {}, { validate: true })); return this._validate({}, _.extend({}, options, {validate: true}));
}, },
// Run validation against the next complete set of model attributes, // Run validation against the next complete set of model attributes,
...@@ -573,23 +731,19 @@ ...@@ -573,23 +731,19 @@
}); });
// Underscore methods that we want to implement on the Model. // Underscore methods that we want to implement on the Model, mapped to the
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; // number of arguments they take.
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1};
// Mix in each Underscore method as a proxy to `Model#attributes`. // Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) { addUnderscoreMethods(Model, modelMethods, 'attributes');
Model.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});
// Backbone.Collection // Backbone.Collection
// ------------------- // -------------------
// If models tend to represent a single row of data, a Backbone Collection is // If models tend to represent a single row of data, a Backbone Collection is
// more analagous to a table full of data ... or a small slice or page of that // more analogous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason // table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents // -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain // belonging to this particular author, and so on. Collections maintain
...@@ -611,6 +765,17 @@ ...@@ -611,6 +765,17 @@
var setOptions = {add: true, remove: true, merge: true}; var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false}; var addOptions = {add: true, remove: false};
// Splices `insert` into `array` at index `at`.
var splice = function(array, insert, at) {
at = Math.min(Math.max(at, 0), array.length);
var tail = Array(array.length - at);
var length = insert.length;
var i;
for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
for (i = 0; i < length; i++) array[i + at] = insert[i];
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
};
// Define the Collection's inheritable methods. // Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, { _.extend(Collection.prototype, Events, {
...@@ -625,7 +790,7 @@ ...@@ -625,7 +790,7 @@
// The JSON representation of a Collection is an array of the // The JSON representation of a Collection is an array of the
// models' attributes. // models' attributes.
toJSON: function(options) { toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); }); return this.map(function(model) { return model.toJSON(options); });
}, },
// Proxy `Backbone.sync` by default. // Proxy `Backbone.sync` by default.
...@@ -633,32 +798,24 @@ ...@@ -633,32 +798,24 @@
return Backbone.sync.apply(this, arguments); return Backbone.sync.apply(this, arguments);
}, },
// Add a model, or list of models to the set. // Add a model, or list of models to the set. `models` may be Backbone
// Models or raw JavaScript objects to be converted to Models, or any
// combination of the two.
add: function(models, options) { add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions)); return this.set(models, _.extend({merge: false}, options, addOptions));
}, },
// Remove a model, or a list of models from the set. // Remove a model, or a list of models from the set.
remove: function(models, options) { remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models); var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models); models = singular ? [models] : models.slice();
options || (options = {}); var removed = this._removeModels(models, options);
var i, l, index, model; if (!options.silent && removed.length) {
for (i = 0, l = models.length; i < l; i++) { options.changes = {added: [], merged: [], removed: removed};
model = models[i] = this.get(models[i]); this.trigger('update', this, options);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
} }
this._removeReference(model, options); return singular ? removed[0] : removed;
}
return singular ? models[0] : models;
}, },
// Update a collection by `set`-ing a new list of models, adding new ones, // Update a collection by `set`-ing a new list of models, adding new ones,
...@@ -666,89 +823,114 @@ ...@@ -666,89 +823,114 @@
// already exist in the collection, as necessary. Similar to **Model#set**, // already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection. // the core operation for updating the data contained by the collection.
set: function(models, options) { set: function(models, options) {
options = _.defaults({}, options, setOptions); if (models == null) return;
if (options.parse) models = this.parse(models, options);
options = _.extend({}, setOptions, options);
if (options.parse && !this._isModel(models)) {
models = this.parse(models, options) || [];
}
var singular = !_.isArray(models); var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : _.clone(models); models = singular ? [models] : models.slice();
var i, l, id, model, attrs, existing, sort;
var at = options.at; var at = options.at;
var targetModel = this.model; if (at != null) at = +at;
var sortable = this.comparator && (at == null) && options.sort !== false; if (at > this.length) at = this.length;
if (at < 0) at += this.length + 1;
var set = [];
var toAdd = [];
var toMerge = [];
var toRemove = [];
var modelMap = {};
var add = options.add;
var merge = options.merge;
var remove = options.remove;
var sort = false;
var sortable = this.comparator && at == null && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null; var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// Turn bare objects into model references, and prevent invalid models // Turn bare objects into model references, and prevent invalid models
// from being added. // from being added.
for (i = 0, l = models.length; i < l; i++) { var model, i;
attrs = models[i] || {}; for (i = 0; i < models.length; i++) {
if (attrs instanceof Model) { model = models[i];
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute || 'id'];
}
// If a duplicate is found, prevent it from being added and // If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model. // optionally merge it into the existing model.
if (existing = this.get(id)) { var existing = this.get(model);
if (remove) modelMap[existing.cid] = true; if (existing) {
if (merge) { if (merge && model !== existing) {
attrs = attrs === model ? model.attributes : attrs; var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options); if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options); existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; toMerge.push(existing);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
} }
models[i] = existing; models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list. // If this is a new, valid model, push it to the `toAdd` list.
} else if (add) { } else if (add) {
model = models[i] = this._prepareModel(attrs, options); model = models[i] = this._prepareModel(model, options);
if (!model) continue; if (model) {
toAdd.push(model); toAdd.push(model);
this._addReference(model, options); this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
} }
// Do not add multiple models with the same `id`.
model = existing || model;
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
modelMap[model.id] = true;
} }
// Remove nonexistent models if appropriate. // Remove stale models.
if (remove) { if (remove) {
for (i = 0, l = this.length; i < l; ++i) { for (i = 0; i < this.length; i++) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
} }
if (toRemove.length) this.remove(toRemove, options); if (toRemove.length) this._removeModels(toRemove, options);
} }
// See if sorting is needed, update `length` and splice in new models. // See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) { var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true; if (sortable) sort = true;
this.length += toAdd.length; splice(this.models, toAdd, at == null ? this.length : at);
if (at != null) { this.length = this.models.length;
for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
}
} }
// Silently sort the collection if appropriate. // Silently sort the collection if appropriate.
if (sort) this.sort({silent: true}); if (sort) this.sort({silent: true});
// Unless silenced, it's time to fire all appropriate add/sort events. // Unless silenced, it's time to fire all appropriate add/sort/update events.
if (!options.silent) { if (!options.silent) {
for (i = 0, l = toAdd.length; i < l; i++) { for (i = 0; i < toAdd.length; i++) {
(model = toAdd[i]).trigger('add', model, this, options); if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
}
if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length || toMerge.length) {
options.changes = {
added: toAdd,
removed: toRemove,
merged: toMerge
};
this.trigger('update', this, options);
} }
if (sort || (order && order.length)) this.trigger('sort', this, options);
} }
// Return the added (or merged) model (or models). // Return the added (or merged) model (or models).
...@@ -760,8 +942,8 @@ ...@@ -760,8 +942,8 @@
// any granular `add` or `remove` events. Fires `reset` when finished. // any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations. // Useful for bulk operations and optimizations.
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options = options ? _.clone(options) : {};
for (var i = 0, l = this.models.length; i < l; i++) { for (var i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options); this._removeReference(this.models[i], options);
} }
options.previousModels = this.models; options.previousModels = this.models;
...@@ -779,8 +961,7 @@ ...@@ -779,8 +961,7 @@
// Remove a model from the end of the collection. // Remove a model from the end of the collection.
pop: function(options) { pop: function(options) {
var model = this.at(this.length - 1); var model = this.at(this.length - 1);
this.remove(model, options); return this.remove(model, options);
return model;
}, },
// Add a model to the beginning of the collection. // Add a model to the beginning of the collection.
...@@ -791,8 +972,7 @@ ...@@ -791,8 +972,7 @@
// Remove a model from the beginning of the collection. // Remove a model from the beginning of the collection.
shift: function(options) { shift: function(options) {
var model = this.at(0); var model = this.at(0);
this.remove(model, options); return this.remove(model, options);
return model;
}, },
// Slice out a sub-array of models from the collection. // Slice out a sub-array of models from the collection.
...@@ -800,27 +980,30 @@ ...@@ -800,27 +980,30 @@
return slice.apply(this.models, arguments); return slice.apply(this.models, arguments);
}, },
// Get a model from the set by id. // Get a model from the set by id, cid, model object with id or cid
// properties, or an attributes object that is transformed through modelId.
get: function(obj) { get: function(obj) {
if (obj == null) return void 0; if (obj == null) return void 0;
return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; return this._byId[obj] ||
this._byId[this.modelId(obj.attributes || obj)] ||
obj.cid && this._byId[obj.cid];
},
// Returns `true` if the model is in the collection.
has: function(obj) {
return this.get(obj) != null;
}, },
// Get the model at the given index. // Get the model at the given index.
at: function(index) { at: function(index) {
if (index < 0) index += this.length;
return this.models[index]; return this.models[index];
}, },
// Return models with matching attributes. Useful for simple cases of // Return models with matching attributes. Useful for simple cases of
// `filter`. // `filter`.
where: function(attrs, first) { where: function(attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : []; return this[first ? 'find' : 'filter'](attrs);
return this[first ? 'find' : 'filter'](function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
}, },
// Return the first model with matching attributes. Useful for simple cases // Return the first model with matching attributes. Useful for simple cases
...@@ -833,37 +1016,39 @@ ...@@ -833,37 +1016,39 @@
// normal circumstances, as the set will maintain sort order as each item // normal circumstances, as the set will maintain sort order as each item
// is added. // is added.
sort: function(options) { sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); var comparator = this.comparator;
if (!comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {}); options || (options = {});
var length = comparator.length;
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
// Run sort based on type of `comparator`. // Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) { if (length === 1 || _.isString(comparator)) {
this.models = this.sortBy(this.comparator, this); this.models = this.sortBy(comparator);
} else { } else {
this.models.sort(_.bind(this.comparator, this)); this.models.sort(comparator);
} }
if (!options.silent) this.trigger('sort', this, options); if (!options.silent) this.trigger('sort', this, options);
return this; return this;
}, },
// Pluck an attribute from each model in the collection. // Pluck an attribute from each model in the collection.
pluck: function(attr) { pluck: function(attr) {
return _.invoke(this.models, 'get', attr); return this.map(attr + '');
}, },
// Fetch the default set of models for this collection, resetting the // Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `reset: true` is passed, the response // collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`. // data will be passed through the `reset` method instead of `set`.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = _.extend({parse: true}, options);
if (options.parse === void 0) options.parse = true;
var success = options.success; var success = options.success;
var collection = this; var collection = this;
options.success = function(resp) { options.success = function(resp) {
var method = options.reset ? 'reset' : 'set'; var method = options.reset ? 'reset' : 'set';
collection[method](resp, options); collection[method](resp, options);
if (success) success(collection, resp, options); if (success) success.call(options.context, collection, resp, options);
collection.trigger('sync', collection, resp, options); collection.trigger('sync', collection, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
...@@ -875,13 +1060,15 @@ ...@@ -875,13 +1060,15 @@
// wait for the server to agree. // wait for the server to agree.
create: function(model, options) { create: function(model, options) {
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false; var wait = options.wait;
if (!options.wait) this.add(model, options); model = this._prepareModel(model, options);
if (!model) return false;
if (!wait) this.add(model, options);
var collection = this; var collection = this;
var success = options.success; var success = options.success;
options.success = function(model, resp) { options.success = function(m, resp, callbackOpts) {
if (options.wait) collection.add(model, options); if (wait) collection.add(m, callbackOpts);
if (success) success(model, resp, options); if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
}; };
model.save(null, options); model.save(null, options);
return model; return model;
...@@ -895,7 +1082,15 @@ ...@@ -895,7 +1082,15 @@
// Create a new collection with an identical list of models as this one. // Create a new collection with an identical list of models as this one.
clone: function() { clone: function() {
return new this.constructor(this.models); return new this.constructor(this.models, {
model: this.model,
comparator: this.comparator
});
},
// Define how to uniquely identify models in the collection.
modelId: function(attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
}, },
// Private method to reset all internal state. Called when the collection // Private method to reset all internal state. Called when the collection
...@@ -909,7 +1104,10 @@ ...@@ -909,7 +1104,10 @@
// Prepare a hash of attributes (or other model) to be added to this // Prepare a hash of attributes (or other model) to be added to this
// collection. // collection.
_prepareModel: function(attrs, options) { _prepareModel: function(attrs, options) {
if (attrs instanceof Model) return attrs; if (this._isModel(attrs)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
options.collection = this; options.collection = this;
var model = new this.model(attrs, options); var model = new this.model(attrs, options);
...@@ -918,16 +1116,53 @@ ...@@ -918,16 +1116,53 @@
return false; return false;
}, },
// Internal method called by both remove and set.
_removeModels: function(models, options) {
var removed = [];
for (var i = 0; i < models.length; i++) {
var model = this.get(models[i]);
if (!model) continue;
var index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
// Remove references before triggering 'remove' event to prevent an
// infinite loop. #3693
delete this._byId[model.cid];
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
removed.push(model);
this._removeReference(model, options);
}
return removed;
},
// Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
_isModel: function(model) {
return model instanceof Model;
},
// Internal method to create a model's ties to a collection. // Internal method to create a model's ties to a collection.
_addReference: function(model, options) { _addReference: function(model, options) {
this._byId[model.cid] = model; this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model; var id = this.modelId(model.attributes);
if (!model.collection) model.collection = this; if (id != null) this._byId[id] = model;
model.on('all', this._onModelEvent, this); model.on('all', this._onModelEvent, this);
}, },
// Internal method to sever a model's ties to a collection. // Internal method to sever a model's ties to a collection.
_removeReference: function(model, options) { _removeReference: function(model, options) {
delete this._byId[model.cid];
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
if (this === model.collection) delete model.collection; if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this); model.off('all', this._onModelEvent, this);
}, },
...@@ -937,11 +1172,17 @@ ...@@ -937,11 +1172,17 @@
// events simply proxy through. "add" and "remove" events that originate // events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored. // in other collections are ignored.
_onModelEvent: function(event, model, collection, options) { _onModelEvent: function(event, model, collection, options) {
if (model) {
if ((event === 'add' || event === 'remove') && collection !== this) return; if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options); if (event === 'destroy') this.remove(model, options);
if (model && event === 'change:' + model.idAttribute) { if (event === 'change') {
delete this._byId[model.previous(model.idAttribute)]; var prevId = this.modelId(model.previousAttributes());
if (model.id != null) this._byId[model.id] = model; var id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
}
}
} }
this.trigger.apply(this, arguments); this.trigger.apply(this, arguments);
} }
...@@ -951,34 +1192,17 @@ ...@@ -951,34 +1192,17 @@
// Underscore methods that we want to implement on the Collection. // Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented // 90% of the core usefulness of Backbone Collections is actually implemented
// right here: // right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
'lastIndexOf', 'isEmpty', 'chain', 'sample']; without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
// Mix in each Underscore method as a proxy to `Collection#models`. // Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) { addUnderscoreMethods(Collection, collectionMethods, 'models');
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
// Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties.
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};
});
// Backbone.View // Backbone.View
// ------------- // -------------
...@@ -995,17 +1219,15 @@ ...@@ -995,17 +1219,15 @@
// if an existing element is not provided... // if an existing element is not provided...
var View = Backbone.View = function(options) { var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view'); this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions)); _.extend(this, _.pick(options, viewOptions));
this._ensureElement(); this._ensureElement();
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
this.delegateEvents();
}; };
// Cached regex to split keys for `delegate`. // Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/; var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties. // List of view options to be set as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods. // Set up all inheritable **Backbone.View** properties and methods.
...@@ -1034,21 +1256,37 @@ ...@@ -1034,21 +1256,37 @@
// Remove this view by taking the element out of the DOM, and removing any // Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners. // applicable Backbone.Events listeners.
remove: function() { remove: function() {
this.$el.remove(); this._removeElement();
this.stopListening(); this.stopListening();
return this; return this;
}, },
// Change the view's element (`this.el` property), including event // Remove this view's element from the document and all event listeners
// re-delegation. // attached to it. Exposed for subclasses using an alternative DOM
setElement: function(element, delegate) { // manipulation API.
if (this.$el) this.undelegateEvents(); _removeElement: function() {
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); this.$el.remove();
this.el = this.$el[0]; },
if (delegate !== false) this.delegateEvents();
// Change the view's element (`this.el` property) and re-delegate the
// view's events on the new element.
setElement: function(element) {
this.undelegateEvents();
this._setElement(element);
this.delegateEvents();
return this; return this;
}, },
// Creates the `this.el` and `this.$el` references for this view using the
// given `el`. `el` can be a CSS selector or an HTML string, a jQuery
// context or an element. Subclasses can override this to utilize an
// alternative DOM manipulation API and are only required to set the
// `this.el` property.
_setElement: function(el) {
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
this.el = this.$el[0];
},
// Set callbacks, where `this.events` is a hash of // Set callbacks, where `this.events` is a hash of
// //
// *{"event selector": "callback"}* // *{"event selector": "callback"}*
...@@ -1062,37 +1300,49 @@ ...@@ -1062,37 +1300,49 @@
// pairs. Callbacks will be bound to the view, with `this` set properly. // pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency. // Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`. // Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) { delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return this; events || (events = _.result(this, 'events'));
if (!events) return this;
this.undelegateEvents(); this.undelegateEvents();
for (var key in events) { for (var key in events) {
var method = events[key]; var method = events[key];
if (!_.isFunction(method)) method = this[events[key]]; if (!_.isFunction(method)) method = this[method];
if (!method) continue; if (!method) continue;
var match = key.match(delegateEventSplitter); var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2]; this.delegate(match[1], match[2], _.bind(method, this));
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
} }
return this; return this;
}, },
// Clears all callbacks previously bound to the view with `delegateEvents`. // Add a single event listener to the view's element (or a child element
// using `selector`). This only works for delegate-able events: not `focus`,
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
delegate: function(eventName, selector, listener) {
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
// Clears all callbacks previously bound to the view by `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple // You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element. // Backbone views attached to the same DOM element.
undelegateEvents: function() { undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid); if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this; return this;
}, },
// A finer-grained `undelegateEvents` for removing a single delegated event.
// `selector` and `listener` are both optional.
undelegate: function(eventName, selector, listener) {
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
// Produces a DOM element to be assigned to your view. Exposed for
// subclasses using an alternative DOM manipulation API.
_createElement: function(tagName) {
return document.createElement(tagName);
},
// Ensure that the View has a DOM element to render into. // Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first // If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create // matching element, and re-assign it to `el`. Otherwise, create
...@@ -1102,11 +1352,17 @@ ...@@ -1102,11 +1352,17 @@
var attrs = _.extend({}, _.result(this, 'attributes')); var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id'); if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className'); if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); this.setElement(this._createElement(_.result(this, 'tagName')));
this.setElement($el, false); this._setAttributes(attrs);
} else { } else {
this.setElement(_.result(this, 'el'), false); this.setElement(_.result(this, 'el'));
} }
},
// Set attributes from a hash on this view's element. Exposed for
// subclasses using an alternative DOM manipulation API.
_setAttributes: function(attributes) {
this.$el.attr(attributes);
} }
}); });
...@@ -1175,14 +1431,13 @@ ...@@ -1175,14 +1431,13 @@
params.processData = false; params.processData = false;
} }
// If we're sending a `PATCH` request, and we're in an old Internet Explorer // Pass along `textStatus` and `errorThrown` from jQuery.
// that still has ActiveX enabled by default, override jQuery to use that var error = options.error;
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. options.error = function(xhr, textStatus, errorThrown) {
if (params.type === 'PATCH' && noXhrPatch) { options.textStatus = textStatus;
params.xhr = function() { options.errorThrown = errorThrown;
return new ActiveXObject("Microsoft.XMLHTTP"); if (error) error.call(options.context, xhr, textStatus, errorThrown);
}; };
}
// Make the request, allowing the user to override any Ajax options. // Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
...@@ -1190,10 +1445,6 @@ ...@@ -1190,10 +1445,6 @@
return xhr; return xhr;
}; };
var noXhrPatch =
typeof window !== 'undefined' && !!window.ActiveXObject &&
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
// Map from CRUD to HTTP for our default `Backbone.sync` implementation. // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = { var methodMap = {
'create': 'POST', 'create': 'POST',
...@@ -1251,17 +1502,18 @@ ...@@ -1251,17 +1502,18 @@
var router = this; var router = this;
Backbone.history.route(route, function(fragment) { Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment); var args = router._extractParameters(route, fragment);
router.execute(callback, args); if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args); router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args); Backbone.history.trigger('route', router, name, args);
}
}); });
return this; return this;
}, },
// Execute a route handler with the provided parameters. This is an // Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup. // excellent place to do pre-route setup or post-route cleanup.
execute: function(callback, args) { execute: function(callback, args, name) {
if (callback) callback.apply(this, args); if (callback) callback.apply(this, args);
}, },
...@@ -1319,7 +1571,7 @@ ...@@ -1319,7 +1571,7 @@
// falls back to polling. // falls back to polling.
var History = Backbone.History = function() { var History = Backbone.History = function() {
this.handlers = []; this.handlers = [];
_.bindAll(this, 'checkUrl'); this.checkUrl = _.bind(this.checkUrl, this);
// Ensure that `History` can be used outside of the browser. // Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
...@@ -1334,12 +1586,6 @@ ...@@ -1334,12 +1586,6 @@
// Cached regex for stripping leading and trailing slashes. // Cached regex for stripping leading and trailing slashes.
var rootStripper = /^\/+|\/+$/g; var rootStripper = /^\/+|\/+$/g;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Cached regex for removing a trailing slash.
var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash. // Cached regex for stripping urls of hash.
var pathStripper = /#.*$/; var pathStripper = /#.*$/;
...@@ -1355,7 +1601,29 @@ ...@@ -1355,7 +1601,29 @@
// Are we at the app root? // Are we at the app root?
atRoot: function() { atRoot: function() {
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; var path = this.location.pathname.replace(/[^\/]$/, '$&/');
return path === this.root && !this.getSearch();
},
// Does the pathname match the root?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
var rootPath = path.slice(0, this.root.length - 1) + '/';
return rootPath === this.root;
},
// Unicode characters in `location.pathname` are percent encoded so they're
// decoded for comparison. `%25` should not be decoded since it may be part
// of an encoded parameter.
decodeFragment: function(fragment) {
return decodeURI(fragment.replace(/%25/g, '%2525'));
},
// In IE6, the hash fragment and search params are incorrect if the
// fragment contains `?`.
getSearch: function() {
var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
return match ? match[0] : '';
}, },
// Gets the true hash value. Cannot use location.hash directly due to bug // Gets the true hash value. Cannot use location.hash directly due to bug
...@@ -1365,14 +1633,19 @@ ...@@ -1365,14 +1633,19 @@
return match ? match[1] : ''; return match ? match[1] : '';
}, },
// Get the cross-browser normalized URL fragment, either from the URL, // Get the pathname and search params, without the root.
// the hash, or the override. getPath: function() {
getFragment: function(fragment, forcePushState) { var path = this.decodeFragment(
this.location.pathname + this.getSearch()
).slice(this.root.length - 1);
return path.charAt(0) === '/' ? path.slice(1) : path;
},
// Get the cross-browser normalized URL fragment from the path or hash.
getFragment: function(fragment) {
if (fragment == null) { if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) { if (this._usePushState || !this._wantsHashChange) {
fragment = decodeURI(this.location.pathname + this.location.search); fragment = this.getPath();
var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else { } else {
fragment = this.getHash(); fragment = this.getHash();
} }
...@@ -1383,7 +1656,7 @@ ...@@ -1383,7 +1656,7 @@
// Start the hash change handling, returning `true` if the current URL matches // Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise. // an existing route, and `false` otherwise.
start: function(options) { start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started"); if (History.started) throw new Error('Backbone.history has already been started');
History.started = true; History.started = true;
// Figure out the initial configuration. Do we need an iframe? // Figure out the initial configuration. Do we need an iframe?
...@@ -1391,36 +1664,16 @@ ...@@ -1391,36 +1664,16 @@
this.options = _.extend({root: '/'}, this.options, options); this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root; this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false; this._wantsHashChange = this.options.hashChange !== false;
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
this._useHashChange = this._wantsHashChange && this._hasHashChange;
this._wantsPushState = !!this.options.pushState; this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); this._hasPushState = !!(this.history && this.history.pushState);
var fragment = this.getFragment(); this._usePushState = this._wantsPushState && this._hasPushState;
var docMode = document.documentMode; this.fragment = this.getFragment();
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
// Normalize root to always include a leading and trailing slash. // Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) {
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = this.location;
// Transition from hashChange to pushState or vice versa if both are // Transition from hashChange to pushState or vice versa if both are
// requested. // requested.
if (this._wantsHashChange && this._wantsPushState) { if (this._wantsHashChange && this._wantsPushState) {
...@@ -1428,27 +1681,75 @@ ...@@ -1428,27 +1681,75 @@
// If we've started off with a route from a `pushState`-enabled // If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it... // browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) { if (!this._hasPushState && !this.atRoot()) {
this.fragment = this.getFragment(null, true); var rootPath = this.root.slice(0, -1) || '/';
this.location.replace(this.root + '#' + this.fragment); this.location.replace(rootPath + '#' + this.getPath());
// Return immediately as browser will do redirect to new url // Return immediately as browser will do redirect to new url
return true; return true;
// Or if we've started out with a hash-based route, but we're currently // Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead... // in a browser where it could be `pushState`-based instead...
} else if (this._hasPushState && this.atRoot() && loc.hash) { } else if (this._hasPushState && this.atRoot()) {
this.fragment = this.getHash().replace(routeStripper, ''); this.navigate(this.getHash(), {replace: true});
this.history.replaceState({}, document.title, this.root + this.fragment);
} }
} }
// Proxy an iframe to handle location events if the browser doesn't
// support the `hashchange` event, HTML5 history, or the user wants
// `hashChange` but not `pushState`.
if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
this.iframe = document.createElement('iframe');
this.iframe.src = 'javascript:0';
this.iframe.style.display = 'none';
this.iframe.tabIndex = -1;
var body = document.body;
// Using `appendChild` will throw on IE < 9 if the document is not ready.
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
iWindow.document.open();
iWindow.document.close();
iWindow.location.hash = '#' + this.fragment;
}
// Add a cross-platform `addEventListener` shim for older browsers.
var addEventListener = window.addEventListener || function(eventName, listener) {
return attachEvent('on' + eventName, listener);
};
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._usePushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
if (!this.options.silent) return this.loadUrl(); if (!this.options.silent) return this.loadUrl();
}, },
// Disable Backbone.history, perhaps temporarily. Not useful in a real app, // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers. // but possibly useful for unit testing Routers.
stop: function() { stop: function() {
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); // Add a cross-platform `removeEventListener` shim for older browsers.
var removeEventListener = window.removeEventListener || function(eventName, listener) {
return detachEvent('on' + eventName, listener);
};
// Remove window listeners.
if (this._usePushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
removeEventListener('hashchange', this.checkUrl, false);
}
// Clean up the iframe if necessary.
if (this.iframe) {
document.body.removeChild(this.iframe);
this.iframe = null;
}
// Some environments will throw when clearing an undefined interval.
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false; History.started = false;
}, },
...@@ -1463,9 +1764,13 @@ ...@@ -1463,9 +1764,13 @@
// calls `loadUrl`, normalizing across the hidden iframe. // calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) { checkUrl: function(e) {
var current = this.getFragment(); var current = this.getFragment();
// If the user pressed the back button, the iframe's hash will have
// changed and we should use that for comparison.
if (current === this.fragment && this.iframe) { if (current === this.fragment && this.iframe) {
current = this.getFragment(this.getHash(this.iframe)); current = this.getHash(this.iframe.contentWindow);
} }
if (current === this.fragment) return false; if (current === this.fragment) return false;
if (this.iframe) this.navigate(current); if (this.iframe) this.navigate(current);
this.loadUrl(); this.loadUrl();
...@@ -1475,8 +1780,10 @@ ...@@ -1475,8 +1780,10 @@
// match, returns `true`. If no defined routes matches the fragment, // match, returns `true`. If no defined routes matches the fragment,
// returns `false`. // returns `false`.
loadUrl: function(fragment) { loadUrl: function(fragment) {
// If the root doesn't match, no routes can match either.
if (!this.matchRoot()) return false;
fragment = this.fragment = this.getFragment(fragment); fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) { return _.some(this.handlers, function(handler) {
if (handler.route.test(fragment)) { if (handler.route.test(fragment)) {
handler.callback(fragment); handler.callback(fragment);
return true; return true;
...@@ -1495,31 +1802,42 @@ ...@@ -1495,31 +1802,42 @@
if (!History.started) return false; if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options}; if (!options || options === true) options = {trigger: !!options};
var url = this.root + (fragment = this.getFragment(fragment || '')); // Normalize the fragment.
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
var rootPath = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
rootPath = rootPath.slice(0, -1) || '/';
}
var url = rootPath + fragment;
// Strip the hash for matching. // Strip the hash and decode for matching.
fragment = fragment.replace(pathStripper, ''); fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
if (this.fragment === fragment) return; if (this.fragment === fragment) return;
this.fragment = fragment; this.fragment = fragment;
// Don't include a trailing slash on the root.
if (fragment === '' && url !== '/') url = url.slice(0, -1);
// If pushState is available, we use it to set the fragment as a real URL. // If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) { if (this._usePushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash // If hash changes haven't been explicitly disabled, update the hash
// fragment to store history. // fragment to store history.
} else if (this._wantsHashChange) { } else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace); this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
var iWindow = this.iframe.contentWindow;
// Opening and closing the iframe tricks IE7 and earlier to push a // Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't // history entry on hash-tag change. When replace is true, we don't
// want this. // want this.
if(!options.replace) this.iframe.document.open().close(); if (!options.replace) {
this._updateHash(this.iframe.location, fragment, options.replace); iWindow.document.open();
iWindow.document.close();
}
this._updateHash(iWindow.location, fragment, options.replace);
} }
// If you've told us that you explicitly don't want fallback hashchange- // If you've told us that you explicitly don't want fallback hashchange-
...@@ -1550,7 +1868,7 @@ ...@@ -1550,7 +1868,7 @@
// Helpers // Helpers
// ------- // -------
// Helper function to correctly set up the prototype chain, for subclasses. // Helper function to correctly set up the prototype chain for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and // Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended. // class properties to be extended.
var extend = function(protoProps, staticProps) { var extend = function(protoProps, staticProps) {
...@@ -1559,7 +1877,7 @@ ...@@ -1559,7 +1877,7 @@
// The constructor function for the new subclass is either defined by you // The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted // (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor. // by us to simply call the parent constructor.
if (protoProps && _.has(protoProps, 'constructor')) { if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor; child = protoProps.constructor;
} else { } else {
...@@ -1570,14 +1888,9 @@ ...@@ -1570,14 +1888,9 @@
_.extend(child, parent, staticProps); _.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling // Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function. // `parent`'s constructor function and add the prototype properties.
var Surrogate = function(){ this.constructor = child; }; child.prototype = _.create(parent.prototype, protoProps);
Surrogate.prototype = parent.prototype; child.prototype.constructor = child;
child.prototype = new Surrogate;
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Set a convenience property in case the parent's prototype is needed // Set a convenience property in case the parent's prototype is needed
// later. // later.
...@@ -1598,11 +1911,10 @@ ...@@ -1598,11 +1911,10 @@
var wrapError = function(model, options) { var wrapError = function(model, options) {
var error = options.error; var error = options.error;
options.error = function(resp) { options.error = function(resp) {
if (error) error(model, resp, options); if (error) error.call(options.context, model, resp, options);
model.trigger('error', model, resp, options); model.trigger('error', model, resp, options);
}; };
}; };
return Backbone; return Backbone;
});
}));
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -197,8 +197,8 @@ label[for='toggle-all'] { ...@@ -197,8 +197,8 @@ label[for='toggle-all'] {
} }
#todo-list li label { #todo-list li label {
white-space: pre-line; white-space: pre;
word-break: break-all; word-break: break-word;
padding: 15px 60px 15px 15px; padding: 15px 60px 15px 15px;
margin-left: 45px; margin-left: 45px;
display: block; display: block;
......
...@@ -114,7 +114,12 @@ ...@@ -114,7 +114,12 @@
})({}); })({});
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
} }
/* jshint ignore:end */ /* jshint ignore:end */
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
xhr.onload = function (e) { xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText); var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) { if (parsedResponse instanceof Array) {
var count = parsedResponse.length var count = parsedResponse.length;
if (count !== 0) { if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues'; issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline'; document.getElementById('issue-count').style.display = 'inline';
......
// Underscore.js 1.7.0 // Underscore.js 1.8.3
// http://underscorejs.org // http://underscorejs.org
// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license. // Underscore may be freely distributed under the MIT license.
(function() { (function() {
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
var var
push = ArrayProto.push, push = ArrayProto.push,
slice = ArrayProto.slice, slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString, toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty; hasOwnProperty = ObjProto.hasOwnProperty;
...@@ -30,7 +29,11 @@ ...@@ -30,7 +29,11 @@
var var
nativeIsArray = Array.isArray, nativeIsArray = Array.isArray,
nativeKeys = Object.keys, nativeKeys = Object.keys,
nativeBind = FuncProto.bind; nativeBind = FuncProto.bind,
nativeCreate = Object.create;
// Naked function reference for surrogate-prototype-swapping.
var Ctor = function(){};
// Create a safe reference to the Underscore object for use below. // Create a safe reference to the Underscore object for use below.
var _ = function(obj) { var _ = function(obj) {
...@@ -52,12 +55,12 @@ ...@@ -52,12 +55,12 @@
} }
// Current version. // Current version.
_.VERSION = '1.7.0'; _.VERSION = '1.8.3';
// Internal function that returns an efficient (for current engines) version // Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other Underscore // of the passed-in callback, to be repeatedly applied in other Underscore
// functions. // functions.
var createCallback = function(func, context, argCount) { var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func; if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) { switch (argCount == null ? 3 : argCount) {
case 1: return function(value) { case 1: return function(value) {
...@@ -81,12 +84,60 @@ ...@@ -81,12 +84,60 @@
// A mostly-internal function to generate callbacks that can be applied // A mostly-internal function to generate callbacks that can be applied
// to each element in a collection, returning the desired result — either // to each element in a collection, returning the desired result — either
// identity, an arbitrary callback, a property matcher, or a property accessor. // identity, an arbitrary callback, a property matcher, or a property accessor.
_.iteratee = function(value, context, argCount) { var cb = function(value, context, argCount) {
if (value == null) return _.identity; if (value == null) return _.identity;
if (_.isFunction(value)) return createCallback(value, context, argCount); if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matches(value); if (_.isObject(value)) return _.matcher(value);
return _.property(value); return _.property(value);
}; };
_.iteratee = function(value, context) {
return cb(value, context, Infinity);
};
// An internal function for creating assigner functions.
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};
// An internal function for creating a new object that inherits from another.
var baseCreate = function(prototype) {
if (!_.isObject(prototype)) return {};
if (nativeCreate) return nativeCreate(prototype);
Ctor.prototype = prototype;
var result = new Ctor;
Ctor.prototype = null;
return result;
};
var property = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
// Helper for collection methods to determine whether a collection
// should be iterated as an array or as an object
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Collection Functions // Collection Functions
// -------------------- // --------------------
...@@ -95,11 +146,10 @@ ...@@ -95,11 +146,10 @@
// Handles raw objects in addition to array-likes. Treats all // Handles raw objects in addition to array-likes. Treats all
// sparse array-likes as if they were dense. // sparse array-likes as if they were dense.
_.each = _.forEach = function(obj, iteratee, context) { _.each = _.forEach = function(obj, iteratee, context) {
if (obj == null) return obj; iteratee = optimizeCb(iteratee, context);
iteratee = createCallback(iteratee, context); var i, length;
var i, length = obj.length; if (isArrayLike(obj)) {
if (length === +length) { for (i = 0, length = obj.length; i < length; i++) {
for (i = 0; i < length; i++) {
iteratee(obj[i], i, obj); iteratee(obj[i], i, obj);
} }
} else { } else {
...@@ -113,77 +163,66 @@ ...@@ -113,77 +163,66 @@
// Return the results of applying the iteratee to each element. // Return the results of applying the iteratee to each element.
_.map = _.collect = function(obj, iteratee, context) { _.map = _.collect = function(obj, iteratee, context) {
if (obj == null) return []; iteratee = cb(iteratee, context);
iteratee = _.iteratee(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj),
length = (keys || obj).length, length = (keys || obj).length,
results = Array(length), results = Array(length);
currentKey;
for (var index = 0; index < length; index++) { for (var index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index; var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj); results[index] = iteratee(obj[currentKey], currentKey, obj);
} }
return results; return results;
}; };
var reduceError = 'Reduce of empty array with no initial value'; // Create a reducing function iterating left or right.
function createReduce(dir) {
// **Reduce** builds up a single result from a list of values, aka `inject`, // Optimized iterator function as using arguments.length
// or `foldl`. // in the main function will deoptimize the, see #1991.
_.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { function iterator(obj, iteratee, memo, keys, index, length) {
if (obj == null) obj = []; for (; index >= 0 && index < length; index += dir) {
iteratee = createCallback(iteratee, context, 4); var currentKey = keys ? keys[index] : index;
var keys = obj.length !== +obj.length && _.keys(obj),
length = (keys || obj).length,
index = 0, currentKey;
if (arguments.length < 3) {
if (!length) throw new TypeError(reduceError);
memo = obj[keys ? keys[index++] : index++];
}
for (; index < length; index++) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj); memo = iteratee(memo, obj[currentKey], currentKey, obj);
} }
return memo; return memo;
}; }
// The right-associative version of reduce, also known as `foldr`. return function(obj, iteratee, memo, context) {
_.reduceRight = _.foldr = function(obj, iteratee, memo, context) { iteratee = optimizeCb(iteratee, context, 4);
if (obj == null) obj = []; var keys = !isArrayLike(obj) && _.keys(obj),
iteratee = createCallback(iteratee, context, 4); length = (keys || obj).length,
var keys = obj.length !== + obj.length && _.keys(obj), index = dir > 0 ? 0 : length - 1;
index = (keys || obj).length, // Determine the initial value if none is provided.
currentKey;
if (arguments.length < 3) { if (arguments.length < 3) {
if (!index) throw new TypeError(reduceError); memo = obj[keys ? keys[index] : index];
memo = obj[keys ? keys[--index] : --index]; index += dir;
}
while (index--) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
} }
return memo; return iterator(obj, iteratee, memo, keys, index, length);
}; };
}
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`.
_.reduce = _.foldl = _.inject = createReduce(1);
// The right-associative version of reduce, also known as `foldr`.
_.reduceRight = _.foldr = createReduce(-1);
// Return the first value which passes a truth test. Aliased as `detect`. // Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, predicate, context) { _.find = _.detect = function(obj, predicate, context) {
var result; var key;
predicate = _.iteratee(predicate, context); if (isArrayLike(obj)) {
_.some(obj, function(value, index, list) { key = _.findIndex(obj, predicate, context);
if (predicate(value, index, list)) { } else {
result = value; key = _.findKey(obj, predicate, context);
return true;
} }
}); if (key !== void 0 && key !== -1) return obj[key];
return result;
}; };
// Return all the elements that pass a truth test. // Return all the elements that pass a truth test.
// Aliased as `select`. // Aliased as `select`.
_.filter = _.select = function(obj, predicate, context) { _.filter = _.select = function(obj, predicate, context) {
var results = []; var results = [];
if (obj == null) return results; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value); if (predicate(value, index, list)) results.push(value);
}); });
...@@ -192,19 +231,17 @@ ...@@ -192,19 +231,17 @@
// Return all the elements for which a truth test fails. // Return all the elements for which a truth test fails.
_.reject = function(obj, predicate, context) { _.reject = function(obj, predicate, context) {
return _.filter(obj, _.negate(_.iteratee(predicate)), context); return _.filter(obj, _.negate(cb(predicate)), context);
}; };
// Determine whether all of the elements match a truth test. // Determine whether all of the elements match a truth test.
// Aliased as `all`. // Aliased as `all`.
_.every = _.all = function(obj, predicate, context) { _.every = _.all = function(obj, predicate, context) {
if (obj == null) return true; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length;
length = (keys || obj).length, for (var index = 0; index < length; index++) {
index, currentKey; var currentKey = keys ? keys[index] : index;
for (index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
if (!predicate(obj[currentKey], currentKey, obj)) return false; if (!predicate(obj[currentKey], currentKey, obj)) return false;
} }
return true; return true;
...@@ -213,24 +250,22 @@ ...@@ -213,24 +250,22 @@
// Determine if at least one element in the object matches a truth test. // Determine if at least one element in the object matches a truth test.
// Aliased as `any`. // Aliased as `any`.
_.some = _.any = function(obj, predicate, context) { _.some = _.any = function(obj, predicate, context) {
if (obj == null) return false; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length;
length = (keys || obj).length, for (var index = 0; index < length; index++) {
index, currentKey; var currentKey = keys ? keys[index] : index;
for (index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
if (predicate(obj[currentKey], currentKey, obj)) return true; if (predicate(obj[currentKey], currentKey, obj)) return true;
} }
return false; return false;
}; };
// Determine if the array or object contains a given value (using `===`). // Determine if the array or object contains a given item (using `===`).
// Aliased as `include`. // Aliased as `includes` and `include`.
_.contains = _.include = function(obj, target) { _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
if (obj == null) return false; if (!isArrayLike(obj)) obj = _.values(obj);
if (obj.length !== +obj.length) obj = _.values(obj); if (typeof fromIndex != 'number' || guard) fromIndex = 0;
return _.indexOf(obj, target) >= 0; return _.indexOf(obj, item, fromIndex) >= 0;
}; };
// Invoke a method (with arguments) on every item in a collection. // Invoke a method (with arguments) on every item in a collection.
...@@ -238,7 +273,8 @@ ...@@ -238,7 +273,8 @@
var args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
var isFunc = _.isFunction(method); var isFunc = _.isFunction(method);
return _.map(obj, function(value) { return _.map(obj, function(value) {
return (isFunc ? method : value[method]).apply(value, args); var func = isFunc ? method : value[method];
return func == null ? func : func.apply(value, args);
}); });
}; };
...@@ -250,13 +286,13 @@ ...@@ -250,13 +286,13 @@
// Convenience version of a common use case of `filter`: selecting only objects // Convenience version of a common use case of `filter`: selecting only objects
// containing specific `key:value` pairs. // containing specific `key:value` pairs.
_.where = function(obj, attrs) { _.where = function(obj, attrs) {
return _.filter(obj, _.matches(attrs)); return _.filter(obj, _.matcher(attrs));
}; };
// Convenience version of a common use case of `find`: getting the first object // Convenience version of a common use case of `find`: getting the first object
// containing specific `key:value` pairs. // containing specific `key:value` pairs.
_.findWhere = function(obj, attrs) { _.findWhere = function(obj, attrs) {
return _.find(obj, _.matches(attrs)); return _.find(obj, _.matcher(attrs));
}; };
// Return the maximum element (or element-based computation). // Return the maximum element (or element-based computation).
...@@ -264,7 +300,7 @@ ...@@ -264,7 +300,7 @@
var result = -Infinity, lastComputed = -Infinity, var result = -Infinity, lastComputed = -Infinity,
value, computed; value, computed;
if (iteratee == null && obj != null) { if (iteratee == null && obj != null) {
obj = obj.length === +obj.length ? obj : _.values(obj); obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) { for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i]; value = obj[i];
if (value > result) { if (value > result) {
...@@ -272,7 +308,7 @@ ...@@ -272,7 +308,7 @@
} }
} }
} else { } else {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
computed = iteratee(value, index, list); computed = iteratee(value, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) { if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
...@@ -289,7 +325,7 @@ ...@@ -289,7 +325,7 @@
var result = Infinity, lastComputed = Infinity, var result = Infinity, lastComputed = Infinity,
value, computed; value, computed;
if (iteratee == null && obj != null) { if (iteratee == null && obj != null) {
obj = obj.length === +obj.length ? obj : _.values(obj); obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) { for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i]; value = obj[i];
if (value < result) { if (value < result) {
...@@ -297,7 +333,7 @@ ...@@ -297,7 +333,7 @@
} }
} }
} else { } else {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
computed = iteratee(value, index, list); computed = iteratee(value, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) { if (computed < lastComputed || computed === Infinity && result === Infinity) {
...@@ -312,7 +348,7 @@ ...@@ -312,7 +348,7 @@
// Shuffle a collection, using the modern version of the // Shuffle a collection, using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
_.shuffle = function(obj) { _.shuffle = function(obj) {
var set = obj && obj.length === +obj.length ? obj : _.values(obj); var set = isArrayLike(obj) ? obj : _.values(obj);
var length = set.length; var length = set.length;
var shuffled = Array(length); var shuffled = Array(length);
for (var index = 0, rand; index < length; index++) { for (var index = 0, rand; index < length; index++) {
...@@ -328,7 +364,7 @@ ...@@ -328,7 +364,7 @@
// The internal `guard` argument allows it to work with `map`. // The internal `guard` argument allows it to work with `map`.
_.sample = function(obj, n, guard) { _.sample = function(obj, n, guard) {
if (n == null || guard) { if (n == null || guard) {
if (obj.length !== +obj.length) obj = _.values(obj); if (!isArrayLike(obj)) obj = _.values(obj);
return obj[_.random(obj.length - 1)]; return obj[_.random(obj.length - 1)];
} }
return _.shuffle(obj).slice(0, Math.max(0, n)); return _.shuffle(obj).slice(0, Math.max(0, n));
...@@ -336,7 +372,7 @@ ...@@ -336,7 +372,7 @@
// Sort the object's values by a criterion produced by an iteratee. // Sort the object's values by a criterion produced by an iteratee.
_.sortBy = function(obj, iteratee, context) { _.sortBy = function(obj, iteratee, context) {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
return _.pluck(_.map(obj, function(value, index, list) { return _.pluck(_.map(obj, function(value, index, list) {
return { return {
value: value, value: value,
...@@ -358,7 +394,7 @@ ...@@ -358,7 +394,7 @@
var group = function(behavior) { var group = function(behavior) {
return function(obj, iteratee, context) { return function(obj, iteratee, context) {
var result = {}; var result = {};
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index) { _.each(obj, function(value, index) {
var key = iteratee(value, index, obj); var key = iteratee(value, index, obj);
behavior(result, value, key); behavior(result, value, key);
...@@ -386,37 +422,24 @@ ...@@ -386,37 +422,24 @@
if (_.has(result, key)) result[key]++; else result[key] = 1; if (_.has(result, key)) result[key]++; else result[key] = 1;
}); });
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iteratee, context) {
iteratee = _.iteratee(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = array.length;
while (low < high) {
var mid = low + high >>> 1;
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
// Safely create a real, live array from anything iterable. // Safely create a real, live array from anything iterable.
_.toArray = function(obj) { _.toArray = function(obj) {
if (!obj) return []; if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj); if (_.isArray(obj)) return slice.call(obj);
if (obj.length === +obj.length) return _.map(obj, _.identity); if (isArrayLike(obj)) return _.map(obj, _.identity);
return _.values(obj); return _.values(obj);
}; };
// Return the number of elements in an object. // Return the number of elements in an object.
_.size = function(obj) { _.size = function(obj) {
if (obj == null) return 0; if (obj == null) return 0;
return obj.length === +obj.length ? obj.length : _.keys(obj).length; return isArrayLike(obj) ? obj.length : _.keys(obj).length;
}; };
// Split a collection into two arrays: one whose elements all satisfy the given // Split a collection into two arrays: one whose elements all satisfy the given
// predicate, and one whose elements all do not satisfy the predicate. // predicate, and one whose elements all do not satisfy the predicate.
_.partition = function(obj, predicate, context) { _.partition = function(obj, predicate, context) {
predicate = _.iteratee(predicate, context); predicate = cb(predicate, context);
var pass = [], fail = []; var pass = [], fail = [];
_.each(obj, function(value, key, obj) { _.each(obj, function(value, key, obj) {
(predicate(value, key, obj) ? pass : fail).push(value); (predicate(value, key, obj) ? pass : fail).push(value);
...@@ -433,30 +456,27 @@ ...@@ -433,30 +456,27 @@
_.first = _.head = _.take = function(array, n, guard) { _.first = _.head = _.take = function(array, n, guard) {
if (array == null) return void 0; if (array == null) return void 0;
if (n == null || guard) return array[0]; if (n == null || guard) return array[0];
if (n < 0) return []; return _.initial(array, array.length - n);
return slice.call(array, 0, n);
}; };
// Returns everything but the last entry of the array. Especially useful on // Returns everything but the last entry of the array. Especially useful on
// the arguments object. Passing **n** will return all the values in // the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with // the array, excluding the last N.
// `_.map`.
_.initial = function(array, n, guard) { _.initial = function(array, n, guard) {
return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
}; };
// Get the last element of an array. Passing **n** will return the last N // Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`. // values in the array.
_.last = function(array, n, guard) { _.last = function(array, n, guard) {
if (array == null) return void 0; if (array == null) return void 0;
if (n == null || guard) return array[array.length - 1]; if (n == null || guard) return array[array.length - 1];
return slice.call(array, Math.max(array.length - n, 0)); return _.rest(array, Math.max(0, array.length - n));
}; };
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
// Especially useful on the arguments object. Passing an **n** will return // Especially useful on the arguments object. Passing an **n** will return
// the rest N values in the array. The **guard** // the rest N values in the array.
// check allows it to work with `_.map`.
_.rest = _.tail = _.drop = function(array, n, guard) { _.rest = _.tail = _.drop = function(array, n, guard) {
return slice.call(array, n == null || guard ? 1 : n); return slice.call(array, n == null || guard ? 1 : n);
}; };
...@@ -467,18 +487,20 @@ ...@@ -467,18 +487,20 @@
}; };
// Internal implementation of a recursive `flatten` function. // Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, strict, output) { var flatten = function(input, shallow, strict, startIndex) {
if (shallow && _.every(input, _.isArray)) { var output = [], idx = 0;
return concat.apply(output, input); for (var i = startIndex || 0, length = getLength(input); i < length; i++) {
}
for (var i = 0, length = input.length; i < length; i++) {
var value = input[i]; var value = input[i];
if (!_.isArray(value) && !_.isArguments(value)) { if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
if (!strict) output.push(value); //flatten current level of array or arguments object
} else if (shallow) { if (!shallow) value = flatten(value, shallow, strict);
push.apply(output, value); var j = 0, len = value.length;
} else { output.length += len;
flatten(value, shallow, strict, output); while (j < len) {
output[idx++] = value[j++];
}
} else if (!strict) {
output[idx++] = value;
} }
} }
return output; return output;
...@@ -486,7 +508,7 @@ ...@@ -486,7 +508,7 @@
// Flatten out an array, either recursively (by default), or just one level. // Flatten out an array, either recursively (by default), or just one level.
_.flatten = function(array, shallow) { _.flatten = function(array, shallow) {
return flatten(array, shallow, false, []); return flatten(array, shallow, false);
}; };
// Return a version of the array that does not contain the specified value(s). // Return a version of the array that does not contain the specified value(s).
...@@ -498,27 +520,26 @@ ...@@ -498,27 +520,26 @@
// been sorted, you have the option of using a faster algorithm. // been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`. // Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iteratee, context) { _.uniq = _.unique = function(array, isSorted, iteratee, context) {
if (array == null) return [];
if (!_.isBoolean(isSorted)) { if (!_.isBoolean(isSorted)) {
context = iteratee; context = iteratee;
iteratee = isSorted; iteratee = isSorted;
isSorted = false; isSorted = false;
} }
if (iteratee != null) iteratee = _.iteratee(iteratee, context); if (iteratee != null) iteratee = cb(iteratee, context);
var result = []; var result = [];
var seen = []; var seen = [];
for (var i = 0, length = array.length; i < length; i++) { for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i]; var value = array[i],
computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted) { if (isSorted) {
if (!i || seen !== value) result.push(value); if (!i || seen !== computed) result.push(value);
seen = value; seen = computed;
} else if (iteratee) { } else if (iteratee) {
var computed = iteratee(value, i, array); if (!_.contains(seen, computed)) {
if (_.indexOf(seen, computed) < 0) {
seen.push(computed); seen.push(computed);
result.push(value); result.push(value);
} }
} else if (_.indexOf(result, value) < 0) { } else if (!_.contains(result, value)) {
result.push(value); result.push(value);
} }
} }
...@@ -528,16 +549,15 @@ ...@@ -528,16 +549,15 @@
// Produce an array that contains the union: each distinct element from all of // Produce an array that contains the union: each distinct element from all of
// the passed-in arrays. // the passed-in arrays.
_.union = function() { _.union = function() {
return _.uniq(flatten(arguments, true, true, [])); return _.uniq(flatten(arguments, true, true));
}; };
// Produce an array that contains every item shared between all the // Produce an array that contains every item shared between all the
// passed-in arrays. // passed-in arrays.
_.intersection = function(array) { _.intersection = function(array) {
if (array == null) return [];
var result = []; var result = [];
var argsLength = arguments.length; var argsLength = arguments.length;
for (var i = 0, length = array.length; i < length; i++) { for (var i = 0, length = getLength(array); i < length; i++) {
var item = array[i]; var item = array[i];
if (_.contains(result, item)) continue; if (_.contains(result, item)) continue;
for (var j = 1; j < argsLength; j++) { for (var j = 1; j < argsLength; j++) {
...@@ -551,7 +571,7 @@ ...@@ -551,7 +571,7 @@
// Take the difference between one array and a number of other arrays. // Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain. // Only the elements present in just the first array will remain.
_.difference = function(array) { _.difference = function(array) {
var rest = flatten(slice.call(arguments, 1), true, true, []); var rest = flatten(arguments, true, true, 1);
return _.filter(array, function(value){ return _.filter(array, function(value){
return !_.contains(rest, value); return !_.contains(rest, value);
}); });
...@@ -559,23 +579,28 @@ ...@@ -559,23 +579,28 @@
// Zip together multiple lists into a single array -- elements that share // Zip together multiple lists into a single array -- elements that share
// an index go together. // an index go together.
_.zip = function(array) { _.zip = function() {
if (array == null) return []; return _.unzip(arguments);
var length = _.max(arguments, 'length').length; };
var results = Array(length);
for (var i = 0; i < length; i++) { // Complement of _.zip. Unzip accepts an array of arrays and groups
results[i] = _.pluck(arguments, i); // each array's elements on shared indices
_.unzip = function(array) {
var length = array && _.max(array, getLength).length || 0;
var result = Array(length);
for (var index = 0; index < length; index++) {
result[index] = _.pluck(array, index);
} }
return results; return result;
}; };
// Converts lists into objects. Pass either a single array of `[key, value]` // Converts lists into objects. Pass either a single array of `[key, value]`
// pairs, or two parallel arrays of the same length -- one of keys, and one of // pairs, or two parallel arrays of the same length -- one of keys, and one of
// the corresponding values. // the corresponding values.
_.object = function(list, values) { _.object = function(list, values) {
if (list == null) return {};
var result = {}; var result = {};
for (var i = 0, length = list.length; i < length; i++) { for (var i = 0, length = getLength(list); i < length; i++) {
if (values) { if (values) {
result[list[i]] = values[i]; result[list[i]] = values[i];
} else { } else {
...@@ -585,40 +610,73 @@ ...@@ -585,40 +610,73 @@
return result; return result;
}; };
// Return the position of the first occurrence of an item in an array, // Generator function to create the findIndex and findLastIndex functions
// or -1 if the item is not included in the array. function createPredicateIndexFinder(dir) {
// If the array is large and already in sort order, pass `true` return function(array, predicate, context) {
// for **isSorted** to use binary search. predicate = cb(predicate, context);
_.indexOf = function(array, item, isSorted) { var length = getLength(array);
if (array == null) return -1; var index = dir > 0 ? 0 : length - 1;
var i = 0, length = array.length; for (; index >= 0 && index < length; index += dir) {
if (isSorted) { if (predicate(array[index], index, array)) return index;
if (typeof isSorted == 'number') {
i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
} else {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
} }
for (; i < length; i++) if (array[i] === item) return i;
return -1; return -1;
}; };
}
// Returns the first index on an array-like that passes a predicate test
_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iteratee, context) {
iteratee = cb(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
_.lastIndexOf = function(array, item, from) { // Generator function to create the indexOf and lastIndexOf functions
if (array == null) return -1; function createIndexFinder(dir, predicateFind, sortedIndex) {
var idx = array.length; return function(array, item, idx) {
if (typeof from == 'number') { var i = 0, length = getLength(array);
idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
} }
while (--idx >= 0) if (array[idx] === item) return idx;
return -1; return -1;
}; };
}
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
// Generate an integer Array containing an arithmetic progression. A port of // Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See // the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range). // [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) { _.range = function(start, stop, step) {
if (arguments.length <= 1) { if (stop == null) {
stop = start || 0; stop = start || 0;
start = 0; start = 0;
} }
...@@ -637,25 +695,25 @@ ...@@ -637,25 +695,25 @@
// Function (ahem) Functions // Function (ahem) Functions
// ------------------ // ------------------
// Reusable constructor function for prototype setting. // Determines whether to execute a function as a constructor
var Ctor = function(){}; // or a normal function with the provided arguments
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) return result;
return self;
};
// Create a function bound to a given object (assigning `this`, and arguments, // Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available. // available.
_.bind = function(func, context) { _.bind = function(func, context) {
var args, bound;
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
bound = function() { var bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
Ctor.prototype = func.prototype;
var self = new Ctor;
Ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (_.isObject(result)) return result;
return self;
}; };
return bound; return bound;
}; };
...@@ -665,15 +723,16 @@ ...@@ -665,15 +723,16 @@
// as a placeholder, allowing any combination of arguments to be pre-filled. // as a placeholder, allowing any combination of arguments to be pre-filled.
_.partial = function(func) { _.partial = function(func) {
var boundArgs = slice.call(arguments, 1); var boundArgs = slice.call(arguments, 1);
return function() { var bound = function() {
var position = 0; var position = 0, length = boundArgs.length;
var args = boundArgs.slice(); var args = Array(length);
for (var i = 0, length = args.length; i < length; i++) { for (var i = 0; i < length; i++) {
if (args[i] === _) args[i] = arguments[position++]; args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
} }
while (position < arguments.length) args.push(arguments[position++]); while (position < arguments.length) args.push(arguments[position++]);
return func.apply(this, args); return executeBound(func, bound, this, this, args);
}; };
return bound;
}; };
// Bind a number of an object's methods to that object. Remaining arguments // Bind a number of an object's methods to that object. Remaining arguments
...@@ -693,7 +752,7 @@ ...@@ -693,7 +752,7 @@
_.memoize = function(func, hasher) { _.memoize = function(func, hasher) {
var memoize = function(key) { var memoize = function(key) {
var cache = memoize.cache; var cache = memoize.cache;
var address = hasher ? hasher.apply(this, arguments) : key; var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
return cache[address]; return cache[address];
}; };
...@@ -712,9 +771,7 @@ ...@@ -712,9 +771,7 @@
// Defers a function, scheduling it to run after the current call stack has // Defers a function, scheduling it to run after the current call stack has
// cleared. // cleared.
_.defer = function(func) { _.defer = _.partial(_.delay, _, 1);
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once // Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run // during a given window of time. Normally, the throttled function will run
...@@ -739,8 +796,10 @@ ...@@ -739,8 +796,10 @@
context = this; context = this;
args = arguments; args = arguments;
if (remaining <= 0 || remaining > wait) { if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = null; timeout = null;
}
previous = now; previous = now;
result = func.apply(context, args); result = func.apply(context, args);
if (!timeout) context = args = null; if (!timeout) context = args = null;
...@@ -761,7 +820,7 @@ ...@@ -761,7 +820,7 @@
var later = function() { var later = function() {
var last = _.now() - timestamp; var last = _.now() - timestamp;
if (last < wait && last > 0) { if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last); timeout = setTimeout(later, wait - last);
} else { } else {
timeout = null; timeout = null;
...@@ -814,7 +873,7 @@ ...@@ -814,7 +873,7 @@
}; };
}; };
// Returns a function that will only be executed after being called N times. // Returns a function that will only be executed on and after the Nth call.
_.after = function(times, func) { _.after = function(times, func) {
return function() { return function() {
if (--times < 1) { if (--times < 1) {
...@@ -823,15 +882,14 @@ ...@@ -823,15 +882,14 @@
}; };
}; };
// Returns a function that will only be executed before being called N times. // Returns a function that will only be executed up to (but not including) the Nth call.
_.before = function(times, func) { _.before = function(times, func) {
var memo; var memo;
return function() { return function() {
if (--times > 0) { if (--times > 0) {
memo = func.apply(this, arguments); memo = func.apply(this, arguments);
} else {
func = null;
} }
if (times <= 1) func = null;
return memo; return memo;
}; };
}; };
...@@ -843,13 +901,47 @@ ...@@ -843,13 +901,47 @@
// Object Functions // Object Functions
// ---------------- // ----------------
// Retrieve the names of an object's properties. // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
var prop = 'constructor';
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}
// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys` // Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = function(obj) { _.keys = function(obj) {
if (!_.isObject(obj)) return []; if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj); if (nativeKeys) return nativeKeys(obj);
var keys = []; var keys = [];
for (var key in obj) if (_.has(obj, key)) keys.push(key); for (var key in obj) if (_.has(obj, key)) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
// Retrieve all the property names of an object.
_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys; return keys;
}; };
...@@ -864,6 +956,21 @@ ...@@ -864,6 +956,21 @@
return values; return values;
}; };
// Returns the results of applying the iteratee to each element of the object
// In contrast to _.map it returns an object
_.mapObject = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = _.keys(obj),
length = keys.length,
results = {},
currentKey;
for (var index = 0; index < length; index++) {
currentKey = keys[index];
results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
// Convert an object into a list of `[key, value]` pairs. // Convert an object into a list of `[key, value]` pairs.
_.pairs = function(obj) { _.pairs = function(obj) {
var keys = _.keys(obj); var keys = _.keys(obj);
...@@ -896,37 +1003,38 @@ ...@@ -896,37 +1003,38 @@
}; };
// Extend a given object with all the properties in passed-in object(s). // Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) { _.extend = createAssigner(_.allKeys);
if (!_.isObject(obj)) return obj;
var source, prop; // Assigns a given object with all the own properties in the passed-in object(s)
for (var i = 1, length = arguments.length; i < length; i++) { // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
source = arguments[i]; _.extendOwn = _.assign = createAssigner(_.keys);
for (prop in source) {
if (hasOwnProperty.call(source, prop)) { // Returns the first key on an object that passes a predicate test
obj[prop] = source[prop]; _.findKey = function(obj, predicate, context) {
} predicate = cb(predicate, context);
} var keys = _.keys(obj), key;
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (predicate(obj[key], key, obj)) return key;
} }
return obj;
}; };
// Return a copy of the object only containing the whitelisted properties. // Return a copy of the object only containing the whitelisted properties.
_.pick = function(obj, iteratee, context) { _.pick = function(object, oiteratee, context) {
var result = {}, key; var result = {}, obj = object, iteratee, keys;
if (obj == null) return result; if (obj == null) return result;
if (_.isFunction(iteratee)) { if (_.isFunction(oiteratee)) {
iteratee = createCallback(iteratee, context); keys = _.allKeys(obj);
for (key in obj) { iteratee = optimizeCb(oiteratee, context);
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
}
} else { } else {
var keys = concat.apply([], slice.call(arguments, 1)); keys = flatten(arguments, false, false, 1);
obj = new Object(obj); iteratee = function(value, key, obj) { return key in obj; };
for (var i = 0, length = keys.length; i < length; i++) { obj = Object(obj);
key = keys[i];
if (key in obj) result[key] = obj[key];
} }
for (var i = 0, length = keys.length; i < length; i++) {
var key = keys[i];
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
} }
return result; return result;
}; };
...@@ -936,7 +1044,7 @@ ...@@ -936,7 +1044,7 @@
if (_.isFunction(iteratee)) { if (_.isFunction(iteratee)) {
iteratee = _.negate(iteratee); iteratee = _.negate(iteratee);
} else { } else {
var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); var keys = _.map(flatten(arguments, false, false, 1), String);
iteratee = function(value, key) { iteratee = function(value, key) {
return !_.contains(keys, key); return !_.contains(keys, key);
}; };
...@@ -945,15 +1053,15 @@ ...@@ -945,15 +1053,15 @@
}; };
// Fill in a given object with default properties. // Fill in a given object with default properties.
_.defaults = function(obj) { _.defaults = createAssigner(_.allKeys, true);
if (!_.isObject(obj)) return obj;
for (var i = 1, length = arguments.length; i < length; i++) { // Creates an object that inherits from the given prototype object.
var source = arguments[i]; // If additional properties are provided then they will be added to the
for (var prop in source) { // created object.
if (obj[prop] === void 0) obj[prop] = source[prop]; _.create = function(prototype, props) {
} var result = baseCreate(prototype);
} if (props) _.extendOwn(result, props);
return obj; return result;
}; };
// Create a (shallow-cloned) duplicate of an object. // Create a (shallow-cloned) duplicate of an object.
...@@ -970,6 +1078,19 @@ ...@@ -970,6 +1078,19 @@
return obj; return obj;
}; };
// Returns whether an object has a given set of `key:value` pairs.
_.isMatch = function(object, attrs) {
var keys = _.keys(attrs), length = keys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
// Internal recursive comparison function for `isEqual`. // Internal recursive comparison function for `isEqual`.
var eq = function(a, b, aStack, bStack) { var eq = function(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical. // Identical objects are equal. `0 === -0`, but they aren't identical.
...@@ -1004,74 +1125,76 @@ ...@@ -1004,74 +1125,76 @@
// of `NaN` are not equivalent. // of `NaN` are not equivalent.
return +a === +b; return +a === +b;
} }
var areArrays = className === '[object Array]';
if (!areArrays) {
if (typeof a != 'object' || typeof b != 'object') return false; if (typeof a != 'object' || typeof b != 'object') return false;
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
_.isFunction(bCtor) && bCtor instanceof bCtor)
&& ('constructor' in a && 'constructor' in b)) {
return false;
}
}
// Assume equality for cyclic structures. The algorithm for detecting cyclic // Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length; var length = aStack.length;
while (length--) { while (length--) {
// Linear search. Performance is inversely proportional to the number of // Linear search. Performance is inversely proportional to the number of
// unique nested structures. // unique nested structures.
if (aStack[length] === a) return bStack[length] === b; if (aStack[length] === a) return bStack[length] === b;
} }
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (
aCtor !== bCtor &&
// Handle Object.create(x) cases
'constructor' in a && 'constructor' in b &&
!(_.isFunction(aCtor) && aCtor instanceof aCtor &&
_.isFunction(bCtor) && bCtor instanceof bCtor)
) {
return false;
}
// Add the first object to the stack of traversed objects. // Add the first object to the stack of traversed objects.
aStack.push(a); aStack.push(a);
bStack.push(b); bStack.push(b);
var size, result;
// Recursively compare objects and arrays. // Recursively compare objects and arrays.
if (className === '[object Array]') { if (areArrays) {
// Compare array lengths to determine if a deep comparison is necessary. // Compare array lengths to determine if a deep comparison is necessary.
size = a.length; length = a.length;
result = size === b.length; if (length !== b.length) return false;
if (result) {
// Deep compare the contents, ignoring non-numeric properties. // Deep compare the contents, ignoring non-numeric properties.
while (size--) { while (length--) {
if (!(result = eq(a[size], b[size], aStack, bStack))) break; if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} }
} else { } else {
// Deep compare objects. // Deep compare objects.
var keys = _.keys(a), key; var keys = _.keys(a), key;
size = keys.length; length = keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality. // Ensure that both objects contain the same number of properties before comparing deep equality.
result = _.keys(b).length === size; if (_.keys(b).length !== length) return false;
if (result) { while (length--) {
while (size--) {
// Deep compare each member // Deep compare each member
key = keys[size]; key = keys[length];
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
} }
} }
// Remove the first object from the stack of traversed objects. // Remove the first object from the stack of traversed objects.
aStack.pop(); aStack.pop();
bStack.pop(); bStack.pop();
return result; return true;
}; };
// Perform a deep comparison to check if two objects are equal. // Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) { _.isEqual = function(a, b) {
return eq(a, b, [], []); return eq(a, b);
}; };
// Is a given array, string, or object empty? // Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties. // An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) { _.isEmpty = function(obj) {
if (obj == null) return true; if (obj == null) return true;
if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false; return _.keys(obj).length === 0;
return true;
}; };
// Is a given value a DOM element? // Is a given value a DOM element?
...@@ -1091,14 +1214,14 @@ ...@@ -1091,14 +1214,14 @@
return type === 'function' || type === 'object' && !!obj; return type === 'function' || type === 'object' && !!obj;
}; };
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
_['is' + name] = function(obj) { _['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']'; return toString.call(obj) === '[object ' + name + ']';
}; };
}); });
// Define a fallback version of the method in browsers (ahem, IE), where // Define a fallback version of the method in browsers (ahem, IE < 9), where
// there isn't any inspectable "Arguments" type. // there isn't any inspectable "Arguments" type.
if (!_.isArguments(arguments)) { if (!_.isArguments(arguments)) {
_.isArguments = function(obj) { _.isArguments = function(obj) {
...@@ -1106,8 +1229,9 @@ ...@@ -1106,8 +1229,9 @@
}; };
} }
// Optimize `isFunction` if appropriate. Work around an IE 11 bug. // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
if (typeof /./ !== 'function') { // IE 11 (#1621), and in Safari 8 (#1929).
if (typeof /./ != 'function' && typeof Int8Array != 'object') {
_.isFunction = function(obj) { _.isFunction = function(obj) {
return typeof obj == 'function' || false; return typeof obj == 'function' || false;
}; };
...@@ -1159,6 +1283,7 @@ ...@@ -1159,6 +1283,7 @@
return value; return value;
}; };
// Predicate-generating functions. Often useful outside of Underscore.
_.constant = function(value) { _.constant = function(value) {
return function() { return function() {
return value; return value;
...@@ -1167,30 +1292,28 @@ ...@@ -1167,30 +1292,28 @@
_.noop = function(){}; _.noop = function(){};
_.property = function(key) { _.property = property;
return function(obj) {
// Generates a function for a given object that returns a given property.
_.propertyOf = function(obj) {
return obj == null ? function(){} : function(key) {
return obj[key]; return obj[key];
}; };
}; };
// Returns a predicate for checking whether an object has a given set of `key:value` pairs. // Returns a predicate for checking whether an object has a given set of
_.matches = function(attrs) { // `key:value` pairs.
var pairs = _.pairs(attrs), length = pairs.length; _.matcher = _.matches = function(attrs) {
attrs = _.extendOwn({}, attrs);
return function(obj) { return function(obj) {
if (obj == null) return !length; return _.isMatch(obj, attrs);
obj = new Object(obj);
for (var i = 0; i < length; i++) {
var pair = pairs[i], key = pair[0];
if (pair[1] !== obj[key] || !(key in obj)) return false;
}
return true;
}; };
}; };
// Run a function **n** times. // Run a function **n** times.
_.times = function(n, iteratee, context) { _.times = function(n, iteratee, context) {
var accum = Array(Math.max(0, n)); var accum = Array(Math.max(0, n));
iteratee = createCallback(iteratee, context, 1); iteratee = optimizeCb(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i); for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum; return accum;
}; };
...@@ -1239,10 +1362,12 @@ ...@@ -1239,10 +1362,12 @@
// If the value of the named `property` is a function then invoke it with the // If the value of the named `property` is a function then invoke it with the
// `object` as context; otherwise, return it. // `object` as context; otherwise, return it.
_.result = function(object, property) { _.result = function(object, property, fallback) {
if (object == null) return void 0; var value = object == null ? void 0 : object[property];
var value = object[property]; if (value === void 0) {
return _.isFunction(value) ? object[property]() : value; value = fallback;
}
return _.isFunction(value) ? value.call(object) : value;
}; };
// Generate a unique integer id (unique within the entire client session). // Generate a unique integer id (unique within the entire client session).
...@@ -1357,8 +1482,8 @@ ...@@ -1357,8 +1482,8 @@
// underscore functions. Wrapped objects may be chained. // underscore functions. Wrapped objects may be chained.
// Helper function to continue chaining intermediate results. // Helper function to continue chaining intermediate results.
var result = function(obj) { var result = function(instance, obj) {
return this._chain ? _(obj).chain() : obj; return instance._chain ? _(obj).chain() : obj;
}; };
// Add your own custom functions to the Underscore object. // Add your own custom functions to the Underscore object.
...@@ -1368,7 +1493,7 @@ ...@@ -1368,7 +1493,7 @@
_.prototype[name] = function() { _.prototype[name] = function() {
var args = [this._wrapped]; var args = [this._wrapped];
push.apply(args, arguments); push.apply(args, arguments);
return result.call(this, func.apply(_, args)); return result(this, func.apply(_, args));
}; };
}); });
}; };
...@@ -1383,7 +1508,7 @@ ...@@ -1383,7 +1508,7 @@
var obj = this._wrapped; var obj = this._wrapped;
method.apply(obj, arguments); method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
return result.call(this, obj); return result(this, obj);
}; };
}); });
...@@ -1391,7 +1516,7 @@ ...@@ -1391,7 +1516,7 @@
_.each(['concat', 'join', 'slice'], function(name) { _.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name]; var method = ArrayProto[name];
_.prototype[name] = function() { _.prototype[name] = function() {
return result.call(this, method.apply(this._wrapped, arguments)); return result(this, method.apply(this._wrapped, arguments));
}; };
}); });
...@@ -1400,6 +1525,14 @@ ...@@ -1400,6 +1525,14 @@
return this._wrapped; return this._wrapped;
}; };
// Provide unwrapping proxy for some methods used in engine operations
// such as arithmetic and JSON stringification.
_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
_.prototype.toString = function() {
return '' + this._wrapped;
};
// AMD registration happens at the end for compatibility with AMD loaders // AMD registration happens at the end for compatibility with AMD loaders
// that may not enforce next-turn semantics on modules. Even though general // that may not enforce next-turn semantics on modules. Even though general
// practice for AMD registration is to be anonymous, underscore registers // practice for AMD registration is to be anonymous, underscore registers
......
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"backbone": "^1.1.2", "backbone": "^1.3.3",
"backbone.localstorage": "^1.1.6", "backbone.localstorage": "^1.1.6",
"backbone.marionette": "^2.4.1", "backbone.marionette": "^3.0.0",
"backbone.radio": "^1.0.1", "backbone.radio": "^2.0.0",
"jquery": "^1.11.2", "jquery": "^1.11.2",
"todomvc-app-css": "^1.0.1", "todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1", "todomvc-common": "^1.0.1",
......
...@@ -11,16 +11,16 @@ The [Backbone.Marionette website](http://marionettejs.com) is a great resource f ...@@ -11,16 +11,16 @@ The [Backbone.Marionette website](http://marionettejs.com) is a great resource f
Here are some links you may find helpful: Here are some links you may find helpful:
* [API Reference](https://github.com/marionettejs/backbone.marionette/tree/master/docs) * [API Reference](http://marionettejs.com/docs/current/)
* [Applications built with Backbone.Marionette](https://github.com/marionettejs/backbone.marionette/wiki/Projects-and-websites-using-marionette) * [Additional Marionette Resources](http://marionettejs.com/additional-resources/)
* [Introduction to Composite JavaScript Apps](https://github.com/marionettejs/backbone.marionette/wiki/Introduction-to-composite-javascript-apps)
* [FAQ](https://github.com/marionettejs/backbone.marionette/wiki#frequently-asked-questions)
* [Backbone.Marionette on GitHub](https://github.com/marionettejs/backbone.marionette) * [Backbone.Marionette on GitHub](https://github.com/marionettejs/backbone.marionette)
Articles and guides from the community: Articles and guides from the community:
* [A Thorough Introduction to Backbone.Marionette](http://coding.smashingmagazine.com/2013/02/11/introduction-backbone-marionette) * [A Thorough Introduction to Backbone.Marionette](http://coding.smashingmagazine.com/2013/02/11/introduction-backbone-marionette)
* [Backbone Marionette: Better Backbone Apps](http://www.joezimjs.com/javascript/backbone-marionette-better-backbone-apps) * [Backbone Marionette: Better Backbone Apps](http://www.joezimjs.com/javascript/backbone-marionette-better-backbone-apps)
* [Marionette Guides](https://www.gitbook.com/book/marionette/marionette-guides/details)
* [Marionette.js v3. Getting started with new version](http://blog.marionettejs.com/2016/08/23/marionette-v3/index.html)
Get help from other Backbone.Marionette users: Get help from other Backbone.Marionette users:
...@@ -32,4 +32,4 @@ _If you have other helpful links to share, or find any of the links above no lon ...@@ -32,4 +32,4 @@ _If you have other helpful links to share, or find any of the links above no lon
## Implementation ## Implementation
This implementation of the application uses Marionette's module system. Variations using RequireJS and a more classic approach to JavaScript modules [are also available here](https://github.com/marionettejs/backbone.marionette/wiki/Projects-and-websites-using-marionette). * [Simple setups with different build tools](https://github.com/marionettejs/marionette-integrations)
\ No newline at end of file
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
<a href="examples/vue/" data-source="http://vuejs.org" data-content="Vue.js provides the benefits of MVVM data binding and a composable component system with an extremely simple and flexible API.">Vue.js</a> <a href="examples/vue/" data-source="http://vuejs.org" data-content="Vue.js provides the benefits of MVVM data binding and a composable component system with an extremely simple and flexible API.">Vue.js</a>
</li> </li>
<li class="routing"> <li class="routing">
<a href="examples/backbone_marionette/" data-source="http://marionettejs.com" data-content="Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications.">MarionetteJS</a> <a href="examples/backbone_marionette/" data-source="http://marionettejs.com" data-content="Backbone.Marionette is a composite application library for Backbone.js that aims to simplify the construction of large scale JavaScript applications.">Marionette.js</a>
</li> </li>
<li class="routing"> <li class="routing">
<a href="examples/troopjs_require/" data-source="https://github.com/troopjs/" data-content="TroopJS attempts to package popular front-end technologies and bind them with minimal effort for the developer. It includes jQuery for DOM manipulation, When.js for promises, RequireJS for modularity and Has.js for feature detection. On top, it includes Pub/Sub support, templating, weaving (widgets to DOM) and auto-wiring.">TroopJS + RequireJS</a> <a href="examples/troopjs_require/" data-source="https://github.com/troopjs/" data-content="TroopJS attempts to package popular front-end technologies and bind them with minimal effort for the developer. It includes jQuery for DOM manipulation, When.js for promises, RequireJS for modularity and Has.js for feature detection. On top, it includes Pub/Sub support, templating, weaving (widgets to DOM) and auto-wiring.">TroopJS + RequireJS</a>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment