Commit 25e31ff1 authored by Sam Saccone's avatar Sam Saccone

Update backbone.marionette to new UI

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