Commit 92328d50 authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #644 from BorisKozo/bugfixes

Marionette - Changes for issue #642 
parents ae3b53c9 4fccfbf2
......@@ -2,9 +2,10 @@
"name": "todomvc-backbone-marionette",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"jquery": "~1.10.2",
"todomvc-common": "~0.1.7",
"underscore": "~1.4.4",
"backbone.localStorage": "~1.1.0",
"backbone.marionette": "~1.0.0-rc6"
"backbone.localStorage": "~1.1.6",
"backbone.marionette": "~1.0.4"
}
}
/**
* Backbone localStorage Adapter
* Version 1.1.0
* Version 1.1.6
*
* https://github.com/jeromegn/Backbone.localStorage
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
if (typeof exports === 'object' && root.require) {
module.exports = factory(require("underscore"), require("backbone"));
} else if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define(["underscore","backbone"], function(_, Backbone) {
// Use global variables if the locals is undefined.
// Use global variables if the locals are undefined.
return factory(_ || root._, Backbone || root.Backbone);
});
} else {
// RequireJS isn't being used. Assume underscore and backbone is loaded in <script> tags
// RequireJS isn't being used. Assume underscore and backbone are loaded in <script> tags
factory(_, Backbone);
}
}(this, function(_, Backbone) {
......@@ -37,6 +39,9 @@ function guid() {
// with a meaningful name, like the name you'd give a table.
// window.Store is deprectated, use Backbone.LocalStorage instead
Backbone.LocalStorage = window.Store = function(name) {
if( !this.localStorage ) {
throw "Backbone.localStorage: Environment does not support localStorage."
}
this.name = name;
var store = this.localStorage().getItem(this.name);
this.records = (store && store.split(",")) || [];
......@@ -77,7 +82,8 @@ _.extend(Backbone.LocalStorage.prototype, {
// Return the array of all models currently in storage.
findAll: function() {
return _(this.records).chain()
// Lodash removed _#chain in v1.0.0-rc.1
return (_.chain || _)(this.records)
.map(function(id){
return this.jsonData(this.localStorage().getItem(this.name+"-"+id));
}, this)
......@@ -104,17 +110,39 @@ _.extend(Backbone.LocalStorage.prototype, {
// 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: function() {
var local = this.localStorage(),
itemRe = new RegExp("^" + this.name + "-");
// Remove id-tracking item (e.g., "foo").
local.removeItem(this.name);
// Lodash removed _#chain in v1.0.0-rc.1
// Match all data items (e.g., "foo-ID") and remove.
(_.chain || _)(local).keys()
.filter(function (k) { return itemRe.test(k); })
.each(function (k) { local.removeItem(k); });
this.records.length = 0;
},
// Size of localStorage.
_storageSize: function() {
return this.localStorage().length;
}
});
// localSync delegate to the model or collection's
// *localStorage* property, which should be an instance of `Store`.
// window.Store.sync and Backbone.localSync is deprectated, 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) {
var store = model.localStorage || model.collection.localStorage;
var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); //If $ is having Deferred - use it.
var resp, errorMessage, syncDfd = Backbone.$.Deferred && Backbone.$.Deferred(); //If $ is having Deferred - use it.
try {
......@@ -134,21 +162,23 @@ Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(m
}
} catch(error) {
if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0)
if (error.code === 22 && store._storageSize() === 0)
errorMessage = "Private browsing is unsupported";
else
errorMessage = error.message;
}
if (resp) {
if (options && options.success)
if (options && options.success) {
if (Backbone.VERSION === "0.9.10") {
options.success(model, resp, options);
} else {
options.success(resp);
}
if (syncDfd)
}
if (syncDfd) {
syncDfd.resolve(resp);
}
} else {
errorMessage = errorMessage ? errorMessage
......
// Backbone.Marionette, v1.0.0-rc6
// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
// http://github.com/marionettejs/backbone.marionette
// MarionetteJS (Backbone.Marionette)
// ----------------------------------
// v1.0.4
//
// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://marionettejs.com
/*!
......@@ -12,11 +17,15 @@
* https://github.com/marionettejs/backbone.wreqr/
*/
// Backbone.BabySitter, v0.0.4
// Copyright (c)2012 Derick Bailey, Muted Solutions, LLC.
// Backbone.BabySitter
// -------------------
// v0.0.6
//
// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
// http://github.com/marionettejs/backbone.babysitter
//
// http://github.com/babysitterjs/backbone.babysitter
// Backbone.ChildViewContainer
// ---------------------------
//
......@@ -28,14 +37,13 @@ Backbone.ChildViewContainer = (function(Backbone, _){
// Container Constructor
// ---------------------
var Container = function(initialViews){
var Container = function(views){
this._views = {};
this._indexByModel = {};
this._indexByCollection = {};
this._indexByCustom = {};
this._updateLength();
this._addInitialViews(initialViews);
_.each(views, this.add, this);
};
// Container Methods
......@@ -45,7 +53,7 @@ Backbone.ChildViewContainer = (function(Backbone, _){
// Add a view to this container. Stores the view
// by `cid` and makes it searchable by the model
// and/or collection of the view. Optionally specify
// cid (and model itself). Optionally specify
// a custom key to store an retrieve the view.
add: function(view, customIndex){
var viewCid = view.cid;
......@@ -58,11 +66,6 @@ Backbone.ChildViewContainer = (function(Backbone, _){
this._indexByModel[view.model.cid] = viewCid;
}
// index it by collection
if (view.collection){
this._indexByCollection[view.collection.cid] = viewCid;
}
// index by custom
if (customIndex){
this._indexByCustom[customIndex] = viewCid;
......@@ -72,18 +75,16 @@ Backbone.ChildViewContainer = (function(Backbone, _){
},
// Find a view by the model that was attached to
// it. Uses the model's `cid` to find it, and
// retrieves the view by it's `cid` from the result
// it. Uses the model's `cid` to find it.
findByModel: function(model){
var viewCid = this._indexByModel[model.cid];
return this.findByCid(viewCid);
return this.findByModelCid(model.cid);
},
// Find a view by the collection that was attached to
// it. Uses the collection's `cid` to find it, and
// retrieves the view by it's `cid` from the result
findByCollection: function(col){
var viewCid = this._indexByCollection[col.cid];
// Find a view by the `cid` of the model that was attached to
// it. Uses the model's `cid` to find the view `cid` and
// retrieve the view using it.
findByModelCid: function(modelCid){
var viewCid = this._indexByModel[modelCid];
return this.findByCid(viewCid);
},
......@@ -113,26 +114,13 @@ Backbone.ChildViewContainer = (function(Backbone, _){
delete this._indexByModel[view.model.cid];
}
// delete collection index
if (view.collection){
delete this._indexByCollection[view.collection.cid];
}
// delete custom index
var cust;
for (var key in this._indexByCustom){
if (this._indexByCustom.hasOwnProperty(key)){
if (this._indexByCustom[key] === viewCid){
cust = key;
break;
}
}
}
if (cust){
delete this._indexByCustom[cust];
_.any(this._indexByCustom, function(cid, key) {
if (cid === viewCid) {
delete this._indexByCustom[key];
return true;
}
}, this);
// remove the view from the container
delete this._views[viewCid];
......@@ -144,44 +132,24 @@ Backbone.ChildViewContainer = (function(Backbone, _){
// Call a method on every view in the container,
// passing parameters to the call method one at a
// time, like `function.call`.
call: function(method, args){
args = Array.prototype.slice.call(arguments, 1);
this.apply(method, args);
call: function(method){
this.apply(method, _.tail(arguments));
},
// Apply a method on every view in the container,
// passing parameters to the call method one at a
// time, like `function.apply`.
apply: function(method, args){
var view;
// fix for IE < 9
args = args || [];
_.each(this._views, function(view, key){
_.each(this._views, function(view){
if (_.isFunction(view[method])){
view[method].apply(view, args);
view[method].apply(view, args || []);
}
});
},
// Update the `.length` attribute on this container
_updateLength: function(){
this.length = _.size(this._views);
},
// set up an initial list of views
_addInitialViews: function(views){
if (!views){ return; }
var view, i,
length = views.length;
for (i=0; i<length; i++){
view = views[i];
this.add(view);
}
}
});
......@@ -207,26 +175,37 @@ Backbone.ChildViewContainer = (function(Backbone, _){
return Container;
})(Backbone, _);
// Backbone.Wreqr, v0.1.1
// Backbone.Wreqr (Backbone.Marionette)
// ----------------------------------
// v0.2.0
//
// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC.
// Distributed under MIT license
//
// http://github.com/marionettejs/backbone.wreqr
Backbone.Wreqr = (function(Backbone, Marionette, _){
"use strict";
var Wreqr = {};
// Handlers
// --------
// A registry of functions to call, given a name
// --------
// A registry of functions to call, given a name
Wreqr.Handlers = (function(Backbone, _){
Wreqr.Handlers = (function(Backbone, _){
"use strict";
// Constructor
// -----------
var Handlers = function(){
this._handlers = {};
var Handlers = function(options){
this.options = options;
this._wreqrHandlers = {};
if (_.isFunction(this.initialize)){
this.initialize(options);
}
};
Handlers.extend = Backbone.Model.extend;
......@@ -234,24 +213,45 @@ Backbone.Wreqr = (function(Backbone, Marionette, _){
// Instance Members
// ----------------
_.extend(Handlers.prototype, {
_.extend(Handlers.prototype, Backbone.Events, {
// Add multiple handlers using an object literal configuration
setHandlers: function(handlers){
_.each(handlers, function(handler, name){
var context = null;
if (_.isObject(handler) && !_.isFunction(handler)){
context = handler.context;
handler = handler.callback;
}
this.setHandler(name, handler, context);
}, this);
},
// Add a handler for the given name, with an
// optional context to run the handler within
addHandler: function(name, handler, context){
setHandler: function(name, handler, context){
var config = {
callback: handler,
context: context
};
this._handlers[name] = config;
this._wreqrHandlers[name] = config;
this.trigger("handler:add", name, handler, context);
},
// Determine whether or not a handler is registered
hasHandler: function(name){
return !! this._wreqrHandlers[name];
},
// Get the currently registered handler for
// the specified name. Throws an exception if
// no handler is found.
getHandler: function(name){
var config = this._handlers[name];
var config = this._wreqrHandlers[name];
if (!config){
throw new Error("Handler not found for '" + name + "'");
......@@ -265,43 +265,148 @@ Backbone.Wreqr = (function(Backbone, Marionette, _){
// Remove a handler for the specified name
removeHandler: function(name){
delete this._handlers[name];
delete this._wreqrHandlers[name];
},
// Remove all handlers from this registry
removeAllHandlers: function(){
this._handlers = {};
this._wreqrHandlers = {};
}
});
return Handlers;
})(Backbone, _);
})(Backbone, _);
// Wreqr.CommandStorage
// --------------------
//
// Store and retrieve commands for execution.
Wreqr.CommandStorage = (function(){
"use strict";
// Constructor function
var CommandStorage = function(options){
this.options = options;
this._commands = {};
if (_.isFunction(this.initialize)){
this.initialize(options);
}
};
// Instance methods
_.extend(CommandStorage.prototype, Backbone.Events, {
// Get an object literal by command name, that contains
// the `commandName` and the `instances` of all commands
// represented as an array of arguments to process
getCommands: function(commandName){
var commands = this._commands[commandName];
// we don't have it, so add it
if (!commands){
// build the configuration
commands = {
command: commandName,
instances: []
};
// store it
this._commands[commandName] = commands;
}
return commands;
},
// Add a command by name, to the storage and store the
// args for the command
addCommand: function(commandName, args){
var command = this.getCommands(commandName);
command.instances.push(args);
},
// Clear all commands for the given `commandName`
clearCommands: function(commandName){
var command = this.getCommands(commandName);
command.instances = [];
}
});
return CommandStorage;
})();
// Wreqr.Commands
// --------------
//
// A simple command pattern implementation. Register a command
// handler and execute it.
Wreqr.Commands = (function(Wreqr){
// --------------
//
// A simple command pattern implementation. Register a command
// handler and execute it.
Wreqr.Commands = (function(Wreqr){
"use strict";
return Wreqr.Handlers.extend({
execute: function(){
var name = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
// default storage type
storageType: Wreqr.CommandStorage,
constructor: function(options){
this.options = options || {};
this._initializeStorage(this.options);
this.on("handler:add", this._executeCommands, this);
var args = Array.prototype.slice.call(arguments);
Wreqr.Handlers.prototype.constructor.apply(this, args);
},
// Execute a named command with the supplied args
execute: function(name, args){
name = arguments[0];
args = Array.prototype.slice.call(arguments, 1);
if (this.hasHandler(name)){
this.getHandler(name).apply(this, args);
} else {
this.storage.addCommand(name, args);
}
},
// Internal method to handle bulk execution of stored commands
_executeCommands: function(name, handler, context){
var command = this.storage.getCommands(name);
// loop through and execute all the stored command instances
_.each(command.instances, function(args){
handler.apply(context, args);
});
this.storage.clearCommands(name);
},
// Internal method to initialize storage either from the type's
// `storageType` or the instance `options.storageType`.
_initializeStorage: function(options){
var storage;
var StorageType = options.storageType || this.storageType;
if (_.isFunction(StorageType)){
storage = new StorageType();
} else {
storage = StorageType;
}
this.storage = storage;
}
});
})(Wreqr);
})(Wreqr);
// Wreqr.RequestResponse
// ---------------------
//
// A simple request/response implementation. Register a
// request handler, and return a response from it
Wreqr.RequestResponse = (function(Wreqr){
// ---------------------
//
// A simple request/response implementation. Register a
// request handler, and return a response from it
Wreqr.RequestResponse = (function(Wreqr){
"use strict";
return Wreqr.Handlers.extend({
......@@ -313,14 +418,14 @@ Backbone.Wreqr = (function(Backbone, Marionette, _){
}
});
})(Wreqr);
})(Wreqr);
// Event Aggregator
// ----------------
// A pub-sub object that can be used to decouple various parts
// of an application through event-driven architecture.
// ----------------
// A pub-sub object that can be used to decouple various parts
// of an application through event-driven architecture.
Wreqr.EventAggregator = (function(Backbone, _){
Wreqr.EventAggregator = (function(Backbone, _){
"use strict";
var EA = function(){};
......@@ -331,23 +436,36 @@ Backbone.Wreqr = (function(Backbone, Marionette, _){
_.extend(EA.prototype, Backbone.Events);
return EA;
})(Backbone, _);
})(Backbone, _);
return Wreqr;
})(Backbone, Backbone.Marionette, _);
var Marionette = (function(Backbone, _, $){
var Marionette = (function(global, Backbone, _){
"use strict";
// Define and export the Marionette namespace
var Marionette = {};
Backbone.Marionette = Marionette;
// Get the DOM manipulator for later use
Marionette.$ = Backbone.$;
// Helpers
// -------
// For slicing `arguments` in functions
var slice = Array.prototype.slice;
var protoSlice = Array.prototype.slice;
function slice(args) {
return protoSlice.call(args);
}
function throwError(message, name) {
var error = new Error(message);
error.name = name || 'Error';
throw error;
}
// Marionette.extend
// -----------------
......@@ -359,7 +477,7 @@ Marionette.extend = Backbone.Model.extend;
// --------------------
// Retrieve an object, function or other value from a target
// object or it's `options`, with `options` taking precedence.
// object or its `options`, with `options` taking precedence.
Marionette.getOption = function(target, optionName){
if (!target || !optionName){ return; }
var value;
......@@ -373,55 +491,6 @@ Marionette.getOption = function(target, optionName){
return value;
};
// Mairionette.createObject
// ------------------------
// A wrapper / shim for `Object.create`. Uses native `Object.create`
// if available, otherwise shims it in place for Marionette to use.
Marionette.createObject = (function(){
var createObject;
// Define this once, and just replace the .prototype on it as needed,
// to improve performance in older / less optimized JS engines
function F() {}
// Check for existing native / shimmed Object.create
if (typeof Object.create === "function"){
// found native/shim, so use it
createObject = Object.create;
} else {
// An implementation of the Boodman/Crockford delegation
// w/ Cornford optimization, as suggested by @unscriptable
// https://gist.github.com/3959151
// native/shim not found, so shim it ourself
createObject = function (o) {
// set the prototype of the function
// so we will get `o` as the prototype
// of the new object instance
F.prototype = o;
// create a new object that inherits from
// the `o` parameter
var child = new F();
// clean up just in case o is really large
F.prototype = null;
// send it back
return child;
};
}
return createObject;
})();
// Trigger an event and a corresponding method name. Examples:
//
// `this.triggerMethod("foo")` will trigger the "foo" event and
......@@ -429,25 +498,35 @@ Marionette.createObject = (function(){
//
// `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and
// call the "onFooBar" method.
Marionette.triggerMethod = function(){
var args = Array.prototype.slice.apply(arguments);
var eventName = args[0];
var segments = eventName.split(":");
var segment, capLetter, methodName = "on";
Marionette.triggerMethod = (function(){
// split the event name on the :
var splitter = /(^|:)(\w)/gi;
for (var i = 0; i < segments.length; i++){
segment = segments[i];
capLetter = segment.charAt(0).toUpperCase();
methodName += capLetter + segment.slice(1);
// take the event section ("section1:section2:section3")
// and turn it in to uppercase name
function getEventName(match, prefix, eventName) {
return eventName.toUpperCase();
}
this.trigger.apply(this, args);
// actual triggerMethod name
var triggerMethod = function(event) {
// get the method name from the event name
var methodName = 'on' + event.replace(splitter, getEventName);
var method = this[methodName];
if (_.isFunction(this[methodName])){
args.shift();
return this[methodName].apply(this, args);
// trigger the event
this.trigger.apply(this, arguments);
// call the onMethodName if it exists
if (_.isFunction(method)) {
// pass all arguments, except the event name
return method.apply(this, _.tail(arguments));
}
};
};
return triggerMethod;
})();
// DOMRefresh
// ----------
......@@ -498,7 +577,7 @@ Marionette.MonitorDOMRefresh = (function(){
// These methods are used to bind/unbind a backbone "entity" (collection/model)
// to methods on a target object.
//
// The first paremter, `target`, must have a `listenTo` method from the
// The first parameter, `target`, must have a `listenTo` method from the
// EventBinder object.
//
// The second parameter is the entity (Backbone.Model or Backbone.Collection)
......@@ -520,7 +599,7 @@ Marionette.MonitorDOMRefresh = (function(){
var method = target[methodName];
if(!method) {
throw new Error("Method '"+ methodName +"' was configured as an event handler, but does not exist.");
throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist.");
}
target.listenTo(entity, evt, method, target);
......@@ -538,7 +617,7 @@ Marionette.MonitorDOMRefresh = (function(){
var methodNames = methods.split(/\s+/);
_.each(methodNames,function(methodName) {
var method = target[method];
var method = target[methodName];
target.stopListening(entity, evt, method, target);
});
}
......@@ -591,7 +670,7 @@ Marionette.MonitorDOMRefresh = (function(){
// and executing them at a later point in time, using jQuery's
// `Deferred` object.
Marionette.Callbacks = function(){
this._deferred = $.Deferred();
this._deferred = Marionette.$.Deferred();
this._callbacks = [];
};
......@@ -619,13 +698,13 @@ _.extend(Marionette.Callbacks.prototype, {
// Resets the list of callbacks to be run, allowing the same list
// to be run multiple times - whenever the `run` method is called.
reset: function(){
var that = this;
var callbacks = this._callbacks;
this._deferred = $.Deferred();
this._deferred = Marionette.$.Deferred();
this._callbacks = [];
_.each(callbacks, function(cb){
that.add(cb.cb, cb.ctx);
});
this.add(cb.cb, cb.ctx);
}, this);
}
});
......@@ -739,12 +818,28 @@ _.extend(Marionette.Region, {
}
// build the region instance
var regionManager = new RegionType({
var region = new RegionType({
el: selector
});
return regionManager;
// 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(selector) {
var parentEl = regionConfig.parentEl;
if (_.isFunction(parentEl)){
parentEl = parentEl();
}
return parentEl.find(selector);
};
}
return region;
}
});
......@@ -762,15 +857,25 @@ _.extend(Marionette.Region.prototype, Backbone.Events, {
show: function(view){
this.ensureEl();
var isViewClosed = view.isClosed || _.isUndefined(view.$el);
var isDifferentView = view !== this.currentView;
if (isDifferentView) {
this.close();
}
view.render();
this.open(view);
Marionette.triggerMethod.call(view, "show");
Marionette.triggerMethod.call(this, "show", view);
if (isDifferentView || isViewClosed) {
this.open(view);
}
this.currentView = view;
Marionette.triggerMethod.call(this, "show", view);
Marionette.triggerMethod.call(view, "show");
},
ensureEl: function(){
......@@ -782,7 +887,7 @@ _.extend(Marionette.Region.prototype, Backbone.Events, {
// Override this method to change how the region finds the
// DOM element that it manages. Return a jQuery selector object.
getEl: function(selector){
return $(selector);
return Marionette.$(selector);
},
// Override this method to change how the new view is
......@@ -797,7 +902,10 @@ _.extend(Marionette.Region.prototype, Backbone.Events, {
var view = this.currentView;
if (!view || view.isClosed){ return; }
// call 'close' or 'remove', depending on which is found
if (view.close) { view.close(); }
else if (view.remove) { view.remove(); }
Marionette.triggerMethod.call(this, "close");
delete this.currentView;
......@@ -824,6 +932,139 @@ _.extend(Marionette.Region.prototype, Backbone.Events, {
// Copy the `extend` function used by Backbone's classes
Marionette.Region.extend = Marionette.extend;
// Marionette.RegionManager
// ------------------------
//
// Manage one or more related `Marionette.Region` objects.
Marionette.RegionManager = (function(Marionette){
var RegionManager = Marionette.Controller.extend({
constructor: function(options){
this._regions = {};
Marionette.Controller.prototype.constructor.call(this, options);
},
// Add multiple regions using an object literal, where
// each key becomes the region name, and each value is
// the region definition.
addRegions: function(regionDefinitions, defaults){
var regions = {};
_.each(regionDefinitions, function(definition, name){
if (typeof definition === "string"){
definition = { selector: definition };
}
if (definition.selector){
definition = _.defaults({}, definition, defaults);
}
var region = this.addRegion(name, definition);
regions[name] = region;
}, this);
return regions;
},
// Add an individual region to the region manager,
// and return the region instance
addRegion: function(name, definition){
var region;
var isObject = _.isObject(definition);
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;
}
this._store(name, region);
this.triggerMethod("region:add", name, region);
return region;
},
// Get a region by name
get: function(name){
return this._regions[name];
},
// Remove a region by name
removeRegion: function(name){
var region = this._regions[name];
this._remove(name, region);
},
// Close all regions in the region manager, and
// remove them
removeRegions: function(){
_.each(this._regions, function(region, name){
this._remove(name, region);
}, this);
},
// Close all regions in the region manager, but
// leave them attached
closeRegions: function(){
_.each(this._regions, function(region, name){
region.close();
}, this);
},
// Close all regions and shut down the region
// manager entirely
close: function(){
this.removeRegions();
var args = Array.prototype.slice.call(arguments);
Marionette.Controller.prototype.close.apply(this, args);
},
// internal method to store regions
_store: function(name, region){
this._regions[name] = region;
this._setLength();
},
// internal method to remove a region
_remove: function(name, region){
region.close();
delete this._regions[name];
this._setLength();
this.triggerMethod("region:remove", name, region);
},
// set the number of regions current held
_setLength: function(){
this.length = _.size(this._regions);
}
});
// Borrowing this code from Backbone.Collection:
// http://backbonejs.org/docs/backbone.html#section-106
//
// Mix in methods from Underscore, for iteration, and other
// collection related features.
var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter',
'select', 'reject', 'every', 'all', 'some', 'any', 'include',
'contains', 'invoke', 'toArray', 'first', 'initial', 'rest',
'last', 'without', 'isEmpty', 'pluck'];
_.each(methods, function(method) {
RegionManager.prototype[method] = function() {
var regions = _.values(this._regions);
var args = [regions].concat(_.toArray(arguments));
return _[method].apply(_, args);
};
});
return RegionManager;
})(Marionette);
// Template Cache
// --------------
......@@ -844,7 +1085,6 @@ _.extend(Marionette.TemplateCache, {
// retrieves the cached version, or loads it
// from the DOM.
get: function(templateId){
var that = this;
var cachedTemplate = this.templateCaches[templateId];
if (!cachedTemplate){
......@@ -864,7 +1104,7 @@ _.extend(Marionette.TemplateCache, {
// `clear("#t1", "#t2", "...")`
clear: function(){
var i;
var args = Array.prototype.slice.apply(arguments);
var args = slice(arguments);
var length = args.length;
if (length > 0){
......@@ -878,14 +1118,12 @@ _.extend(Marionette.TemplateCache, {
});
// TemplateCache instance methods, allowing each
// template cache object to manage it's own state
// template cache object to manage its own state
// and know whether or not it has been loaded
_.extend(Marionette.TemplateCache.prototype, {
// Internal method to load the template
load: function(){
var that = this;
// Guard clause to prevent loading this template more than once
if (this.compiledTemplate){
return this.compiledTemplate;
......@@ -904,13 +1142,10 @@ _.extend(Marionette.TemplateCache.prototype, {
// using a template-loader plugin as described here:
// https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs
loadTemplate: function(templateId){
var template = $(templateId).html();
var template = Marionette.$(templateId).html();
if (!template || template.length === 0){
var msg = "Could not find template: '" + templateId + "'";
var err = new Error(msg);
err.name = "NoTemplateError";
throw err;
throwError("Could not find template: '" + templateId + "'", "NoTemplateError");
}
return template;
......@@ -938,9 +1173,21 @@ Marionette.Renderer = {
// template function. Override this method to provide your own
// custom rendering and template handling for all of Marionette.
render: function(template, data){
var templateFunc = typeof template === 'function' ? template : Marionette.TemplateCache.get(template);
var html = templateFunc(data);
return html;
if (!template) {
var error = new Error("Cannot render the template since it's false, null or undefined.");
error.name = "TemplateNotFoundError";
throw error;
}
var templateFunc;
if (typeof template === "function"){
templateFunc = template;
} else {
templateFunc = Marionette.TemplateCache.get(template);
}
return templateFunc(data);
}
};
......@@ -993,7 +1240,6 @@ Marionette.View = Backbone.View.extend({
configureTriggers: function(){
if (!this.triggers) { return; }
var that = this;
var triggerEvents = {};
// Allow `triggers` to be configured as a function
......@@ -1006,11 +1252,11 @@ Marionette.View = Backbone.View.extend({
// build the event handler function for the DOM event
triggerEvents[key] = function(e){
// stop the event in it's tracks
// stop the event in its tracks
if (e && e.preventDefault){ e.preventDefault(); }
if (e && e.stopPropagation){ e.stopPropagation(); }
// buil the args for the event
// build the args for the event
var args = {
view: this,
model: this.model,
......@@ -1018,10 +1264,10 @@ Marionette.View = Backbone.View.extend({
};
// trigger the event
that.triggerMethod(value, args);
this.triggerMethod(value, args);
};
});
}, this);
return triggerEvents;
},
......@@ -1079,6 +1325,10 @@ Marionette.View = Backbone.View.extend({
this.isClosed = true;
this.triggerMethod("close");
// unbind UI elements
this.unbindUIElements();
// remove the view from the DOM
this.remove();
},
......@@ -1087,20 +1337,37 @@ Marionette.View = Backbone.View.extend({
bindUIElements: function(){
if (!this.ui) { return; }
var that = this;
if (!this.uiBindings) {
// We want to store the ui hash in uiBindings, since afterwards the values in the ui hash
// will be overridden with jQuery selectors.
this.uiBindings = _.result(this, "ui");
// store the ui hash in _uiBindings so they can be reset later
// and so re-rendering the view will be able to find the bindings
if (!this._uiBindings){
this._uiBindings = this.ui;
}
// refreshing the associated selectors since they should point to the newly rendered elements.
// get the bindings result, as a function or otherwise
var bindings = _.result(this, "_uiBindings");
// empty the ui so we don't have anything to start with
this.ui = {};
_.each(_.keys(this.uiBindings), function(key) {
var selector = that.uiBindings[key];
that.ui[key] = that.$(selector);
});
// bind each of the selectors
_.each(_.keys(bindings), function(key) {
var selector = bindings[key];
this.ui[key] = this.$(selector);
}, this);
},
// This method unbinds the elements specified in the "ui" hash
unbindUIElements: function(){
if (!this.ui){ return; }
// delete all of the existing ui bindings
_.each(this.ui, function($el, name){
delete this.ui[name];
}, this);
// reset the ui element to the original bindings configuration
this.ui = this._uiBindings;
delete this._uiBindings;
}
});
......@@ -1111,9 +1378,11 @@ Marionette.View = Backbone.View.extend({
// with underscore.js templates, serializing the view's model or collection,
// and calling several methods on extended views, such as `onRender`.
Marionette.ItemView = Marionette.View.extend({
// Setting up the inheritance chain which allows changes to
// Marionette.View.prototype.constructor which allows overriding
constructor: function(){
var args = Array.prototype.slice.apply(arguments);
Marionette.View.prototype.constructor.apply(this, args);
Marionette.View.prototype.constructor.apply(this, slice(arguments));
},
// Serialize the model or collection for the view. If a model is
......@@ -1151,6 +1420,7 @@ Marionette.ItemView = Marionette.View.extend({
var template = this.getTemplate();
var html = Marionette.Renderer.render(template, data);
this.$el.html(html);
this.bindUIElements();
......@@ -1167,8 +1437,7 @@ Marionette.ItemView = Marionette.View.extend({
this.triggerMethod('item:before:close');
var args = Array.prototype.slice.apply(arguments);
Marionette.View.prototype.close.apply(this, args);
Marionette.View.prototype.close.apply(this, slice(arguments));
this.triggerMethod('item:closed');
}
......@@ -1188,8 +1457,7 @@ Marionette.CollectionView = Marionette.View.extend({
constructor: function(options){
this._initChildViewStorage();
var args = Array.prototype.slice.apply(arguments);
Marionette.View.prototype.constructor.apply(this, args);
Marionette.View.prototype.constructor.apply(this, slice(arguments));
this._initialEvents();
},
......@@ -1263,12 +1531,11 @@ Marionette.CollectionView = Marionette.View.extend({
// Internal method to loop through each item in the
// collection view and show it
showCollection: function(){
var that = this;
var ItemView;
this.collection.each(function(item, index){
ItemView = that.getItemView(item);
that.addItemView(item, ItemView, index);
});
ItemView = this.getItemView(item);
this.addItemView(item, ItemView, index);
}, this);
},
// Internal method to show an empty view in place of
......@@ -1301,9 +1568,7 @@ Marionette.CollectionView = Marionette.View.extend({
var itemView = Marionette.getOption(this, "itemView");
if (!itemView){
var err = new Error("An `itemView` must be specified");
err.name = "NoItemViewError";
throw err;
throwError("An `itemView` must be specified", "NoItemViewError");
}
return itemView;
......@@ -1312,12 +1577,10 @@ Marionette.CollectionView = Marionette.View.extend({
// Render the child item's view and add it to the
// HTML for the collection view.
addItemView: function(item, ItemView, index){
var that = this;
// get the itemViewOptions if any were specified
var itemViewOptions = Marionette.getOption(this, "itemViewOptions");
if (_.isFunction(itemViewOptions)){
itemViewOptions = itemViewOptions.call(this, item);
itemViewOptions = itemViewOptions.call(this, item, index);
}
// build the view
......@@ -1354,7 +1617,7 @@ Marionette.CollectionView = Marionette.View.extend({
// Forward all child item view events through the parent,
// prepending "itemview:" to the event name
this.listenTo(view, "all", function(){
var args = slice.call(arguments);
var args = slice(arguments);
args[0] = prefix + ":" + args[0];
args.splice(1, 0, view);
......@@ -1371,8 +1634,7 @@ Marionette.CollectionView = Marionette.View.extend({
// Build an `itemView` for every model in the collection.
buildItemView: function(item, ItemViewType, itemViewOptions){
var options = _.extend({model: item}, itemViewOptions);
var view = new ItemViewType(options);
return view;
return new ItemViewType(options);
},
// get the child view by item it holds, and remove it
......@@ -1390,9 +1652,9 @@ Marionette.CollectionView = Marionette.View.extend({
if (view){
this.stopListening(view);
if (view.close){
view.close();
}
// call 'close' or 'remove', depending on which is found
if (view.close) { view.close(); }
else if (view.remove) { view.remove(); }
this.children.remove(view);
}
......@@ -1431,8 +1693,7 @@ Marionette.CollectionView = Marionette.View.extend({
this.closeChildren();
this.triggerMethod("collection:closed");
var args = Array.prototype.slice.apply(arguments);
Marionette.View.prototype.close.apply(this, args);
Marionette.View.prototype.close.apply(this, slice(arguments));
},
// Close the child views that this collection view
......@@ -1453,11 +1714,11 @@ Marionette.CollectionView = Marionette.View.extend({
// Extends directly from CollectionView and also renders an
// an item view as `modelView`, for the top leaf
Marionette.CompositeView = Marionette.CollectionView.extend({
constructor: function(options){
var args = Array.prototype.slice.apply(arguments);
Marionette.CollectionView.apply(this, args);
this.itemView = this.getItemView();
// Setting up the inheritance chain which allows changes to
// Marionette.CollectionView.prototype.constructor which allows overriding
constructor: function(){
Marionette.CollectionView.prototype.constructor.apply(this, slice(arguments));
},
// Configured the initial events that the composite view
......@@ -1479,9 +1740,7 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
var itemView = Marionette.getOption(this, "itemView") || this.constructor;
if (!itemView){
var err = new Error("An `itemView` must be specified");
err.name = "NoItemViewError";
throw err;
throwError("An `itemView` must be specified", "NoItemViewError");
}
return itemView;
......@@ -1504,6 +1763,7 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
// this again will tell the model's view to re-render itself
// but the collection will not re-render.
render: function(){
this.isRendered = true;
this.isClosed = false;
this.resetItemViewContainer();
......@@ -1524,15 +1784,10 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
},
_renderChildren: function(){
if (this.isRendered){
Marionette.CollectionView.prototype._renderChildren.call(this);
this.triggerMethod("composite:collection:rendered");
},
// Render the collection for the composite view
renderCollection: function(){
var args = Array.prototype.slice.apply(arguments);
Marionette.CollectionView.prototype.render.apply(this, args);
}
},
// Render an individual model, if we have one, as
......@@ -1551,7 +1806,7 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
// `itemViewContainer` (a jQuery selector). Override this method to
// provide custom logic of how the child item view instances have their
// HTML appended to the composite view instance.
appendHtml: function(cv, iv){
appendHtml: function(cv, iv, index){
var $container = this.getItemViewContainer(cv);
$container.append(iv.el);
},
......@@ -1569,9 +1824,7 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
var selector = _.result(containerView, "itemViewContainer");
container = containerView.$(selector);
if (container.length <= 0) {
var err = new Error("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer);
err.name = "ItemViewContainerMissingError";
throw err;
throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError");
}
} else {
......@@ -1603,14 +1856,15 @@ Marionette.CompositeView = Marionette.CollectionView.extend({
Marionette.Layout = Marionette.ItemView.extend({
regionType: Marionette.Region,
// Ensure the regions are avialable when the `initialize` method
// Ensure the regions are available when the `initialize` method
// is called.
constructor: function () {
constructor: function (options) {
options = options || {};
this._firstRender = true;
this.initializeRegions();
this._initializeRegions(options);
var args = Array.prototype.slice.apply(arguments);
Marionette.ItemView.apply(this, args);
Marionette.ItemView.prototype.constructor.call(this, options);
},
// Layout's render will use the existing region objects the
......@@ -1623,11 +1877,14 @@ Marionette.Layout = Marionette.ItemView.extend({
// if this is the first render, don't do anything to
// reset the regions
this._firstRender = false;
} else if (this.isClosed){
// a previously closed layout means we need to
// completely re-initialize the regions
this._initializeRegions();
} else {
// If this is not the first render call, then we need to
// re-initializing the `el` for each region
this.closeRegions();
this.reInitializeRegions();
this._reInitializeRegions();
}
var args = Array.prototype.slice.apply(arguments);
......@@ -1639,75 +1896,82 @@ Marionette.Layout = Marionette.ItemView.extend({
// Handle closing regions, and then close the view itself.
close: function () {
if (this.isClosed){ return; }
this.closeRegions();
this.destroyRegions();
this.regionManager.close();
var args = Array.prototype.slice.apply(arguments);
Marionette.ItemView.prototype.close.apply(this, args);
},
// Initialize the regions that have been defined in a
// `regions` attribute on this layout. The key of the
// hash becomes an attribute on the layout object directly.
// For example: `regions: { menu: ".menu-container" }`
// will product a `layout.menu` object which is a region
// that controls the `.menu-container` DOM element.
initializeRegions: function () {
if (!this.regionManagers){
this.regionManagers = {};
}
// Add a single region, by name, to the layout
addRegion: function(name, definition){
var regions = {};
regions[name] = definition;
return this.addRegions(regions)[name];
},
// Add multiple regions as a {name: definition, name2: def2} object literal
addRegions: function(regions){
this.regions = _.extend(this.regions || {}, regions);
return this._buildRegions(regions);
},
// Remove a single region from the Layout, by name
removeRegion: function(name){
return this.regionManager.removeRegion(name);
},
// internal method to build regions
_buildRegions: function(regions){
var that = this;
var regions = this.regions || {};
_.each(regions, function (region, name) {
var regionManager = Marionette.Region.buildRegion(region, that.regionType);
regionManager.getEl = function(selector){
return that.$(selector);
var defaults = {
parentEl: function(){ return that.$el; }
};
that.regionManagers[name] = regionManager;
that[name] = regionManager;
});
return this.regionManager.addRegions(regions, defaults);
},
// Internal method to initialize the regions that have been defined in a
// `regions` attribute on this layout.
_initializeRegions: function (options) {
var regions;
this._initRegionManager();
if (_.isFunction(this.regions)) {
regions = this.regions(options);
} else {
regions = this.regions || {};
}
this.addRegions(regions);
},
// 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
reInitializeRegions: function(){
if (this.regionManagers && _.size(this.regionManagers)===0){
this.initializeRegions();
} else {
_.each(this.regionManagers, function(region){
_reInitializeRegions: function(){
this.regionManager.closeRegions();
this.regionManager.each(function(region){
region.reset();
});
}
},
// Close all of the regions that have been opened by
// this layout. This method is called when the layout
// itself is closed.
closeRegions: function () {
var that = this;
_.each(this.regionManagers, function (manager, name) {
manager.close();
// Internal method to initialize the region manager
// and all regions in it
_initRegionManager: function(){
this.regionManager = new Marionette.RegionManager();
this.listenTo(this.regionManager, "region:add", function(name, region){
this[name] = region;
this.trigger("region:add", name, region);
});
},
// Destroys all of the regions by removing references
// from the Layout
destroyRegions: function(){
var that = this;
_.each(this.regionManagers, function (manager, name) {
delete that[name];
this.listenTo(this.regionManager, "region:remove", function(name, region){
delete this[name];
this.trigger("region:remove", name, region);
});
this.regionManagers = {};
}
});
// AppRouter
// ---------
......@@ -1720,7 +1984,7 @@ Marionette.Layout = Marionette.ItemView.extend({
//
// App routers can only take one `controller` object.
// It is recommended that you divide your controller
// objects in to smaller peices of related functionality
// objects in to smaller pieces of related functionality
// and have multiple routers / controllers, instead of
// just one giant router and controller.
//
......@@ -1729,8 +1993,7 @@ Marionette.Layout = Marionette.ItemView.extend({
Marionette.AppRouter = Backbone.Router.extend({
constructor: function(options){
var args = Array.prototype.slice.apply(arguments);
Backbone.Router.prototype.constructor.apply(this, args);
Backbone.Router.prototype.constructor.apply(this, slice(arguments));
this.options = options;
......@@ -1743,34 +2006,19 @@ Marionette.AppRouter = Backbone.Router.extend({
// Internal method to process the `appRoutes` for the
// router, and turn them in to routes that trigger the
// specified method on the specified `controller`.
processAppRoutes: function(controller, appRoutes){
var method, methodName;
var route, routesLength, i;
var routes = [];
var router = this;
for(route in appRoutes){
if (appRoutes.hasOwnProperty(route)){
routes.unshift([route, appRoutes[route]]);
}
}
processAppRoutes: function(controller, appRoutes) {
var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes
routesLength = routes.length;
for (i = 0; i < routesLength; i++){
route = routes[i][0];
methodName = routes[i][1];
method = controller[methodName];
_.each(routeNames, function(route) {
var methodName = appRoutes[route];
var method = controller[methodName];
if (!method){
var msg = "Method '" + methodName + "' was not found on the controller";
var err = new Error(msg);
err.name = "NoMethodError";
throw err;
if (!method) {
throw new Error("Method '" + methodName + "' was not found on the controller");
}
method = _.bind(method, controller);
router.route(route, methodName, method);
}
this.route(route, methodName, _.bind(method, controller));
}, this);
}
});
......@@ -1782,7 +2030,8 @@ Marionette.AppRouter = Backbone.Router.extend({
// Stores and starts up `Region` objects, includes an
// event aggregator as `app.vent`
Marionette.Application = function(options){
this.initCallbacks = new Marionette.Callbacks();
this._initRegionManager();
this._initCallbacks = new Marionette.Callbacks();
this.vent = new Backbone.Wreqr.EventAggregator();
this.commands = new Backbone.Wreqr.Commands();
this.reqres = new Backbone.Wreqr.RequestResponse();
......@@ -1810,7 +2059,7 @@ _.extend(Marionette.Application.prototype, Backbone.Events, {
// method is called, or run immediately if added after `start`
// has already been called.
addInitializer: function(initializer){
this.initCallbacks.add(initializer);
this._initCallbacks.add(initializer);
},
// kick off all of the application's processes.
......@@ -1818,7 +2067,7 @@ _.extend(Marionette.Application.prototype, Backbone.Events, {
// to the app, and runs all of the initializer functions
start: function(options){
this.triggerMethod("initialize:before", options);
this.initCallbacks.run(options, this);
this._initCallbacks.run(options, this);
this.triggerMethod("initialize:after", options);
this.triggerMethod("start", options);
......@@ -1827,32 +2076,40 @@ _.extend(Marionette.Application.prototype, Backbone.Events, {
// Add regions to your app.
// Accepts a hash of named strings or Region objects
// addRegions({something: "#someRegion"})
// addRegions{{something: Region.extend({el: "#someRegion"}) });
// addRegions({something: Region.extend({el: "#someRegion"}) });
addRegions: function(regions){
var that = this;
_.each(regions, function (region, name) {
var regionManager = Marionette.Region.buildRegion(region, Marionette.Region);
that[name] = regionManager;
});
return this._regionManager.addRegions(regions);
},
// Removes a region from your app.
// Accepts the regions name
// removeRegion('myRegion')
removeRegion: function(region) {
this[region].close();
delete this[region];
this._regionManager.removeRegion(region);
},
// Create a module, attached to the application
module: function(moduleNames, moduleDefinition){
// slice the args, and add this application object as the
// first argument of the array
var args = slice.call(arguments);
var args = slice(arguments);
args.unshift(this);
// see the Marionette.Module object for more information
return Marionette.Module.create.apply(Marionette.Module, args);
},
// Internal method to set up the region manager
_initRegionManager: function(){
this._regionManager = new Marionette.RegionManager();
this.listenTo(this._regionManager, "region:add", function(name, region){
this[name] = region;
});
this.listenTo(this._regionManager, "region:remove", function(name, region){
delete this[name];
});
}
});
......@@ -1896,7 +2153,7 @@ _.extend(Marionette.Module.prototype, Backbone.Events, {
this._finalizerCallbacks.add(callback);
},
// Start the module, and run all of it's initializers
// Start the module, and run all of its initializers
start: function(options){
// Prevent re-starting a module that is already started
if (this._isInitialized){ return; }
......@@ -1904,11 +2161,7 @@ _.extend(Marionette.Module.prototype, Backbone.Events, {
// start the sub-modules (depth-first hierarchy)
_.each(this.submodules, function(mod){
// check to see if we should start the sub-module with this parent
var startWithParent = true;
startWithParent = mod.startWithParent;
// start the sub-module
if (startWithParent){
if (mod.startWithParent){
mod.start(options);
}
});
......@@ -1962,7 +2215,7 @@ _.extend(Marionette.Module.prototype, Backbone.Events, {
this.app,
Backbone,
Marionette,
$, _,
Marionette.$, _,
customArgs
]);
......@@ -1983,12 +2236,11 @@ _.extend(Marionette.Module, {
// Create a module, hanging off the app parameter as the parent object.
create: function(app, moduleNames, moduleDefinition){
var that = this;
var module = app;
// get the custom args passed in after the module definition and
// get rid of the module name and definition function
var customArgs = slice.apply(arguments);
var customArgs = slice(arguments);
customArgs.splice(0, 3);
// split the module names and get the length
......@@ -2002,9 +2254,9 @@ _.extend(Marionette.Module, {
// Loop through all the parts of the module definition
_.each(moduleNames, function(moduleName, i){
var parentModule = module;
module = that._getModule(parentModule, moduleName, app);
that._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
});
module = this._getModule(parentModule, moduleName, app);
this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs);
}, this);
// Return the last module in the definition chain
return module;
......@@ -2051,7 +2303,6 @@ _.extend(Marionette.Module, {
// `and` the two together, ensuring a single `false` will prevent it
// from starting with the parent
var tmp = module.startWithParent;
module.startWithParent = module.startWithParent && startWithParent;
// setup auto-start if needed
......@@ -2075,4 +2326,4 @@ _.extend(Marionette.Module, {
return Marionette;
})(Backbone, _, $ || window.jQuery || window.Zepto || window.ender);
})(this, Backbone, _);
// Backbone.js 0.9.10
// Backbone.js 1.0.0
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc.
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
......@@ -18,14 +18,14 @@
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to array methods.
// Create local references to array methods we'll want to use later.
var array = [];
var push = array.push;
var slice = array.slice;
var splice = array.splice;
// The top-level namespace. All public Backbone classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
// be attached to this. Exported for both the browser and the server.
var Backbone;
if (typeof exports !== 'undefined') {
Backbone = exports;
......@@ -34,14 +34,15 @@
}
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '0.9.10';
Backbone.VERSION = '1.0.0';
// 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, or Ender owns the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender;
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
......@@ -64,45 +65,6 @@
// Backbone.Events
// ---------------
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
} else if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
} else {
return true;
}
};
// Optimized internal dispatch function for triggering events. Tries to
// keep the usual cases speedy (most Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length;
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx);
return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]);
return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]);
return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]);
return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
}
};
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
......@@ -115,29 +77,27 @@
//
var Events = Backbone.Events = {
// Bind one or more space separated events, or an events map,
// to a `callback` function. Passing `"all"` will bind the callback to
// all events fired.
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on: function(name, callback, context) {
if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this;
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
this._events || (this._events = {});
var list = this._events[name] || (this._events[name] = []);
list.push({callback: callback, context: context, ctx: context || this});
var events = this._events[name] || (this._events[name] = []);
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind events to only be triggered a single time. After the first time
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
once: function(name, callback, context) {
if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this;
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
this.on(name, once, context);
return this;
return this.on(name, once, context);
},
// Remove one or many callbacks. If `context` is null, removes all
......@@ -145,7 +105,7 @@
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
off: function(name, callback, context) {
var list, 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 (!name && !callback && !context) {
this._events = {};
......@@ -155,19 +115,18 @@
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (list = this._events[name]) {
events = [];
if (events = this._events[name]) {
this._events[name] = retain = [];
if (callback || context) {
for (j = 0, k = list.length; j < k; j++) {
ev = list[j];
if ((callback && callback !== ev.callback &&
callback !== ev.callback._callback) ||
for (j = 0, k = events.length; j < k; j++) {
ev = events[j];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
events.push(ev);
retain.push(ev);
}
}
}
this._events[name] = events;
if (!retain.length) delete this._events[name];
}
}
......@@ -189,34 +148,81 @@
return this;
},
// An inversion-of-control version of `on`. Tell *this* object to listen to
// an event in another object ... keeping track of what it's listening to.
listenTo: function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = obj;
obj.on(name, typeof name === 'object' ? this : callback, this);
return this;
},
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function(obj, name, callback) {
var listeners = this._listeners;
if (!listeners) return;
if (obj) {
obj.off(name, typeof name === 'object' ? this : callback, this);
if (!name && !callback) delete listeners[obj._listenerId];
} else {
if (!listeners) return this;
var deleteListener = !name && !callback;
if (typeof name === 'object') callback = this;
if (obj) (listeners = {})[obj._listenerId] = obj;
for (var id in listeners) {
listeners[id].off(name, callback, this);
}
this._listeners = {};
if (deleteListener) delete this._listeners[id];
}
return this;
}
};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
}
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
}
return true;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); 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 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);
}
};
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = obj;
if (typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility.
Events.bind = Events.on;
......@@ -229,15 +235,21 @@
// Backbone.Model
// --------------
// Create a new model, with defined attributes. A client id (`cid`)
// Backbone **Models** are the basic data object in the framework --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var defaults;
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId('c');
this.attributes = {};
if (options && options.collection) this.collection = options.collection;
if (options && options.parse) attrs = this.parse(attrs, options) || {};
_.extend(this, _.pick(options, modelOptions));
if (options.parse) attrs = this.parse(attrs, options) || {};
if (defaults = _.result(this, 'defaults')) {
attrs = _.defaults({}, attrs, defaults);
}
......@@ -246,12 +258,18 @@
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.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// The value returned during the last failed validation.
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
......@@ -265,7 +283,8 @@
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default.
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
......@@ -286,10 +305,9 @@
return this.get(attr) != null;
},
// ----------------------------------------------------------------------
// Set a hash of model attributes on the object, firing `"change"` unless
// you choose to silence it.
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
......@@ -343,6 +361,8 @@
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
......@@ -355,14 +375,13 @@
return this;
},
// Remove an attribute from the model, firing `"change"` unless you choose
// to silence it. `unset` is a noop if the attribute doesn't exist.
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"` unless you choose
// to silence it.
// Clear all attributes on the model, firing `"change"`.
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
......@@ -406,19 +425,20 @@
return _.clone(this._previousAttributes);
},
// ---------------------------------------------------------------------
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overriden,
// model differs from its current attributes, they will be overridden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(model, resp, options) {
options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
......@@ -426,7 +446,7 @@
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
var attrs, success, method, xhr, attributes = this.attributes;
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
......@@ -452,8 +472,9 @@
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
success = options.success;
options.success = function(model, resp, options) {
var model = this;
var success = options.success;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
......@@ -462,9 +483,10 @@
return false;
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
// Finish configuring and sending the Ajax request.
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
......@@ -487,15 +509,17 @@
model.trigger('destroy', model, model.collection, options);
};
options.success = function(model, resp, options) {
options.success = function(resp) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
if (this.isNew()) {
options.success(this, null, options);
options.success();
return false;
}
wrapError(this, options);
var xhr = this.sync('delete', this, options);
if (!options.wait) destroy();
......@@ -529,39 +553,61 @@
// Check if the model is currently in a valid state.
isValid: function(options) {
return !this.validate || !this.validate(this.attributes, options);
return this._validate({}, _.extend(options || {}, { validate: true }));
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire a general
// `"error"` event and call the error callback, if specified.
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, options || {});
this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error}));
return false;
}
});
// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
Model.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});
// Backbone.Collection
// -------------------
// Provides a standard collection class for our sets of models, ordered
// or unordered. If a `comparator` is specified, the Collection will maintain
// If models tend to represent a single row of data, a Backbone Collection is
// more analagous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain
// indexes of their models, both in order, and for lookup by `id`.
// Create a new **Collection**, perhaps to contain a specific type of `model`.
// If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.url) this.url = options.url;
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this.models = [];
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options));
};
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, merge: false, remove: false};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
......@@ -586,34 +632,62 @@
// Add a model, or list of models to the set.
add: function(models, options) {
return this.set(models, _.defaults(options || {}, addOptions));
},
// Remove a model, or a list of models from the set.
remove: function(models, options) {
models = _.isArray(models) ? models.slice() : [models];
options || (options = {});
var i, l, model, attrs, existing, doSort, add, at, sort, sortAttr;
add = [];
at = options.at;
sort = this.comparator && (at == null) && options.sort != false;
sortAttr = _.isString(this.comparator) ? this.comparator : null;
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
}
return this;
},
// Update a collection by `set`-ing a new list of models, adding new ones,
// removing models that are no longer present, and merging models that
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
set: function(models, options) {
options = _.defaults(options || {}, setOptions);
if (options.parse) models = this.parse(models, options);
if (!_.isArray(models)) models = models ? [models] : [];
var i, l, model, attrs, existing, sort;
var at = options.at;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, l = models.length; i < l; i++) {
if (!(model = this._prepareModel(attrs = models[i], options))) {
this.trigger('invalid', this, attrs, options);
continue;
}
if (!(model = this._prepareModel(models[i], options))) continue;
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
if (existing = this.get(model)) {
if (options.remove) modelMap[existing.cid] = true;
if (options.merge) {
existing.set(attrs === model ? model.attributes : attrs, options);
if (sort && !doSort && existing.hasChanged(sortAttr)) doSort = true;
}
continue;
existing.set(model.attributes, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
}
// This is a new model, push it to the `add` list.
add.push(model);
// This is a new model, push it to the `toAdd` list.
} else if (options.add) {
toAdd.push(model);
// Listen to added models' events, and index models for lookup by
// `id` and by `cid`.
......@@ -621,53 +695,55 @@
this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model;
}
}
// Remove nonexistent models if appropriate.
if (options.remove) {
for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
if (toRemove.length) this.remove(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (add.length) {
if (sort) doSort = true;
this.length += add.length;
if (toAdd.length) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
splice.apply(this.models, [at, 0].concat(add));
splice.apply(this.models, [at, 0].concat(toAdd));
} else {
push.apply(this.models, add);
push.apply(this.models, toAdd);
}
}
// Silently sort the collection if appropriate.
if (doSort) this.sort({silent: true});
if (sort) this.sort({silent: true});
if (options.silent) return this;
// Trigger `add` events.
for (i = 0, l = add.length; i < l; i++) {
(model = add[i]).trigger('add', model, this, options);
for (i = 0, l = toAdd.length; i < l; i++) {
(model = toAdd[i]).trigger('add', model, this, options);
}
// Trigger `sort` if the collection was sorted.
if (doSort) this.trigger('sort', this, options);
if (sort) this.trigger('sort', this, options);
return this;
},
// Remove a model, or a list of models from the set.
remove: function(models, options) {
models = _.isArray(models) ? models.slice() : [models];
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations.
reset: function(models, options) {
options || (options = {});
var i, l, index, model;
for (i = 0, l = models.length; i < l; i++) {
model = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model);
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
options.previousModels = this.models;
this._reset();
this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return this;
},
......@@ -707,8 +783,7 @@
// Get a model from the set by id.
get: function(obj) {
if (obj == null) return void 0;
this._idAttr || (this._idAttr = this.model.prototype.idAttribute);
return this._byId[obj.id || obj.cid || obj[this._idAttr] || obj];
return this._byId[obj.id != null ? obj.id : obj.cid || obj];
},
// Get the model at the given index.
......@@ -716,10 +791,11 @@
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of `filter`.
where: function(attrs) {
if (_.isEmpty(attrs)) return [];
return this.filter(function(model) {
// Return models with matching attributes. Useful for simple cases of
// `filter`.
where: function(attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : [];
return this[first ? 'find' : 'filter'](function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
......@@ -727,13 +803,17 @@
});
},
// Return the first model with matching attributes. Useful for simple cases
// of `find`.
findWhere: function(attrs) {
return this.where(attrs, true);
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
if (!this.comparator) {
throw new Error('Cannot sort a set without a comparator');
}
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
// Run sort based on type of `comparator`.
......@@ -747,75 +827,36 @@
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: function(attr) {
return _.invoke(this.models, 'get', attr);
},
// Smartly update a collection with a change set of models, adding,
// removing, and merging as necessary.
update: function(models, options) {
options = _.extend({add: true, merge: true, remove: true}, options);
if (options.parse) models = this.parse(models, options);
var model, i, l, existing;
var add = [], remove = [], modelMap = {};
// Allow a single model (or no argument) to be passed.
if (!_.isArray(models)) models = models ? [models] : [];
// Proxy to `add` for this case, no need to iterate...
if (options.add && !options.remove) return this.add(models, options);
// Determine which models to add and merge, and which to remove.
for (i = 0, l = models.length; i < l; i++) {
model = models[i];
existing = this.get(model);
if (options.remove && existing) modelMap[existing.cid] = true;
if ((options.add && !existing) || (options.merge && existing)) {
add.push(model);
}
}
if (options.remove) {
for (i = 0, l = this.models.length; i < l; i++) {
model = this.models[i];
if (!modelMap[model.cid]) remove.push(model);
}
}
// Remove models (if applicable) before we add and merge the rest.
if (remove.length) this.remove(remove, options);
if (add.length) this.add(add, options);
return this;
},
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any `add` or `remove` events. Fires `reset` when finished.
reset: function(models, options) {
options || (options = {});
if (options.parse) models = this.parse(models, options);
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i]);
}
options.previousModels = this.models.slice();
this._reset();
if (models) this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return this;
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `update: true` is passed, the response
// data will be passed through the `update` method instead of `reset`.
// collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
options.success = function(collection, resp, options) {
var method = options.update ? 'update' : 'reset';
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options);
if (success) success(collection, resp, options);
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
......@@ -828,7 +869,7 @@
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(model, resp, options) {
options.success = function(resp) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
......@@ -847,14 +888,16 @@
return new this.constructor(this.models);
},
// Reset all internal state. Called when the collection is reset.
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
this.length = 0;
this.models.length = 0;
this.models = [];
this._byId = {};
},
// Prepare a model or hash of attributes to be added to this collection.
// Prepare a hash of attributes (or other model) to be added to this
// collection.
_prepareModel: function(attrs, options) {
if (attrs instanceof Model) {
if (!attrs.collection) attrs.collection = this;
......@@ -863,11 +906,14 @@
options || (options = {});
options.collection = this;
var model = new this.model(attrs, options);
if (!model._validate(attrs, options)) return false;
if (!model._validate(attrs, options)) {
this.trigger('invalid', this, attrs, options);
return false;
}
return model;
},
// Internal method to remove a model's ties to a collection.
// Internal method to sever a model's ties to a collection.
_removeReference: function(model) {
if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this);
......@@ -885,19 +931,13 @@
if (model.id != null) this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
},
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);
}
});
// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
......@@ -927,48 +967,288 @@
};
});
// Backbone.Router
// ---------------
// Backbone.View
// -------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
// Backbone Views are almost more convention than they are actual code. A View
// is simply a JavaScript object that represents a logical chunk of UI in the
// DOM. This might be a single item, an entire list, a sidebar or panel, or
// even the surrounding frame which wraps your whole app. Defining a chunk of
// UI as a **View** allows you to define your DOM events declaratively, without
// having to worry about render order ... and makes it easy for the view to
// react to specific changes in the state of your models.
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save'
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return this;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
return this;
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid);
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.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
});
// Backbone.sync
// -------------
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
// 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
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && window.ActiveXObject &&
!(window.external && window.external.msActiveXFilteringEnabled)) {
params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP");
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
// Override this if you'd like to use a different library.
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
// Backbone.Router
// ---------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
Backbone.history.route(route, _.bind(function(fragment) {
var args = this._extractParameters(route, fragment);
callback && callback.apply(this, args);
this.trigger.apply(this, ['route:' + name].concat(args));
this.trigger('route', name, args);
Backbone.history.trigger('route', this, name, args);
}, this));
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
callback && callback.apply(router, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
return this;
},
......@@ -983,6 +1263,7 @@
// routes can be defined at the bottom of the route map.
_bindRoutes: function() {
if (!this.routes) return;
this.routes = _.result(this, 'routes');
var route, routes = _.keys(this.routes);
while ((route = routes.pop()) != null) {
this.route(route, this.routes[route]);
......@@ -1002,9 +1283,13 @@
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted parameters.
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) {
return route.exec(fragment).slice(1);
var params = route.exec(fragment).slice(1);
return _.map(params, function(param) {
return param ? decodeURIComponent(param) : null;
});
}
});
......@@ -1012,8 +1297,11 @@
// Backbone.History
// ----------------
// Handles cross-browser history management, based on URL fragments. If the
// browser does not support `onhashchange`, falls back to polling.
// Handles cross-browser history management, based on either
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
// and URL fragments. If the browser supports neither (old IE, natch),
// falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
......@@ -1224,230 +1512,6 @@
// Create the default Backbone.history.
Backbone.history = new History;
// Backbone.View
// -------------
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be prefered to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save'
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) throw new Error('Method "' + events[key] + '" does not exist');
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid);
},
// Performs the initial configuration of a View with a set of options.
// Keys with special meaning *(model, collection, id, className)*, are
// attached directly to the view.
_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.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
});
// Backbone.sync
// -------------
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
var success = options.success;
options.success = function(resp) {
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
var error = options.error;
options.error = function(xhr) {
if (error) error(model, xhr, options);
model.trigger('error', model, xhr, options);
};
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
// Helpers
// -------
......@@ -1495,4 +1559,13 @@
throw new Error('A "url" property or function must be specified');
};
// Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) {
var error = options.error;
options.error = function(resp) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
};
}).call(this);
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -136,10 +136,6 @@
}
function getFile(file, callback) {
if (!location.host) {
return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
}
var xhr = new XMLHttpRequest();
xhr.open('GET', findRoot() + file, true);
......
......@@ -32,7 +32,7 @@ TodoMVC.module('Layout', function (Layout, App, Backbone) {
// Layout Footer View
// ------------------
Layout.Footer = Backbone.Marionette.Layout.extend({
Layout.Footer = Backbone.Marionette.ItemView.extend({
template: '#template-footer',
// UI bindings create cached attributes that
......
......@@ -70,6 +70,7 @@ TodoMVC.module('TodoList.Views', function (Views, App, Backbone, Marionette, $)
}
if (e.which === ESC_KEY) {
this.ui.edit.val(this.model.get('title'));
this.$el.removeClass('editing');
}
}
......
......@@ -53,7 +53,7 @@ TodoMVC.module('TodoList', function (TodoList, App, Backbone, Marionette, $, _)
// Set the filter to show complete or all items
filterItems: function (filter) {
App.vent.trigger('todoList:filter', filter.trim() || '');
App.vent.trigger('todoList:filter', (filter && filter.trim()) || '');
}
});
......
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