Commit ef62053b authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #1136 from samccone/sjs/backbone.marionette

update marionette example to latest UI
parents c898a78d bf69d6e6
node_modules/backbone.marionette
!node_modules/backbone.marionette/lib/backbone.marionette.js
node_modules/todomvc-app-css
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common
!node_modules/todomvc-common/base.js
!node_modules/todomvc-common/base.css
node_modules/jquery
!node_modules/jquery/dist/jquery.js
node_modules/underscore
!node_modules/underscore/underscore.js
node_modules/backbone
!node_modules/backbone/backbone.js
node_modules/backbone.localstorage
!node_modules/backbone.localstorage/backbone.localStorage.js
{
"name": "todomvc-backbone-marionette",
"version": "0.0.0",
"dependencies": {
"jquery": "~1.10.2",
"todomvc-common": "~0.3.0",
"underscore": "~1.4.4",
"backbone.localStorage": "~1.1.6",
"backbone.marionette": "~2.1.0"
}
}
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
#footer { #footer {
display: none; display: none;
} }
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Marionette • TodoMVC</title> <title>Marionette • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/app.css">
</head> </head>
<body> <body>
...@@ -19,7 +20,7 @@ ...@@ -19,7 +20,7 @@
and <a href="http://github.com/derickbailey">Derick Bailey</a> and <a href="http://github.com/derickbailey">Derick Bailey</a>
using <a href="http://marionettejs.com">Backbone.Marionette</a> using <a href="http://marionettejs.com">Backbone.Marionette</a>
</p> </p>
<p>Further variations on the Backbone.Marionette app are also <a href="https://github.com/marionettejs/backbone.marionette/wiki/Projects-and-websites-using-marionette">available</a>.</p> <p>See who else is using marionette at <a href="http://marionettejs.com/">marionettejs.com</a>.</p>
</footer> </footer>
<script type="text/html" id="template-footer"> <script type="text/html" id="template-footer">
<span id="todo-count"> <span id="todo-count">
...@@ -36,9 +37,7 @@ ...@@ -36,9 +37,7 @@
<a href="#/completed">Completed</a> <a href="#/completed">Completed</a>
</li> </li>
</ul> </ul>
<button id="clear-completed" <% if (!completedCount) { %>class="hidden"<% } %>> <button id="clear-completed" <% if (!completedCount) { %>class="hidden"<% } %>>Clear completed (<%= completedCount %>)</button>
Clear completed (<%= completedCount %>)
</button>
</script> </script>
<script type="text/html" id="template-header"> <script type="text/html" id="template-header">
<h1>todos</h1> <h1>todos</h1>
...@@ -58,12 +57,12 @@ ...@@ -58,12 +57,12 @@
<ul id="todo-list"></ul> <ul id="todo-list"></ul>
</script> </script>
<!-- vendor libraries --> <!-- vendor libraries -->
<script src="bower_components/todomvc-common/base.js"></script> <script src="node_modules/todomvc-common/base.js"></script>
<script src="bower_components/jquery/jquery.js"></script> <script src="node_modules/jquery/dist/jquery.js"></script>
<script src="bower_components/underscore/underscore.js"></script> <script src="node_modules/underscore/underscore.js"></script>
<script src="bower_components/backbone/backbone.js"></script> <script src="node_modules/backbone/backbone.js"></script>
<script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script> <script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<script src="bower_components/backbone.marionette/lib/backbone.marionette.js"></script> <script src="node_modules/backbone.marionette/lib/backbone.marionette.js"></script>
<!-- application --> <!-- application -->
<script src="js/TodoMVC.js"></script> <script src="js/TodoMVC.js"></script>
<script src="js/TodoMVC.Todos.js"></script> <script src="js/TodoMVC.Todos.js"></script>
......
/** /**
* Backbone localStorage Adapter * Backbone localStorage Adapter
* Version 1.1.6 * Version 1.1.16
* *
* https://github.com/jeromegn/Backbone.localStorage * https://github.com/jeromegn/Backbone.localStorage
*/ */
(function (root, factory) { (function (root, factory) {
if (typeof exports === 'object' && root.require) { if (typeof exports === 'object' && typeof require === 'function') {
module.exports = factory(require("underscore"), require("backbone")); module.exports = factory(require("backbone"));
} else if (typeof define === "function" && define.amd) { } else if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module. // AMD. Register as an anonymous module.
define(["underscore","backbone"], function(_, Backbone) { define(["backbone"], function(Backbone) {
// Use global variables if the locals are undefined. // Use global variables if the locals are undefined.
return factory(_ || root._, Backbone || root.Backbone); return factory(Backbone || root.Backbone);
}); });
} else { } else {
// RequireJS isn't being used. Assume underscore and backbone are loaded in <script> tags factory(Backbone);
factory(_, Backbone);
} }
}(this, function(_, Backbone) { }(this, function(Backbone) {
// A simple module to replace `Backbone.sync` with *localStorage*-based // A simple module to replace `Backbone.sync` with *localStorage*-based
// persistence. Models are given GUIDS, and saved into a JSON object. Simple // persistence. Models are given GUIDS, and saved into a JSON object. Simple
// as that. // as that.
// Hold reference to Underscore.js and Backbone.js in the closure in order
// to make things work even if they are removed from the global namespace
// Generate four random hex digits. // Generate four random hex digits.
function S4() { function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1); return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
...@@ -35,19 +31,49 @@ function guid() { ...@@ -35,19 +31,49 @@ function guid() {
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}; };
function isObject(item) {
return item === Object(item);
}
function contains(array, item) {
var i = array.length;
while (i--) if (array[i] === item) return true;
return false;
}
function extend(obj, props) {
for (var key in props) obj[key] = props[key]
return obj;
}
function result(object, property) {
if (object == null) return void 0;
var value = object[property];
return (typeof value === 'function') ? object[property]() : value;
}
// Our Store is represented by a single JS object in *localStorage*. Create it // Our Store is represented by a single JS object in *localStorage*. Create it
// with a meaningful name, like the name you'd give a table. // with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead // window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) { Backbone.LocalStorage = window.Store = function(name, serializer) {
if( !this.localStorage ) { if( !this.localStorage ) {
throw "Backbone.localStorage: Environment does not support localStorage." throw "Backbone.localStorage: Environment does not support localStorage."
} }
this.name = name; this.name = name;
this.serializer = serializer || {
serialize: function(item) {
return isObject(item) ? JSON.stringify(item) : item;
},
// fix for "illegal access" error on Android when JSON.parse is passed null
deserialize: function (data) {
return data && JSON.parse(data);
}
};
var store = this.localStorage().getItem(this.name); var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || []; this.records = (store && store.split(",")) || [];
}; };
_.extend(Backbone.LocalStorage.prototype, { extend(Backbone.LocalStorage.prototype, {
// Save the current state of the **Store** to *localStorage*. // Save the current state of the **Store** to *localStorage*.
save: function() { save: function() {
...@@ -57,11 +83,11 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -57,11 +83,11 @@ _.extend(Backbone.LocalStorage.prototype, {
// Add a model, giving it a (hopefully)-unique GUID, if it doesn't already // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
// have an id of it's own. // have an id of it's own.
create: function(model) { create: function(model) {
if (!model.id) { if (!model.id && model.id !== 0) {
model.id = guid(); model.id = guid();
model.set(model.idAttribute, model.id); model.set(model.idAttribute, model.id);
} }
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
this.records.push(model.id.toString()); this.records.push(model.id.toString());
this.save(); this.save();
return this.find(model); return this.find(model);
...@@ -69,36 +95,40 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -69,36 +95,40 @@ _.extend(Backbone.LocalStorage.prototype, {
// Update a model by replacing its copy in `this.data`. // Update a model by replacing its copy in `this.data`.
update: function(model) { update: function(model) {
this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model)); this.localStorage().setItem(this._itemName(model.id), this.serializer.serialize(model));
if (!_.include(this.records, model.id.toString())) var modelId = model.id.toString();
this.records.push(model.id.toString()); this.save(); if (!contains(this.records, modelId)) {
this.records.push(modelId);
this.save();
}
return this.find(model); return this.find(model);
}, },
// Retrieve a model from `this.data` by id. // Retrieve a model from `this.data` by id.
find: function(model) { find: function(model) {
return this.jsonData(this.localStorage().getItem(this.name+"-"+model.id)); return this.serializer.deserialize(this.localStorage().getItem(this._itemName(model.id)));
}, },
// Return the array of all models currently in storage. // Return the array of all models currently in storage.
findAll: function() { findAll: function() {
// Lodash removed _#chain in v1.0.0-rc.1 var result = [];
return (_.chain || _)(this.records) for (var i = 0, id, data; i < this.records.length; i++) {
.map(function(id){ id = this.records[i];
return this.jsonData(this.localStorage().getItem(this.name+"-"+id)); data = this.serializer.deserialize(this.localStorage().getItem(this._itemName(id)));
}, this) if (data != null) result.push(data);
.compact() }
.value(); return result;
}, },
// Delete a model from `this.data`, returning it. // Delete a model from `this.data`, returning it.
destroy: function(model) { destroy: function(model) {
if (model.isNew()) this.localStorage().removeItem(this._itemName(model.id));
return false var modelId = model.id.toString();
this.localStorage().removeItem(this.name+"-"+model.id); for (var i = 0, id; i < this.records.length; i++) {
this.records = _.reject(this.records, function(id){ if (this.records[i] === modelId) {
return id === model.id.toString(); this.records.splice(i, 1);
}); }
}
this.save(); this.save();
return model; return model;
}, },
...@@ -107,11 +137,6 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -107,11 +137,6 @@ _.extend(Backbone.LocalStorage.prototype, {
return localStorage; return localStorage;
}, },
// fix for "illegal access" error on Android when JSON.parse is passed null
jsonData: function (data) {
return data && JSON.parse(data);
},
// Clear localStorage for specific collection. // Clear localStorage for specific collection.
_clear: function() { _clear: function() {
var local = this.localStorage(), var local = this.localStorage(),
...@@ -120,11 +145,12 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -120,11 +145,12 @@ _.extend(Backbone.LocalStorage.prototype, {
// Remove id-tracking item (e.g., "foo"). // Remove id-tracking item (e.g., "foo").
local.removeItem(this.name); local.removeItem(this.name);
// Lodash removed _#chain in v1.0.0-rc.1
// Match all data items (e.g., "foo-ID") and remove. // Match all data items (e.g., "foo-ID") and remove.
(_.chain || _)(local).keys() for (var k in local) {
.filter(function (k) { return itemRe.test(k); }) if (itemRe.test(k)) {
.each(function (k) { local.removeItem(k); }); local.removeItem(k);
}
}
this.records.length = 0; this.records.length = 0;
}, },
...@@ -132,6 +158,10 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -132,6 +158,10 @@ _.extend(Backbone.LocalStorage.prototype, {
// Size of localStorage. // Size of localStorage.
_storageSize: function() { _storageSize: function() {
return this.localStorage().length; return this.localStorage().length;
},
_itemName: function(id) {
return this.name+"-"+id;
} }
}); });
...@@ -140,9 +170,13 @@ _.extend(Backbone.LocalStorage.prototype, { ...@@ -140,9 +170,13 @@ _.extend(Backbone.LocalStorage.prototype, {
// *localStorage* property, which should be an instance of `Store`. // *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead // window.Store.sync and Backbone.localSync is deprecated, use Backbone.LocalStorage.sync instead
Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) { Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) {
var store = model.localStorage || model.collection.localStorage; var store = result(model, 'localStorage') || result(model.collection, 'localStorage');
var resp, errorMessage, syncDfd = Backbone.$.Deferred && Backbone.$.Deferred(); //If $ is having Deferred - use it. var resp, errorMessage;
//If $ is having Deferred - use it.
var syncDfd = Backbone.$ ?
(Backbone.$.Deferred && Backbone.$.Deferred()) :
(Backbone.Deferred && Backbone.Deferred());
try { try {
...@@ -204,8 +238,10 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m ...@@ -204,8 +238,10 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m
Backbone.ajaxSync = Backbone.sync; Backbone.ajaxSync = Backbone.sync;
Backbone.getSyncMethod = function(model) { Backbone.getSyncMethod = function(model, options) {
if(model.localStorage || (model.collection && model.collection.localStorage)) { var forceAjaxSync = options && options.ajaxSync;
if(!forceAjaxSync && (result(model, 'localStorage') || result(model.collection, 'localStorage'))) {
return Backbone.localSync; return Backbone.localSync;
} }
...@@ -215,7 +251,7 @@ Backbone.getSyncMethod = function(model) { ...@@ -215,7 +251,7 @@ Backbone.getSyncMethod = function(model) {
// Override 'Backbone.sync' to default to localSync, // Override 'Backbone.sync' to default to localSync,
// the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync'
Backbone.sync = function(method, model, options) { Backbone.sync = function(method, model, options) {
return Backbone.getSyncMethod(model).apply(this, [method, model, options]); return Backbone.getSyncMethod(model, options).apply(this, [method, model, options]);
}; };
return Backbone.LocalStorage; return Backbone.LocalStorage;
......
// MarionetteJS (Backbone.Marionette) // MarionetteJS (Backbone.Marionette)
// ---------------------------------- // ----------------------------------
// v2.1.0 // v2.3.2
// //
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. // Copyright (c)2015 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license // Distributed under MIT license
// //
// http://marionettejs.com // http://marionettejs.com
...@@ -19,24 +19,26 @@ ...@@ -19,24 +19,26 @@
(function(root, factory) { (function(root, factory) {
/* istanbul ignore next */
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(['backbone', 'underscore'], function(Backbone, _) { define(['backbone', 'underscore'], function(Backbone, _) {
return (root.Marionette = factory(root, Backbone, _)); return (root.Marionette = root.Mn = factory(root, Backbone, _));
}); });
} else if (typeof exports !== 'undefined') { } else if (typeof exports !== 'undefined') {
var Backbone = require('backbone'); var Backbone = require('backbone');
var _ = require('underscore'); var _ = require('underscore');
module.exports = factory(root, Backbone, _); module.exports = factory(root, Backbone, _);
} else { } else {
root.Marionette = factory(root, root.Backbone, root._); root.Marionette = root.Mn = factory(root, root.Backbone, root._);
} }
}(this, function(root, Backbone, _) { }(this, function(root, Backbone, _) {
'use strict'; 'use strict';
/* istanbul ignore next */
// Backbone.BabySitter // Backbone.BabySitter
// ------------------- // -------------------
// v0.1.4 // v0.1.5
// //
// Copyright (c)2014 Derick Bailey, Muted Solutions, LLC. // Copyright (c)2014 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license // Distributed under MIT license
...@@ -165,13 +167,15 @@ ...@@ -165,13 +167,15 @@
// return the public API // return the public API
return Container; return Container;
}(Backbone, _); }(Backbone, _);
Backbone.ChildViewContainer.VERSION = "0.1.4"; Backbone.ChildViewContainer.VERSION = "0.1.5";
Backbone.ChildViewContainer.noConflict = function() { Backbone.ChildViewContainer.noConflict = function() {
Backbone.ChildViewContainer = previousChildViewContainer; Backbone.ChildViewContainer = previousChildViewContainer;
return this; return this;
}; };
return Backbone.ChildViewContainer; return Backbone.ChildViewContainer;
})(Backbone, _); })(Backbone, _);
/* istanbul ignore next */
// Backbone.Wreqr (Backbone.Marionette) // Backbone.Wreqr (Backbone.Marionette)
// ---------------------------------- // ----------------------------------
// v1.3.1 // v1.3.1
...@@ -489,7 +493,7 @@ ...@@ -489,7 +493,7 @@
var Marionette = Backbone.Marionette = {}; var Marionette = Backbone.Marionette = {};
Marionette.VERSION = '2.1.0'; Marionette.VERSION = '2.3.2';
Marionette.noConflict = function() { Marionette.noConflict = function() {
root.Marionette = previousMarionette; root.Marionette = previousMarionette;
...@@ -501,26 +505,26 @@ ...@@ -501,26 +505,26 @@
// Get the Deferred creator for later use // Get the Deferred creator for later use
Marionette.Deferred = Backbone.$.Deferred; Marionette.Deferred = Backbone.$.Deferred;
/* jshint unused: false */ /* jshint unused: false *//* global console */
// Helpers // Helpers
// ------- // -------
// For slicing `arguments` in functions
var slice = Array.prototype.slice;
function throwError(message, name) {
var error = new Error(message);
error.name = name || 'Error';
throw error;
}
// Marionette.extend // Marionette.extend
// ----------------- // -----------------
// Borrow the Backbone `extend` method so we can use it as needed // Borrow the Backbone `extend` method so we can use it as needed
Marionette.extend = Backbone.Model.extend; Marionette.extend = Backbone.Model.extend;
// Marionette.isNodeAttached
// -------------------------
// Determine if `el` is a child of the document
Marionette.isNodeAttached = function(el) {
return Backbone.$.contains(document.documentElement, el);
};
// Marionette.getOption // Marionette.getOption
// -------------------- // --------------------
...@@ -528,15 +532,11 @@ ...@@ -528,15 +532,11 @@
// object or its `options`, with `options` taking precedence. // object or its `options`, with `options` taking precedence.
Marionette.getOption = function(target, optionName) { Marionette.getOption = function(target, optionName) {
if (!target || !optionName) { return; } if (!target || !optionName) { return; }
var value;
if (target.options && (target.options[optionName] !== undefined)) { if (target.options && (target.options[optionName] !== undefined)) {
value = target.options[optionName]; return target.options[optionName];
} else { } else {
value = target[optionName]; return target[optionName];
} }
return value;
}; };
// Proxy `Marionette.getOption` // Proxy `Marionette.getOption`
...@@ -544,44 +544,67 @@ ...@@ -544,44 +544,67 @@
return Marionette.getOption(this, optionName); return Marionette.getOption(this, optionName);
}; };
// Similar to `_.result`, this is a simple helper
// If a function is provided we call it with context
// otherwise just return the value. If the value is
// undefined return a default value
Marionette._getValue = function(value, context, params) {
if (_.isFunction(value)) {
// We need to ensure that params is not undefined
// to prevent `apply` from failing in ie8
params = params || [];
value = value.apply(context, params);
}
return value;
};
// Marionette.normalizeMethods // Marionette.normalizeMethods
// ---------------------- // ----------------------
// Pass in a mapping of events => functions or function names // Pass in a mapping of events => functions or function names
// and return a mapping of events => functions // and return a mapping of events => functions
Marionette.normalizeMethods = function(hash) { Marionette.normalizeMethods = function(hash) {
var normalizedHash = {}; return _.reduce(hash, function(normalizedHash, method, name) {
_.each(hash, function(method, name) {
if (!_.isFunction(method)) { if (!_.isFunction(method)) {
method = this[method]; method = this[method];
} }
if (!method) { if (method) {
return;
}
normalizedHash[name] = method; normalizedHash[name] = method;
}, this); }
return normalizedHash; return normalizedHash;
}, {}, this);
}; };
// utility method for parsing @ui. syntax strings
// into associated selector
Marionette.normalizeUIString = function(uiString, ui) {
return uiString.replace(/@ui\.[a-zA-Z_$0-9]*/g, function(r) {
return ui[r.slice(4)];
});
};
// allows for the use of the @ui. syntax within // allows for the use of the @ui. syntax within
// a given key for triggers and events // a given key for triggers and events
// swaps the @ui with the associated selector // swaps the @ui with the associated selector.
// Returns a new, non-mutated, parsed events hash.
Marionette.normalizeUIKeys = function(hash, ui) { Marionette.normalizeUIKeys = function(hash, ui) {
if (typeof(hash) === 'undefined') { return _.reduce(hash, function(memo, val, key) {
return; var normalizedKey = Marionette.normalizeUIString(key, ui);
} memo[normalizedKey] = val;
return memo;
}, {});
};
_.each(_.keys(hash), function(v) { // allows for the use of the @ui. syntax within
var pattern = /@ui\.[a-zA-Z_$0-9]*/g; // a given value for regions
if (v.match(pattern)) { // swaps the @ui with the associated selector
hash[v.replace(pattern, function(r) { Marionette.normalizeUIValues = function(hash, ui) {
return ui[r.slice(4)]; _.each(hash, function(val, key) {
})] = hash[v]; if (_.isString(val)) {
delete hash[v]; hash[key] = Marionette.normalizeUIString(val, ui);
} }
}); });
return hash; return hash;
}; };
...@@ -604,15 +627,31 @@ ...@@ -604,15 +627,31 @@
}); });
}; };
// Trigger an event and/or a corresponding method name. Examples: var deprecate = Marionette.deprecate = function(message, test) {
// if (_.isObject(message)) {
// `this.triggerMethod("foo")` will trigger the "foo" event and message = (
// call the "onFoo" method. message.prev + ' is going to be removed in the future. ' +
// 'Please use ' + message.next + ' instead.' +
// `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and (message.url ? ' See: ' + message.url : '')
// call the "onFooBar" method. );
Marionette.triggerMethod = (function() { }
if ((test === undefined || !test) && !deprecate._cache[message]) {
deprecate._warn('Deprecation warning: ' + message);
deprecate._cache[message] = true;
}
};
deprecate._warn = typeof console !== 'undefined' && (console.warn || console.log) || function() {};
deprecate._cache = {};
/* jshint maxstatements: 14, maxcomplexity: 7 */
// Trigger Method
// --------------
Marionette._triggerMethod = (function() {
// split the event name on the ":" // split the event name on the ":"
var splitter = /(^|:)(\w)/gi; var splitter = /(^|:)(\w)/gi;
...@@ -622,90 +661,109 @@ ...@@ -622,90 +661,109 @@
return eventName.toUpperCase(); return eventName.toUpperCase();
} }
// actual triggerMethod implementation return function(context, event, args) {
var triggerMethod = function(event) { var noEventArg = arguments.length < 3;
if (noEventArg) {
args = event;
event = args[0];
}
// get the method name from the event name // get the method name from the event name
var methodName = 'on' + event.replace(splitter, getEventName); var methodName = 'on' + event.replace(splitter, getEventName);
var method = this[methodName]; var method = context[methodName];
var result; var result;
// call the onMethodName if it exists // call the onMethodName if it exists
if (_.isFunction(method)) { if (_.isFunction(method)) {
// pass all arguments, except the event name // pass all args, except the event name
result = method.apply(this, _.tail(arguments)); result = method.apply(context, noEventArg ? _.rest(args) : args);
} }
// trigger the event, if a trigger method exists // trigger the event, if a trigger method exists
if (_.isFunction(this.trigger)) { if (_.isFunction(context.trigger)) {
this.trigger.apply(this, arguments); if (noEventArg + args.length > 1) {
context.trigger.apply(context, noEventArg ? args : [event].concat(_.rest(args, 0)));
} else {
context.trigger(event);
}
} }
return result; return result;
}; };
return triggerMethod;
})(); })();
// DOMRefresh // Trigger an event and/or a corresponding method name. Examples:
// ----------
// //
// `this.triggerMethod("foo")` will trigger the "foo" event and
// call the "onFoo" method.
//
// `this.triggerMethod("foo:bar")` will trigger the "foo:bar" event and
// call the "onFooBar" method.
Marionette.triggerMethod = function(event) {
return Marionette._triggerMethod(this, arguments);
};
// triggerMethodOn invokes triggerMethod on a specific context
//
// e.g. `Marionette.triggerMethodOn(view, 'show')`
// will trigger a "show" event or invoke onShow the view.
Marionette.triggerMethodOn = function(context) {
var fnc = _.isFunction(context.triggerMethod) ?
context.triggerMethod :
Marionette.triggerMethod;
return fnc.apply(context, _.rest(arguments));
};
// DOM Refresh
// -----------
// Monitor a view's state, and after it has been rendered and shown // Monitor a view's state, and after it has been rendered and shown
// in the DOM, trigger a "dom:refresh" event every time it is // in the DOM, trigger a "dom:refresh" event every time it is
// re-rendered. // re-rendered.
Marionette.MonitorDOMRefresh = (function(documentElement) { Marionette.MonitorDOMRefresh = function(view) {
// track when the view has been shown in the DOM, // track when the view has been shown in the DOM,
// using a Marionette.Region (or by other means of triggering "show") // using a Marionette.Region (or by other means of triggering "show")
function handleShow(view) { function handleShow() {
view._isShown = true; view._isShown = true;
triggerDOMRefresh(view); triggerDOMRefresh();
} }
// track when the view has been rendered // track when the view has been rendered
function handleRender(view) { function handleRender() {
view._isRendered = true; view._isRendered = true;
triggerDOMRefresh(view); triggerDOMRefresh();
} }
// Trigger the "dom:refresh" event and corresponding "onDomRefresh" method // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method
function triggerDOMRefresh(view) { function triggerDOMRefresh() {
if (view._isShown && view._isRendered && isInDOM(view)) { if (view._isShown && view._isRendered && Marionette.isNodeAttached(view.el)) {
if (_.isFunction(view.triggerMethod)) { if (_.isFunction(view.triggerMethod)) {
view.triggerMethod('dom:refresh'); view.triggerMethod('dom:refresh');
} }
} }
} }
function isInDOM(view) { view.on({
return Backbone.$.contains(documentElement, view.el); show: handleShow,
} render: handleRender
// Export public API
return function(view) {
view.listenTo(view, 'show', function() {
handleShow(view);
});
view.listenTo(view, 'render', function() {
handleRender(view);
}); });
}; };
})(document.documentElement);
/* jshint maxparams: 5 */ /* jshint maxparams: 5 */
// Marionette.bindEntityEvents & unbindEntityEvents // Bind Entity Events & Unbind Entity Events
// --------------------------- // -----------------------------------------
// //
// These methods are used to bind/unbind a backbone "entity" (collection/model) // These methods are used to bind/unbind a backbone "entity" (e.g. collection/model)
// to methods on a target object. // to methods on a target object.
// //
// The first parameter, `target`, must have a `listenTo` method from the // The first parameter, `target`, must have the Backbone.Events module mixed in.
// EventBinder object.
// //
// The second parameter is the entity (Backbone.Model or Backbone.Collection) // The second parameter is the `entity` (Backbone.Model, Backbone.Collection or
// to bind the events from. // any object that has Backbone.Events mixed in) to bind the events from.
// //
// The third parameter is a hash of { "event:name": "eventHandler" } // The third parameter is a hash of { "event:name": "eventHandler" }
// configuration. Multiple handlers can be separated by a space. A // configuration. Multiple handlers can be separated by a space. A
...@@ -723,7 +781,7 @@ ...@@ -723,7 +781,7 @@
var method = target[methodName]; var method = target[methodName];
if (!method) { if (!method) {
throwError('Method "' + methodName + throw new Marionette.Error('Method "' + methodName +
'" was configured as an event handler, but does not exist.'); '" was configured as an event handler, but does not exist.');
} }
...@@ -757,11 +815,17 @@ ...@@ -757,11 +815,17 @@
function iterateEvents(target, entity, bindings, functionCallback, stringCallback) { function iterateEvents(target, entity, bindings, functionCallback, stringCallback) {
if (!entity || !bindings) { return; } if (!entity || !bindings) { return; }
// allow the bindings to be a function // type-check bindings
if (_.isFunction(bindings)) { if (!_.isObject(bindings)) {
bindings = bindings.call(target); throw new Marionette.Error({
message: 'Bindings must be an object or function.',
url: 'marionette.functions.html#marionettebindentityevents'
});
} }
// allow the bindings to be a function
bindings = Marionette._getValue(bindings, target);
// iterate the bindings and bind them // iterate the bindings and bind them
_.each(bindings, function(methods, evt) { _.each(bindings, function(methods, evt) {
...@@ -797,6 +861,45 @@ ...@@ -797,6 +861,45 @@
})(Marionette); })(Marionette);
// Error
// -----
var errorProps = ['description', 'fileName', 'lineNumber', 'name', 'message', 'number'];
Marionette.Error = Marionette.extend.call(Error, {
urlRoot: 'http://marionettejs.com/docs/v' + Marionette.VERSION + '/',
constructor: function(message, options) {
if (_.isObject(message)) {
options = message;
message = options.message;
} else if (!options) {
options = {};
}
var error = Error.call(this, message);
_.extend(this, _.pick(error, errorProps), _.pick(options, errorProps));
this.captureStackTrace();
if (options.url) {
this.url = this.urlRoot + options.url;
}
},
captureStackTrace: function() {
if (Error.captureStackTrace) {
Error.captureStackTrace(this, Marionette.Error);
}
},
toString: function() {
return this.name + ': ' + this.message + (this.url ? ' See: ' + this.url : '');
}
});
Marionette.Error.extend = Marionette.extend;
// Callbacks // Callbacks
// --------- // ---------
...@@ -847,9 +950,9 @@ ...@@ -847,9 +950,9 @@
} }
}); });
// Marionette Controller // Controller
// --------------------- // ----------
//
// A multi-purpose object to use as a controller for // A multi-purpose object to use as a controller for
// modules and routers, and as a mediator for workflow // modules and routers, and as a mediator for workflow
// and coordination of other objects, views, and more. // and coordination of other objects, views, and more.
...@@ -869,9 +972,8 @@ ...@@ -869,9 +972,8 @@
// Ensure it can trigger events with Backbone.Events // Ensure it can trigger events with Backbone.Events
_.extend(Marionette.Controller.prototype, Backbone.Events, { _.extend(Marionette.Controller.prototype, Backbone.Events, {
destroy: function() { destroy: function() {
var args = slice.call(arguments); Marionette._triggerMethod(this, 'before:destroy', arguments);
this.triggerMethod.apply(this, ['before:destroy'].concat(args)); Marionette._triggerMethod(this, 'destroy', arguments);
this.triggerMethod.apply(this, ['destroy'].concat(args));
this.stopListening(); this.stopListening();
this.off(); this.off();
...@@ -887,16 +989,15 @@ ...@@ -887,16 +989,15 @@
}); });
// Marionette Object // Object
// --------------------- // ------
//
// A Base Class that other Classes should descend from. // A Base Class that other Classes should descend from.
// Object borrows many conventions and utilities from Backbone. // Object borrows many conventions and utilities from Backbone.
Marionette.Object = function(options) { Marionette.Object = function(options) {
this.options = _.extend({}, _.result(this, 'options'), options); this.options = _.extend({}, _.result(this, 'options'), options);
this.initialize(this.options); this.initialize.apply(this, arguments);
}; };
Marionette.Object.extend = Marionette.extend; Marionette.Object.extend = Marionette.extend;
...@@ -904,7 +1005,8 @@ ...@@ -904,7 +1005,8 @@
// Object Methods // Object Methods
// -------------- // --------------
_.extend(Marionette.Object.prototype, { // Ensure it can trigger events with Backbone.Events
_.extend(Marionette.Object.prototype, Backbone.Events, {
//this is a noop method intended to be overridden by classes that extend from this base //this is a noop method intended to be overridden by classes that extend from this base
initialize: function() {}, initialize: function() {},
...@@ -922,25 +1024,26 @@ ...@@ -922,25 +1024,26 @@
// Proxy `getOption` to enable getting options from this or this.options by name. // Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption, getOption: Marionette.proxyGetOption,
// Proxy `unbindEntityEvents` to enable binding view's events from another entity. // Proxy `bindEntityEvents` to enable binding view's events from another entity.
bindEntityEvents: Marionette.proxyBindEntityEvents, bindEntityEvents: Marionette.proxyBindEntityEvents,
// Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
unbindEntityEvents: Marionette.proxyUnbindEntityEvents unbindEntityEvents: Marionette.proxyUnbindEntityEvents
}); });
// Ensure it can trigger events with Backbone.Events /* jshint maxcomplexity: 16, maxstatements: 45, maxlen: 120 */
_.extend(Marionette.Object.prototype, Backbone.Events);
/* jshint maxcomplexity: 10, maxstatements: 29 */
// Region // Region
// ------ // ------
//
// Manage the visual regions of your composite application. See // Manage the visual regions of your composite application. See
// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ // http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/
Marionette.Region = function(options) { Marionette.Region = Marionette.Object.extend({
constructor: function (options) {
// set options temporarily so that we can get `el`.
// options will be overriden by Object.constructor
this.options = options || {}; this.options = options || {};
this.el = this.getOption('el'); this.el = this.getOption('el');
...@@ -948,107 +1051,16 @@ ...@@ -948,107 +1051,16 @@
this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el; this.el = this.el instanceof Backbone.$ ? this.el[0] : this.el;
if (!this.el) { if (!this.el) {
throwError('An "el" must be specified for a region.', 'NoElError'); throw new Marionette.Error({
name: 'NoElError',
message: 'An "el" must be specified for a region.'
});
} }
this.$el = this.getEl(this.el); this.$el = this.getEl(this.el);
Marionette.Object.call(this, options);
if (this.initialize) {
var args = slice.apply(arguments);
this.initialize.apply(this, args);
}
};
// Region Class methods
// -------------------
_.extend(Marionette.Region, {
// Build an instance of a region by passing in a configuration object
// and a default region class to use if none is specified in the config.
//
// The config object should either be a string as a jQuery DOM selector,
// a Region class directly, or an object literal that specifies both
// a selector and regionClass:
//
// ```js
// {
// selector: "#foo",
// regionClass: MyCustomRegion
// }
// ```
//
buildRegion: function(regionConfig, DefaultRegionClass) {
if (_.isString(regionConfig)) {
return this._buildRegionFromSelector(regionConfig, DefaultRegionClass);
}
if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) {
return this._buildRegionFromObject(regionConfig, DefaultRegionClass);
}
if (_.isFunction(regionConfig)) {
return this._buildRegionFromRegionClass(regionConfig);
}
throwError('Improper region configuration type. Please refer ' +
'to http://marionettejs.com/docs/marionette.region.html#region-configuration-types');
}, },
// Build the region from a string selector like '#foo-region'
_buildRegionFromSelector: function(selector, DefaultRegionClass) {
return new DefaultRegionClass({ el: selector });
},
// Build the region from a configuration object
// ```js
// { selector: '#foo', regionClass: FooRegion }
// ```
_buildRegionFromObject: function(regionConfig, DefaultRegionClass) {
var RegionClass = regionConfig.regionClass || DefaultRegionClass;
var options = _.omit(regionConfig, 'selector', 'regionClass');
if (regionConfig.selector && !options.el) {
options.el = regionConfig.selector;
}
var region = new RegionClass(options);
// override the `getEl` function if we have a parentEl
// this must be overridden to ensure the selector is found
// on the first use of the region. if we try to assign the
// region's `el` to `parentEl.find(selector)` in the object
// literal to build the region, the element will not be
// guaranteed to be in the DOM already, and will cause problems
if (regionConfig.parentEl) {
region.getEl = function(el) {
if (_.isObject(el)) {
return Backbone.$(el);
}
var parentEl = regionConfig.parentEl;
if (_.isFunction(parentEl)) {
parentEl = parentEl();
}
return parentEl.find(el);
};
}
return region;
},
// Build the region directly from a given `RegionClass`
_buildRegionFromRegionClass: function(RegionClass) {
return new RegionClass();
}
});
// Region Instance Methods
// -----------------------
_.extend(Marionette.Region.prototype, Backbone.Events, {
// Displays a backbone view instance inside of the region. // Displays a backbone view instance inside of the region.
// Handles calling the `render` method for you. Reads content // Handles calling the `render` method for you. Reads content
// directly from the `el` attribute. Also calls an optional // directly from the `el` attribute. Also calls an optional
...@@ -1058,27 +1070,47 @@ ...@@ -1058,27 +1070,47 @@
// the old view being destroyed on show. // the old view being destroyed on show.
// The `forceShow` option can be used to force a view to be // The `forceShow` option can be used to force a view to be
// re-rendered if it's already shown in the region. // re-rendered if it's already shown in the region.
show: function(view, options){ show: function(view, options){
this._ensureElement(); if (!this._ensureElement()) {
return;
}
this._ensureViewIsIntact(view);
var showOptions = options || {}; var showOptions = options || {};
var isDifferentView = view !== this.currentView; var isDifferentView = view !== this.currentView;
var preventDestroy = !!showOptions.preventDestroy; var preventDestroy = !!showOptions.preventDestroy;
var forceShow = !!showOptions.forceShow; var forceShow = !!showOptions.forceShow;
// we are only changing the view if there is a view to change to begin with // We are only changing the view if there is a current view to change to begin with
var isChangingView = !!this.currentView; var isChangingView = !!this.currentView;
// only destroy the view if we don't want to preventDestroy and the view is different // Only destroy the current view if we don't want to `preventDestroy` and if
var _shouldDestroyView = !preventDestroy && isDifferentView; // the view given in the first argument is different than `currentView`
var _shouldDestroyView = isDifferentView && !preventDestroy;
// Only show the view given in the first argument if it is different than
// the current view or if we want to re-show the view. Note that if
// `_shouldDestroyView` is true, then `_shouldShowView` is also necessarily true.
var _shouldShowView = isDifferentView || forceShow;
if (isChangingView) {
this.triggerMethod('before:swapOut', this.currentView, this, options);
}
if (this.currentView) {
delete this.currentView._parent;
}
if (_shouldDestroyView) { if (_shouldDestroyView) {
this.empty(); this.empty();
}
// show the view if the view is different or if you want to re-show the view // A `destroy` event is attached to the clean up manually removed views.
var _shouldShowView = isDifferentView || forceShow; // We need to detach this event when a new view is going to be shown as it
// is no longer relevant.
} else if (isChangingView && _shouldShowView) {
this.currentView.off('destroy', this.empty, this);
}
if (_shouldShowView) { if (_shouldShowView) {
...@@ -1087,42 +1119,73 @@ ...@@ -1087,42 +1119,73 @@
// If this happens we need to remove the reference // If this happens we need to remove the reference
// to the currentView since once a view has been destroyed // to the currentView since once a view has been destroyed
// we can not reuse it. // we can not reuse it.
view.once('destroy', _.bind(this.empty, this)); view.once('destroy', this.empty, this);
view.render(); view.render();
view._parent = this;
if (isChangingView) { if (isChangingView) {
this.triggerMethod('before:swap', view); this.triggerMethod('before:swap', view, this, options);
} }
this.triggerMethod('before:show', view); this.triggerMethod('before:show', view, this, options);
Marionette.triggerMethodOn(view, 'before:show', view, this, options);
if (_.isFunction(view.triggerMethod)) { if (isChangingView) {
view.triggerMethod('before:show'); this.triggerMethod('swapOut', this.currentView, this, options);
} else { }
this.triggerMethod.call(view, 'before:show');
// An array of views that we're about to display
var attachedRegion = Marionette.isNodeAttached(this.el);
// The views that we're about to attach to the document
// It's important that we prevent _getNestedViews from being executed unnecessarily
// as it's a potentially-slow method
var displayedViews = [];
var triggerBeforeAttach = showOptions.triggerBeforeAttach || this.triggerBeforeAttach;
var triggerAttach = showOptions.triggerAttach || this.triggerAttach;
if (attachedRegion && triggerBeforeAttach) {
displayedViews = this._displayedViews(view);
this._triggerAttach(displayedViews, 'before:');
} }
this.attachHtml(view); this.attachHtml(view);
this.currentView = view; this.currentView = view;
if (isChangingView) { if (attachedRegion && triggerAttach) {
this.triggerMethod('swap', view); displayedViews = this._displayedViews(view);
this._triggerAttach(displayedViews);
} }
this.triggerMethod('show', view); if (isChangingView) {
this.triggerMethod('swap', view, this, options);
if (_.isFunction(view.triggerMethod)) {
view.triggerMethod('show');
} else {
this.triggerMethod.call(view, 'show');
} }
this.triggerMethod('show', view, this, options);
Marionette.triggerMethodOn(view, 'show', view, this, options);
return this; return this;
} }
return this; return this;
}, },
triggerBeforeAttach: true,
triggerAttach: true,
_triggerAttach: function(views, prefix) {
var eventName = (prefix || '') + 'attach';
_.each(views, function(view) {
Marionette.triggerMethodOn(view, eventName, view, this);
}, this);
},
_displayedViews: function(view) {
return _.union([view], _.result(view, '_getNestedViews') || []);
},
_ensureElement: function(){ _ensureElement: function(){
if (!_.isObject(this.el)) { if (!_.isObject(this.el)) {
this.$el = this.getEl(this.el); this.$el = this.getEl(this.el);
...@@ -1130,21 +1193,43 @@ ...@@ -1130,21 +1193,43 @@
} }
if (!this.$el || this.$el.length === 0) { if (!this.$el || this.$el.length === 0) {
throwError('An "el" ' + this.$el.selector + ' must exist in DOM'); if (this.getOption('allowMissingEl')) {
return false;
} else {
throw new Marionette.Error('An "el" ' + this.$el.selector + ' must exist in DOM');
}
} }
return true;
}, },
// Override this method to change how the region finds the _ensureViewIsIntact: function(view) {
// DOM element that it manages. Return a jQuery selector object. if (!view) {
throw new Marionette.Error({
name: 'ViewNotValid',
message: 'The view passed is undefined and therefore invalid. You must pass a view instance to show.'
});
}
if (view.isDestroyed) {
throw new Marionette.Error({
name: 'ViewDestroyedError',
message: 'View (cid: "' + view.cid + '") has already been destroyed and cannot be used.'
});
}
},
// Override this method to change how the region finds the DOM
// element that it manages. Return a jQuery selector object scoped
// to a provided parent el or the document if none exists.
getEl: function(el) { getEl: function(el) {
return Backbone.$(el); return Backbone.$(el, Marionette._getValue(this.options.parentEl, this));
}, },
// Override this method to change how the new view is // Override this method to change how the new view is
// appended to the `$el` that the region is managing // appended to the `$el` that the region is managing
attachHtml: function(view) { attachHtml: function(view) {
// empty the node and append new view this.$el.contents().detach();
this.el.innerHTML='';
this.el.appendChild(view.el); this.el.appendChild(view.el);
}, },
...@@ -1157,6 +1242,7 @@ ...@@ -1157,6 +1242,7 @@
// we should not remove anything // we should not remove anything
if (!view) { return; } if (!view) { return; }
view.off('destroy', this.empty, this);
this.triggerMethod('before:empty', view); this.triggerMethod('before:empty', view);
this._destroyView(); this._destroyView();
this.triggerMethod('empty', view); this.triggerMethod('empty', view);
...@@ -1175,6 +1261,10 @@ ...@@ -1175,6 +1261,10 @@
view.destroy(); view.destroy();
} else if (view.remove) { } else if (view.remove) {
view.remove(); view.remove();
// appending isDestroyed to raw Backbone View allows regions
// to throw a ViewDestroyedError for this view
view.isDestroyed = true;
} }
}, },
...@@ -1207,29 +1297,84 @@ ...@@ -1207,29 +1297,84 @@
delete this.$el; delete this.$el;
return this; return this;
}
}, },
// Proxy `getOption` to enable getting options from this or this.options by name. // Static Methods
getOption: Marionette.proxyGetOption, {
// import the `triggerMethod` to trigger events with corresponding // Build an instance of a region by passing in a configuration object
// methods if the method exists // and a default region class to use if none is specified in the config.
triggerMethod: Marionette.triggerMethod //
// The config object should either be a string as a jQuery DOM selector,
// a Region class directly, or an object literal that specifies a selector,
// a custom regionClass, and any options to be supplied to the region:
//
// ```js
// {
// selector: "#foo",
// regionClass: MyCustomRegion,
// allowMissingEl: false
// }
// ```
//
buildRegion: function(regionConfig, DefaultRegionClass) {
if (_.isString(regionConfig)) {
return this._buildRegionFromSelector(regionConfig, DefaultRegionClass);
}
if (regionConfig.selector || regionConfig.el || regionConfig.regionClass) {
return this._buildRegionFromObject(regionConfig, DefaultRegionClass);
}
if (_.isFunction(regionConfig)) {
return this._buildRegionFromRegionClass(regionConfig);
}
throw new Marionette.Error({
message: 'Improper region configuration type.',
url: 'marionette.region.html#region-configuration-types'
}); });
},
// Copy the `extend` function used by Backbone's classes // Build the region from a string selector like '#foo-region'
Marionette.Region.extend = Marionette.extend; _buildRegionFromSelector: function(selector, DefaultRegionClass) {
return new DefaultRegionClass({ el: selector });
},
// Marionette.RegionManager // Build the region from a configuration object
// ------------------------ // ```js
// // { selector: '#foo', regionClass: FooRegion, allowMissingEl: false }
// Manage one or more related `Marionette.Region` objects. // ```
Marionette.RegionManager = (function(Marionette) { _buildRegionFromObject: function(regionConfig, DefaultRegionClass) {
var RegionClass = regionConfig.regionClass || DefaultRegionClass;
var options = _.omit(regionConfig, 'selector', 'regionClass');
if (regionConfig.selector && !options.el) {
options.el = regionConfig.selector;
}
var RegionManager = Marionette.Controller.extend({ return new RegionClass(options);
},
// Build the region directly from a given `RegionClass`
_buildRegionFromRegionClass: function(RegionClass) {
return new RegionClass();
}
});
// Region Manager
// --------------
// Manage one or more related `Marionette.Region` objects.
Marionette.RegionManager = Marionette.Controller.extend({
constructor: function(options) { constructor: function(options) {
this._regions = {}; this._regions = {};
Marionette.Controller.call(this, options); Marionette.Controller.call(this, options);
this.addRegions(this.getOption('regions'));
}, },
// Add multiple regions using an object literal or a // Add multiple regions using an object literal or a
...@@ -1237,26 +1382,19 @@ ...@@ -1237,26 +1382,19 @@
// each key becomes the region name, and each value is // each key becomes the region name, and each value is
// the region definition. // the region definition.
addRegions: function(regionDefinitions, defaults) { addRegions: function(regionDefinitions, defaults) {
if (_.isFunction(regionDefinitions)) { regionDefinitions = Marionette._getValue(regionDefinitions, this, arguments);
regionDefinitions = regionDefinitions.apply(this, arguments);
}
var regions = {}; return _.reduce(regionDefinitions, function(regions, definition, name) {
_.each(regionDefinitions, function(definition, name) {
if (_.isString(definition)) { if (_.isString(definition)) {
definition = {selector: definition}; definition = {selector: definition};
} }
if (definition.selector) { if (definition.selector) {
definition = _.defaults({}, definition, defaults); definition = _.defaults({}, definition, defaults);
} }
var region = this.addRegion(name, definition); regions[name] = this.addRegion(name, definition);
regions[name] = region;
}, this);
return regions; return regions;
}, {}, this);
}, },
// Add an individual region to the region manager, // Add an individual region to the region manager,
...@@ -1264,20 +1402,15 @@ ...@@ -1264,20 +1402,15 @@
addRegion: function(name, definition) { addRegion: function(name, definition) {
var region; var region;
var isObject = _.isObject(definition); if (definition instanceof Marionette.Region) {
var isString = _.isString(definition);
var hasSelector = !!definition.selector;
if (isString || (isObject && hasSelector)) {
region = Marionette.Region.buildRegion(definition, Marionette.Region);
} else if (_.isFunction(definition)) {
region = Marionette.Region.buildRegion(definition, Marionette.Region);
} else {
region = definition; region = definition;
} else {
region = Marionette.Region.buildRegion(definition, Marionette.Region);
} }
this.triggerMethod('before:add:region', name, region); this.triggerMethod('before:add:region', name, region);
region._parent = this;
this._store(name, region); this._store(name, region);
this.triggerMethod('add:region', name, region); this.triggerMethod('add:region', name, region);
...@@ -1318,10 +1451,7 @@ ...@@ -1318,10 +1451,7 @@
// leave them attached // leave them attached
emptyRegions: function() { emptyRegions: function() {
var regions = this.getRegions(); var regions = this.getRegions();
_.each(regions, function(region) { _.invoke(regions, 'empty');
region.empty();
}, this);
return regions; return regions;
}, },
...@@ -1343,6 +1473,8 @@ ...@@ -1343,6 +1473,8 @@
this.triggerMethod('before:remove:region', name, region); this.triggerMethod('before:remove:region', name, region);
region.empty(); region.empty();
region.stopListening(); region.stopListening();
delete region._parent;
delete this._regions[name]; delete this._regions[name];
this._setLength(); this._setLength();
this.triggerMethod('remove:region', name, region); this.triggerMethod('remove:region', name, region);
...@@ -1352,13 +1484,9 @@ ...@@ -1352,13 +1484,9 @@
_setLength: function() { _setLength: function() {
this.length = _.size(this._regions); this.length = _.size(this._regions);
} }
}); });
Marionette.actAsCollection(RegionManager.prototype, '_regions'); Marionette.actAsCollection(Marionette.RegionManager.prototype, '_regions');
return RegionManager;
})(Marionette);
// Template Cache // Template Cache
...@@ -1399,7 +1527,7 @@ ...@@ -1399,7 +1527,7 @@
// `clear("#t1", "#t2", "...")` // `clear("#t1", "#t2", "...")`
clear: function() { clear: function() {
var i; var i;
var args = slice.call(arguments); var args = _.toArray(arguments);
var length = args.length; var length = args.length;
if (length > 0) { if (length > 0) {
...@@ -1440,7 +1568,10 @@ ...@@ -1440,7 +1568,10 @@
var template = Backbone.$(templateId).html(); var template = Backbone.$(templateId).html();
if (!template || template.length === 0) { if (!template || template.length === 0) {
throwError('Could not find template: "' + templateId + '"', 'NoTemplateError'); throw new Marionette.Error({
name: 'NoTemplateError',
message: 'Could not find template: "' + templateId + '"'
});
} }
return template; return template;
...@@ -1468,16 +1599,13 @@ ...@@ -1468,16 +1599,13 @@
// custom rendering and template handling for all of Marionette. // custom rendering and template handling for all of Marionette.
render: function(template, data) { render: function(template, data) {
if (!template) { if (!template) {
throwError('Cannot render the template since its false, null or undefined.', throw new Marionette.Error({
'TemplateNotFoundError'); name: 'TemplateNotFoundError',
message: 'Cannot render the template since its false, null or undefined.'
});
} }
var templateFunc; var templateFunc = _.isFunction(template) ? template : Marionette.TemplateCache.get(template);
if (typeof template === 'function') {
templateFunc = template;
} else {
templateFunc = Marionette.TemplateCache.get(template);
}
return templateFunc(data); return templateFunc(data);
} }
...@@ -1485,31 +1613,30 @@ ...@@ -1485,31 +1613,30 @@
/* jshint maxlen: 114, nonew: false */ /* jshint maxlen: 114, nonew: false */
// Marionette.View // View
// --------------- // ----
// The core view class that other Marionette views extend from. // The core view class that other Marionette views extend from.
Marionette.View = Backbone.View.extend({ Marionette.View = Backbone.View.extend({
isDestroyed: false,
constructor: function(options) { constructor: function(options) {
_.bindAll(this, 'render'); _.bindAll(this, 'render');
options = Marionette._getValue(options, this);
// this exposes view options to the view initializer // this exposes view options to the view initializer
// this is a backfill since backbone removed the assignment // this is a backfill since backbone removed the assignment
// of this.options // of this.options
// at some point however this may be removed // at some point however this may be removed
this.options = _.extend({}, _.result(this, 'options'), _.isFunction(options) ? options.call(this) : options); this.options = _.extend({}, _.result(this, 'options'), options);
// parses out the @ui DSL for events
this.events = this.normalizeUIKeys(_.result(this, 'events'));
if (_.isObject(this.behaviors)) { this._behaviors = Marionette.Behaviors(this);
new Marionette.Behaviors(this);
}
Backbone.View.apply(this, arguments); Backbone.View.apply(this, arguments);
Marionette.MonitorDOMRefresh(this); Marionette.MonitorDOMRefresh(this);
this.listenTo(this, 'show', this.onShowCalled); this.on('show', this.onShowCalled);
}, },
// Get the template for this view // Get the template for this view
...@@ -1523,7 +1650,7 @@ ...@@ -1523,7 +1650,7 @@
// Serialize a model by returning its attributes. Clones // Serialize a model by returning its attributes. Clones
// the attributes to allow modification. // the attributes to allow modification.
serializeModel: function(model){ serializeModel: function(model){
return model.toJSON.apply(model, slice.call(arguments, 1)); return model.toJSON.apply(model, _.rest(arguments));
}, },
// Mix in template helper methods. Looks for a // Mix in template helper methods. Looks for a
...@@ -1534,65 +1661,39 @@ ...@@ -1534,65 +1661,39 @@
mixinTemplateHelpers: function(target) { mixinTemplateHelpers: function(target) {
target = target || {}; target = target || {};
var templateHelpers = this.getOption('templateHelpers'); var templateHelpers = this.getOption('templateHelpers');
if (_.isFunction(templateHelpers)) { templateHelpers = Marionette._getValue(templateHelpers, this);
templateHelpers = templateHelpers.call(this);
}
return _.extend(target, templateHelpers); return _.extend(target, templateHelpers);
}, },
// normalize the keys of passed hash with the views `ui` selectors.
// `{"@ui.foo": "bar"}`
normalizeUIKeys: function(hash) { normalizeUIKeys: function(hash) {
var uiBindings = _.result(this, '_uiBindings');
return Marionette.normalizeUIKeys(hash, uiBindings || _.result(this, 'ui'));
},
// normalize the values of passed hash with the views `ui` selectors.
// `{foo: "@ui.bar"}`
normalizeUIValues: function(hash) {
var ui = _.result(this, 'ui'); var ui = _.result(this, 'ui');
var uiBindings = _.result(this, '_uiBindings'); var uiBindings = _.result(this, '_uiBindings');
return Marionette.normalizeUIKeys(hash, uiBindings || ui); return Marionette.normalizeUIValues(hash, uiBindings || ui);
}, },
// Configure `triggers` to forward DOM events to view // Configure `triggers` to forward DOM events to view
// events. `triggers: {"click .foo": "do:foo"}` // events. `triggers: {"click .foo": "do:foo"}`
configureTriggers: function() { configureTriggers: function() {
if (!this.triggers) { return; } if (!this.triggers) { return; }
var triggerEvents = {};
// Allow `triggers` to be configured as a function
var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
// Configure the triggers, prevent default
// action and stop propagation of DOM events
_.each(triggers, function(value, key) {
var hasOptions = _.isObject(value);
var eventName = hasOptions ? value.event : value;
// build the event handler function for the DOM event
triggerEvents[key] = function(e) {
// stop the event in its tracks
if (e) {
var prevent = e.preventDefault;
var stop = e.stopPropagation;
var shouldPrevent = hasOptions ? value.preventDefault : prevent;
var shouldStop = hasOptions ? value.stopPropagation : stop;
if (shouldPrevent && prevent) { prevent.apply(e); }
if (shouldStop && stop) { stop.apply(e); }
}
// build the args for the event
var args = {
view: this,
model: this.model,
collection: this.collection
};
// trigger the event
this.triggerMethod(eventName, args);
};
}, this); // Allow `triggers` to be configured as a function
var triggers = this.normalizeUIKeys(_.result(this, 'triggers'));
return triggerEvents; // Configure the triggers, prevent default
// action and stop propagation of DOM events
return _.reduce(triggers, function(events, value, key) {
events[key] = this._buildViewTrigger(value);
return events;
}, {}, this);
}, },
// Overriding Backbone.View's delegateEvents to handle // Overriding Backbone.View's delegateEvents to handle
...@@ -1601,25 +1702,32 @@ ...@@ -1601,25 +1702,32 @@
this._delegateDOMEvents(events); this._delegateDOMEvents(events);
this.bindEntityEvents(this.model, this.getOption('modelEvents')); this.bindEntityEvents(this.model, this.getOption('modelEvents'));
this.bindEntityEvents(this.collection, this.getOption('collectionEvents')); this.bindEntityEvents(this.collection, this.getOption('collectionEvents'));
_.each(this._behaviors, function(behavior) {
behavior.bindEntityEvents(this.model, behavior.getOption('modelEvents'));
behavior.bindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
}, this);
return this; return this;
}, },
// internal method to delegate DOM events and triggers // internal method to delegate DOM events and triggers
_delegateDOMEvents: function(events) { _delegateDOMEvents: function(eventsArg) {
events = events || this.events; var events = Marionette._getValue(eventsArg || this.events, this);
if (_.isFunction(events)) { events = events.call(this); }
// normalize ui keys // normalize ui keys
events = this.normalizeUIKeys(events); events = this.normalizeUIKeys(events);
if(_.isUndefined(eventsArg)) {this.events = events;}
var combinedEvents = {}; var combinedEvents = {};
// look up if this view has behavior events // look up if this view has behavior events
var behaviorEvents = _.result(this, 'behaviorEvents') || {}; var behaviorEvents = _.result(this, 'behaviorEvents') || {};
var triggers = this.configureTriggers(); var triggers = this.configureTriggers();
var behaviorTriggers = _.result(this, 'behaviorTriggers') || {};
// behavior events will be overriden by view events and or triggers // behavior events will be overriden by view events and or triggers
_.extend(combinedEvents, behaviorEvents, events, triggers); _.extend(combinedEvents, behaviorEvents, events, triggers, behaviorTriggers);
Backbone.View.prototype.delegateEvents.call(this, combinedEvents); Backbone.View.prototype.delegateEvents.call(this, combinedEvents);
}, },
...@@ -1627,10 +1735,16 @@ ...@@ -1627,10 +1735,16 @@
// Overriding Backbone.View's undelegateEvents to handle unbinding // Overriding Backbone.View's undelegateEvents to handle unbinding
// the `triggers`, `modelEvents`, and `collectionEvents` config // the `triggers`, `modelEvents`, and `collectionEvents` config
undelegateEvents: function() { undelegateEvents: function() {
var args = slice.call(arguments); Backbone.View.prototype.undelegateEvents.apply(this, arguments);
Backbone.View.prototype.undelegateEvents.apply(this, args);
this.unbindEntityEvents(this.model, this.getOption('modelEvents')); this.unbindEntityEvents(this.model, this.getOption('modelEvents'));
this.unbindEntityEvents(this.collection, this.getOption('collectionEvents')); this.unbindEntityEvents(this.collection, this.getOption('collectionEvents'));
_.each(this._behaviors, function(behavior) {
behavior.unbindEntityEvents(this.model, behavior.getOption('modelEvents'));
behavior.unbindEntityEvents(this.collection, behavior.getOption('collectionEvents'));
}, this);
return this; return this;
}, },
...@@ -1640,9 +1754,10 @@ ...@@ -1640,9 +1754,10 @@
// Internal helper method to verify whether the view hasn't been destroyed // Internal helper method to verify whether the view hasn't been destroyed
_ensureViewIsIntact: function() { _ensureViewIsIntact: function() {
if (this.isDestroyed) { if (this.isDestroyed) {
var err = new Error('Cannot use a view thats already been destroyed.'); throw new Marionette.Error({
err.name = 'ViewDestroyedError'; name: 'ViewDestroyedError',
throw err; message: 'View (cid: "' + this.cid + '") has already been destroyed and cannot be used.'
});
} }
}, },
...@@ -1653,7 +1768,7 @@ ...@@ -1653,7 +1768,7 @@
destroy: function() { destroy: function() {
if (this.isDestroyed) { return; } if (this.isDestroyed) { return; }
var args = slice.call(arguments); var args = _.toArray(arguments);
this.triggerMethod.apply(this, ['before:destroy'].concat(args)); this.triggerMethod.apply(this, ['before:destroy'].concat(args));
...@@ -1668,12 +1783,24 @@ ...@@ -1668,12 +1783,24 @@
// remove the view from the DOM // remove the view from the DOM
this.remove(); this.remove();
// Call destroy on each behavior after
// destroying the view.
// This unbinds event listeners
// that behaviors have registered for.
_.invoke(this._behaviors, 'destroy', args);
return this; return this;
}, },
bindUIElements: function() {
this._bindUIElements();
_.invoke(this._behaviors, this._bindUIElements);
},
// This method binds the elements specified in the "ui" hash inside the view's code with // This method binds the elements specified in the "ui" hash inside the view's code with
// the associated jQuery selectors. // the associated jQuery selectors.
bindUIElements: function() { _bindUIElements: function() {
if (!this.ui) { return; } if (!this.ui) { return; }
// store the ui hash in _uiBindings so they can be reset later // store the ui hash in _uiBindings so they can be reset later
...@@ -1689,14 +1816,18 @@ ...@@ -1689,14 +1816,18 @@
this.ui = {}; this.ui = {};
// bind each of the selectors // bind each of the selectors
_.each(_.keys(bindings), function(key) { _.each(bindings, function(selector, key) {
var selector = bindings[key];
this.ui[key] = this.$(selector); this.ui[key] = this.$(selector);
}, this); }, this);
}, },
// This method unbinds the elements specified in the "ui" hash // This method unbinds the elements specified in the "ui" hash
unbindUIElements: function() { unbindUIElements: function() {
this._unbindUIElements();
_.invoke(this._behaviors, this._unbindUIElements);
},
_unbindUIElements: function() {
if (!this.ui || !this._uiBindings) { return; } if (!this.ui || !this._uiBindings) { return; }
// delete all of the existing ui bindings // delete all of the existing ui bindings
...@@ -1709,9 +1840,81 @@ ...@@ -1709,9 +1840,81 @@
delete this._uiBindings; delete this._uiBindings;
}, },
// Internal method to create an event handler for a given `triggerDef` like
// 'click:foo'
_buildViewTrigger: function(triggerDef) {
var hasOptions = _.isObject(triggerDef);
var options = _.defaults({}, (hasOptions ? triggerDef : {}), {
preventDefault: true,
stopPropagation: true
});
var eventName = hasOptions ? options.event : triggerDef;
return function(e) {
if (e) {
if (e.preventDefault && options.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation && options.stopPropagation) {
e.stopPropagation();
}
}
var args = {
view: this,
model: this.model,
collection: this.collection
};
this.triggerMethod(eventName, args);
};
},
setElement: function() {
var ret = Backbone.View.prototype.setElement.apply(this, arguments);
// proxy behavior $el to the view's $el.
// This is needed because a view's $el proxy
// is not set until after setElement is called.
_.invoke(this._behaviors, 'proxyViewProperties', this);
return ret;
},
// import the `triggerMethod` to trigger events with corresponding // import the `triggerMethod` to trigger events with corresponding
// methods if the method exists // methods if the method exists
triggerMethod: Marionette.triggerMethod, triggerMethod: function() {
var triggerMethod = Marionette._triggerMethod;
var ret = triggerMethod(this, arguments);
var behaviors = this._behaviors;
// Use good ol' for as this is a very hot function
for (var i = 0, length = behaviors && behaviors.length; i < length; i++) {
triggerMethod(behaviors[i], arguments);
}
return ret;
},
// This method returns any views that are immediate
// children of this view
_getImmediateChildren: function() {
return [];
},
// Returns an array of every nested view within this view
_getNestedViews: function() {
var children = this._getImmediateChildren();
if (!children.length) { return children; }
return _.reduce(children, function(memo, view) {
if (!view._getNestedViews) { return memo; }
return memo.concat(view._getNestedViews());
}, children);
},
// Imports the "normalizeMethods" to transform hashes of // Imports the "normalizeMethods" to transform hashes of
// events=>function references/names to a hash of events=>function references // events=>function references/names to a hash of events=>function references
...@@ -1720,7 +1923,7 @@ ...@@ -1720,7 +1923,7 @@
// Proxy `getOption` to enable getting options from this or this.options by name. // Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption, getOption: Marionette.proxyGetOption,
// Proxy `unbindEntityEvents` to enable binding view's events from another entity. // Proxy `bindEntityEvents` to enable binding view's events from another entity.
bindEntityEvents: Marionette.proxyBindEntityEvents, bindEntityEvents: Marionette.proxyBindEntityEvents,
// Proxy `unbindEntityEvents` to enable unbinding view's events from another entity. // Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
...@@ -1749,21 +1952,27 @@ ...@@ -1749,21 +1952,27 @@
// You can override the `serializeData` method in your own view definition, // You can override the `serializeData` method in your own view definition,
// to provide custom serialization for your view's data. // to provide custom serialization for your view's data.
serializeData: function(){ serializeData: function(){
var data = {}; if (!this.model && !this.collection) {
return {};
if (this.model) {
data = _.partial(this.serializeModel, this.model).apply(this, arguments);
} }
else if (this.collection) {
data = { items: _.partial(this.serializeCollection, this.collection).apply(this, arguments) }; var args = [this.model || this.collection];
if (arguments.length) {
args.push.apply(args, arguments);
} }
return data; if (this.model) {
return this.serializeModel.apply(this, args);
} else {
return {
items: this.serializeCollection.apply(this, args)
};
}
}, },
// Serialize a collection by serializing each of its models. // Serialize a collection by serializing each of its models.
serializeCollection: function(collection){ serializeCollection: function(collection){
return collection.toJSON.apply(collection, slice.call(arguments, 1)); return collection.toJSON.apply(collection, _.rest(arguments));
}, },
// Render the view, defaulting to underscore.js templates. // Render the view, defaulting to underscore.js templates.
...@@ -1797,8 +2006,10 @@ ...@@ -1797,8 +2006,10 @@
} }
if (!template) { if (!template) {
throwError('Cannot render the template since it is null or undefined.', throw new Marionette.Error({
'UndefinedTemplateError'); name: 'UndefinedTemplateError',
message: 'Cannot render the template since it is null or undefined.'
});
} }
// Add in entity data and template helpers // Add in entity data and template helpers
...@@ -1813,7 +2024,7 @@ ...@@ -1813,7 +2024,7 @@
}, },
// Attaches the content of a given view. // Attaches the content of a given view.
// This method can be overriden to optimize rendering, // This method can be overridden to optimize rendering,
// or to render in a non standard way. // or to render in a non standard way.
// //
// For example, using `innerHTML` instead of `$el.html` // For example, using `innerHTML` instead of `$el.html`
...@@ -1828,14 +2039,6 @@ ...@@ -1828,14 +2039,6 @@
this.$el.html(html); this.$el.html(html);
return this; return this;
},
// Override the default destroy event to add a few
// more events that are triggered.
destroy: function() {
if (this.isDestroyed) { return; }
return Marionette.View.prototype.destroy.apply(this, arguments);
} }
}); });
...@@ -1858,13 +2061,15 @@ ...@@ -1858,13 +2061,15 @@
// This will fallback onto appending childView's to the end. // This will fallback onto appending childView's to the end.
constructor: function(options){ constructor: function(options){
var initOptions = options || {}; var initOptions = options || {};
if (_.isUndefined(this.sort)){
this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort; this.sort = _.isUndefined(initOptions.sort) ? true : initOptions.sort;
}
this.once('render', this._initialEvents);
this._initChildViewStorage(); this._initChildViewStorage();
Marionette.View.apply(this, arguments); Marionette.View.apply(this, arguments);
this._initialEvents();
this.initRenderBuffer(); this.initRenderBuffer();
}, },
...@@ -1891,23 +2096,24 @@ ...@@ -1891,23 +2096,24 @@
_triggerBeforeShowBufferedChildren: function() { _triggerBeforeShowBufferedChildren: function() {
if (this._isShown) { if (this._isShown) {
_.invoke(this._bufferedChildren, 'triggerMethod', 'before:show'); _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'before:show'));
} }
}, },
_triggerShowBufferedChildren: function() { _triggerShowBufferedChildren: function() {
if (this._isShown) { if (this._isShown) {
_.each(this._bufferedChildren, function (child) { _.each(this._bufferedChildren, _.partial(this._triggerMethodOnChild, 'show'));
if (_.isFunction(child.triggerMethod)) {
child.triggerMethod('show');
} else {
Marionette.triggerMethod.call(child, 'show');
}
});
this._bufferedChildren = []; this._bufferedChildren = [];
} }
}, },
// Internal method for _.each loops to call `Marionette.triggerMethodOn` on
// a child view
_triggerMethodOnChild: function(event, childView) {
Marionette.triggerMethodOn(childView, event);
},
// Configured the initial events that the collection view // Configured the initial events that the collection view
// binds to. // binds to.
_initialEvents: function() { _initialEvents: function() {
...@@ -1938,14 +2144,8 @@ ...@@ -1938,14 +2144,8 @@
}, },
// Override from `Marionette.View` to trigger show on child views // Override from `Marionette.View` to trigger show on child views
onShowCalled: function(){ onShowCalled: function() {
this.children.each(function(child){ this.children.each(_.partial(this._triggerMethodOnChild, 'show'));
if (_.isFunction(child.triggerMethod)) {
child.triggerMethod('show');
} else {
Marionette.triggerMethod.call(child, 'show');
}
});
}, },
// Render children views. Override this method to // Render children views. Override this method to
...@@ -1981,6 +2181,9 @@ ...@@ -1981,6 +2181,9 @@
} }
}, },
// Internal reference to what index a `emptyView` is.
_emptyViewIndex: -1,
// Internal method. Separated so that CompositeView can have // Internal method. Separated so that CompositeView can have
// more control over events being triggered, around the rendering // more control over events being triggered, around the rendering
// process // process
...@@ -2029,8 +2232,12 @@ ...@@ -2029,8 +2232,12 @@
// rendered empty, and then a child is added to the collection. // rendered empty, and then a child is added to the collection.
destroyEmptyView: function() { destroyEmptyView: function() {
if (this._showingEmptyView) { if (this._showingEmptyView) {
this.triggerMethod('before:remove:empty');
this.destroyChildren(); this.destroyChildren();
delete this._showingEmptyView; delete this._showingEmptyView;
this.triggerMethod('remove:empty');
} }
}, },
...@@ -2040,25 +2247,30 @@ ...@@ -2040,25 +2247,30 @@
}, },
// Render and show the emptyView. Similar to addChild method // Render and show the emptyView. Similar to addChild method
// but "child:added" events are not fired, and the event from // but "add:child" events are not fired, and the event from
// emptyView are not forwarded // emptyView are not forwarded
addEmptyView: function(child, EmptyView){ addEmptyView: function(child, EmptyView) {
// get the emptyViewOptions, falling back to childViewOptions // get the emptyViewOptions, falling back to childViewOptions
var emptyViewOptions = this.getOption('emptyViewOptions') || var emptyViewOptions = this.getOption('emptyViewOptions') ||
this.getOption('childViewOptions'); this.getOption('childViewOptions');
if (_.isFunction(emptyViewOptions)){ if (_.isFunction(emptyViewOptions)){
emptyViewOptions = emptyViewOptions.call(this); emptyViewOptions = emptyViewOptions.call(this, child, this._emptyViewIndex);
} }
// build the empty view // build the empty view
var view = this.buildChildView(child, EmptyView, emptyViewOptions); var view = this.buildChildView(child, EmptyView, emptyViewOptions);
view._parent = this;
// Proxy emptyView events
this.proxyChildEvents(view);
// trigger the 'before:show' event on `view` if the collection view // trigger the 'before:show' event on `view` if the collection view
// has already been shown // has already been shown
if (this._isShown){ if (this._isShown) {
this.triggerMethod.call(view, 'before:show'); Marionette.triggerMethodOn(view, 'before:show');
} }
// Store the `emptyView` like a `childView` so we can properly // Store the `emptyView` like a `childView` so we can properly
...@@ -2066,12 +2278,12 @@ ...@@ -2066,12 +2278,12 @@
this.children.add(view); this.children.add(view);
// Render it and show it // Render it and show it
this.renderChildView(view, -1); this.renderChildView(view, this._emptyViewIndex);
// call the 'show' method if the collection view // call the 'show' method if the collection view
// has already been shown // has already been shown
if (this._isShown){ if (this._isShown) {
this.triggerMethod.call(view, 'show'); Marionette.triggerMethodOn(view, 'show');
} }
}, },
...@@ -2085,7 +2297,10 @@ ...@@ -2085,7 +2297,10 @@
var childView = this.getOption('childView'); var childView = this.getOption('childView');
if (!childView) { if (!childView) {
throwError('A "childView" must be specified', 'NoChildViewError'); throw new Marionette.Error({
name: 'NoChildViewError',
message: 'A "childView" must be specified'
});
} }
return childView; return childView;
...@@ -2097,9 +2312,7 @@ ...@@ -2097,9 +2312,7 @@
// in order to keep the children in sync with the collection. // in order to keep the children in sync with the collection.
addChild: function(child, ChildView, index) { addChild: function(child, ChildView, index) {
var childViewOptions = this.getOption('childViewOptions'); var childViewOptions = this.getOption('childViewOptions');
if (_.isFunction(childViewOptions)) { childViewOptions = Marionette._getValue(childViewOptions, this, [child, index]);
childViewOptions = childViewOptions.call(this, child, index);
}
var view = this.buildChildView(child, ChildView, childViewOptions); var view = this.buildChildView(child, ChildView, childViewOptions);
...@@ -2108,6 +2321,8 @@ ...@@ -2108,6 +2321,8 @@
this._addChildView(view, index); this._addChildView(view, index);
view._parent = this;
return view; return view;
}, },
...@@ -2121,22 +2336,14 @@ ...@@ -2121,22 +2336,14 @@
if (increment) { if (increment) {
// assign the index to the view // assign the index to the view
view._index = index; view._index = index;
// increment the index of views after this one
this.children.each(function (laterView) {
if (laterView._index >= view._index) {
laterView._index++;
} }
});
} // update the indexes of views after this one
else {
// decrement the index of views after this one
this.children.each(function (laterView) { this.children.each(function (laterView) {
if (laterView._index >= view._index) { if (laterView._index >= view._index) {
laterView._index--; laterView._index += increment ? 1 : -1;
} }
}); });
}
}, },
...@@ -2153,12 +2360,8 @@ ...@@ -2153,12 +2360,8 @@
this.children.add(view); this.children.add(view);
this.renderChildView(view, index); this.renderChildView(view, index);
if (this._isShown && !this.isBuffering){ if (this._isShown && !this.isBuffering) {
if (_.isFunction(view.triggerMethod)) { Marionette.triggerMethodOn(view, 'show');
view.triggerMethod('show');
} else {
Marionette.triggerMethod.call(view, 'show');
}
} }
this.triggerMethod('add:child', view); this.triggerMethod('add:child', view);
...@@ -2189,6 +2392,7 @@ ...@@ -2189,6 +2392,7 @@
if (view.destroy) { view.destroy(); } if (view.destroy) { view.destroy(); }
else if (view.remove) { view.remove(); } else if (view.remove) { view.remove(); }
delete view._parent;
this.stopListening(view); this.stopListening(view);
this.children.remove(view); this.children.remove(view);
this.triggerMethod('remove:child', view); this.triggerMethod('remove:child', view);
...@@ -2201,7 +2405,7 @@ ...@@ -2201,7 +2405,7 @@
}, },
// check if the collection is empty // check if the collection is empty
isEmpty: function(collection) { isEmpty: function() {
return !this.collection || this.collection.length === 0; return !this.collection || this.collection.length === 0;
}, },
...@@ -2297,7 +2501,7 @@ ...@@ -2297,7 +2501,7 @@
// Forward all child view events through the parent, // Forward all child view events through the parent,
// prepending "childview:" to the event name // prepending "childview:" to the event name
this.listenTo(view, 'all', function() { this.listenTo(view, 'all', function() {
var args = slice.call(arguments); var args = _.toArray(arguments);
var rootEvent = args[0]; var rootEvent = args[0];
var childEvents = this.normalizeMethods(_.result(this, 'childEvents')); var childEvents = this.normalizeMethods(_.result(this, 'childEvents'));
...@@ -2311,6 +2515,10 @@ ...@@ -2311,6 +2515,10 @@
this.triggerMethod.apply(this, args); this.triggerMethod.apply(this, args);
}, this); }, this);
},
_getImmediateChildren: function() {
return _.values(this.children._views);
} }
}); });
...@@ -2340,7 +2548,7 @@ ...@@ -2340,7 +2548,7 @@
// Bind only after composite view is rendered to avoid adding child views // Bind only after composite view is rendered to avoid adding child views
// to nonexistent childViewContainer // to nonexistent childViewContainer
this.once('render', function() {
if (this.collection) { if (this.collection) {
this.listenTo(this.collection, 'add', this._onCollectionAdd); this.listenTo(this.collection, 'add', this._onCollectionAdd);
this.listenTo(this.collection, 'remove', this._onCollectionRemove); this.listenTo(this.collection, 'remove', this._onCollectionRemove);
...@@ -2350,8 +2558,6 @@ ...@@ -2350,8 +2558,6 @@
this.listenTo(this.collection, 'sort', this._sortViews); this.listenTo(this.collection, 'sort', this._sortViews);
} }
} }
});
}, },
// Retrieve the `childView` to be used when rendering each of // Retrieve the `childView` to be used when rendering each of
...@@ -2361,14 +2567,10 @@ ...@@ -2361,14 +2567,10 @@
getChildView: function(child) { getChildView: function(child) {
var childView = this.getOption('childView') || this.constructor; var childView = this.getOption('childView') || this.constructor;
if (!childView) {
throwError('A "childView" must be specified', 'NoChildViewError');
}
return childView; return childView;
}, },
// Serialize the collection for the view. // Serialize the model for the view.
// You can override the `serializeData` method in your own view // You can override the `serializeData` method in your own view
// definition, to provide custom serialization for your view's data. // definition, to provide custom serialization for your view's data.
serializeData: function() { serializeData: function() {
...@@ -2381,9 +2583,7 @@ ...@@ -2381,9 +2583,7 @@
return data; return data;
}, },
// Renders the model once, and the collection once. Calling // Renders the model and the collection.
// this again will tell the model's view to re-render itself
// but the collection will not re-render.
render: function() { render: function() {
this._ensureViewIsIntact(); this._ensureViewIsIntact();
this.isRendered = true; this.isRendered = true;
...@@ -2425,7 +2625,7 @@ ...@@ -2425,7 +2625,7 @@
}, },
// Attaches the content of the root. // Attaches the content of the root.
// This method can be overriden to optimize rendering, // This method can be overridden to optimize rendering,
// or to render in a non standard way. // or to render in a non standard way.
// //
// For example, using `innerHTML` instead of `$el.html` // For example, using `innerHTML` instead of `$el.html`
...@@ -2452,13 +2652,13 @@ ...@@ -2452,13 +2652,13 @@
// Overidden from CollectionView to ensure view is appended to // Overidden from CollectionView to ensure view is appended to
// childViewContainer // childViewContainer
_insertAfter: function (childView) { _insertAfter: function (childView) {
var $container = this.getChildViewContainer(this); var $container = this.getChildViewContainer(this, childView);
$container.append(childView.el); $container.append(childView.el);
}, },
// Internal method to ensure an `$childViewContainer` exists, for the // Internal method to ensure an `$childViewContainer` exists, for the
// `attachHtml` method to use. // `attachHtml` method to use.
getChildViewContainer: function(containerView) { getChildViewContainer: function(containerView, childView) {
if ('$childViewContainer' in containerView) { if ('$childViewContainer' in containerView) {
return containerView.$childViewContainer; return containerView.$childViewContainer;
} }
...@@ -2467,7 +2667,7 @@ ...@@ -2467,7 +2667,7 @@
var childViewContainer = Marionette.getOption(containerView, 'childViewContainer'); var childViewContainer = Marionette.getOption(containerView, 'childViewContainer');
if (childViewContainer) { if (childViewContainer) {
var selector = _.isFunction(childViewContainer) ? childViewContainer.call(containerView) : childViewContainer; var selector = Marionette._getValue(childViewContainer, containerView);
if (selector.charAt(0) === '@' && containerView.ui) { if (selector.charAt(0) === '@' && containerView.ui) {
container = containerView.ui[selector.substr(4)]; container = containerView.ui[selector.substr(4)];
...@@ -2476,8 +2676,10 @@ ...@@ -2476,8 +2676,10 @@
} }
if (container.length <= 0) { if (container.length <= 0) {
throwError('The specified "childViewContainer" was not found: ' + throw new Marionette.Error({
containerView.childViewContainer, 'ChildViewContainerMissingError'); name: 'ChildViewContainerMissingError',
message: 'The specified "childViewContainer" was not found: ' + containerView.childViewContainer
});
} }
} else { } else {
...@@ -2496,8 +2698,8 @@ ...@@ -2496,8 +2698,8 @@
} }
}); });
// LayoutView // Layout View
// ---------- // -----------
// Used for managing application layoutViews, nested layoutViews and // Used for managing application layoutViews, nested layoutViews and
// multiple regions within an application or sub-application. // multiple regions within an application or sub-application.
...@@ -2549,7 +2751,6 @@ ...@@ -2549,7 +2751,6 @@
// Add a single region, by name, to the layoutView // Add a single region, by name, to the layoutView
addRegion: function(name, definition) { addRegion: function(name, definition) {
this.triggerMethod('before:region:add', name);
var regions = {}; var regions = {};
regions[name] = definition; regions[name] = definition;
return this._buildRegions(regions)[name]; return this._buildRegions(regions)[name];
...@@ -2563,7 +2764,6 @@ ...@@ -2563,7 +2764,6 @@
// Remove a single region from the LayoutView, by name // Remove a single region from the LayoutView, by name
removeRegion: function(name) { removeRegion: function(name) {
this.triggerMethod('before:region:remove', name);
delete this.regions[name]; delete this.regions[name];
return this.regionManager.removeRegion(name); return this.regionManager.removeRegion(name);
}, },
...@@ -2582,11 +2782,9 @@ ...@@ -2582,11 +2782,9 @@
// internal method to build regions // internal method to build regions
_buildRegions: function(regions) { _buildRegions: function(regions) {
var that = this;
var defaults = { var defaults = {
regionClass: this.getOption('regionClass'), regionClass: this.getOption('regionClass'),
parentEl: function() { return that.$el; } parentEl: _.partial(_.result, this, 'el')
}; };
return this.regionManager.addRegions(regions, defaults); return this.regionManager.addRegions(regions, defaults);
...@@ -2598,36 +2796,31 @@ ...@@ -2598,36 +2796,31 @@
var regions; var regions;
this._initRegionManager(); this._initRegionManager();
if (_.isFunction(this.regions)) { regions = Marionette._getValue(this.regions, this, [options]) || {};
regions = this.regions(options);
} else {
regions = this.regions || {};
}
// Enable users to define `regions` as instance options. // Enable users to define `regions` as instance options.
var regionOptions = this.getOption.call(options, 'regions'); var regionOptions = this.getOption.call(options, 'regions');
// enable region options to be a function // enable region options to be a function
if (_.isFunction(regionOptions)) { regionOptions = Marionette._getValue(regionOptions, this, [options]);
regionOptions = regionOptions.call(this, options);
}
_.extend(regions, regionOptions); _.extend(regions, regionOptions);
// Normalize region selectors hash to allow
// a user to use the @ui. syntax.
regions = this.normalizeUIValues(regions);
this.addRegions(regions); this.addRegions(regions);
}, },
// Internal method to re-initialize all of the regions by updating the `el` that // Internal method to re-initialize all of the regions by updating the `el` that
// they point to // they point to
_reInitializeRegions: function() { _reInitializeRegions: function() {
this.regionManager.emptyRegions(); this.regionManager.invoke('reset');
this.regionManager.each(function(region) {
region.reset();
});
}, },
// Enable easy overiding of the default `RegionManager` // Enable easy overriding of the default `RegionManager`
// for customized region interactions and buisness specific // for customized region interactions and business specific
// view logic for better control over single regions. // view logic for better control over single regions.
getRegionManager: function() { getRegionManager: function() {
return new Marionette.RegionManager(); return new Marionette.RegionManager();
...@@ -2637,6 +2830,7 @@ ...@@ -2637,6 +2830,7 @@
// and all regions in it // and all regions in it
_initRegionManager: function() { _initRegionManager: function() {
this.regionManager = this.getRegionManager(); this.regionManager = this.getRegionManager();
this.regionManager._parent = this;
this.listenTo(this.regionManager, 'before:add:region', function(name) { this.listenTo(this.regionManager, 'before:add:region', function(name) {
this.triggerMethod('before:add:region', name); this.triggerMethod('before:add:region', name);
...@@ -2655,20 +2849,27 @@ ...@@ -2655,20 +2849,27 @@
delete this[name]; delete this[name];
this.triggerMethod('remove:region', name, region); this.triggerMethod('remove:region', name, region);
}); });
},
_getImmediateChildren: function() {
return _.chain(this.regionManager.getRegions())
.pluck('currentView')
.compact()
.value();
} }
}); });
// Behavior // Behavior
// ----------- // --------
// A Behavior is an isolated set of DOM / // A Behavior is an isolated set of DOM /
// user interactions that can be mixed into any View. // user interactions that can be mixed into any View.
// Behaviors allow you to blackbox View specific interactions // Behaviors allow you to blackbox View specific interactions
// into portable logical chunks, keeping your views simple and your code DRY. // into portable logical chunks, keeping your views simple and your code DRY.
Marionette.Behavior = (function(_, Backbone) { Marionette.Behavior = Marionette.Object.extend({
function Behavior(options, view) { constructor: function(options, view) {
// Setup reference to the view. // Setup reference to the view.
// this comes in handle when a behavior // this comes in handle when a behavior
// wants to directly talk up the chain // wants to directly talk up the chain
...@@ -2677,62 +2878,48 @@ ...@@ -2677,62 +2878,48 @@
this.defaults = _.result(this, 'defaults') || {}; this.defaults = _.result(this, 'defaults') || {};
this.options = _.extend({}, this.defaults, options); this.options = _.extend({}, this.defaults, options);
Marionette.Object.apply(this, arguments);
},
// proxy behavior $ method to the view // proxy behavior $ method to the view
// this is useful for doing jquery DOM lookups // this is useful for doing jquery DOM lookups
// scoped to behaviors view. // scoped to behaviors view.
this.$ = function() { $: function() {
return this.view.$.apply(this.view, arguments); return this.view.$.apply(this.view, arguments);
}; },
// Call the initialize method passing
// the arguments from the instance constructor
this.initialize.apply(this, arguments);
}
_.extend(Behavior.prototype, Backbone.Events, {
initialize: function() {},
// stopListening to behavior `onListen` events. // Stops the behavior from listening to events.
// Overrides Object#destroy to prevent additional events from being triggered.
destroy: function() { destroy: function() {
this.stopListening(); this.stopListening();
}, },
// import the `triggerMethod` to trigger events with corresponding proxyViewProperties: function (view) {
// methods if the method exists this.$el = view.$el;
triggerMethod: Marionette.triggerMethod, this.el = view.el;
}
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption,
// Proxy `unbindEntityEvents` to enable binding view's events from another entity.
bindEntityEvents: Marionette.proxyBindEntityEvents,
// Proxy `unbindEntityEvents` to enable unbinding view's events from another entity.
unbindEntityEvents: Marionette.proxyUnbindEntityEvents
}); });
// Borrow Backbones extend implementation /* jshint maxlen: 143 */
// this allows us to setup a proper // Behaviors
// inheritence pattern that follow in suite // ---------
// with the rest of Marionette views.
Behavior.extend = Marionette.extend;
return Behavior;
})(_, Backbone);
/* jshint maxlen: 143, nonew: false */
// Marionette.Behaviors
// --------
// Behaviors is a utility class that takes care of // Behaviors is a utility class that takes care of
// glueing your behavior instances to their given View. // gluing your behavior instances to their given View.
// The most important part of this class is that you // The most important part of this class is that you
// **MUST** override the class level behaviorsLookup // **MUST** override the class level behaviorsLookup
// method for things to work properly. // method for things to work properly.
Marionette.Behaviors = (function(Marionette, _) { Marionette.Behaviors = (function(Marionette, _) {
// Borrow event splitter from Backbone
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
function Behaviors(view, behaviors) { function Behaviors(view, behaviors) {
if (!_.isObject(view.behaviors)) {
return {};
}
// Behaviors defined on a view can be a flat object literal // Behaviors defined on a view can be a flat object literal
// or it can be a function that returns an object. // or it can be a function that returns an object.
behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors')); behaviors = Behaviors.parseBehaviors(view, behaviors || _.result(view, 'behaviors'));
...@@ -2741,86 +2928,23 @@ ...@@ -2741,86 +2928,23 @@
// calling the methods first on each behavior // calling the methods first on each behavior
// and then eventually calling the method on the view. // and then eventually calling the method on the view.
Behaviors.wrap(view, behaviors, _.keys(methods)); Behaviors.wrap(view, behaviors, _.keys(methods));
return behaviors;
} }
var methods = { var methods = {
setElement: function(setElement, behaviors) { behaviorTriggers: function(behaviorTriggers, behaviors) {
setElement.apply(this, _.tail(arguments, 2)); var triggerBuilder = new BehaviorTriggersBuilder(this, behaviors);
return triggerBuilder.buildBehaviorTriggers();
// proxy behavior $el to the view's $el.
// This is needed because a view's $el proxy
// is not set until after setElement is called.
_.each(behaviors, function(b) {
b.$el = this.$el;
b.el = this.el;
}, this);
return this;
},
destroy: function(destroy, behaviors) {
var args = _.tail(arguments, 2);
destroy.apply(this, args);
// Call destroy on each behavior after
// destroying the view.
// This unbinds event listeners
// that behaviors have registerd for.
_.invoke(behaviors, 'destroy', args);
return this;
},
bindUIElements: function(bindUIElements, behaviors) {
bindUIElements.apply(this);
_.invoke(behaviors, bindUIElements);
},
unbindUIElements: function(unbindUIElements, behaviors) {
unbindUIElements.apply(this);
_.invoke(behaviors, unbindUIElements);
},
triggerMethod: function(triggerMethod, behaviors) {
var args = _.tail(arguments, 2);
triggerMethod.apply(this, args);
_.each(behaviors, function(b) {
triggerMethod.apply(b, args);
});
},
delegateEvents: function(delegateEvents, behaviors) {
var args = _.tail(arguments, 2);
delegateEvents.apply(this, args);
_.each(behaviors, function(b) {
Marionette.bindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents'));
Marionette.bindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents'));
}, this);
return this;
},
undelegateEvents: function(undelegateEvents, behaviors) {
var args = _.tail(arguments, 2);
undelegateEvents.apply(this, args);
_.each(behaviors, function(b) {
Marionette.unbindEntityEvents(b, this.model, Marionette.getOption(b, 'modelEvents'));
Marionette.unbindEntityEvents(b, this.collection, Marionette.getOption(b, 'collectionEvents'));
}, this);
return this;
}, },
behaviorEvents: function(behaviorEvents, behaviors) { behaviorEvents: function(behaviorEvents, behaviors) {
var _behaviorsEvents = {}; var _behaviorsEvents = {};
var viewUI = _.result(this, 'ui'); var viewUI = this._uiBindings || _.result(this, 'ui');
_.each(behaviors, function(b, i) { _.each(behaviors, function(b, i) {
var _events = {}; var _events = {};
var behaviorEvents = _.clone(_.result(b, 'events')) || {}; var behaviorEvents = _.clone(_.result(b, 'events')) || {};
var behaviorUI = _.result(b, 'ui'); var behaviorUI = b._uiBindings || _.result(b, 'ui');
// Construct an internal UI hash first using // Construct an internal UI hash first using
// the views UI hash and then the behaviors UI hash. // the views UI hash and then the behaviors UI hash.
...@@ -2833,21 +2957,25 @@ ...@@ -2833,21 +2957,25 @@
// a user to use the @ui. syntax. // a user to use the @ui. syntax.
behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, ui); behaviorEvents = Marionette.normalizeUIKeys(behaviorEvents, ui);
_.each(_.keys(behaviorEvents), function(key) { var j = 0;
// Append white-space at the end of each key to prevent behavior key collisions. _.each(behaviorEvents, function(behaviour, key) {
// This is relying on the fact that backbone events considers "click .foo" the same as var match = key.match(delegateEventSplitter);
// "click .foo ".
// +2 is used because new Array(1) or 0 is "" and not " " // Set event name to be namespaced using the view cid,
var whitespace = (new Array(i + 2)).join(' '); // the behavior index, and the behavior event index
var eventKey = key + whitespace; // to generate a non colliding event namespace
var handler = _.isFunction(behaviorEvents[key]) ? behaviorEvents[key] : b[behaviorEvents[key]]; // http://api.jquery.com/event.namespace/
var eventName = match[1] + '.' + [this.cid, i, j++, ' '].join(''),
selector = match[2];
var eventKey = eventName + selector;
var handler = _.isFunction(behaviour) ? behaviour : b[behaviour];
_events[eventKey] = _.bind(handler, b); _events[eventKey] = _.bind(handler, b);
}); }, this);
_behaviorsEvents = _.extend(_behaviorsEvents, _events); _behaviorsEvents = _.extend(_behaviorsEvents, _events);
}); }, this);
return _behaviorsEvents; return _behaviorsEvents;
} }
...@@ -2865,9 +2993,10 @@ ...@@ -2865,9 +2993,10 @@
// } // }
// ``` // ```
behaviorsLookup: function() { behaviorsLookup: function() {
throw new Error('You must define where your behaviors are stored.' + throw new Marionette.Error({
'See https://github.com/marionettejs/backbone.marionette' + message: 'You must define where your behaviors are stored.',
'/blob/master/docs/marionette.behaviors.md#behaviorslookup'); url: 'marionette.behaviors.html#behaviorslookup'
});
}, },
// Takes care of getting the behavior class // Takes care of getting the behavior class
...@@ -2881,7 +3010,7 @@ ...@@ -2881,7 +3010,7 @@
} }
// Get behavior class can be either a flat object or a method // Get behavior class can be either a flat object or a method
return _.isFunction(Behaviors.behaviorsLookup) ? Behaviors.behaviorsLookup.apply(this, arguments)[key] : Behaviors.behaviorsLookup[key]; return Marionette._getValue(Behaviors.behaviorsLookup, this, [options, key])[key];
}, },
// Iterate over the behaviors object, for each behavior // Iterate over the behaviors object, for each behavior
...@@ -2909,13 +3038,51 @@ ...@@ -2909,13 +3038,51 @@
} }
}); });
// Class to build handlers for `triggers` on behaviors
// for views
function BehaviorTriggersBuilder(view, behaviors) {
this._view = view;
this._viewUI = _.result(view, 'ui');
this._behaviors = behaviors;
this._triggers = {};
}
_.extend(BehaviorTriggersBuilder.prototype, {
// Main method to build the triggers hash with event keys and handlers
buildBehaviorTriggers: function() {
_.each(this._behaviors, this._buildTriggerHandlersForBehavior, this);
return this._triggers;
},
// Internal method to build all trigger handlers for a given behavior
_buildTriggerHandlersForBehavior: function(behavior, i) {
var ui = _.extend({}, this._viewUI, _.result(behavior, 'ui'));
var triggersHash = _.clone(_.result(behavior, 'triggers')) || {};
triggersHash = Marionette.normalizeUIKeys(triggersHash, ui);
_.each(triggersHash, _.bind(this._setHandlerForBehavior, this, behavior, i));
},
// Internal method to create and assign the trigger handler for a given
// behavior
_setHandlerForBehavior: function(behavior, i, eventName, trigger) {
// Unique identifier for the `this._triggers` hash
var triggerKey = trigger.replace(/^\S+/, function(triggerName) {
return triggerName + '.' + 'behaviortriggers' + i;
});
this._triggers[triggerKey] = this._view._buildViewTrigger(eventName);
}
});
return Behaviors; return Behaviors;
})(Marionette, _); })(Marionette, _);
// AppRouter // App Router
// --------- // ----------
// Reduce the boilerplate code of handling route events // Reduce the boilerplate code of handling route events
// and then calling a single method on another object. // and then calling a single method on another object.
...@@ -2935,10 +3102,10 @@ ...@@ -2935,10 +3102,10 @@
Marionette.AppRouter = Backbone.Router.extend({ Marionette.AppRouter = Backbone.Router.extend({
constructor: function(options) { constructor: function(options) {
Backbone.Router.apply(this, arguments);
this.options = options || {}; this.options = options || {};
Backbone.Router.apply(this, arguments);
var appRoutes = this.getOption('appRoutes'); var appRoutes = this.getOption('appRoutes');
var controller = this._getController(); var controller = this._getController();
this.processAppRoutes(controller, appRoutes); this.processAppRoutes(controller, appRoutes);
...@@ -2955,11 +3122,10 @@ ...@@ -2955,11 +3122,10 @@
// process the route event and trigger the onRoute // process the route event and trigger the onRoute
// method call, if it exists // method call, if it exists
_processOnRoute: function(routeName, routeArgs) { _processOnRoute: function(routeName, routeArgs) {
// find the path that matched // make sure an onRoute before trying to call it
var routePath = _.invert(this.getOption('appRoutes'))[routeName];
// make sure an onRoute is there, and call it
if (_.isFunction(this.onRoute)) { if (_.isFunction(this.onRoute)) {
// find the path that matches the current route
var routePath = _.invert(this.getOption('appRoutes'))[routeName];
this.onRoute(routeName, routePath, routeArgs); this.onRoute(routeName, routePath, routeArgs);
} }
}, },
...@@ -2985,14 +3151,20 @@ ...@@ -2985,14 +3151,20 @@
var method = controller[methodName]; var method = controller[methodName];
if (!method) { if (!method) {
throwError('Method "' + methodName + '" was not found on the controller'); throw new Marionette.Error('Method "' + methodName + '" was not found on the controller');
} }
this.route(route, methodName, _.bind(method, controller)); this.route(route, methodName, _.bind(method, controller));
}, },
// Proxy `getOption` to enable getting options from this or this.options by name. // Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption getOption: Marionette.proxyGetOption,
triggerMethod: Marionette.triggerMethod,
bindEntityEvents: Marionette.proxyBindEntityEvents,
unbindEntityEvents: Marionette.proxyUnbindEntityEvents
}); });
// Application // Application
...@@ -3001,15 +3173,16 @@ ...@@ -3001,15 +3173,16 @@
// Contain and manage the composite application as a whole. // Contain and manage the composite application as a whole.
// Stores and starts up `Region` objects, includes an // Stores and starts up `Region` objects, includes an
// event aggregator as `app.vent` // event aggregator as `app.vent`
Marionette.Application = function(options) { Marionette.Application = Marionette.Object.extend({
constructor: function(options) {
this._initializeRegions(options); this._initializeRegions(options);
this._initCallbacks = new Marionette.Callbacks(); this._initCallbacks = new Marionette.Callbacks();
this.submodules = {}; this.submodules = {};
_.extend(this, options); _.extend(this, options);
this._initChannel(); this._initChannel();
}; Marionette.Object.call(this, options);
},
_.extend(Marionette.Application.prototype, Backbone.Events, {
// Command execution, facilitated by Backbone.Wreqr.Commands // Command execution, facilitated by Backbone.Wreqr.Commands
execute: function() { execute: function() {
this.commands.execute.apply(this.commands, arguments); this.commands.execute.apply(this.commands, arguments);
...@@ -3074,9 +3247,7 @@ ...@@ -3074,9 +3247,7 @@
// Overwrite the module class if the user specifies one // Overwrite the module class if the user specifies one
var ModuleClass = Marionette.Module.getClass(moduleDefinition); var ModuleClass = Marionette.Module.getClass(moduleDefinition);
// slice the args, and add this application object as the var args = _.toArray(arguments);
// first argument of the array
var args = slice.call(arguments);
args.unshift(this); args.unshift(this);
// see the Marionette.Module object for more information // see the Marionette.Module object for more information
...@@ -3116,23 +3287,24 @@ ...@@ -3116,23 +3287,24 @@
// Internal method to set up the region manager // Internal method to set up the region manager
_initRegionManager: function() { _initRegionManager: function() {
this._regionManager = this.getRegionManager(); this._regionManager = this.getRegionManager();
this._regionManager._parent = this;
this.listenTo(this._regionManager, 'before:add:region', function(name) { this.listenTo(this._regionManager, 'before:add:region', function() {
this.triggerMethod('before:add:region', name); Marionette._triggerMethod(this, 'before:add:region', arguments);
}); });
this.listenTo(this._regionManager, 'add:region', function(name, region) { this.listenTo(this._regionManager, 'add:region', function(name, region) {
this[name] = region; this[name] = region;
this.triggerMethod('add:region', name, region); Marionette._triggerMethod(this, 'add:region', arguments);
}); });
this.listenTo(this._regionManager, 'before:remove:region', function(name) { this.listenTo(this._regionManager, 'before:remove:region', function() {
this.triggerMethod('before:remove:region', name); Marionette._triggerMethod(this, 'before:remove:region', arguments);
}); });
this.listenTo(this._regionManager, 'remove:region', function(name, region) { this.listenTo(this._regionManager, 'remove:region', function(name) {
delete this[name]; delete this[name];
this.triggerMethod('remove:region', name, region); Marionette._triggerMethod(this, 'remove:region', arguments);
}); });
}, },
...@@ -3143,19 +3315,9 @@ ...@@ -3143,19 +3315,9 @@
this.vent = _.result(this, 'vent') || this.channel.vent; this.vent = _.result(this, 'vent') || this.channel.vent;
this.commands = _.result(this, 'commands') || this.channel.commands; this.commands = _.result(this, 'commands') || this.channel.commands;
this.reqres = _.result(this, 'reqres') || this.channel.reqres; this.reqres = _.result(this, 'reqres') || this.channel.reqres;
}, }
// import the `triggerMethod` to trigger events with corresponding
// methods if the method exists
triggerMethod: Marionette.triggerMethod,
// Proxy `getOption` to enable getting options from this or this.options by name.
getOption: Marionette.proxyGetOption
}); });
// Copy the `extend` function used by Backbone's classes
Marionette.Application.extend = Marionette.extend;
/* jshint maxparams: 9 */ /* jshint maxparams: 9 */
// Module // Module
...@@ -3179,9 +3341,6 @@ ...@@ -3179,9 +3341,6 @@
// within a module. // within a module.
this.app = app; this.app = app;
// By default modules start with their parents.
this.startWithParent = true;
if (_.isFunction(this.initialize)) { if (_.isFunction(this.initialize)) {
this.initialize(moduleName, app, this.options); this.initialize(moduleName, app, this.options);
} }
...@@ -3193,6 +3352,9 @@ ...@@ -3193,6 +3352,9 @@
// can be used as an event aggregator or pub/sub. // can be used as an event aggregator or pub/sub.
_.extend(Marionette.Module.prototype, Backbone.Events, { _.extend(Marionette.Module.prototype, Backbone.Events, {
// By default modules start with their parents.
startWithParent: true,
// 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 when extending Marionette.Module. // initialization logic when extending Marionette.Module.
initialize: function() {}, initialize: function() {},
...@@ -3243,7 +3405,7 @@ ...@@ -3243,7 +3405,7 @@
// stop the sub-modules; depth-first, to make sure the // stop the sub-modules; depth-first, to make sure the
// sub-modules are stopped / finalized before parents // sub-modules are stopped / finalized before parents
_.each(this.submodules, function(mod) { mod.stop(); }); _.invoke(this.submodules, 'stop');
// run the finalizers // run the finalizers
this._finalizerCallbacks.run(undefined, this); this._finalizerCallbacks.run(undefined, this);
...@@ -3302,8 +3464,7 @@ ...@@ -3302,8 +3464,7 @@
// get the custom args passed in after the module definition and // get the custom args passed in after the module definition and
// get rid of the module name and definition function // get rid of the module name and definition function
var customArgs = slice.call(arguments); var customArgs = _.rest(arguments, 3);
customArgs.splice(0, 3);
// Split the module names and get the number of submodules. // Split the module names and get the number of submodules.
// i.e. an example module name of `Doge.Wow.Amaze` would // i.e. an example module name of `Doge.Wow.Amaze` would
......
// Backbone.js 1.0.0 // Backbone.js 1.1.2
// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2010-2014 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(){ (function(root, factory) {
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore');
factory(root, exports, _);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(this, function(root, Backbone, _, $) {
// Initial Setup // Initial Setup
// ------------- // -------------
// Save a reference to the global object (`window` in the browser, `exports`
// on the server).
var root = this;
// Save the previous value of the `Backbone` variable, so that it can be // Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used. // restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone; var previousBackbone = root.Backbone;
...@@ -24,25 +40,12 @@ ...@@ -24,25 +40,12 @@
var slice = array.slice; var slice = array.slice;
var splice = array.splice; var splice = array.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both the browser and the server.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
} else {
Backbone = root.Backbone = {};
}
// 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.0.0'; Backbone.VERSION = '1.1.2';
// Require Underscore, if we're on the server, and it's not already present.
var _ = root._;
if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
// 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.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object. // to its previous owner. Returns a reference to this Backbone object.
...@@ -52,7 +55,7 @@ ...@@ -52,7 +55,7 @@
}; };
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header. // set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false; Backbone.emulateHTTP = false;
...@@ -108,10 +111,9 @@ ...@@ -108,10 +111,9 @@
var retain, ev, events, names, i, l, j, k; var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) { if (!name && !callback && !context) {
this._events = {}; this._events = void 0;
return this; return this;
} }
names = name ? [name] : _.keys(this._events); names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) { for (i = 0, l = names.length; i < l; i++) {
name = names[i]; name = names[i];
...@@ -151,14 +153,15 @@ ...@@ -151,14 +153,15 @@
// 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) { stopListening: function(obj, name, callback) {
var listeners = this._listeners; var listeningTo = this._listeningTo;
if (!listeners) return this; if (!listeningTo) return this;
var deleteListener = !name && !callback; var remove = !name && !callback;
if (typeof name === 'object') callback = this; if (!callback && typeof name === 'object') callback = this;
if (obj) (listeners = {})[obj._listenerId] = obj; if (obj) (listeningTo = {})[obj._listenId] = obj;
for (var id in listeners) { for (var id in listeningTo) {
listeners[id].off(name, callback, this); obj = listeningTo[id];
if (deleteListener) delete this._listeners[id]; obj.off(name, callback, this);
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
} }
return this; return this;
} }
...@@ -204,7 +207,7 @@ ...@@ -204,7 +207,7 @@
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
} }
}; };
...@@ -215,10 +218,10 @@ ...@@ -215,10 +218,10 @@
// listening to. // listening to.
_.each(listenMethods, function(implementation, method) { _.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) { Events[method] = function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {}); var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeners[id] = obj; listeningTo[id] = obj;
if (typeof name === 'object') callback = this; if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this); obj[implementation](name, callback, this);
return this; return this;
}; };
...@@ -243,24 +246,18 @@ ...@@ -243,24 +246,18 @@
// Create a new model with the specified attributes. A client id (`cid`) // Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you. // is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) { var Model = Backbone.Model = function(attributes, options) {
var defaults;
var attrs = attributes || {}; var attrs = attributes || {};
options || (options = {}); options || (options = {});
this.cid = _.uniqueId('c'); this.cid = _.uniqueId('c');
this.attributes = {}; this.attributes = {};
_.extend(this, _.pick(options, modelOptions)); if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {}; if (options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) { attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
attrs = _.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);
}; };
// A list of options to be attached directly to the model, if provided.
var modelOptions = ['url', 'urlRoot', 'collection'];
// Attach all inheritable methods to the Model prototype. // Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, { _.extend(Model.prototype, Events, {
...@@ -355,7 +352,7 @@ ...@@ -355,7 +352,7 @@
// Trigger all relevant attribute changes. // Trigger all relevant attribute changes.
if (!silent) { if (!silent) {
if (changes.length) this._pending = true; if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) { for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options); this.trigger('change:' + changes[i], this, current[changes[i]], options);
} }
...@@ -366,6 +363,7 @@ ...@@ -366,6 +363,7 @@
if (changing) return this; if (changing) return this;
if (!silent) { if (!silent) {
while (this._pending) { while (this._pending) {
options = this._pending;
this._pending = false; this._pending = false;
this.trigger('change', this, options); this.trigger('change', this, options);
} }
...@@ -456,13 +454,16 @@ ...@@ -456,13 +454,16 @@
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
// If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`.
if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false;
options = _.extend({validate: true}, options); options = _.extend({validate: true}, options);
// Do not persist invalid models. // If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) {
if (!this.set(attrs, options)) return false;
} else {
if (!this._validate(attrs, options)) return false; if (!this._validate(attrs, options)) return false;
}
// Set temporary attributes if `{wait: true}`. // Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) { if (attrs && options.wait) {
...@@ -530,9 +531,12 @@ ...@@ -530,9 +531,12 @@
// using Backbone's restful methods, override this to change the endpoint // using Backbone's restful methods, override this to change the endpoint
// that will be called. // that will be called.
url: function() { url: function() {
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base; if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.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
...@@ -548,7 +552,7 @@ ...@@ -548,7 +552,7 @@
// A model is new if it has never been saved to the server, and lacks an id. // A model is new if it has never been saved to the server, and lacks an id.
isNew: function() { isNew: function() {
return this.id == null; return !this.has(this.idAttribute);
}, },
// Check if the model is currently in a valid state. // Check if the model is currently in a valid state.
...@@ -563,7 +567,7 @@ ...@@ -563,7 +567,7 @@
attrs = _.extend({}, this.attributes, attrs); attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null; var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true; if (!error) return true;
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false; return false;
} }
...@@ -596,7 +600,6 @@ ...@@ -596,7 +600,6 @@
// its models in sort order, as they're added and removed. // its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) { var Collection = Backbone.Collection = function(models, options) {
options || (options = {}); options || (options = {});
if (options.url) this.url = options.url;
if (options.model) this.model = options.model; if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator; if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset(); this._reset();
...@@ -606,7 +609,7 @@ ...@@ -606,7 +609,7 @@
// Default options for `Collection#set`. // Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true}; var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, merge: false, remove: false}; var addOptions = {add: true, remove: false};
// Define the Collection's inheritable methods. // Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, { _.extend(Collection.prototype, Events, {
...@@ -632,16 +635,17 @@ ...@@ -632,16 +635,17 @@
// Add a model, or list of models to the set. // Add a model, or list of models to the set.
add: function(models, options) { add: function(models, options) {
return this.set(models, _.defaults(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) {
models = _.isArray(models) ? models.slice() : [models]; var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
options || (options = {}); options || (options = {});
var i, l, index, model; var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) { for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]); model = models[i] = this.get(models[i]);
if (!model) continue; if (!model) continue;
delete this._byId[model.id]; delete this._byId[model.id];
delete this._byId[model.cid]; delete this._byId[model.cid];
...@@ -652,9 +656,9 @@ ...@@ -652,9 +656,9 @@
options.index = index; options.index = index;
model.trigger('remove', model, this, options); model.trigger('remove', model, this, options);
} }
this._removeReference(model); this._removeReference(model, options);
} }
return this; 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,
...@@ -662,43 +666,57 @@ ...@@ -662,43 +666,57 @@
// 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); options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options); if (options.parse) models = this.parse(models, options);
if (!_.isArray(models)) models = models ? [models] : []; var singular = !_.isArray(models);
var i, l, model, attrs, existing, sort; models = singular ? (models ? [models] : []) : _.clone(models);
var i, l, id, model, attrs, existing, sort;
var at = options.at; var at = options.at;
var targetModel = this.model;
var sortable = this.comparator && (at == null) && options.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 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++) { for (i = 0, l = models.length; i < l; i++) {
if (!(model = this._prepareModel(models[i], options))) continue; attrs = models[i] || {};
if (attrs instanceof Model) {
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(model)) { if (existing = this.get(id)) {
if (options.remove) modelMap[existing.cid] = true; if (remove) modelMap[existing.cid] = true;
if (options.merge) { if (merge) {
existing.set(model.attributes, options); attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
} }
models[i] = existing;
// This is a new model, push it to the `toAdd` list. // If this is a new, valid model, push it to the `toAdd` list.
} else if (options.add) { } else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model); toAdd.push(model);
this._addReference(model, options);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
model.on('all', this._onModelEvent, this);
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = 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 nonexistent models if appropriate.
if (options.remove) { if (remove) {
for (i = 0, l = this.length; i < l; ++i) { for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
} }
...@@ -706,29 +724,35 @@ ...@@ -706,29 +724,35 @@
} }
// 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) { if (toAdd.length || (order && order.length)) {
if (sortable) sort = true; if (sortable) sort = true;
this.length += toAdd.length; this.length += toAdd.length;
if (at != null) { if (at != null) {
splice.apply(this.models, [at, 0].concat(toAdd)); for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else { } else {
push.apply(this.models, toAdd); 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});
if (options.silent) return this; // Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
// Trigger `add` events.
for (i = 0, l = toAdd.length; i < l; i++) { for (i = 0, l = toAdd.length; i < l; i++) {
(model = toAdd[i]).trigger('add', model, this, options); (model = toAdd[i]).trigger('add', model, this, options);
} }
if (sort || (order && order.length)) this.trigger('sort', this, options);
}
// Trigger `sort` if the collection was sorted. // Return the added (or merged) model (or models).
if (sort) this.trigger('sort', this, options); return singular ? models[0] : models;
return this;
}, },
// When you have more items than you want to add or remove individually, // When you have more items than you want to add or remove individually,
...@@ -738,20 +762,18 @@ ...@@ -738,20 +762,18 @@
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) { for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]); this._removeReference(this.models[i], options);
} }
options.previousModels = this.models; options.previousModels = this.models;
this._reset(); this._reset();
this.add(models, _.extend({silent: true}, options)); models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options); if (!options.silent) this.trigger('reset', this, options);
return this; return models;
}, },
// Add a model to the end of the collection. // Add a model to the end of the collection.
push: function(model, options) { push: function(model, options) {
model = this._prepareModel(model, options); return this.add(model, _.extend({at: this.length}, options));
this.add(model, _.extend({at: this.length}, options));
return model;
}, },
// Remove a model from the end of the collection. // Remove a model from the end of the collection.
...@@ -763,9 +785,7 @@ ...@@ -763,9 +785,7 @@
// Add a model to the beginning of the collection. // Add a model to the beginning of the collection.
unshift: function(model, options) { unshift: function(model, options) {
model = this._prepareModel(model, options); return this.add(model, _.extend({at: 0}, options));
this.add(model, _.extend({at: 0}, options));
return model;
}, },
// Remove a model from the beginning of the collection. // Remove a model from the beginning of the collection.
...@@ -776,14 +796,14 @@ ...@@ -776,14 +796,14 @@
}, },
// Slice out a sub-array of models from the collection. // Slice out a sub-array of models from the collection.
slice: function(begin, end) { slice: function() {
return this.models.slice(begin, end); return slice.apply(this.models, arguments);
}, },
// Get a model from the set by id. // Get a model from the set by id.
get: function(obj) { get: function(obj) {
if (obj == null) return void 0; if (obj == null) return void 0;
return this._byId[obj.id != null ? obj.id : obj.cid || obj]; return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid];
}, },
// Get the model at the given index. // Get the model at the given index.
...@@ -827,16 +847,6 @@ ...@@ -827,16 +847,6 @@
return this; return this;
}, },
// Figure out the smallest index at which a model should be inserted so as
// to maintain order.
sortedIndex: function(model, value, context) {
value || (value = this.comparator);
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _.sortedIndex(this.models, model, iterator, context);
},
// 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 _.invoke(this.models, 'get', attr);
...@@ -869,7 +879,7 @@ ...@@ -869,7 +879,7 @@
if (!options.wait) this.add(model, options); if (!options.wait) this.add(model, options);
var collection = this; var collection = this;
var success = options.success; var success = options.success;
options.success = function(resp) { options.success = function(model, resp) {
if (options.wait) collection.add(model, options); if (options.wait) collection.add(model, options);
if (success) success(model, resp, options); if (success) success(model, resp, options);
}; };
...@@ -899,22 +909,25 @@ ...@@ -899,22 +909,25 @@
// 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) { if (attrs instanceof Model) return attrs;
if (!attrs.collection) attrs.collection = this; options = options ? _.clone(options) : {};
return attrs;
}
options || (options = {});
options.collection = this; options.collection = this;
var model = new this.model(attrs, options); var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) { if (!model.validationError) return model;
this.trigger('invalid', this, attrs, options); this.trigger('invalid', this, model.validationError, options);
return false; return false;
} },
return model;
// Internal method to create a model's ties to a collection.
_addReference: function(model, options) {
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
if (!model.collection) model.collection = 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) { _removeReference: function(model, options) {
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);
}, },
...@@ -942,8 +955,8 @@ ...@@ -942,8 +955,8 @@
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
'isEmpty', 'chain']; 'lastIndexOf', 'isEmpty', 'chain', 'sample'];
// 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) { _.each(methods, function(method) {
...@@ -955,7 +968,7 @@ ...@@ -955,7 +968,7 @@
}); });
// Underscore methods that take a property name as an argument. // Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy']; var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties. // Use attributes instead of properties.
_.each(attributeMethods, function(method) { _.each(attributeMethods, function(method) {
...@@ -982,7 +995,8 @@ ...@@ -982,7 +995,8 @@
// 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');
this._configure(options || {}); options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement(); this._ensureElement();
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
this.delegateEvents(); this.delegateEvents();
...@@ -1001,7 +1015,7 @@ ...@@ -1001,7 +1015,7 @@
tagName: 'div', tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the // jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible. // current view. This should be preferred to global lookups where possible.
$: function(selector) { $: function(selector) {
return this.$el.find(selector); return this.$el.find(selector);
}, },
...@@ -1041,7 +1055,7 @@ ...@@ -1041,7 +1055,7 @@
// //
// { // {
// 'mousedown .title': 'edit', // 'mousedown .title': 'edit',
// 'click .button': 'save' // 'click .button': 'save',
// 'click .open': function(e) { ... } // 'click .open': function(e) { ... }
// } // }
// //
...@@ -1079,16 +1093,6 @@ ...@@ -1079,16 +1093,6 @@
return this; return this;
}, },
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(e.g. model, collection, id, className)* are
// attached directly to the view. See `viewOptions` for an exhaustive
// list.
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
this.options = options;
},
// 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
...@@ -1174,8 +1178,7 @@ ...@@ -1174,8 +1178,7 @@
// If we're sending a `PATCH` request, and we're in an old Internet Explorer // If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that // that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && window.ActiveXObject && if (params.type === 'PATCH' && noXhrPatch) {
!(window.external && window.external.msActiveXFilteringEnabled)) {
params.xhr = function() { params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP"); return new ActiveXObject("Microsoft.XMLHTTP");
}; };
...@@ -1187,6 +1190,10 @@ ...@@ -1187,6 +1190,10 @@
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',
...@@ -1244,7 +1251,7 @@ ...@@ -1244,7 +1251,7 @@
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);
callback && callback.apply(router, args); router.execute(callback, args);
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);
...@@ -1252,6 +1259,12 @@ ...@@ -1252,6 +1259,12 @@
return this; return this;
}, },
// Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup.
execute: function(callback, args) {
if (callback) callback.apply(this, args);
},
// Simple proxy to `Backbone.history` to save a fragment into the history. // Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) { navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options); Backbone.history.navigate(fragment, options);
...@@ -1275,11 +1288,11 @@ ...@@ -1275,11 +1288,11 @@
_routeToRegExp: function(route) { _routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&') route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?') .replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional){ .replace(namedParam, function(match, optional) {
return optional ? match : '([^\/]+)'; return optional ? match : '([^/?]+)';
}) })
.replace(splatParam, '(.*?)'); .replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '$'); return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
}, },
// Given a route, and a URL fragment that it matches, return the array of // Given a route, and a URL fragment that it matches, return the array of
...@@ -1287,7 +1300,9 @@ ...@@ -1287,7 +1300,9 @@
// treated as `null` to normalize cross-browser behavior. // treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) { _extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1); var params = route.exec(fragment).slice(1);
return _.map(params, function(param) { return _.map(params, function(param, i) {
// Don't decode the search params.
if (i === params.length - 1) return param || null;
return param ? decodeURIComponent(param) : null; return param ? decodeURIComponent(param) : null;
}); });
} }
...@@ -1325,6 +1340,9 @@ ...@@ -1325,6 +1340,9 @@
// Cached regex for removing a trailing slash. // Cached regex for removing a trailing slash.
var trailingSlash = /\/$/; var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash.
var pathStripper = /#.*$/;
// Has the history handling already been started? // Has the history handling already been started?
History.started = false; History.started = false;
...@@ -1335,6 +1353,11 @@ ...@@ -1335,6 +1353,11 @@
// twenty times a second. // twenty times a second.
interval: 50, interval: 50,
// Are we at the app root?
atRoot: function() {
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root;
},
// 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
// in Firefox where location.hash will always be decoded. // in Firefox where location.hash will always be decoded.
getHash: function(window) { getHash: function(window) {
...@@ -1347,9 +1370,9 @@ ...@@ -1347,9 +1370,9 @@
getFragment: function(fragment, forcePushState) { getFragment: function(fragment, forcePushState) {
if (fragment == null) { if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) { if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = this.location.pathname; fragment = decodeURI(this.location.pathname + this.location.search);
var root = this.root.replace(trailingSlash, ''); var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else { } else {
fragment = this.getHash(); fragment = this.getHash();
} }
...@@ -1365,7 +1388,7 @@ ...@@ -1365,7 +1388,7 @@
// Figure out the initial configuration. Do we need an iframe? // Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available? // Is pushState desired ... is it available?
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._wantsPushState = !!this.options.pushState; this._wantsPushState = !!this.options.pushState;
...@@ -1378,7 +1401,8 @@ ...@@ -1378,7 +1401,8 @@
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) { if (oldIE && this._wantsHashChange) {
this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment); this.navigate(fragment);
} }
...@@ -1396,21 +1420,26 @@ ...@@ -1396,21 +1420,26 @@
// opened by a non-pushState browser. // opened by a non-pushState browser.
this.fragment = fragment; this.fragment = fragment;
var loc = this.location; var loc = this.location;
var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root;
// If we've started off with a route from a `pushState`-enabled browser, // Transition from hashChange to pushState or vice versa if both are
// but we're currently in a browser that doesn't support it... // requested.
if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { if (this._wantsHashChange && this._wantsPushState) {
// 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...
if (!this._hasPushState && !this.atRoot()) {
this.fragment = this.getFragment(null, true); this.fragment = this.getFragment(null, true);
this.location.replace(this.root + this.location.search + '#' + this.fragment); this.location.replace(this.root + '#' + this.fragment);
// 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._wantsPushState && this._hasPushState && atRoot && loc.hash) { } else if (this._hasPushState && this.atRoot() && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, ''); this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); this.history.replaceState({}, document.title, this.root + this.fragment);
}
} }
if (!this.options.silent) return this.loadUrl(); if (!this.options.silent) return this.loadUrl();
...@@ -1420,7 +1449,7 @@ ...@@ -1420,7 +1449,7 @@
// 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); Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
clearInterval(this._checkUrlInterval); if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false; History.started = false;
}, },
...@@ -1439,21 +1468,20 @@ ...@@ -1439,21 +1468,20 @@
} }
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(this.getHash()); this.loadUrl();
}, },
// Attempt to load the current URL fragment. If a route succeeds with a // Attempt to load the current URL fragment. If a route succeeds with a
// 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(fragmentOverride) { loadUrl: function(fragment) {
var fragment = this.fragment = this.getFragment(fragmentOverride); fragment = this.fragment = this.getFragment(fragment);
var matched = _.any(this.handlers, function(handler) { return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) { if (handler.route.test(fragment)) {
handler.callback(fragment); handler.callback(fragment);
return true; return true;
} }
}); });
return matched;
}, },
// Save a fragment into the hash history, or replace the URL state if the // Save a fragment into the hash history, or replace the URL state if the
...@@ -1465,11 +1493,18 @@ ...@@ -1465,11 +1493,18 @@
// you wish to modify the current URL without adding an entry to the history. // you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) { navigate: function(fragment, options) {
if (!History.started) return false; if (!History.started) return false;
if (!options || options === true) options = {trigger: options}; if (!options || options === true) options = {trigger: !!options};
fragment = this.getFragment(fragment || '');
var url = this.root + (fragment = this.getFragment(fragment || ''));
// Strip the hash for matching.
fragment = fragment.replace(pathStripper, '');
if (this.fragment === fragment) return; if (this.fragment === fragment) return;
this.fragment = fragment; this.fragment = fragment;
var url = this.root + 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._hasPushState) {
...@@ -1492,7 +1527,7 @@ ...@@ -1492,7 +1527,7 @@
} else { } else {
return this.location.assign(url); return this.location.assign(url);
} }
if (options.trigger) this.loadUrl(fragment); if (options.trigger) return this.loadUrl(fragment);
}, },
// Update the hash location, either replacing the current entry, or adding // Update the hash location, either replacing the current entry, or adding
...@@ -1560,7 +1595,7 @@ ...@@ -1560,7 +1595,7 @@
}; };
// Wrap an optional error callback with a fallback error event. // Wrap an optional error callback with a fallback error event.
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(model, resp, options);
...@@ -1568,4 +1603,6 @@ ...@@ -1568,4 +1603,6 @@
}; };
}; };
}).call(this); return Backbone;
}));
...@@ -12,25 +12,30 @@ button { ...@@ -12,25 +12,30 @@ button {
font-size: 100%; font-size: 100%;
vertical-align: baseline; vertical-align: baseline;
font-family: inherit; font-family: inherit;
font-weight: inherit;
color: inherit; color: inherit;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none; -ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
} }
body { body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em; line-height: 1.4em;
background: #eaeaea url('bg.png'); background: #f5f5f5;
color: #4d4d4d; color: #4d4d4d;
width: 550px; min-width: 230px;
max-width: 550px;
margin: 0 auto; margin: 0 auto;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased; -ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
font-weight: 300;
} }
button, button,
...@@ -38,78 +43,50 @@ input[type="checkbox"] { ...@@ -38,78 +43,50 @@ input[type="checkbox"] {
outline: none; outline: none;
} }
.hidden {
display: none;
}
#todoapp { #todoapp {
background: #fff; background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0; margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative; position: relative;
border-top-left-radius: 2px; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
border-top-right-radius: 2px; 0 25px 50px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
} }
#todoapp input::-webkit-input-placeholder { #todoapp input::-webkit-input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp input::-moz-placeholder { #todoapp input::-moz-placeholder {
font-style: italic; font-style: italic;
color: #a9a9a9; font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
} }
#todoapp h1 { #todoapp h1 {
position: absolute; position: absolute;
top: -120px; top: -155px;
width: 100%; width: 100%;
font-size: 70px; font-size: 100px;
font-weight: bold; font-weight: 100;
text-align: center; text-align: center;
color: #b3b3b3; color: rgba(175, 47, 47, 0.15);
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility; -webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility; -moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility; -ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo, #new-todo,
.edit { .edit {
position: relative; position: relative;
...@@ -117,6 +94,7 @@ input[type="checkbox"] { ...@@ -117,6 +94,7 @@ input[type="checkbox"] {
width: 100%; width: 100%;
font-size: 24px; font-size: 24px;
font-family: inherit; font-family: inherit;
font-weight: inherit;
line-height: 1.4em; line-height: 1.4em;
border: 0; border: 0;
outline: none; outline: none;
...@@ -124,29 +102,25 @@ input[type="checkbox"] { ...@@ -124,29 +102,25 @@ input[type="checkbox"] {
padding: 6px; padding: 6px;
border: 1px solid #999; border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-moz-box-sizing: border-box;
-ms-box-sizing: border-box; -ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased; -moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased; -ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased; font-smoothing: antialiased;
} }
#new-todo { #new-todo {
padding: 16px 16px 16px 60px; padding: 16px 16px 16px 60px;
border: none; border: none;
background: rgba(0, 0, 0, 0.02); background: rgba(0, 0, 0, 0.003);
z-index: 2; box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
box-shadow: none;
} }
#main { #main {
position: relative; position: relative;
z-index: 2; z-index: 2;
border-top: 1px dotted #adadad; border-top: 1px solid #e6e6e6;
} }
label[for='toggle-all'] { label[for='toggle-all'] {
...@@ -155,19 +129,19 @@ label[for='toggle-all'] { ...@@ -155,19 +129,19 @@ label[for='toggle-all'] {
#toggle-all { #toggle-all {
position: absolute; position: absolute;
top: -42px; top: -55px;
left: -4px; left: -12px;
width: 40px; width: 60px;
height: 34px;
text-align: center; text-align: center;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
} }
#toggle-all:before { #toggle-all:before {
content: '»'; content: '';
font-size: 28px; font-size: 22px;
color: #d9d9d9; color: #e6e6e6;
padding: 0 25px 7px; padding: 10px 27px 10px 27px;
} }
#toggle-all:checked:before { #toggle-all:checked:before {
...@@ -183,7 +157,7 @@ label[for='toggle-all'] { ...@@ -183,7 +157,7 @@ label[for='toggle-all'] {
#todo-list li { #todo-list li {
position: relative; position: relative;
font-size: 24px; font-size: 24px;
border-bottom: 1px dotted #ccc; border-bottom: 1px solid #ededed;
} }
#todo-list li:last-child { #todo-list li:last-child {
...@@ -215,28 +189,18 @@ label[for='toggle-all'] { ...@@ -215,28 +189,18 @@ label[for='toggle-all'] {
top: 0; top: 0;
bottom: 0; bottom: 0;
margin: auto 0; margin: auto 0;
/* Mobile Safari */ border: none; /* Mobile Safari */
border: none;
-webkit-appearance: none; -webkit-appearance: none;
-ms-appearance: none; -ms-appearance: none;
-o-appearance: none;
appearance: none; appearance: none;
} }
#todo-list li .toggle:after { #todo-list li .toggle:after {
content: '✔'; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
} }
#todo-list li .toggle:checked:after { #todo-list li .toggle:checked:after {
color: #85ada7; content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
} }
#todo-list li label { #todo-list li label {
...@@ -246,12 +210,11 @@ label[for='toggle-all'] { ...@@ -246,12 +210,11 @@ label[for='toggle-all'] {
margin-left: 45px; margin-left: 45px;
display: block; display: block;
line-height: 1.2; line-height: 1.2;
-webkit-transition: color 0.4s;
transition: color 0.4s; transition: color 0.4s;
} }
#todo-list li.completed label { #todo-list li.completed label {
color: #a9a9a9; color: #d9d9d9;
text-decoration: line-through; text-decoration: line-through;
} }
...@@ -264,21 +227,18 @@ label[for='toggle-all'] { ...@@ -264,21 +227,18 @@ label[for='toggle-all'] {
width: 40px; width: 40px;
height: 40px; height: 40px;
margin: auto 0; margin: auto 0;
font-size: 22px; font-size: 30px;
color: #a88a8a; color: #cc9a9a;
-webkit-transition: all 0.2s; margin-bottom: 11px;
transition: all 0.2s; transition: color 0.2s ease-out;
} }
#todo-list li .destroy:hover { #todo-list li .destroy:hover {
text-shadow: 0 0 1px #000, color: #af5b5e;
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
transform: scale(1.3);
} }
#todo-list li .destroy:after { #todo-list li .destroy:after {
content: ''; content: '×';
} }
#todo-list li:hover .destroy { #todo-list li:hover .destroy {
...@@ -295,29 +255,25 @@ label[for='toggle-all'] { ...@@ -295,29 +255,25 @@ label[for='toggle-all'] {
#footer { #footer {
color: #777; color: #777;
padding: 0 15px; padding: 10px 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px; height: 20px;
z-index: 1;
text-align: center; text-align: center;
border-top: 1px solid #e6e6e6;
} }
#footer:before { #footer:before {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 31px; bottom: 0;
left: 0; left: 0;
height: 50px; height: 50px;
z-index: -1; overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 6px 0 -3px rgba(255, 255, 255, 0.8), 0 8px 0 -3px #f6f6f6,
0 7px 1px -3px rgba(0, 0, 0, 0.3), 0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 43px 0 -6px rgba(255, 255, 255, 0.8), 0 16px 0 -6px #f6f6f6,
0 44px 2px -6px rgba(0, 0, 0, 0.2); 0 17px 2px -6px rgba(0, 0, 0, 0.2);
} }
#todo-count { #todo-count {
...@@ -325,6 +281,10 @@ label[for='toggle-all'] { ...@@ -325,6 +281,10 @@ label[for='toggle-all'] {
text-align: left; text-align: left;
} }
#todo-count strong {
font-weight: 300;
}
#filters { #filters {
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -339,49 +299,72 @@ label[for='toggle-all'] { ...@@ -339,49 +299,72 @@ label[for='toggle-all'] {
} }
#filters li a { #filters li a {
color: #83756f; color: inherit;
margin: 2px; margin: 3px;
padding: 3px 7px;
text-decoration: none; text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
} }
#filters li a.selected { #filters li a.selected {
font-weight: bold; border-color: rgba(175, 47, 47, 0.2);
} }
#clear-completed { #clear-completed,
html #clear-completed:active {
float: right; float: right;
position: relative; position: relative;
line-height: 20px; line-height: 20px;
text-decoration: none; text-decoration: none;
background: rgba(0, 0, 0, 0.1); cursor: pointer;
font-size: 11px; visibility: hidden;
padding: 0 10px; position: relative;
border-radius: 3px; }
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
right: 0;
white-space: nowrap;
} }
#clear-completed:hover { #clear-completed:hover::after {
background: rgba(0, 0, 0, 0.15); text-decoration: underline;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
} }
#info { #info {
margin: 65px auto 0; margin: 65px auto 0;
color: #a6a6a6; color: #bfbfbf;
font-size: 12px; font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center; text-align: center;
} }
#info p {
line-height: 1;
}
#info a { #info a {
color: inherit; color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
text-decoration: underline;
} }
/* /*
Hack to remove background from Mobile Safari. Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera Can't use it globally since it destroys checkboxes in Firefox
*/ */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all, #toggle-all,
#todo-list li .toggle { #todo-list li .toggle {
...@@ -393,10 +376,6 @@ label[for='toggle-all'] { ...@@ -393,10 +376,6 @@ label[for='toggle-all'] {
} }
#toggle-all { #toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
transform: rotate(90deg); transform: rotate(90deg);
-webkit-appearance: none; -webkit-appearance: none;
...@@ -404,151 +383,12 @@ label[for='toggle-all'] { ...@@ -404,151 +383,12 @@ label[for='toggle-all'] {
} }
} }
.hidden { @media (max-width: 430px) {
display: none; #footer {
} height: 50px;
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
} }
.learn-bar #todoapp { #filters {
width: 550px; bottom: 10px;
margin: 130px auto 40px auto;
} }
} }
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}
/* global _ */
(function () { (function () {
'use strict'; 'use strict';
/* jshint ignore:start */
// Underscore's Template Module // Underscore's Template Module
// Courtesy of underscorejs.org // Courtesy of underscorejs.org
var _ = (function (_) { var _ = (function (_) {
...@@ -114,6 +116,7 @@ ...@@ -114,6 +116,7 @@
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')); 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'));
} }
/* jshint ignore:end */
function redirect() { function redirect() {
if (location.hostname === 'tastejs.github.io') { if (location.hostname === 'tastejs.github.io') {
...@@ -175,13 +178,17 @@ ...@@ -175,13 +178,17 @@
if (learnJSON.backend) { if (learnJSON.backend) {
this.frameworkJSON = learnJSON.backend; this.frameworkJSON = learnJSON.backend;
this.frameworkJSON.issueLabel = framework;
this.append({ this.append({
backend: true backend: true
}); });
} else if (learnJSON[framework]) { } else if (learnJSON[framework]) {
this.frameworkJSON = learnJSON[framework]; this.frameworkJSON = learnJSON[framework];
this.frameworkJSON.issueLabel = framework;
this.append(); this.append();
} }
this.fetchIssueCount();
} }
Learn.prototype.append = function (opts) { Learn.prototype.append = function (opts) {
...@@ -212,6 +219,26 @@ ...@@ -212,6 +219,26 @@
document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
}; };
Learn.prototype.fetchIssueCount = function () {
var issueLink = document.getElementById('issue-count-link');
if (issueLink) {
var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
}
}
};
xhr.send();
}
};
redirect(); redirect();
getFile('learn.json', Learn); getFile('learn.json', Learn);
})(); })();
// Underscore.js 1.4.4 // Underscore.js 1.7.0
// http://underscorejs.org // http://underscorejs.org
// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2009-2014 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() {
...@@ -8,20 +8,18 @@ ...@@ -8,20 +8,18 @@
// Baseline setup // Baseline setup
// -------------- // --------------
// Establish the root object, `window` in the browser, or `global` on the server. // Establish the root object, `window` in the browser, or `exports` on the server.
var root = this; var root = this;
// Save the previous value of the `_` variable. // Save the previous value of the `_` variable.
var previousUnderscore = root._; var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version: // Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes. // Create quick reference variables for speed access to core prototypes.
var push = ArrayProto.push, var
push = ArrayProto.push,
slice = ArrayProto.slice, slice = ArrayProto.slice,
concat = ArrayProto.concat, concat = ArrayProto.concat,
toString = ObjProto.toString, toString = ObjProto.toString,
...@@ -30,15 +28,6 @@ ...@@ -30,15 +28,6 @@
// All **ECMAScript 5** native function implementations that we hope to use // All **ECMAScript 5** native function implementations that we hope to use
// are declared here. // are declared here.
var var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray, nativeIsArray = Array.isArray,
nativeKeys = Object.keys, nativeKeys = Object.keys,
nativeBind = FuncProto.bind; nativeBind = FuncProto.bind;
...@@ -52,8 +41,7 @@ ...@@ -52,8 +41,7 @@
// Export the Underscore object for **Node.js**, with // Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in // backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier, // the browser, add `_` as a global object.
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') { if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) { if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _; exports = module.exports = _;
...@@ -64,98 +52,125 @@ ...@@ -64,98 +52,125 @@
} }
// Current version. // Current version.
_.VERSION = '1.4.4'; _.VERSION = '1.7.0';
// Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other Underscore
// functions.
var createCallback = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
case 2: return function(value, other) {
return func.call(context, value, other);
};
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
// A mostly-internal function to generate callbacks that can be applied
// to each element in a collection, returning the desired result — either
// identity, an arbitrary callback, a property matcher, or a property accessor.
_.iteratee = function(value, context, argCount) {
if (value == null) return _.identity;
if (_.isFunction(value)) return createCallback(value, context, argCount);
if (_.isObject(value)) return _.matches(value);
return _.property(value);
};
// Collection Functions // Collection Functions
// -------------------- // --------------------
// The cornerstone, an `each` implementation, aka `forEach`. // The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects. // Handles raw objects in addition to array-likes. Treats all
// Delegates to **ECMAScript 5**'s native `forEach` if available. // sparse array-likes as if they were dense.
var each = _.each = _.forEach = function(obj, iterator, context) { _.each = _.forEach = function(obj, iteratee, context) {
if (obj == null) return; if (obj == null) return obj;
if (nativeForEach && obj.forEach === nativeForEach) { iteratee = createCallback(iteratee, context);
obj.forEach(iterator, context); var i, length = obj.length;
} else if (obj.length === +obj.length) { if (length === +length) {
for (var i = 0, l = obj.length; i < l; i++) { for (i = 0; i < length; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return; iteratee(obj[i], i, obj);
} }
} else { } else {
for (var key in obj) { var keys = _.keys(obj);
if (_.has(obj, key)) { for (i = 0, length = keys.length; i < length; i++) {
if (iterator.call(context, obj[key], key, obj) === breaker) return; iteratee(obj[keys[i]], keys[i], obj);
}
} }
} }
return obj;
}; };
// Return the results of applying the iterator to each element. // Return the results of applying the iteratee to each element.
// Delegates to **ECMAScript 5**'s native `map` if available. _.map = _.collect = function(obj, iteratee, context) {
_.map = _.collect = function(obj, iterator, context) { if (obj == null) return [];
var results = []; iteratee = _.iteratee(iteratee, context);
if (obj == null) return results; var keys = obj.length !== +obj.length && _.keys(obj),
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); length = (keys || obj).length,
each(obj, function(value, index, list) { results = Array(length),
results[results.length] = iterator.call(context, value, index, list); currentKey;
}); for (var index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results; return results;
}; };
var reduceError = 'Reduce of empty array with no initial value'; var reduceError = 'Reduce of empty array with no initial value';
// **Reduce** builds up a single result from a list of values, aka `inject`, // **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. // or `foldl`.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = []; if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) { iteratee = createCallback(iteratee, context, 4);
if (context) iterator = _.bind(iterator, context); var keys = obj.length !== +obj.length && _.keys(obj),
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); length = (keys || obj).length,
} index = 0, currentKey;
each(obj, function(value, index, list) { if (arguments.length < 3) {
if (!initial) { if (!length) throw new TypeError(reduceError);
memo = value; memo = obj[keys ? keys[index++] : index++];
initial = true; }
} else { for (; index < length; index++) {
memo = iterator.call(context, memo, value, index, list); currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
} }
});
if (!initial) throw new TypeError(reduceError);
return memo; return memo;
}; };
// The right-associative version of reduce, also known as `foldr`. // The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iteratee, memo, context) {
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = []; if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { iteratee = createCallback(iteratee, context, 4);
if (context) iterator = _.bind(iterator, context); var keys = obj.length !== + obj.length && _.keys(obj),
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); index = (keys || obj).length,
} currentKey;
var length = obj.length; if (arguments.length < 3) {
if (length !== +length) { if (!index) throw new TypeError(reduceError);
var keys = _.keys(obj); memo = obj[keys ? keys[--index] : --index];
length = keys.length;
} }
each(obj, function(value, index, list) { while (index--) {
index = keys ? keys[--length] : --length; currentKey = keys ? keys[index] : index;
if (!initial) { memo = iteratee(memo, obj[currentKey], currentKey, obj);
memo = obj[index];
initial = true;
} else {
memo = iterator.call(context, memo, obj[index], index, list);
} }
});
if (!initial) throw new TypeError(reduceError);
return memo; return memo;
}; };
// 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, iterator, context) { _.find = _.detect = function(obj, predicate, context) {
var result; var result;
any(obj, function(value, index, list) { predicate = _.iteratee(predicate, context);
if (iterator.call(context, value, index, list)) { _.some(obj, function(value, index, list) {
if (predicate(value, index, list)) {
result = value; result = value;
return true; return true;
} }
...@@ -164,61 +179,58 @@ ...@@ -164,61 +179,58 @@
}; };
// Return all the elements that pass a truth test. // Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`. // Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) { _.filter = _.select = function(obj, predicate, context) {
var results = []; var results = [];
if (obj == null) return results; if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); predicate = _.iteratee(predicate, context);
each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value; if (predicate(value, index, list)) results.push(value);
}); });
return results; return results;
}; };
// Return all the elements for which a truth test fails. // Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) { _.reject = function(obj, predicate, context) {
return _.filter(obj, function(value, index, list) { return _.filter(obj, _.negate(_.iteratee(predicate)), context);
return !iterator.call(context, value, index, list);
}, context);
}; };
// Determine whether all of the elements match a truth test. // Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`. // Aliased as `all`.
_.every = _.all = function(obj, iterator, context) { _.every = _.all = function(obj, predicate, context) {
iterator || (iterator = _.identity); if (obj == null) return true;
var result = true; predicate = _.iteratee(predicate, context);
if (obj == null) return result; var keys = obj.length !== +obj.length && _.keys(obj),
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); length = (keys || obj).length,
each(obj, function(value, index, list) { index, currentKey;
if (!(result = result && iterator.call(context, value, index, list))) return breaker; for (index = 0; index < length; index++) {
}); currentKey = keys ? keys[index] : index;
return !!result; if (!predicate(obj[currentKey], currentKey, obj)) return false;
}
return true;
}; };
// 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.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`. // Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) { _.some = _.any = function(obj, predicate, context) {
iterator || (iterator = _.identity); if (obj == null) return false;
var result = false; predicate = _.iteratee(predicate, context);
if (obj == null) return result; var keys = obj.length !== +obj.length && _.keys(obj),
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); length = (keys || obj).length,
each(obj, function(value, index, list) { index, currentKey;
if (result || (result = iterator.call(context, value, index, list))) return breaker; for (index = 0; index < length; index++) {
}); currentKey = keys ? keys[index] : index;
return !!result; if (predicate(obj[currentKey], currentKey, obj)) return true;
}
return false;
}; };
// Determine if the array or object contains a given value (using `===`). // Determine if the array or object contains a given value (using `===`).
// Aliased as `include`. // Aliased as `include`.
_.contains = _.include = function(obj, target) { _.contains = _.include = function(obj, target) {
if (obj == null) return false; if (obj == null) return false;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; if (obj.length !== +obj.length) obj = _.values(obj);
return any(obj, function(value) { return _.indexOf(obj, target) >= 0;
return value === target;
});
}; };
// Invoke a method (with arguments) on every item in a collection. // Invoke a method (with arguments) on every item in a collection.
...@@ -232,83 +244,104 @@ ...@@ -232,83 +244,104 @@
// Convenience version of a common use case of `map`: fetching a property. // Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) { _.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; }); return _.map(obj, _.property(key));
}; };
// 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, first) { _.where = function(obj, attrs) {
if (_.isEmpty(attrs)) return first ? null : []; return _.filter(obj, _.matches(attrs));
return _[first ? 'find' : 'filter'](obj, function(value) {
for (var key in attrs) {
if (attrs[key] !== value[key]) return false;
}
return true;
});
}; };
// 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 _.where(obj, attrs, true); return _.find(obj, _.matches(attrs));
}; };
// Return the maximum element or (element-based computation). // Return the maximum element (or element-based computation).
// Can't optimize arrays of integers longer than 65,535 elements. _.max = function(obj, iteratee, context) {
// See: https://bugs.webkit.org/show_bug.cgi?id=80797 var result = -Infinity, lastComputed = -Infinity,
_.max = function(obj, iterator, context) { value, computed;
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { if (iteratee == null && obj != null) {
return Math.max.apply(Math, obj); obj = obj.length === +obj.length ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value > result) {
result = value;
}
}
} else {
iteratee = _.iteratee(iteratee, context);
_.each(obj, function(value, index, list) {
computed = iteratee(value, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
result = value;
lastComputed = computed;
} }
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity, value: -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
}); });
return result.value; }
return result;
}; };
// Return the minimum element (or element-based computation). // Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) { _.min = function(obj, iteratee, context) {
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { var result = Infinity, lastComputed = Infinity,
return Math.min.apply(Math, obj); value, computed;
} if (iteratee == null && obj != null) {
if (!iterator && _.isEmpty(obj)) return Infinity; obj = obj.length === +obj.length ? obj : _.values(obj);
var result = {computed : Infinity, value: Infinity}; for (var i = 0, length = obj.length; i < length; i++) {
each(obj, function(value, index, list) { value = obj[i];
var computed = iterator ? iterator.call(context, value, index, list) : value; if (value < result) {
computed < result.computed && (result = {value : value, computed : computed}); result = value;
}
}
} else {
iteratee = _.iteratee(iteratee, context);
_.each(obj, function(value, index, list) {
computed = iteratee(value, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) {
result = value;
lastComputed = computed;
}
}); });
return result.value; }
return result;
}; };
// Shuffle an array. // Shuffle a collection, using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
_.shuffle = function(obj) { _.shuffle = function(obj) {
var rand; var set = obj && obj.length === +obj.length ? obj : _.values(obj);
var index = 0; var length = set.length;
var shuffled = []; var shuffled = Array(length);
each(obj, function(value) { for (var index = 0, rand; index < length; index++) {
rand = _.random(index++); rand = _.random(0, index);
shuffled[index - 1] = shuffled[rand]; if (rand !== index) shuffled[index] = shuffled[rand];
shuffled[rand] = value; shuffled[rand] = set[index];
}); }
return shuffled; return shuffled;
}; };
// An internal function to generate lookup iterators. // Sample **n** random values from a collection.
var lookupIterator = function(value) { // If **n** is not specified, returns a single random element.
return _.isFunction(value) ? value : function(obj){ return obj[value]; }; // The internal `guard` argument allows it to work with `map`.
_.sample = function(obj, n, guard) {
if (n == null || guard) {
if (obj.length !== +obj.length) obj = _.values(obj);
return obj[_.random(obj.length - 1)];
}
return _.shuffle(obj).slice(0, Math.max(0, n));
}; };
// Sort the object's values by a criterion produced by an iterator. // Sort the object's values by a criterion produced by an iteratee.
_.sortBy = function(obj, value, context) { _.sortBy = function(obj, iteratee, context) {
var iterator = lookupIterator(value); iteratee = _.iteratee(iteratee, context);
return _.pluck(_.map(obj, function(value, index, list) { return _.pluck(_.map(obj, function(value, index, list) {
return { return {
value : value, value: value,
index : index, index: index,
criteria : iterator.call(context, value, index, list) criteria: iteratee(value, index, list)
}; };
}).sort(function(left, right) { }).sort(function(left, right) {
var a = left.criteria; var a = left.criteria;
...@@ -317,53 +350,56 @@ ...@@ -317,53 +350,56 @@
if (a > b || a === void 0) return 1; if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1; if (a < b || b === void 0) return -1;
} }
return left.index < right.index ? -1 : 1; return left.index - right.index;
}), 'value'); }), 'value');
}; };
// An internal function used for aggregate "group by" operations. // An internal function used for aggregate "group by" operations.
var group = function(obj, value, context, behavior) { var group = function(behavior) {
return function(obj, iteratee, context) {
var result = {}; var result = {};
var iterator = lookupIterator(value || _.identity); iteratee = _.iteratee(iteratee, context);
each(obj, function(value, index) { _.each(obj, function(value, index) {
var key = iterator.call(context, value, index, obj); var key = iteratee(value, index, obj);
behavior(result, key, value); behavior(result, value, key);
}); });
return result; return result;
}; };
};
// Groups the object's values by a criterion. Pass either a string attribute // Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion. // to group by, or a function that returns the criterion.
_.groupBy = function(obj, value, context) { _.groupBy = group(function(result, value, key) {
return group(obj, value, context, function(result, key, value) { if (_.has(result, key)) result[key].push(value); else result[key] = [value];
(_.has(result, key) ? result[key] : (result[key] = [])).push(value); });
// Indexes the object's values by a criterion, similar to `groupBy`, but for
// when you know that your index values will be unique.
_.indexBy = group(function(result, value, key) {
result[key] = value;
}); });
};
// Counts instances of an object that group by a certain criterion. Pass // Counts instances of an object that group by a certain criterion. Pass
// either a string attribute to count by, or a function that returns the // either a string attribute to count by, or a function that returns the
// criterion. // criterion.
_.countBy = function(obj, value, context) { _.countBy = group(function(result, value, key) {
return group(obj, value, context, function(result, key) { if (_.has(result, key)) result[key]++; else result[key] = 1;
if (!_.has(result, key)) result[key] = 0;
result[key]++;
}); });
};
// Use a comparator function to figure out the smallest index at which // 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. // an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator, context) { _.sortedIndex = function(array, obj, iteratee, context) {
iterator = iterator == null ? _.identity : lookupIterator(iterator); iteratee = _.iteratee(iteratee, context, 1);
var value = iterator.call(context, obj); var value = iteratee(obj);
var low = 0, high = array.length; var low = 0, high = array.length;
while (low < high) { while (low < high) {
var mid = (low + high) >>> 1; var mid = low + high >>> 1;
iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
} }
return low; return low;
}; };
// Safely convert anything iterable into a real, live array. // 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);
...@@ -374,7 +410,18 @@ ...@@ -374,7 +410,18 @@
// 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 obj.length === +obj.length ? obj.length : _.keys(obj).length;
};
// Split a collection into two arrays: one whose elements all satisfy the given
// predicate, and one whose elements all do not satisfy the predicate.
_.partition = function(obj, predicate, context) {
predicate = _.iteratee(predicate, context);
var pass = [], fail = [];
_.each(obj, function(value, key, obj) {
(predicate(value, key, obj) ? pass : fail).push(value);
});
return [pass, fail];
}; };
// Array Functions // Array Functions
...@@ -385,7 +432,9 @@ ...@@ -385,7 +432,9 @@
// allows it to work with `_.map`. // allows it to work with `_.map`.
_.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;
return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; if (n == null || guard) return array[0];
if (n < 0) return [];
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
...@@ -393,18 +442,15 @@ ...@@ -393,18 +442,15 @@
// the array, excluding the last N. The **guard** check allows it to work with // the array, excluding the last N. The **guard** check allows it to work with
// `_.map`. // `_.map`.
_.initial = function(array, n, guard) { _.initial = function(array, n, guard) {
return slice.call(array, 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. The **guard** check allows it to work with `_.map`.
_.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) { if (n == null || guard) return array[array.length - 1];
return slice.call(array, Math.max(array.length - n, 0)); return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
}; };
// 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`.
...@@ -412,7 +458,7 @@ ...@@ -412,7 +458,7 @@
// the rest N values in the array. The **guard** // the rest N values in the array. The **guard**
// check allows it to work with `_.map`. // 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);
}; };
// Trim out all falsy values from an array. // Trim out all falsy values from an array.
...@@ -421,20 +467,26 @@ ...@@ -421,20 +467,26 @@
}; };
// Internal implementation of a recursive `flatten` function. // Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, output) { var flatten = function(input, shallow, strict, output) {
each(input, function(value) { if (shallow && _.every(input, _.isArray)) {
if (_.isArray(value)) { return concat.apply(output, input);
shallow ? push.apply(output, value) : flatten(value, shallow, output); }
for (var i = 0, length = input.length; i < length; i++) {
var value = input[i];
if (!_.isArray(value) && !_.isArguments(value)) {
if (!strict) output.push(value);
} else if (shallow) {
push.apply(output, value);
} else { } else {
output.push(value); flatten(value, shallow, strict, output);
}
} }
});
return output; return output;
}; };
// Return a completely flattened version of an array. // Flatten out an array, either recursively (by default), or just one level.
_.flatten = function(array, shallow) { _.flatten = function(array, shallow) {
return flatten(array, shallow, []); 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).
...@@ -445,56 +497,74 @@ ...@@ -445,56 +497,74 @@
// Produce a duplicate-free version of the array. If the array has already // Produce a duplicate-free version of the array. If the array has already
// 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, iterator, context) { _.uniq = _.unique = function(array, isSorted, iteratee, context) {
if (_.isFunction(isSorted)) { if (array == null) return [];
context = iterator; if (!_.isBoolean(isSorted)) {
iterator = isSorted; context = iteratee;
iteratee = isSorted;
isSorted = false; isSorted = false;
} }
var initial = iterator ? _.map(array, iterator, context) : array; if (iteratee != null) iteratee = _.iteratee(iteratee, context);
var results = []; var result = [];
var seen = []; var seen = [];
each(initial, function(value, index) { for (var i = 0, length = array.length; i < length; i++) {
if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { var value = array[i];
seen.push(value); if (isSorted) {
results.push(array[index]); if (!i || seen !== value) result.push(value);
seen = value;
} else if (iteratee) {
var computed = iteratee(value, i, array);
if (_.indexOf(seen, computed) < 0) {
seen.push(computed);
result.push(value);
} }
}); } else if (_.indexOf(result, value) < 0) {
return results; result.push(value);
}
}
return result;
}; };
// 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(concat.apply(ArrayProto, arguments)); 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) {
var rest = slice.call(arguments, 1); if (array == null) return [];
return _.filter(_.uniq(array), function(item) { var result = [];
return _.every(rest, function(other) { var argsLength = arguments.length;
return _.indexOf(other, item) >= 0; for (var i = 0, length = array.length; i < length; i++) {
}); var item = array[i];
}); if (_.contains(result, item)) continue;
for (var j = 1; j < argsLength; j++) {
if (!_.contains(arguments[j], item)) break;
}
if (j === argsLength) result.push(item);
}
return result;
}; };
// 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 = concat.apply(ArrayProto, slice.call(arguments, 1)); var rest = flatten(slice.call(arguments, 1), true, true, []);
return _.filter(array, function(value){ return !_.contains(rest, value); }); return _.filter(array, function(value){
return !_.contains(rest, value);
});
}; };
// 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() { _.zip = function(array) {
var args = slice.call(arguments); if (array == null) return [];
var length = _.max(_.pluck(args, 'length')); var length = _.max(arguments, 'length').length;
var results = new Array(length); var results = Array(length);
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
results[i] = _.pluck(args, "" + i); results[i] = _.pluck(arguments, i);
} }
return results; return results;
}; };
...@@ -505,7 +575,7 @@ ...@@ -505,7 +575,7 @@
_.object = function(list, values) { _.object = function(list, values) {
if (list == null) return {}; if (list == null) return {};
var result = {}; var result = {};
for (var i = 0, l = list.length; i < l; i++) { for (var i = 0, length = list.length; i < length; i++) {
if (values) { if (values) {
result[list[i]] = values[i]; result[list[i]] = values[i];
} else { } else {
...@@ -515,37 +585,32 @@ ...@@ -515,37 +585,32 @@
return result; return result;
}; };
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), // Return the position of the first occurrence of an item in an array,
// we need this function. Return the position of the first occurrence of an // or -1 if the item is not included in the array.
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true` // If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search. // for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) { _.indexOf = function(array, item, isSorted) {
if (array == null) return -1; if (array == null) return -1;
var i = 0, l = array.length; var i = 0, length = array.length;
if (isSorted) { if (isSorted) {
if (typeof isSorted == 'number') { if (typeof isSorted == 'number') {
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
} else { } else {
i = _.sortedIndex(array, item); i = _.sortedIndex(array, item);
return array[i] === item ? i : -1; return array[i] === item ? i : -1;
} }
} }
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); for (; i < length; i++) if (array[i] === item) return i;
for (; i < l; i++) if (array[i] === item) return i;
return -1; return -1;
}; };
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item, from) { _.lastIndexOf = function(array, item, from) {
if (array == null) return -1; if (array == null) return -1;
var hasIndex = from != null; var idx = array.length;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { if (typeof from == 'number') {
return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1);
} }
var i = (hasIndex ? from : array.length); while (--idx >= 0) if (array[idx] === item) return idx;
while (i--) if (array[i] === item) return i;
return -1; return -1;
}; };
...@@ -557,15 +622,13 @@ ...@@ -557,15 +622,13 @@
stop = start || 0; stop = start || 0;
start = 0; start = 0;
} }
step = arguments[2] || 1; step = step || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0); var length = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0; var range = Array(length);
var range = new Array(len);
while(idx < len) { for (var idx = 0; idx < length; idx++, start += step) {
range[idx++] = start; range[idx] = start;
start += step;
} }
return range; return range;
...@@ -574,50 +637,77 @@ ...@@ -574,50 +637,77 @@
// Function (ahem) Functions // Function (ahem) Functions
// ------------------ // ------------------
// Reusable constructor function for prototype setting.
var Ctor = function(){};
// 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) {
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); var args, bound;
var args = slice.call(arguments, 2); if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
return function() { if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
return func.apply(context, args.concat(slice.call(arguments))); args = slice.call(arguments, 2);
}; bound = function() {
if (!(this instanceof bound)) return func.apply(context, 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;
}; };
// Partially apply a function by creating a version that has had some of its // Partially apply a function by creating a version that has had some of its
// arguments pre-filled, without changing its dynamic `this` context. // arguments pre-filled, without changing its dynamic `this` context. _ acts
// as a placeholder, allowing any combination of arguments to be pre-filled.
_.partial = function(func) { _.partial = function(func) {
var args = slice.call(arguments, 1); var boundArgs = slice.call(arguments, 1);
return function() { return function() {
return func.apply(this, args.concat(slice.call(arguments))); var position = 0;
var args = boundArgs.slice();
for (var i = 0, length = args.length; i < length; i++) {
if (args[i] === _) args[i] = arguments[position++];
}
while (position < arguments.length) args.push(arguments[position++]);
return func.apply(this, args);
}; };
}; };
// Bind all of an object's methods to that object. Useful for ensuring that // Bind a number of an object's methods to that object. Remaining arguments
// all callbacks defined on an object belong to it. // are the method names to be bound. Useful for ensuring that all callbacks
// defined on an object belong to it.
_.bindAll = function(obj) { _.bindAll = function(obj) {
var funcs = slice.call(arguments, 1); var i, length = arguments.length, key;
if (funcs.length === 0) funcs = _.functions(obj); if (length <= 1) throw new Error('bindAll must be passed function names');
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); for (i = 1; i < length; i++) {
key = arguments[i];
obj[key] = _.bind(obj[key], obj);
}
return obj; return obj;
}; };
// Memoize an expensive function by storing its results. // Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) { _.memoize = function(func, hasher) {
var memo = {}; var memoize = function(key) {
hasher || (hasher = _.identity); var cache = memoize.cache;
return function() { var address = hasher ? hasher.apply(this, arguments) : key;
var key = hasher.apply(this, arguments); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); return cache[address];
}; };
memoize.cache = {};
return memoize;
}; };
// Delays a function for the given number of milliseconds, and then calls // Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied. // it with the arguments supplied.
_.delay = function(func, wait) { _.delay = function(func, wait) {
var args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(null, args); }, wait); return setTimeout(function(){
return func.apply(null, args);
}, wait);
}; };
// 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
...@@ -627,26 +717,34 @@ ...@@ -627,26 +717,34 @@
}; };
// 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. // during a given window of time. Normally, the throttled function will run
_.throttle = function(func, wait) { // as much as it can, without ever going more than once per `wait` duration;
var context, args, timeout, result; // but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0; var previous = 0;
if (!options) options = {};
var later = function() { var later = function() {
previous = new Date; previous = options.leading === false ? 0 : _.now();
timeout = null; timeout = null;
result = func.apply(context, args); result = func.apply(context, args);
if (!timeout) context = args = null;
}; };
return function() { return function() {
var now = new Date; var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous); var remaining = wait - (now - previous);
context = this; context = this;
args = arguments; args = arguments;
if (remaining <= 0) { if (remaining <= 0 || remaining > wait) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = null; timeout = null;
previous = now; previous = now;
result = func.apply(context, args); result = func.apply(context, args);
} else if (!timeout) { if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining); timeout = setTimeout(later, remaining);
} }
return result; return result;
...@@ -658,31 +756,34 @@ ...@@ -658,31 +756,34 @@
// N milliseconds. If `immediate` is passed, trigger the function on the // N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing. // leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) { _.debounce = function(func, wait, immediate) {
var timeout, result; var timeout, args, context, timestamp, result;
return function() {
var context = this, args = arguments;
var later = function() { var later = function() {
var last = _.now() - timestamp;
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null; timeout = null;
if (!immediate) result = func.apply(context, args); if (!immediate) {
}; result = func.apply(context, args);
var callNow = immediate && !timeout; if (!timeout) context = args = null;
clearTimeout(timeout); }
timeout = setTimeout(later, wait); }
if (callNow) result = func.apply(context, args);
return result;
};
}; };
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() { return function() {
if (ran) return memo; context = this;
ran = true; args = arguments;
memo = func.apply(this, arguments); timestamp = _.now();
func = null; var callNow = immediate && !timeout;
return memo; if (!timeout) timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
context = args = null;
}
return result;
}; };
}; };
...@@ -690,29 +791,31 @@ ...@@ -690,29 +791,31 @@
// allowing you to adjust arguments, run code before and after, and // allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function. // conditionally execute the original function.
_.wrap = function(func, wrapper) { _.wrap = function(func, wrapper) {
return _.partial(wrapper, func);
};
// Returns a negated version of the passed-in predicate.
_.negate = function(predicate) {
return function() { return function() {
var args = [func]; return !predicate.apply(this, arguments);
push.apply(args, arguments);
return wrapper.apply(this, args);
}; };
}; };
// Returns a function that is the composition of a list of functions, each // Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows. // consuming the return value of the function that follows.
_.compose = function() { _.compose = function() {
var funcs = arguments;
return function() {
var args = arguments; var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) { var start = args.length - 1;
args = [funcs[i].apply(this, args)]; return function() {
} var i = start;
return args[0]; var result = args[start].apply(this, arguments);
while (i--) result = args[i].call(this, result);
return result;
}; };
}; };
// Returns a function that will only be executed after being called N times. // Returns a function that will only be executed after being called N times.
_.after = function(times, func) { _.after = function(times, func) {
if (times <= 0) return func();
return function() { return function() {
if (--times < 1) { if (--times < 1) {
return func.apply(this, arguments); return func.apply(this, arguments);
...@@ -720,36 +823,65 @@ ...@@ -720,36 +823,65 @@
}; };
}; };
// Returns a function that will only be executed before being called N times.
_.before = function(times, func) {
var memo;
return function() {
if (--times > 0) {
memo = func.apply(this, arguments);
} else {
func = null;
}
return memo;
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = _.partial(_.before, 2);
// Object Functions // Object Functions
// ---------------- // ----------------
// Retrieve the names of an object's properties. // Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys` // Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) { _.keys = function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object'); if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj);
var keys = []; var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; for (var key in obj) if (_.has(obj, key)) keys.push(key);
return keys; return keys;
}; };
// Retrieve the values of an object's properties. // Retrieve the values of an object's properties.
_.values = function(obj) { _.values = function(obj) {
var values = []; var keys = _.keys(obj);
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values; return values;
}; };
// 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 pairs = []; var keys = _.keys(obj);
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); var length = keys.length;
var pairs = Array(length);
for (var i = 0; i < length; i++) {
pairs[i] = [keys[i], obj[keys[i]]];
}
return pairs; return pairs;
}; };
// Invert the keys and values of an object. The values must be serializable. // Invert the keys and values of an object. The values must be serializable.
_.invert = function(obj) { _.invert = function(obj) {
var result = {}; var result = {};
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
result[obj[keys[i]]] = keys[i];
}
return result; return result;
}; };
...@@ -765,45 +897,62 @@ ...@@ -765,45 +897,62 @@
// 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 = function(obj) {
each(slice.call(arguments, 1), function(source) { if (!_.isObject(obj)) return obj;
if (source) { var source, prop;
for (var prop in source) { for (var i = 1, length = arguments.length; i < length; i++) {
source = arguments[i];
for (prop in source) {
if (hasOwnProperty.call(source, prop)) {
obj[prop] = source[prop]; obj[prop] = source[prop];
} }
} }
}); }
return obj; 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) { _.pick = function(obj, iteratee, context) {
var copy = {}; var result = {}, key;
var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); if (obj == null) return result;
each(keys, function(key) { if (_.isFunction(iteratee)) {
if (key in obj) copy[key] = obj[key]; iteratee = createCallback(iteratee, context);
}); for (key in obj) {
return copy; var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
}
} else {
var keys = concat.apply([], slice.call(arguments, 1));
obj = new Object(obj);
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (key in obj) result[key] = obj[key];
}
}
return result;
}; };
// Return a copy of the object without the blacklisted properties. // Return a copy of the object without the blacklisted properties.
_.omit = function(obj) { _.omit = function(obj, iteratee, context) {
var copy = {}; if (_.isFunction(iteratee)) {
var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); iteratee = _.negate(iteratee);
for (var key in obj) { } else {
if (!_.contains(keys, key)) copy[key] = obj[key]; var keys = _.map(concat.apply([], slice.call(arguments, 1)), String);
iteratee = function(value, key) {
return !_.contains(keys, key);
};
} }
return copy; return _.pick(obj, iteratee, context);
}; };
// Fill in a given object with default properties. // Fill in a given object with default properties.
_.defaults = function(obj) { _.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) { if (!_.isObject(obj)) return obj;
if (source) { for (var i = 1, length = arguments.length; i < length; i++) {
var source = arguments[i];
for (var prop in source) { for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop]; if (obj[prop] === void 0) obj[prop] = source[prop];
} }
} }
});
return obj; return obj;
}; };
...@@ -824,8 +973,8 @@ ...@@ -824,8 +973,8 @@
// 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.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a == 1 / b; if (a === b) return a !== 0 || 1 / a === 1 / b;
// A strict comparison is necessary because `null == undefined`. // A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b; if (a == null || b == null) return a === b;
// Unwrap any wrapped objects. // Unwrap any wrapped objects.
...@@ -833,29 +982,27 @@ ...@@ -833,29 +982,27 @@
if (b instanceof _) b = b._wrapped; if (b instanceof _) b = b._wrapped;
// Compare `[[Class]]` names. // Compare `[[Class]]` names.
var className = toString.call(a); var className = toString.call(a);
if (className != toString.call(b)) return false; if (className !== toString.call(b)) return false;
switch (className) { switch (className) {
// Strings, numbers, dates, and booleans are compared by value. // Strings, numbers, regular expressions, dates, and booleans are compared by value.
case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
case '[object String]': case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`. // equivalent to `new String("5")`.
return a == String(b); return '' + a === '' + b;
case '[object Number]': case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // `NaN`s are equivalent, but non-reflexive.
// other numeric values. // Object(NaN) is equivalent to NaN
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]': case '[object Date]':
case '[object Boolean]': case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their // Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations // millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent. // of `NaN` are not equivalent.
return +a == +b; return +a === +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
} }
if (typeof a != 'object' || typeof b != 'object') return false; if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic // Assume equality for cyclic structures. The algorithm for detecting cyclic
...@@ -864,17 +1011,29 @@ ...@@ -864,17 +1011,29 @@
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 = 0, result = true; var size, result;
// Recursively compare objects and arrays. // Recursively compare objects and arrays.
if (className == '[object Array]') { if (className === '[object Array]') {
// 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; size = a.length;
result = size == b.length; result = size === b.length;
if (result) { if (result) {
// Deep compare the contents, ignoring non-numeric properties. // Deep compare the contents, ignoring non-numeric properties.
while (size--) { while (size--) {
...@@ -882,28 +1041,17 @@ ...@@ -882,28 +1041,17 @@
} }
} }
} else { } else {
// Objects with different constructors are not equivalent, but `Object`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))) {
return false;
}
// Deep compare objects. // Deep compare objects.
for (var key in a) { var keys = _.keys(a), key;
if (_.has(a, key)) { size = keys.length;
// Count the expected number of properties. // Ensure that both objects contain the same number of properties before comparing deep equality.
size++; result = _.keys(b).length === size;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) { if (result) {
for (key in b) { while (size--) {
if (_.has(b, key) && !(size--)) break; // Deep compare each member
key = keys[size];
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
} }
result = !size;
} }
} }
// Remove the first object from the stack of traversed objects. // Remove the first object from the stack of traversed objects.
...@@ -921,7 +1069,7 @@ ...@@ -921,7 +1069,7 @@
// 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)) return obj.length === 0; if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false; for (var key in obj) if (_.has(obj, key)) return false;
return true; return true;
}; };
...@@ -934,18 +1082,19 @@ ...@@ -934,18 +1082,19 @@
// Is a given value an array? // Is a given value an array?
// Delegates to ECMA5's native Array.isArray // Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) { _.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]'; return toString.call(obj) === '[object Array]';
}; };
// Is a given variable an object? // Is a given variable an object?
_.isObject = function(obj) { _.isObject = function(obj) {
return obj === Object(obj); var type = typeof 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.
each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
_['is' + name] = function(obj) { _['is' + name] = function(obj) {
return toString.call(obj) == '[object ' + name + ']'; return toString.call(obj) === '[object ' + name + ']';
}; };
}); });
...@@ -953,14 +1102,14 @@ ...@@ -953,14 +1102,14 @@
// 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) {
return !!(obj && _.has(obj, 'callee')); return _.has(obj, 'callee');
}; };
} }
// Optimize `isFunction` if appropriate. // Optimize `isFunction` if appropriate. Work around an IE 11 bug.
if (typeof (/./) !== 'function') { if (typeof /./ !== 'function') {
_.isFunction = function(obj) { _.isFunction = function(obj) {
return typeof obj === 'function'; return typeof obj == 'function' || false;
}; };
} }
...@@ -971,12 +1120,12 @@ ...@@ -971,12 +1120,12 @@
// Is the given value `NaN`? (NaN is the only number which does not equal itself). // Is the given value `NaN`? (NaN is the only number which does not equal itself).
_.isNaN = function(obj) { _.isNaN = function(obj) {
return _.isNumber(obj) && obj != +obj; return _.isNumber(obj) && obj !== +obj;
}; };
// Is a given value a boolean? // Is a given value a boolean?
_.isBoolean = function(obj) { _.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
}; };
// Is a given value equal to null? // Is a given value equal to null?
...@@ -992,7 +1141,7 @@ ...@@ -992,7 +1141,7 @@
// Shortcut function for checking if an object has a given property directly // Shortcut function for checking if an object has a given property directly
// on itself (in other words, not on a prototype). // on itself (in other words, not on a prototype).
_.has = function(obj, key) { _.has = function(obj, key) {
return hasOwnProperty.call(obj, key); return obj != null && hasOwnProperty.call(obj, key);
}; };
// Utility Functions // Utility Functions
...@@ -1005,15 +1154,44 @@ ...@@ -1005,15 +1154,44 @@
return this; return this;
}; };
// Keep the identity function around for default iterators. // Keep the identity function around for default iteratees.
_.identity = function(value) { _.identity = function(value) {
return value; return value;
}; };
_.constant = function(value) {
return function() {
return value;
};
};
_.noop = function(){};
_.property = function(key) {
return function(obj) {
return obj[key];
};
};
// Returns a predicate for checking whether an object has a given set of `key:value` pairs.
_.matches = function(attrs) {
var pairs = _.pairs(attrs), length = pairs.length;
return function(obj) {
if (obj == null) return !length;
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, iterator, context) { _.times = function(n, iteratee, context) {
var accum = Array(n); var accum = Array(Math.max(0, n));
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); iteratee = createCallback(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum; return accum;
}; };
...@@ -1026,53 +1204,45 @@ ...@@ -1026,53 +1204,45 @@
return min + Math.floor(Math.random() * (max - min + 1)); return min + Math.floor(Math.random() * (max - min + 1));
}; };
// A (possibly faster) way to get the current timestamp as an integer.
_.now = Date.now || function() {
return new Date().getTime();
};
// List of HTML entities for escaping. // List of HTML entities for escaping.
var entityMap = { var escapeMap = {
escape: {
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
"'": '&#x27;', "'": '&#x27;',
'/': '&#x2F;' '`': '&#x60;'
}
};
entityMap.unescape = _.invert(entityMap.escape);
// Regexes containing the keys and values listed immediately above.
var entityRegexes = {
escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
}; };
var unescapeMap = _.invert(escapeMap);
// Functions for escaping and unescaping strings to/from HTML interpolation. // Functions for escaping and unescaping strings to/from HTML interpolation.
_.each(['escape', 'unescape'], function(method) { var createEscaper = function(map) {
_[method] = function(string) { var escaper = function(match) {
if (string == null) return ''; return map[match];
return ('' + string).replace(entityRegexes[method], function(match) {
return entityMap[method][match];
});
}; };
}); // Regexes for identifying a key that needs to be escaped
var source = '(?:' + _.keys(map).join('|') + ')';
var testRegexp = RegExp(source);
var replaceRegexp = RegExp(source, 'g');
return function(string) {
string = string == null ? '' : '' + string;
return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
};
};
_.escape = createEscaper(escapeMap);
_.unescape = createEscaper(unescapeMap);
// If the value of the named property is a function then invoke it; // If the value of the named `property` is a function then invoke it with the
// otherwise, return it. // `object` as context; otherwise, return it.
_.result = function(object, property) { _.result = function(object, property) {
if (object == null) return null; if (object == null) return void 0;
var value = object[property]; var value = object[property];
return _.isFunction(value) ? value.call(object) : value; return _.isFunction(value) ? object[property]() : value;
};
// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result.call(this, func.apply(_, args));
};
});
}; };
// Generate a unique integer id (unique within the entire client session). // Generate a unique integer id (unique within the entire client session).
...@@ -1103,22 +1273,26 @@ ...@@ -1103,22 +1273,26 @@
'\\': '\\', '\\': '\\',
'\r': 'r', '\r': 'r',
'\n': 'n', '\n': 'n',
'\t': 't',
'\u2028': 'u2028', '\u2028': 'u2028',
'\u2029': 'u2029' '\u2029': 'u2029'
}; };
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
// JavaScript micro-templating, similar to John Resig's implementation. // JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace, // Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code. // and correctly escapes quotes within interpolated code.
_.template = function(text, data, settings) { // NB: `oldSettings` only exists for backwards compatibility.
var render; _.template = function(text, settings, oldSettings) {
if (!settings && oldSettings) settings = oldSettings;
settings = _.defaults({}, settings, _.templateSettings); settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation. // Combine delimiters into one regular expression via alternation.
var matcher = new RegExp([ var matcher = RegExp([
(settings.escape || noMatch).source, (settings.escape || noMatch).source,
(settings.interpolate || noMatch).source, (settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source (settings.evaluate || noMatch).source
...@@ -1128,19 +1302,18 @@ ...@@ -1128,19 +1302,18 @@
var index = 0; var index = 0;
var source = "__p+='"; var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset) source += text.slice(index, offset).replace(escaper, escapeChar);
.replace(escaper, function(match) { return '\\' + escapes[match]; }); index = offset + match.length;
if (escape) { if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} } else if (interpolate) {
if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} } else if (evaluate) {
if (evaluate) {
source += "';\n" + evaluate + "\n__p+='"; source += "';\n" + evaluate + "\n__p+='";
} }
index = offset + match.length;
// Adobe VMs need the match returned to produce the correct offest.
return match; return match;
}); });
source += "';\n"; source += "';\n";
...@@ -1150,29 +1323,31 @@ ...@@ -1150,29 +1323,31 @@
source = "var __t,__p='',__j=Array.prototype.join," + source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" + "print=function(){__p+=__j.call(arguments,'');};\n" +
source + "return __p;\n"; source + 'return __p;\n';
try { try {
render = new Function(settings.variable || 'obj', '_', source); var render = new Function(settings.variable || 'obj', '_', source);
} catch (e) { } catch (e) {
e.source = source; e.source = source;
throw e; throw e;
} }
if (data) return render(data, _);
var template = function(data) { var template = function(data) {
return render.call(this, data, _); return render.call(this, data, _);
}; };
// Provide the compiled function source as a convenience for precompilation. // Provide the compiled source as a convenience for precompilation.
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template; return template;
}; };
// Add a "chain" function, which will delegate to the wrapper. // Add a "chain" function. Start chaining a wrapped Underscore object.
_.chain = function(obj) { _.chain = function(obj) {
return _(obj).chain(); var instance = _(obj);
instance._chain = true;
return instance;
}; };
// OOP // OOP
...@@ -1186,41 +1361,55 @@ ...@@ -1186,41 +1361,55 @@
return this._chain ? _(obj).chain() : obj; return this._chain ? _(obj).chain() : obj;
}; };
// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return result.call(this, func.apply(_, args));
};
});
};
// Add all of the Underscore functions to the wrapper object. // Add all of the Underscore functions to the wrapper object.
_.mixin(_); _.mixin(_);
// Add all mutator Array functions to the wrapper. // Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name]; var method = ArrayProto[name];
_.prototype[name] = function() { _.prototype[name] = function() {
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.call(this, obj);
}; };
}); });
// Add all accessor Array functions to the wrapper. // Add all accessor Array functions to the wrapper.
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.call(this, method.apply(this._wrapped, arguments));
}; };
}); });
_.extend(_.prototype, {
// Start chaining a wrapped Underscore object.
chain: function() {
this._chain = true;
return this;
},
// Extracts the result from a wrapped and chained object. // Extracts the result from a wrapped and chained object.
value: function() { _.prototype.value = function() {
return this._wrapped; return this._wrapped;
} };
// AMD registration happens at the end for compatibility with AMD loaders
// that may not enforce next-turn semantics on modules. Even though general
// practice for AMD registration is to be anonymous, underscore registers
// as a named module because, like jQuery, it is a base library that is
// popular enough to be bundled in a third party lib, but not be part of
// an AMD load request. Those cases could generate an error when an
// anonymous define() is called outside of a loader request.
if (typeof define === 'function' && define.amd) {
define('underscore', [], function() {
return _;
}); });
}
}).call(this); }.call(this));
{
"private": true,
"dependencies": {
"backbone": "^1.1.2",
"backbone.localstorage": "^1.1.6",
"backbone.marionette": "^2.3.2",
"jquery": "^1.11.2",
"todomvc-app-css": "^1.0.1",
"todomvc-common": "^1.0.1",
"underscore": "^1.6.0"
}
}
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