Commit 083ba9fe authored by Kevin Malakoff's avatar Kevin Malakoff Committed by Sam Saccone

Update Knockback to 1.0.0 and simplified.

parent 24f9d0df
node_modules/knockback/* node_modules/backbone/*
!node_modules/knockback/knockback.js !node_modules/backbone/backbone.js
!node_modules/knockback/node_modules
node_modules/knockback/node_modules/backbone/*
!node_modules/knockback/node_modules/backbone/backbone.js
node_modules/knockback/node_modules/underscore/* node_modules/backbone.localStorage/*
!node_modules/knockback/node_modules/underscore/underscore.js !node_modules/backbone.localStorage/backbone.localStorage.js
node_modules/knockback/node_modules/knockout/*
!node_modules/knockback/node_modules/knockout/build
node_modules/knockback/node_modules/knockout/build/*
!node_modules/knockback/node_modules/knockout/build/output
node_modules/knockback/node_modules/knockout/build/output/*
!node_modules/knockback/node_modules/knockout/build/output/knockout-latest.debug.js
node_modules/jquery/* node_modules/jquery/*
!node_modules/jquery/dist !node_modules/jquery/dist
node_modules/jquery/dist/* node_modules/jquery/dist/*
!node_modules/jquery/dist/jquery.js !node_modules/jquery/dist/jquery.js
node_modules/knockback/*
!node_modules/knockback/knockback.js
node_modules/knockout/*
!node_modules/knockout/build
node_modules/knockout/build/*
!node_modules/knockout/build/output
node_modules/knockout/build/output/*
!node_modules/knockout/build/output/knockout-latest.debug.js
node_modules/todomvc-app-css/* node_modules/todomvc-app-css/*
!node_modules/todomvc-app-css/index.css !node_modules/todomvc-app-css/index.css
node_modules/todomvc-common/* node_modules/todomvc-common/*
!node_modules/todomvc-common/base.css !node_modules/todomvc-common/base.css
!node_modules/todomvc-common/base.js !node_modules/todomvc-common/base.js
node_modules/underscore/*
!node_modules/underscore/underscore.js
...@@ -7,28 +7,28 @@ ...@@ -7,28 +7,28 @@
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css"> <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head> </head>
<body> <body>
<section id="todoapp" kb-inject="AppViewModel"> <section class="todoapp" kb-inject="AppViewModel">
<header id="header"> <header class="header">
<h1>todos</h1> <h1>todos</h1>
<input id="new-todo" type="text" data-bind="value: title, valueUpdate: 'afterkeydown', event: {keyup: onAddTodo}" placeholder="What needs to be done?" autofocus> <input class="new-todo" type="text" data-bind="value: title, valueUpdate: 'afterkeydown', event: {keyup: onAddTodo}" placeholder="What needs to be done?" autofocus>
</header> </header>
<section id="main" data-bind="block: tasks_exist"> <section class="main" data-bind="visible: tasks_exist">
<input id="toggle-all" type="checkbox" data-bind="checked: all_completed"> <input class="toggle-all" type="checkbox" data-bind="checked: all_completed">
<label for="toggle-all">Mark all as complete</label> <label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: todos"> <ul class="todo-list" data-bind="foreach: todos">
<li data-bind="css: {completed: completed, editing: editing}"> <li data-bind="css: {completed: completed, editing: editing}">
<div class="view" data-bind="event: {dblclick: onCheckEditBegin}"> <div class="view" data-bind="event: {dblclick: onCheckEditBegin}">
<input class="toggle" type="checkbox" data-bind="checked: completed" checked> <input class="toggle" type="checkbox" data-bind="checked: completed" checked>
<label data-bind="text: title"></label> <label data-bind="text: title"></label>
<button class="destroy" data-bind="click: onDestroyTodo"></button> <button class="destroy" data-bind="click: onDestroy"></button>
</div> </div>
<input class="edit" type="text" data-bind="value: title, selectAndFocus: editing, event: {blur: onCheckEditEnd, keyup: onCheckEditEnd}"> <input class="edit" type="text" data-bind="value: edit_title, event: {blur: onCheckEditEnd, keyup: onCheckEditEnd}">
</li> </li>
</ul> </ul>
</section> </section>
<footer id="footer" data-bind="block: tasks_exist"> <footer class="footer" data-bind="visible: tasks_exist">
<span id="todo-count" data-bind="html: loc.remaining_message"></span> <span class="todo-count" data-bind="html: loc.remaining_message"></span>
<ul id="filters"> <ul class="filters">
<li> <li>
<a href="#/" data-bind="css: {selected: list_filter_mode()==''}">All</a> <a href="#/" data-bind="css: {selected: list_filter_mode()==''}">All</a>
</li> </li>
...@@ -39,10 +39,10 @@ ...@@ -39,10 +39,10 @@
<a href="#/completed" data-bind="css: {selected: list_filter_mode()=='completed'}">Completed</a> <a href="#/completed" data-bind="css: {selected: list_filter_mode()=='completed'}">Completed</a>
</li> </li>
</ul> </ul>
<button id="clear-completed" data-bind="text: loc.clear_message, block: loc.clear_message, click: onDestroyCompleted"></button> <button class="clear-completed" data-bind="text: loc.clear_message, visible: loc.clear_message, click: onClearCompleted"></button>
</footer> </footer>
</section> </section>
<footer id="info"> <footer class="info">
<p>Double-click to edit a todo</p> <p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/kmalakoff">Kevin Malakoff</a>. <br/> <p>Created by <a href="https://github.com/kmalakoff">Kevin Malakoff</a>. <br/>
Please try out the <a href="http://kmalakoff.github.com/knockback-todos-app/">enhanced version</a> <br/> Please try out the <a href="http://kmalakoff.github.com/knockback-todos-app/">enhanced version</a> <br/>
...@@ -54,15 +54,13 @@ ...@@ -54,15 +54,13 @@
<!-- App Dependencies --> <!-- App Dependencies -->
<script src="node_modules/todomvc-common/base.js"></script> <script src="node_modules/todomvc-common/base.js"></script>
<script src="node_modules/jquery/dist/jquery.js"></script> <script src="node_modules/jquery/dist/jquery.js"></script>
<script src="node_modules/knockback/node_modules/underscore/underscore.js"></script> <script src="node_modules/underscore/underscore.js"></script>
<script src="node_modules/knockback/node_modules/backbone/backbone.js"></script> <script src="node_modules/backbone/backbone.js"></script>
<script src="node_modules/knockback/node_modules/knockout/build/output/knockout-latest.debug.js"></script> <script src="node_modules/knockout/build/output/knockout-latest.debug.js"></script>
<script src="node_modules/knockback/knockback.js"></script> <script src="node_modules/knockback/knockback.js"></script>
<script src="node_modules/backbone.localStorage/backbone.localStorage.js"></script> <script src="node_modules/backbone.localstorage/backbone.localStorage.js"></script>
<!-- App and Components --> <!-- App and Components -->
<script src="js/lib/knockout-extended-bindings.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/models/todo_collection.js"></script> <script src="js/models/todo_collection.js"></script>
<script src="js/viewmodels/todo.js"></script> <script src="js/viewmodels/todo.js"></script>
<script src="js/viewmodels/app.js"></script> <script src="js/viewmodels/app.js"></script>
......
// Generated by CoffeeScript 1.8.0
(function() {
ko.bindingHandlers.dblclick = {
init: function(element, value_accessor) {
return $(element).dblclick(ko.utils.unwrapObservable(value_accessor()));
}
};
ko.bindingHandlers.block = {
update: function(element, value_accessor) {
return element.style.display = ko.utils.unwrapObservable(value_accessor()) ? 'block' : 'none';
}
};
ko.bindingHandlers.selectAndFocus = {
init: function(element, value_accessor, all_bindings_accessor) {
ko.bindingHandlers.hasfocus.init(element, value_accessor, all_bindings_accessor);
return ko.utils.registerEventHandler(element, 'focus', function() {
return element.select();
});
},
update: function(element, value_accessor) {
ko.utils.unwrapObservable(value_accessor());
return _.defer(function() {
return ko.bindingHandlers.hasfocus.update(element, value_accessor);
});
}
};
}).call(this);
// Generated by CoffeeScript 1.8.0
(function() {
var __hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
window.Todo = (function(_super) {
__extends(Todo, _super);
function Todo() {
return Todo.__super__.constructor.apply(this, arguments);
}
Todo.prototype.completed = function(completed) {
if (arguments.length === 0) {
return !!this.get('completed');
}
return this.save({
completed: completed ? new Date() : null
});
};
return Todo;
})(Backbone.Model);
}).call(this);
// Generated by CoffeeScript 1.8.0 // Generated by CoffeeScript 1.10.0
(function() { (function() {
var __hasProp = {}.hasOwnProperty, var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; hasProp = {}.hasOwnProperty;
window.TodoCollection = (function(_super) { window.TodoCollection = (function(superClass) {
__extends(TodoCollection, _super); extend(TodoCollection, superClass);
function TodoCollection() { function TodoCollection() {
return TodoCollection.__super__.constructor.apply(this, arguments); return TodoCollection.__super__.constructor.apply(this, arguments);
...@@ -12,35 +12,6 @@ ...@@ -12,35 +12,6 @@
TodoCollection.prototype.localStorage = new Store('todos-knockback'); TodoCollection.prototype.localStorage = new Store('todos-knockback');
TodoCollection.prototype.model = Todo;
TodoCollection.prototype.completedCount = function() {
return this.models.reduce((function(prev, cur) {
return prev + (cur.completed() ? 1 : 0);
}), 0);
};
TodoCollection.prototype.remainingCount = function() {
return this.models.length - this.completedCount();
};
TodoCollection.prototype.completeAll = function(completed) {
return this.each(function(todo) {
return todo.completed(completed);
});
};
TodoCollection.prototype.destroyCompleted = function() {
var completed_tasks, model, _i, _len;
completed_tasks = this.filter(function(todo) {
return todo.completed();
});
for (_i = 0, _len = completed_tasks.length; _i < _len; _i++) {
model = completed_tasks[_i];
model.destroy();
}
};
return TodoCollection; return TodoCollection;
})(Backbone.Collection); })(Backbone.Collection);
......
// Generated by CoffeeScript 1.8.0 // Generated by CoffeeScript 1.10.0
(function() { (function() {
var ENTER_KEY; var ENTER_KEY,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
ENTER_KEY = 13; ENTER_KEY = 13;
window.AppViewModel = function() { window.AppViewModel = (function() {
var filter_fn, router; function AppViewModel() {
this.collections = { this.onClearCompleted = bind(this.onClearCompleted, this);
todos: new TodoCollection() this.onAddTodo = bind(this.onAddTodo, this);
}; var filter_fn, fn, i, len, ref, route, router;
this.collections.todos.fetch();
this.list_filter_mode = ko.observable(''); this.list_filter_mode = ko.observable('');
filter_fn = ko.computed((function(_this) { filter_fn = ko.computed((function(_this) {
return function() { return function() {
switch (_this.list_filter_mode()) { switch (_this.list_filter_mode()) {
case 'active': case 'active':
return function(model) { return (function(model) {
return !model.completed(); return !model.get('completed');
}; });
case 'completed': case 'completed':
return function(model) { return (function(model) {
return model.completed(); return model.get('completed');
}; });
default: default:
return function() { return (function() {
return true; return true;
}; });
} }
}; };
})(this)); })(this));
this.todos = kb.collectionObservable(this.collections.todos, { this.todos = kb.collectionObservable(new TodoCollection(), TodoViewModel, {
view_model: TodoViewModel,
filters: filter_fn filters: filter_fn
}); });
this.todos_changed = kb.triggeredObservable(this.collections.todos, 'change add remove'); this.todos.collection().fetch();
this._todos_changed = kb.triggeredObservable(this.todos.collection(), 'change add remove');
this.tasks_exist = ko.computed((function(_this) { this.tasks_exist = ko.computed((function(_this) {
return function() { return function() {
_this.todos_changed(); _this._todos_changed();
return !!_this.collections.todos.length; return !!_this.todos.collection().models.length;
}; };
})(this)); })(this));
this.title = ko.observable(''); this.title = ko.observable('');
this.onAddTodo = (function(_this) { this.completed_count = ko.computed((function(_this) {
return function(view_model, event) {
if (!$.trim(_this.title()) || (event.keyCode !== ENTER_KEY)) {
return true;
}
_this.collections.todos.create({
title: $.trim(_this.title())
});
return _this.title('');
};
})(this);
this.remaining_count = ko.computed((function(_this) {
return function() { return function() {
_this.todos_changed(); _this._todos_changed();
return _this.collections.todos.remainingCount(); return _this.todos.collection().filter(function(model) {
return model.get('completed');
}).length;
}; };
})(this)); })(this));
this.completed_count = ko.computed((function(_this) { this.remaining_count = ko.computed((function(_this) {
return function() { return function() {
_this.todos_changed(); _this._todos_changed();
return _this.collections.todos.completedCount(); return _this.todos.collection().length - _this.completed_count();
}; };
})(this)); })(this));
this.all_completed = ko.computed({ this.all_completed = ko.computed({
...@@ -72,15 +63,14 @@ ...@@ -72,15 +63,14 @@
})(this), })(this),
write: (function(_this) { write: (function(_this) {
return function(completed) { return function(completed) {
return _this.collections.todos.completeAll(completed); return _this.todos.collection().each(function(model) {
return model.save({
completed: completed
});
});
}; };
})(this) })(this)
}); });
this.onDestroyCompleted = (function(_this) {
return function() {
return _this.collections.todos.destroyCompleted();
};
})(this);
this.loc = { this.loc = {
remaining_message: ko.computed((function(_this) { remaining_message: ko.computed((function(_this) {
return function() { return function() {
...@@ -89,9 +79,8 @@ ...@@ -89,9 +79,8 @@
})(this)), })(this)),
clear_message: ko.computed((function(_this) { clear_message: ko.computed((function(_this) {
return function() { return function() {
var count; if (_this.completed_count()) {
if ((count = _this.completed_count())) { return 'Clear completed';
return "Clear completed";
} else { } else {
return ''; return '';
} }
...@@ -99,22 +88,46 @@ ...@@ -99,22 +88,46 @@
})(this)) })(this))
}; };
router = new Backbone.Router; router = new Backbone.Router;
router.route('', null, (function(_this) { ref = ['', 'active', 'completed'];
return function() { fn = (function(_this) {
return _this.list_filter_mode(''); return function(route) {
}; return router.route(route, null, function() {
})(this)); return _this.list_filter_mode(route);
router.route('active', null, (function(_this) { });
return function() {
return _this.list_filter_mode('active');
};
})(this));
router.route('completed', null, (function(_this) {
return function() {
return _this.list_filter_mode('completed');
}; };
})(this)); })(this);
for (i = 0, len = ref.length; i < len; i++) {
route = ref[i];
fn(route);
}
Backbone.history.start(); Backbone.history.start();
}
AppViewModel.prototype.onAddTodo = function(vm, event) {
if (!$.trim(this.title()) || (event.keyCode !== ENTER_KEY)) {
return;
}
this.todos.collection().create({
title: $.trim(this.title())
});
return this.title('');
};
AppViewModel.prototype.onClearCompleted = function() {
var i, len, model, ref, results;
ref = this.todos.collection().filter(function(model) {
return model.get('completed');
});
results = [];
for (i = 0, len = ref.length; i < len; i++) {
model = ref[i];
results.push(model.destroy());
}
return results;
}; };
return AppViewModel;
})();
}).call(this); }).call(this);
// Generated by CoffeeScript 1.8.0 // Generated by CoffeeScript 1.10.0
(function() { (function() {
window.TodoViewModel = function(model) { var ENTER_KEY, ESCAPE_KEY,
var ENTER_KEY, ESCAPE_KEY; bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
ENTER_KEY = 13; ENTER_KEY = 13;
ESCAPE_KEY = 27; ESCAPE_KEY = 27;
this.editing = ko.observable(false);
this.completed = kb.observable(model, { window.TodoViewModel = (function(superClass) {
key: 'completed', extend(TodoViewModel, superClass);
read: (function() {
return model.completed(); function TodoViewModel(model, options) {
}), this.onCheckEditEnd = bind(this.onCheckEditEnd, this);
write: (function(completed) { this.onCheckEditBegin = bind(this.onCheckEditBegin, this);
return model.completed(completed); this.onDestroy = bind(this.onDestroy, this);
}) TodoViewModel.__super__.constructor.call(this, model, {
}, this); requires: ['title', 'completed']
this.title = kb.observable(model, { }, options);
key: 'title', this.completed.subscribe((function(_this) {
write: ((function(_this) { return function(completed) {
return function(title) { return _this.model().save({
if ($.trim(title)) { completed: completed
model.save({
title: $.trim(title)
});
} else {
_.defer(function() {
return model.destroy();
}); });
}
return _this.editing(false);
}; };
})(this)) })(this));
}, this); this.edit_title = ko.observable();
this.onDestroyTodo = (function(_this) { this.editing = ko.observable(false);
return function() { }
return model.destroy();
TodoViewModel.prototype.onDestroy = function() {
return this.model().destroy();
}; };
})(this);
this.onCheckEditBegin = (function(_this) { TodoViewModel.prototype.onCheckEditBegin = function() {
return function() { if (this.editing()) {
if (!_this.editing()) { return;
_this.editing(true);
return $('.todo-input').focus();
} }
this.edit_title(this.title());
this.editing(true);
return $('.todo-input').focus();
}; };
})(this);
this.onCheckEditEnd = (function(_this) { TodoViewModel.prototype.onCheckEditEnd = function(vm, event) {
return function(view_model, event) { var title;
if (!this.editing()) {
return;
}
if (event.keyCode === ESCAPE_KEY) { if (event.keyCode === ESCAPE_KEY) {
_this.editing(false); this.editing(false);
} }
if ((event.keyCode === ENTER_KEY) || (event.type === 'blur')) { if ((event.keyCode === ENTER_KEY) || (event.type === 'blur')) {
$('.todo-input').blur(); $('.todo-input').blur();
return _this.editing(false); title = this.edit_title();
} if ($.trim(title)) {
this.model().set({
title: title
}).save({
title: $.trim(title)
});
} else {
_.defer((function(_this) {
return function() {
return _this.model().destroy();
}; };
})(this); })(this));
}
return this.editing(false);
}
}; };
return TodoViewModel;
})(kb.ViewModel);
}).call(this); }).call(this);
// Backbone.js 1.1.2 // Backbone.js 1.2.3
// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license. // Backbone may be freely distributed under the MIT license.
// For all details and documentation: // For all details and documentation:
// http://backbonejs.org // http://backbonejs.org
(function(root, factory) { (function(factory) {
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global);
// Set up Backbone appropriately for the environment. Start with AMD. // Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
...@@ -17,15 +22,16 @@ ...@@ -17,15 +22,16 @@
// Next for Node.js or CommonJS. jQuery may not be needed as a module. // Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') { } else if (typeof exports !== 'undefined') {
var _ = require('underscore'); var _ = require('underscore'), $;
factory(root, exports, _); try { $ = require('jquery'); } catch(e) {}
factory(root, exports, _, $);
// Finally, as a browser global. // Finally, as a browser global.
} else { } else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
} }
}(this, function(root, Backbone, _, $) { }(function(root, Backbone, _, $) {
// Initial Setup // Initial Setup
// ------------- // -------------
...@@ -34,14 +40,11 @@ ...@@ -34,14 +40,11 @@
// restored later on, if `noConflict` is used. // restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone; var previousBackbone = root.Backbone;
// Create local references to array methods we'll want to use later. // Create a local reference to a common array method we'll want to use later.
var array = []; var slice = Array.prototype.slice;
var push = array.push;
var slice = array.slice;
var splice = array.splice;
// Current version of the library. Keep in sync with `package.json`. // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.1.2'; Backbone.VERSION = '1.2.3';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable. // the `$` variable.
...@@ -60,17 +63,65 @@ ...@@ -60,17 +63,65 @@
Backbone.emulateHTTP = false; Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct // Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as // `application/json` requests ... this will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a // `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`. // form param named `model`.
Backbone.emulateJSON = false; Backbone.emulateJSON = false;
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};
// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
// Backbone.Events // Backbone.Events
// --------------- // ---------------
// A module that can be mixed in to *any object* in order to provide it with // A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback // a custom event channel. You may bind a callback to an event with `on` or
// functions to an event; `trigger`-ing an event fires all callbacks in // remove with `off`; `trigger`-ing an event fires all callbacks in
// succession. // succession.
// //
// var object = {}; // var object = {};
...@@ -78,123 +129,234 @@ ...@@ -78,123 +129,234 @@
// object.on('expand', function(){ alert('expanded'); }); // object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand'); // object.trigger('expand');
// //
var Events = Backbone.Events = { var Events = Backbone.Events = {};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for (names = _.keys(name); i < names.length ; i++) {
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
// Handle space separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
events = iteratee(events, name, callback, opts);
}
return events;
};
// Bind an event to a `callback` function. Passing `"all"` will bind // Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired. // the callback to all events fired.
on: function(name, callback, context) { Events.on = function(name, callback, context) {
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; return internalOn(this, name, callback, context);
this._events || (this._events = {}); };
var events = this._events[name] || (this._events[name] = []);
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind an event to only be triggered a single time. After the first time // Guard the `listening` argument from the public API.
// the callback is invoked, it will be removed. var internalOn = function(obj, name, callback, context, listening) {
once: function(name, callback, context) { obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; context: context,
var self = this; ctx: obj,
var once = _.once(function() { listening: listening
self.off(name, once);
callback.apply(this, arguments);
}); });
once._callback = callback;
return this.on(name, once, context);
},
// Remove one or many callbacks. If `context` is null, removes all if (listening) {
// callbacks with that function. If `callback` is null, removes all var listeners = obj._listeners || (obj._listeners = {});
// callbacks for the event. If `name` is null, removes all bound listeners[listening.id] = listening;
// callbacks for all events.
off: function(name, callback, context) {
var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) {
this._events = void 0;
return this;
}
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (events = this._events[name]) {
this._events[name] = retain = [];
if (callback || context) {
for (j = 0, k = events.length; j < k; j++) {
ev = events[j];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
retain.push(ev);
}
}
}
if (!retain.length) delete this._events[name];
} }
return obj;
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
} }
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this; return this;
}, };
// Trigger one or many events, firing all bound callbacks. Callbacks are // The reducing API that adds a callback to the `events` object.
// passed the same arguments as `trigger` is, apart from the event name var onApi = function(events, name, callback, options) {
// (unless you're listening on `"all"`, which will cause your callback to if (callback) {
// receive the true name of the event as the first argument). var handlers = events[name] || (events[name] = []);
trigger: function(name) { var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
}
return events;
};
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
if (!this._events) return this; if (!this._events) return this;
var args = slice.call(arguments, 1); this._events = eventsApi(offApi, this._events, name, callback, {
if (!eventsApi(this, 'trigger', name, args)) return this; context: context,
var events = this._events[name]; listeners: this._listeners
var allEvents = this._events.all; });
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this; return this;
}, };
// Tell this object to stop listening to either specific events ... or // Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to. // to every object it's currently listening to.
stopListening: function(obj, name, callback) { Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo; var listeningTo = this._listeningTo;
if (!listeningTo) return this; if (!listeningTo) return this;
var remove = !name && !callback;
if (!callback && typeof name === 'object') callback = this; var ids = obj ? [obj._listenId] : _.keys(listeningTo);
if (obj) (listeningTo = {})[obj._listenId] = obj;
for (var id in listeningTo) { for (var i = 0; i < ids.length; i++) {
obj = listeningTo[id]; var listening = listeningTo[ids[i]];
obj.off(name, callback, this);
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; // If listening doesn't exist, this object is not currently
} // listening to obj. Break out early.
return this; if (!listening) break;
listening.obj.off(name, callback, this);
} }
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
}; };
// Regular expression used to split event strings. // The reducing API that removes a callback from the `events` object.
var eventSplitter = /\s+/; var offApi = function(events, name, callback, options) {
if (!events) return;
// Implement fancy features of the Events API such as multiple event var i = 0, listening;
// names `"change blur"` and jQuery-style event maps `{change: action}` var context = options.context, listeners = options.listeners;
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps. // Delete all events listeners and "drop" events.
if (typeof name === 'object') { if (!name && !callback && !context) {
for (var key in name) { var ids = _.keys(listeners);
obj[action].apply(obj, [key, name[key]].concat(rest)); for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
} }
return false; return;
} }
// Handle space separated event names. var names = name ? [name] : _.keys(events);
if (eventSplitter.test(name)) { for (; i < names.length; i++) {
var names = name.split(eventSplitter); name = names[i];
for (var i = 0, l = names.length; i < l; i++) { var handlers = events[name];
obj[action].apply(obj, [names[i]].concat(rest));
// Bail out if there are no events stored.
if (!handlers) break;
// Replace events if there are any remaining. Otherwise, clean up.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
} else {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
} }
return false;
} }
return true; // Update tail event if the list has any events. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
if (_.size(events)) return events;
};
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
return this.on(events, void 0, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
var onceMap = function(map, name, callback, offer) {
if (callback) {
var once = map[name] = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
}
return map;
};
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
var args = Array(length);
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, cb, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
}; };
// A difficult-to-believe, but optimized internal dispatch function for // A difficult-to-believe, but optimized internal dispatch function for
...@@ -211,22 +373,6 @@ ...@@ -211,22 +373,6 @@
} }
}; };
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility. // Aliases for backwards compatibility.
Events.bind = Events.on; Events.bind = Events.on;
Events.unbind = Events.off; Events.unbind = Events.off;
...@@ -248,7 +394,7 @@ ...@@ -248,7 +394,7 @@
var Model = Backbone.Model = function(attributes, options) { var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {}; var attrs = attributes || {};
options || (options = {}); options || (options = {});
this.cid = _.uniqueId('c'); this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {}; this.attributes = {};
if (options.collection) this.collection = options.collection; if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {}; if (options.parse) attrs = this.parse(attrs, options) || {};
...@@ -271,6 +417,10 @@ ...@@ -271,6 +417,10 @@
// CouchDB users may want to set this to `"_id"`. // CouchDB users may want to set this to `"_id"`.
idAttribute: 'id', idAttribute: 'id',
// The prefix is used to create the client id which is used to identify models locally.
// You may want to override this if you're experiencing name clashes with model ids.
cidPrefix: 'c',
// Initialize is an empty function by default. Override it with your own // Initialize is an empty function by default. Override it with your own
// initialization logic. // initialization logic.
initialize: function(){}, initialize: function(){},
...@@ -302,14 +452,19 @@ ...@@ -302,14 +452,19 @@
return this.get(attr) != null; return this.get(attr) != null;
}, },
// Special-cased proxy to underscore's `_.matches` method.
matches: function(attrs) {
return !!_.iteratee(attrs, this)(this.attributes);
},
// Set a hash of model attributes on the object, firing `"change"`. This is // Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying // the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast. // anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) { set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this; if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (typeof key === 'object') { if (typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
...@@ -323,37 +478,40 @@ ...@@ -323,37 +478,40 @@
if (!this._validate(attrs, options)) return false; if (!this._validate(attrs, options)) return false;
// Extract attributes and options. // Extract attributes and options.
unset = options.unset; var unset = options.unset;
silent = options.silent; var silent = options.silent;
changes = []; var changes = [];
changing = this._changing; var changing = this._changing;
this._changing = true; this._changing = true;
if (!changing) { if (!changing) {
this._previousAttributes = _.clone(this.attributes); this._previousAttributes = _.clone(this.attributes);
this.changed = {}; this.changed = {};
} }
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`. var current = this.attributes;
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; var changed = this.changed;
var prev = this._previousAttributes;
// For each `set` attribute, update or delete the current value. // For each `set` attribute, update or delete the current value.
for (attr in attrs) { for (var attr in attrs) {
val = attrs[attr]; val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr); if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) { if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val; changed[attr] = val;
} else { } else {
delete this.changed[attr]; delete changed[attr];
} }
unset ? delete current[attr] : current[attr] = val; unset ? delete current[attr] : current[attr] = val;
} }
// Update the `id`.
this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes. // Trigger all relevant attribute changes.
if (!silent) { if (!silent) {
if (changes.length) this._pending = options; if (changes.length) this._pending = options;
for (var i = 0, l = changes.length; i < l; i++) { for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options); this.trigger('change:' + changes[i], this, current[changes[i]], options);
} }
} }
...@@ -401,13 +559,14 @@ ...@@ -401,13 +559,14 @@
// determining if there *would be* a change. // determining if there *would be* a change.
changedAttributes: function(diff) { changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes; var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
for (var attr in diff) { for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue; var val = diff[attr];
(changed || (changed = {}))[attr] = val; if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
} }
return changed; return _.size(changed) ? changed : false;
}, },
// Get the previous value of an attribute, recorded at the time the last // Get the previous value of an attribute, recorded at the time the last
...@@ -423,17 +582,16 @@ ...@@ -423,17 +582,16 @@
return _.clone(this._previousAttributes); return _.clone(this._previousAttributes);
}, },
// Fetch the model from the server. If the server's representation of the // Fetch the model from the server, merging the response with the model's
// model differs from its current attributes, they will be overridden, // local attributes. Any changed attributes will trigger a "change" event.
// triggering a `"change"` event.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = _.extend({parse: true}, options);
if (options.parse === void 0) options.parse = true;
var model = this; var model = this;
var success = options.success; var success = options.success;
options.success = function(resp) { options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false; var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (success) success(model, resp, options); if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
...@@ -444,9 +602,8 @@ ...@@ -444,9 +602,8 @@
// If the server returns an attributes hash that differs, the model's // If the server returns an attributes hash that differs, the model's
// state will be `set` again. // state will be `set` again.
save: function(key, val, options) { save: function(key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments. // Handle both `"key", value` and `{key: value}` -style arguments.
var attrs;
if (key == null || typeof key === 'object') { if (key == null || typeof key === 'object') {
attrs = key; attrs = key;
options = val; options = val;
...@@ -454,46 +611,43 @@ ...@@ -454,46 +611,43 @@
(attrs = {})[key] = val; (attrs = {})[key] = val;
} }
options = _.extend({validate: true}, options); options = _.extend({validate: true, parse: true}, options);
var wait = options.wait;
// If we're not waiting and attributes exist, save acts as // If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if // `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set. // the model will be valid when the attributes, if any, are set.
if (attrs && !options.wait) { if (attrs && !wait) {
if (!this.set(attrs, options)) return false; if (!this.set(attrs, options)) return false;
} else { } else {
if (!this._validate(attrs, options)) return false; if (!this._validate(attrs, options)) return false;
} }
// Set temporary attributes if `{wait: true}`.
if (attrs && options.wait) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally) // After a successful server-side save, the client is (optionally)
// updated with the server-side state. // updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this; var model = this;
var success = options.success; var success = options.success;
var attributes = this.attributes;
options.success = function(resp) { options.success = function(resp) {
// Ensure attributes are restored during synchronous saves. // Ensure attributes are restored during synchronous saves.
model.attributes = attributes; model.attributes = attributes;
var serverAttrs = model.parse(resp, options); var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { if (serverAttrs && !model.set(serverAttrs, options)) return false;
return false; if (success) success.call(options.context, model, resp, options);
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options); model.trigger('sync', model, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); // Set temporary attributes if `{wait: true}` to properly find new ids.
if (method === 'patch') options.attrs = attrs; if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
xhr = this.sync(method, this, options);
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this.sync(method, this, options);
// Restore attributes. // Restore attributes.
if (attrs && options.wait) this.attributes = attributes; this.attributes = attributes;
return xhr; return xhr;
}, },
...@@ -505,25 +659,27 @@ ...@@ -505,25 +659,27 @@
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
var model = this; var model = this;
var success = options.success; var success = options.success;
var wait = options.wait;
var destroy = function() { var destroy = function() {
model.stopListening();
model.trigger('destroy', model, model.collection, options); model.trigger('destroy', model, model.collection, options);
}; };
options.success = function(resp) { options.success = function(resp) {
if (options.wait || model.isNew()) destroy(); if (wait) destroy();
if (success) success(model, resp, options); if (success) success.call(options.context, model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options); if (!model.isNew()) model.trigger('sync', model, resp, options);
}; };
var xhr = false;
if (this.isNew()) { if (this.isNew()) {
options.success(); _.defer(options.success);
return false; } else {
}
wrapError(this, options); wrapError(this, options);
xhr = this.sync('delete', this, options);
var xhr = this.sync('delete', this, options); }
if (!options.wait) destroy(); if (!wait) destroy();
return xhr; return xhr;
}, },
...@@ -536,7 +692,8 @@ ...@@ -536,7 +692,8 @@
_.result(this.collection, 'url') || _.result(this.collection, 'url') ||
urlError(); urlError();
if (this.isNew()) return base; if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); var id = this.get(this.idAttribute);
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
}, },
// **parse** converts a response into the hash of attributes to be `set` on // **parse** converts a response into the hash of attributes to be `set` on
...@@ -557,7 +714,7 @@ ...@@ -557,7 +714,7 @@
// Check if the model is currently in a valid state. // Check if the model is currently in a valid state.
isValid: function(options) { isValid: function(options) {
return this._validate({}, _.extend(options || {}, { validate: true })); return this._validate({}, _.defaults({validate: true}, options));
}, },
// Run validation against the next complete set of model attributes, // Run validation against the next complete set of model attributes,
...@@ -573,23 +730,19 @@ ...@@ -573,23 +730,19 @@
}); });
// Underscore methods that we want to implement on the Model. // Underscore methods that we want to implement on the Model, mapped to the
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; // number of arguments they take.
var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1 };
// Mix in each Underscore method as a proxy to `Model#attributes`. // Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) { addUnderscoreMethods(Model, modelMethods, 'attributes');
Model.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});
// Backbone.Collection // Backbone.Collection
// ------------------- // -------------------
// If models tend to represent a single row of data, a Backbone Collection is // If models tend to represent a single row of data, a Backbone Collection is
// more analagous to a table full of data ... or a small slice or page of that // more analogous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason // table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents // -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain // belonging to this particular author, and so on. Collections maintain
...@@ -611,6 +764,16 @@ ...@@ -611,6 +764,16 @@
var setOptions = {add: true, remove: true, merge: true}; var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false}; var addOptions = {add: true, remove: false};
// Splices `insert` into `array` at index `at`.
var splice = function(array, insert, at) {
at = Math.min(Math.max(at, 0), array.length);
var tail = Array(array.length - at);
var length = insert.length;
for (var i = 0; i < tail.length; i++) tail[i] = array[i + at];
for (i = 0; i < length; i++) array[i + at] = insert[i];
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
};
// Define the Collection's inheritable methods. // Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, { _.extend(Collection.prototype, Events, {
...@@ -625,7 +788,7 @@ ...@@ -625,7 +788,7 @@
// The JSON representation of a Collection is an array of the // The JSON representation of a Collection is an array of the
// models' attributes. // models' attributes.
toJSON: function(options) { toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); }); return this.map(function(model) { return model.toJSON(options); });
}, },
// Proxy `Backbone.sync` by default. // Proxy `Backbone.sync` by default.
...@@ -633,32 +796,21 @@ ...@@ -633,32 +796,21 @@
return Backbone.sync.apply(this, arguments); return Backbone.sync.apply(this, arguments);
}, },
// Add a model, or list of models to the set. // Add a model, or list of models to the set. `models` may be Backbone
// Models or raw JavaScript objects to be converted to Models, or any
// combination of the two.
add: function(models, options) { add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions)); return this.set(models, _.extend({merge: false}, options, addOptions));
}, },
// Remove a model, or a list of models from the set. // Remove a model, or a list of models from the set.
remove: function(models, options) { remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models); var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models); models = singular ? [models] : _.clone(models);
options || (options = {}); var removed = this._removeModels(models, options);
var i, l, index, model; if (!options.silent && removed) this.trigger('update', this, options);
for (i = 0, l = models.length; i < l; i++) { return singular ? removed[0] : removed;
model = models[i] = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model, options);
}
return singular ? models[0] : models;
}, },
// Update a collection by `set`-ing a new list of models, adding new ones, // Update a collection by `set`-ing a new list of models, adding new ones,
...@@ -666,78 +818,88 @@ ...@@ -666,78 +818,88 @@
// already exist in the collection, as necessary. Similar to **Model#set**, // already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection. // the core operation for updating the data contained by the collection.
set: function(models, options) { set: function(models, options) {
if (models == null) return;
options = _.defaults({}, options, setOptions); options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options); if (options.parse && !this._isModel(models)) models = this.parse(models, options);
var singular = !_.isArray(models); var singular = !_.isArray(models);
models = singular ? (models ? [models] : []) : _.clone(models); models = singular ? [models] : models.slice();
var i, l, id, model, attrs, existing, sort;
var at = options.at; var at = options.at;
var targetModel = this.model; if (at != null) at = +at;
if (at < 0) at += this.length + 1;
var set = [];
var toAdd = [];
var toRemove = [];
var modelMap = {};
var add = options.add;
var merge = options.merge;
var remove = options.remove;
var sort = false;
var sortable = this.comparator && (at == null) && options.sort !== false; var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null; var sortAttr = _.isString(this.comparator) ? this.comparator : null;
var toAdd = [], toRemove = [], modelMap = {};
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
// Turn bare objects into model references, and prevent invalid models // Turn bare objects into model references, and prevent invalid models
// from being added. // from being added.
for (i = 0, l = models.length; i < l; i++) { var model;
attrs = models[i] || {}; for (var i = 0; i < models.length; i++) {
if (attrs instanceof Model) { model = models[i];
id = model = attrs;
} else {
id = attrs[targetModel.prototype.idAttribute || 'id'];
}
// If a duplicate is found, prevent it from being added and // If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model. // optionally merge it into the existing model.
if (existing = this.get(id)) { var existing = this.get(model);
if (remove) modelMap[existing.cid] = true; if (existing) {
if (merge) { if (merge && model !== existing) {
attrs = attrs === model ? model.attributes : attrs; var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options); if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options); existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
} }
models[i] = existing; models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list. // If this is a new, valid model, push it to the `toAdd` list.
} else if (add) { } else if (add) {
model = models[i] = this._prepareModel(attrs, options); model = models[i] = this._prepareModel(model, options);
if (!model) continue; if (model) {
toAdd.push(model); toAdd.push(model);
this._addReference(model, options); this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
} }
// Do not add multiple models with the same `id`.
model = existing || model;
if (order && (model.isNew() || !modelMap[model.id])) order.push(model);
modelMap[model.id] = true;
} }
// Remove nonexistent models if appropriate. // Remove stale models.
if (remove) { if (remove) {
for (i = 0, l = this.length; i < l; ++i) { for (i = 0; i < this.length; i++) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
} }
if (toRemove.length) this.remove(toRemove, options); if (toRemove.length) this._removeModels(toRemove, options);
} }
// See if sorting is needed, update `length` and splice in new models. // See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) { var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
return model !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true; if (sortable) sort = true;
this.length += toAdd.length; splice(this.models, toAdd, at == null ? this.length : at);
if (at != null) { this.length = this.models.length;
for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
}
} }
// Silently sort the collection if appropriate. // Silently sort the collection if appropriate.
...@@ -745,10 +907,13 @@ ...@@ -745,10 +907,13 @@
// Unless silenced, it's time to fire all appropriate add/sort events. // Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) { if (!options.silent) {
for (i = 0, l = toAdd.length; i < l; i++) { for (i = 0; i < toAdd.length; i++) {
(model = toAdd[i]).trigger('add', model, this, options); if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
} }
if (sort || (order && order.length)) this.trigger('sort', this, options); if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length) this.trigger('update', this, options);
} }
// Return the added (or merged) model (or models). // Return the added (or merged) model (or models).
...@@ -760,8 +925,8 @@ ...@@ -760,8 +925,8 @@
// any granular `add` or `remove` events. Fires `reset` when finished. // any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations. // Useful for bulk operations and optimizations.
reset: function(models, options) { reset: function(models, options) {
options || (options = {}); options = options ? _.clone(options) : {};
for (var i = 0, l = this.models.length; i < l; i++) { for (var i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options); this._removeReference(this.models[i], options);
} }
options.previousModels = this.models; options.previousModels = this.models;
...@@ -779,8 +944,7 @@ ...@@ -779,8 +944,7 @@
// Remove a model from the end of the collection. // Remove a model from the end of the collection.
pop: function(options) { pop: function(options) {
var model = this.at(this.length - 1); var model = this.at(this.length - 1);
this.remove(model, options); return this.remove(model, options);
return model;
}, },
// Add a model to the beginning of the collection. // Add a model to the beginning of the collection.
...@@ -791,8 +955,7 @@ ...@@ -791,8 +955,7 @@
// Remove a model from the beginning of the collection. // Remove a model from the beginning of the collection.
shift: function(options) { shift: function(options) {
var model = this.at(0); var model = this.at(0);
this.remove(model, options); return this.remove(model, options);
return model;
}, },
// Slice out a sub-array of models from the collection. // Slice out a sub-array of models from the collection.
...@@ -803,24 +966,20 @@ ...@@ -803,24 +966,20 @@
// Get a model from the set by id. // Get a model from the set by id.
get: function(obj) { get: function(obj) {
if (obj == null) return void 0; if (obj == null) return void 0;
return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
}, },
// Get the model at the given index. // Get the model at the given index.
at: function(index) { at: function(index) {
if (index < 0) index += this.length;
return this.models[index]; return this.models[index];
}, },
// Return models with matching attributes. Useful for simple cases of // Return models with matching attributes. Useful for simple cases of
// `filter`. // `filter`.
where: function(attrs, first) { where: function(attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : []; return this[first ? 'find' : 'filter'](attrs);
return this[first ? 'find' : 'filter'](function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
}, },
// Return the first model with matching attributes. Useful for simple cases // Return the first model with matching attributes. Useful for simple cases
...@@ -833,16 +992,19 @@ ...@@ -833,16 +992,19 @@
// normal circumstances, as the set will maintain sort order as each item // normal circumstances, as the set will maintain sort order as each item
// is added. // is added.
sort: function(options) { sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); var comparator = this.comparator;
if (!comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {}); options || (options = {});
var length = comparator.length;
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
// Run sort based on type of `comparator`. // Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) { if (length === 1 || _.isString(comparator)) {
this.models = this.sortBy(this.comparator, this); this.models = this.sortBy(comparator);
} else { } else {
this.models.sort(_.bind(this.comparator, this)); this.models.sort(comparator);
} }
if (!options.silent) this.trigger('sort', this, options); if (!options.silent) this.trigger('sort', this, options);
return this; return this;
}, },
...@@ -856,14 +1018,13 @@ ...@@ -856,14 +1018,13 @@
// collection when they arrive. If `reset: true` is passed, the response // collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`. // data will be passed through the `reset` method instead of `set`.
fetch: function(options) { fetch: function(options) {
options = options ? _.clone(options) : {}; options = _.extend({parse: true}, options);
if (options.parse === void 0) options.parse = true;
var success = options.success; var success = options.success;
var collection = this; var collection = this;
options.success = function(resp) { options.success = function(resp) {
var method = options.reset ? 'reset' : 'set'; var method = options.reset ? 'reset' : 'set';
collection[method](resp, options); collection[method](resp, options);
if (success) success(collection, resp, options); if (success) success.call(options.context, collection, resp, options);
collection.trigger('sync', collection, resp, options); collection.trigger('sync', collection, resp, options);
}; };
wrapError(this, options); wrapError(this, options);
...@@ -875,13 +1036,15 @@ ...@@ -875,13 +1036,15 @@
// wait for the server to agree. // wait for the server to agree.
create: function(model, options) { create: function(model, options) {
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false; var wait = options.wait;
if (!options.wait) this.add(model, options); model = this._prepareModel(model, options);
if (!model) return false;
if (!wait) this.add(model, options);
var collection = this; var collection = this;
var success = options.success; var success = options.success;
options.success = function(model, resp) { options.success = function(model, resp, callbackOpts) {
if (options.wait) collection.add(model, options); if (wait) collection.add(model, callbackOpts);
if (success) success(model, resp, options); if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
}; };
model.save(null, options); model.save(null, options);
return model; return model;
...@@ -895,7 +1058,15 @@ ...@@ -895,7 +1058,15 @@
// Create a new collection with an identical list of models as this one. // Create a new collection with an identical list of models as this one.
clone: function() { clone: function() {
return new this.constructor(this.models); return new this.constructor(this.models, {
model: this.model,
comparator: this.comparator
});
},
// Define how to uniquely identify models in the collection.
modelId: function (attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
}, },
// Private method to reset all internal state. Called when the collection // Private method to reset all internal state. Called when the collection
...@@ -909,7 +1080,10 @@ ...@@ -909,7 +1080,10 @@
// Prepare a hash of attributes (or other model) to be added to this // Prepare a hash of attributes (or other model) to be added to this
// collection. // collection.
_prepareModel: function(attrs, options) { _prepareModel: function(attrs, options) {
if (attrs instanceof Model) return attrs; if (this._isModel(attrs)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {};
options.collection = this; options.collection = this;
var model = new this.model(attrs, options); var model = new this.model(attrs, options);
...@@ -918,16 +1092,47 @@ ...@@ -918,16 +1092,47 @@
return false; return false;
}, },
// Internal method called by both remove and set.
_removeModels: function(models, options) {
var removed = [];
for (var i = 0; i < models.length; i++) {
var model = this.get(models[i]);
if (!model) continue;
var index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
removed.push(model);
this._removeReference(model, options);
}
return removed.length ? removed : false;
},
// Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
_isModel: function (model) {
return model instanceof Model;
},
// Internal method to create a model's ties to a collection. // Internal method to create a model's ties to a collection.
_addReference: function(model, options) { _addReference: function(model, options) {
this._byId[model.cid] = model; this._byId[model.cid] = model;
if (model.id != null) this._byId[model.id] = model; var id = this.modelId(model.attributes);
if (!model.collection) model.collection = this; if (id != null) this._byId[id] = model;
model.on('all', this._onModelEvent, this); model.on('all', this._onModelEvent, this);
}, },
// Internal method to sever a model's ties to a collection. // Internal method to sever a model's ties to a collection.
_removeReference: function(model, options) { _removeReference: function(model, options) {
delete this._byId[model.cid];
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
if (this === model.collection) delete model.collection; if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this); model.off('all', this._onModelEvent, this);
}, },
...@@ -939,9 +1144,13 @@ ...@@ -939,9 +1144,13 @@
_onModelEvent: function(event, model, collection, options) { _onModelEvent: function(event, model, collection, options) {
if ((event === 'add' || event === 'remove') && collection !== this) return; if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options); if (event === 'destroy') this.remove(model, options);
if (model && event === 'change:' + model.idAttribute) { if (event === 'change') {
delete this._byId[model.previous(model.idAttribute)]; var prevId = this.modelId(model.previousAttributes());
if (model.id != null) this._byId[model.id] = model; var id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
}
} }
this.trigger.apply(this, arguments); this.trigger.apply(this, arguments);
} }
...@@ -951,34 +1160,17 @@ ...@@ -951,34 +1160,17 @@
// Underscore methods that we want to implement on the Collection. // Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented // 90% of the core usefulness of Backbone Collections is actually implemented
// right here: // right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
'lastIndexOf', 'isEmpty', 'chain', 'sample']; without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3};
// Mix in each Underscore method as a proxy to `Collection#models`. // Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) { addUnderscoreMethods(Collection, collectionMethods, 'models');
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
// Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties.
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};
});
// Backbone.View // Backbone.View
// ------------- // -------------
...@@ -995,17 +1187,15 @@ ...@@ -995,17 +1187,15 @@
// if an existing element is not provided... // if an existing element is not provided...
var View = Backbone.View = function(options) { var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view'); this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions)); _.extend(this, _.pick(options, viewOptions));
this._ensureElement(); this._ensureElement();
this.initialize.apply(this, arguments); this.initialize.apply(this, arguments);
this.delegateEvents();
}; };
// Cached regex to split keys for `delegate`. // Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/; var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties. // List of view options to be set as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods. // Set up all inheritable **Backbone.View** properties and methods.
...@@ -1034,21 +1224,37 @@ ...@@ -1034,21 +1224,37 @@
// Remove this view by taking the element out of the DOM, and removing any // Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners. // applicable Backbone.Events listeners.
remove: function() { remove: function() {
this.$el.remove(); this._removeElement();
this.stopListening(); this.stopListening();
return this; return this;
}, },
// Change the view's element (`this.el` property), including event // Remove this view's element from the document and all event listeners
// re-delegation. // attached to it. Exposed for subclasses using an alternative DOM
setElement: function(element, delegate) { // manipulation API.
if (this.$el) this.undelegateEvents(); _removeElement: function() {
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); this.$el.remove();
this.el = this.$el[0]; },
if (delegate !== false) this.delegateEvents();
// Change the view's element (`this.el` property) and re-delegate the
// view's events on the new element.
setElement: function(element) {
this.undelegateEvents();
this._setElement(element);
this.delegateEvents();
return this; return this;
}, },
// Creates the `this.el` and `this.$el` references for this view using the
// given `el`. `el` can be a CSS selector or an HTML string, a jQuery
// context or an element. Subclasses can override this to utilize an
// alternative DOM manipulation API and are only required to set the
// `this.el` property.
_setElement: function(el) {
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
this.el = this.$el[0];
},
// Set callbacks, where `this.events` is a hash of // Set callbacks, where `this.events` is a hash of
// //
// *{"event selector": "callback"}* // *{"event selector": "callback"}*
...@@ -1062,37 +1268,49 @@ ...@@ -1062,37 +1268,49 @@
// pairs. Callbacks will be bound to the view, with `this` set properly. // pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency. // Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`. // Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) { delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return this; events || (events = _.result(this, 'events'));
if (!events) return this;
this.undelegateEvents(); this.undelegateEvents();
for (var key in events) { for (var key in events) {
var method = events[key]; var method = events[key];
if (!_.isFunction(method)) method = this[events[key]]; if (!_.isFunction(method)) method = this[method];
if (!method) continue; if (!method) continue;
var match = key.match(delegateEventSplitter); var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2]; this.delegate(match[1], match[2], _.bind(method, this));
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
} }
return this; return this;
}, },
// Clears all callbacks previously bound to the view with `delegateEvents`. // Add a single event listener to the view's element (or a child element
// using `selector`). This only works for delegate-able events: not `focus`,
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
delegate: function(eventName, selector, listener) {
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
// Clears all callbacks previously bound to the view by `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple // You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element. // Backbone views attached to the same DOM element.
undelegateEvents: function() { undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid); if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
// A finer-grained `undelegateEvents` for removing a single delegated event.
// `selector` and `listener` are both optional.
undelegate: function(eventName, selector, listener) {
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
return this; return this;
}, },
// Produces a DOM element to be assigned to your view. Exposed for
// subclasses using an alternative DOM manipulation API.
_createElement: function(tagName) {
return document.createElement(tagName);
},
// Ensure that the View has a DOM element to render into. // Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first // If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create // matching element, and re-assign it to `el`. Otherwise, create
...@@ -1102,11 +1320,17 @@ ...@@ -1102,11 +1320,17 @@
var attrs = _.extend({}, _.result(this, 'attributes')); var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id'); if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className'); if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); this.setElement(this._createElement(_.result(this, 'tagName')));
this.setElement($el, false); this._setAttributes(attrs);
} else { } else {
this.setElement(_.result(this, 'el'), false); this.setElement(_.result(this, 'el'));
} }
},
// Set attributes from a hash on this view's element. Exposed for
// subclasses using an alternative DOM manipulation API.
_setAttributes: function(attributes) {
this.$el.attr(attributes);
} }
}); });
...@@ -1175,14 +1399,13 @@ ...@@ -1175,14 +1399,13 @@
params.processData = false; params.processData = false;
} }
// If we're sending a `PATCH` request, and we're in an old Internet Explorer // Pass along `textStatus` and `errorThrown` from jQuery.
// that still has ActiveX enabled by default, override jQuery to use that var error = options.error;
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. options.error = function(xhr, textStatus, errorThrown) {
if (params.type === 'PATCH' && noXhrPatch) { options.textStatus = textStatus;
params.xhr = function() { options.errorThrown = errorThrown;
return new ActiveXObject("Microsoft.XMLHTTP"); if (error) error.call(options.context, xhr, textStatus, errorThrown);
}; };
}
// Make the request, allowing the user to override any Ajax options. // Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
...@@ -1190,10 +1413,6 @@ ...@@ -1190,10 +1413,6 @@
return xhr; return xhr;
}; };
var noXhrPatch =
typeof window !== 'undefined' && !!window.ActiveXObject &&
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent);
// Map from CRUD to HTTP for our default `Backbone.sync` implementation. // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = { var methodMap = {
'create': 'POST', 'create': 'POST',
...@@ -1251,17 +1470,18 @@ ...@@ -1251,17 +1470,18 @@
var router = this; var router = this;
Backbone.history.route(route, function(fragment) { Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment); var args = router._extractParameters(route, fragment);
router.execute(callback, args); if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args)); router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args); router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args); Backbone.history.trigger('route', router, name, args);
}
}); });
return this; return this;
}, },
// Execute a route handler with the provided parameters. This is an // Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup. // excellent place to do pre-route setup or post-route cleanup.
execute: function(callback, args) { execute: function(callback, args, name) {
if (callback) callback.apply(this, args); if (callback) callback.apply(this, args);
}, },
...@@ -1319,7 +1539,7 @@ ...@@ -1319,7 +1539,7 @@
// falls back to polling. // falls back to polling.
var History = Backbone.History = function() { var History = Backbone.History = function() {
this.handlers = []; this.handlers = [];
_.bindAll(this, 'checkUrl'); this.checkUrl = _.bind(this.checkUrl, this);
// Ensure that `History` can be used outside of the browser. // Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
...@@ -1334,12 +1554,6 @@ ...@@ -1334,12 +1554,6 @@
// Cached regex for stripping leading and trailing slashes. // Cached regex for stripping leading and trailing slashes.
var rootStripper = /^\/+|\/+$/g; var rootStripper = /^\/+|\/+$/g;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Cached regex for removing a trailing slash.
var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash. // Cached regex for stripping urls of hash.
var pathStripper = /#.*$/; var pathStripper = /#.*$/;
...@@ -1355,7 +1569,29 @@ ...@@ -1355,7 +1569,29 @@
// Are we at the app root? // Are we at the app root?
atRoot: function() { atRoot: function() {
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; var path = this.location.pathname.replace(/[^\/]$/, '$&/');
return path === this.root && !this.getSearch();
},
// Does the pathname match the root?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
var root = path.slice(0, this.root.length - 1) + '/';
return root === this.root;
},
// Unicode characters in `location.pathname` are percent encoded so they're
// decoded for comparison. `%25` should not be decoded since it may be part
// of an encoded parameter.
decodeFragment: function(fragment) {
return decodeURI(fragment.replace(/%25/g, '%2525'));
},
// In IE6, the hash fragment and search params are incorrect if the
// fragment contains `?`.
getSearch: function() {
var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
return match ? match[0] : '';
}, },
// Gets the true hash value. Cannot use location.hash directly due to bug // Gets the true hash value. Cannot use location.hash directly due to bug
...@@ -1365,14 +1601,19 @@ ...@@ -1365,14 +1601,19 @@
return match ? match[1] : ''; return match ? match[1] : '';
}, },
// Get the cross-browser normalized URL fragment, either from the URL, // Get the pathname and search params, without the root.
// the hash, or the override. getPath: function() {
getFragment: function(fragment, forcePushState) { var path = this.decodeFragment(
this.location.pathname + this.getSearch()
).slice(this.root.length - 1);
return path.charAt(0) === '/' ? path.slice(1) : path;
},
// Get the cross-browser normalized URL fragment from the path or hash.
getFragment: function(fragment) {
if (fragment == null) { if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) { if (this._usePushState || !this._wantsHashChange) {
fragment = decodeURI(this.location.pathname + this.location.search); fragment = this.getPath();
var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else { } else {
fragment = this.getHash(); fragment = this.getHash();
} }
...@@ -1383,7 +1624,7 @@ ...@@ -1383,7 +1624,7 @@
// Start the hash change handling, returning `true` if the current URL matches // Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise. // an existing route, and `false` otherwise.
start: function(options) { start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started"); if (History.started) throw new Error('Backbone.history has already been started');
History.started = true; History.started = true;
// Figure out the initial configuration. Do we need an iframe? // Figure out the initial configuration. Do we need an iframe?
...@@ -1391,36 +1632,16 @@ ...@@ -1391,36 +1632,16 @@
this.options = _.extend({root: '/'}, this.options, options); this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root; this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false; this._wantsHashChange = this.options.hashChange !== false;
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
this._useHashChange = this._wantsHashChange && this._hasHashChange;
this._wantsPushState = !!this.options.pushState; this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); this._hasPushState = !!(this.history && this.history.pushState);
var fragment = this.getFragment(); this._usePushState = this._wantsPushState && this._hasPushState;
var docMode = document.documentMode; this.fragment = this.getFragment();
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
// Normalize root to always include a leading and trailing slash. // Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) {
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = this.location;
// Transition from hashChange to pushState or vice versa if both are // Transition from hashChange to pushState or vice versa if both are
// requested. // requested.
if (this._wantsHashChange && this._wantsPushState) { if (this._wantsHashChange && this._wantsPushState) {
...@@ -1428,18 +1649,48 @@ ...@@ -1428,18 +1649,48 @@
// If we've started off with a route from a `pushState`-enabled // If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it... // browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) { if (!this._hasPushState && !this.atRoot()) {
this.fragment = this.getFragment(null, true); var root = this.root.slice(0, -1) || '/';
this.location.replace(this.root + '#' + this.fragment); this.location.replace(root + '#' + this.getPath());
// Return immediately as browser will do redirect to new url // Return immediately as browser will do redirect to new url
return true; return true;
// Or if we've started out with a hash-based route, but we're currently // Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead... // in a browser where it could be `pushState`-based instead...
} else if (this._hasPushState && this.atRoot() && loc.hash) { } else if (this._hasPushState && this.atRoot()) {
this.fragment = this.getHash().replace(routeStripper, ''); this.navigate(this.getHash(), {replace: true});
this.history.replaceState({}, document.title, this.root + this.fragment); }
}
// Proxy an iframe to handle location events if the browser doesn't
// support the `hashchange` event, HTML5 history, or the user wants
// `hashChange` but not `pushState`.
if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
this.iframe = document.createElement('iframe');
this.iframe.src = 'javascript:0';
this.iframe.style.display = 'none';
this.iframe.tabIndex = -1;
var body = document.body;
// Using `appendChild` will throw on IE < 9 if the document is not ready.
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
iWindow.document.open();
iWindow.document.close();
iWindow.location.hash = '#' + this.fragment;
} }
// Add a cross-platform `addEventListener` shim for older browsers.
var addEventListener = window.addEventListener || function (eventName, listener) {
return attachEvent('on' + eventName, listener);
};
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._usePushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
} }
if (!this.options.silent) return this.loadUrl(); if (!this.options.silent) return this.loadUrl();
...@@ -1448,7 +1699,25 @@ ...@@ -1448,7 +1699,25 @@
// Disable Backbone.history, perhaps temporarily. Not useful in a real app, // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers. // but possibly useful for unit testing Routers.
stop: function() { stop: function() {
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); // Add a cross-platform `removeEventListener` shim for older browsers.
var removeEventListener = window.removeEventListener || function (eventName, listener) {
return detachEvent('on' + eventName, listener);
};
// Remove window listeners.
if (this._usePushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
removeEventListener('hashchange', this.checkUrl, false);
}
// Clean up the iframe if necessary.
if (this.iframe) {
document.body.removeChild(this.iframe);
this.iframe = null;
}
// Some environments will throw when clearing an undefined interval.
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false; History.started = false;
}, },
...@@ -1463,9 +1732,13 @@ ...@@ -1463,9 +1732,13 @@
// calls `loadUrl`, normalizing across the hidden iframe. // calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) { checkUrl: function(e) {
var current = this.getFragment(); var current = this.getFragment();
// If the user pressed the back button, the iframe's hash will have
// changed and we should use that for comparison.
if (current === this.fragment && this.iframe) { if (current === this.fragment && this.iframe) {
current = this.getFragment(this.getHash(this.iframe)); current = this.getHash(this.iframe.contentWindow);
} }
if (current === this.fragment) return false; if (current === this.fragment) return false;
if (this.iframe) this.navigate(current); if (this.iframe) this.navigate(current);
this.loadUrl(); this.loadUrl();
...@@ -1475,8 +1748,10 @@ ...@@ -1475,8 +1748,10 @@
// match, returns `true`. If no defined routes matches the fragment, // match, returns `true`. If no defined routes matches the fragment,
// returns `false`. // returns `false`.
loadUrl: function(fragment) { loadUrl: function(fragment) {
// If the root doesn't match, no routes can match either.
if (!this.matchRoot()) return false;
fragment = this.fragment = this.getFragment(fragment); fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) { return _.some(this.handlers, function(handler) {
if (handler.route.test(fragment)) { if (handler.route.test(fragment)) {
handler.callback(fragment); handler.callback(fragment);
return true; return true;
...@@ -1495,31 +1770,42 @@ ...@@ -1495,31 +1770,42 @@
if (!History.started) return false; if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options}; if (!options || options === true) options = {trigger: !!options};
var url = this.root + (fragment = this.getFragment(fragment || '')); // Normalize the fragment.
fragment = this.getFragment(fragment || '');
// Strip the hash for matching. // Don't include a trailing slash on the root.
fragment = fragment.replace(pathStripper, ''); var root = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
root = root.slice(0, -1) || '/';
}
var url = root + fragment;
// Strip the hash and decode for matching.
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
if (this.fragment === fragment) return; if (this.fragment === fragment) return;
this.fragment = fragment; this.fragment = fragment;
// Don't include a trailing slash on the root.
if (fragment === '' && url !== '/') url = url.slice(0, -1);
// If pushState is available, we use it to set the fragment as a real URL. // If pushState is available, we use it to set the fragment as a real URL.
if (this._hasPushState) { if (this._usePushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash // If hash changes haven't been explicitly disabled, update the hash
// fragment to store history. // fragment to store history.
} else if (this._wantsHashChange) { } else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace); this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
var iWindow = this.iframe.contentWindow;
// Opening and closing the iframe tricks IE7 and earlier to push a // Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't // history entry on hash-tag change. When replace is true, we don't
// want this. // want this.
if(!options.replace) this.iframe.document.open().close(); if (!options.replace) {
this._updateHash(this.iframe.location, fragment, options.replace); iWindow.document.open();
iWindow.document.close();
}
this._updateHash(iWindow.location, fragment, options.replace);
} }
// If you've told us that you explicitly don't want fallback hashchange- // If you've told us that you explicitly don't want fallback hashchange-
...@@ -1550,7 +1836,7 @@ ...@@ -1550,7 +1836,7 @@
// Helpers // Helpers
// ------- // -------
// Helper function to correctly set up the prototype chain, for subclasses. // Helper function to correctly set up the prototype chain for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and // Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended. // class properties to be extended.
var extend = function(protoProps, staticProps) { var extend = function(protoProps, staticProps) {
...@@ -1559,7 +1845,7 @@ ...@@ -1559,7 +1845,7 @@
// The constructor function for the new subclass is either defined by you // The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted // (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor. // by us to simply call the parent constructor.
if (protoProps && _.has(protoProps, 'constructor')) { if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor; child = protoProps.constructor;
} else { } else {
...@@ -1570,7 +1856,7 @@ ...@@ -1570,7 +1856,7 @@
_.extend(child, parent, staticProps); _.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling // Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function. // `parent` constructor function.
var Surrogate = function(){ this.constructor = child; }; var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype; Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate; child.prototype = new Surrogate;
...@@ -1598,7 +1884,7 @@ ...@@ -1598,7 +1884,7 @@
var wrapError = function(model, options) { var wrapError = function(model, options) {
var error = options.error; var error = options.error;
options.error = function(resp) { options.error = function(resp) {
if (error) error(model, resp, options); if (error) error.call(options.context, model, resp, options);
model.trigger('error', model, resp, options); model.trigger('error', model, resp, options);
}; };
}; };
......
/*! /*!
* jQuery JavaScript Library v2.1.3 * jQuery JavaScript Library v2.1.4
* http://jquery.com/ * http://jquery.com/
* *
* Includes Sizzle.js * Includes Sizzle.js
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
* Released under the MIT license * Released under the MIT license
* http://jquery.org/license * http://jquery.org/license
* *
* Date: 2014-12-18T15:11Z * Date: 2015-04-28T16:01Z
*/ */
(function( global, factory ) { (function( global, factory ) {
...@@ -67,7 +67,7 @@ var ...@@ -67,7 +67,7 @@ var
// Use the correct document accordingly with window argument (sandbox) // Use the correct document accordingly with window argument (sandbox)
document = window.document, document = window.document,
version = "2.1.3", version = "2.1.4",
// Define a local copy of jQuery // Define a local copy of jQuery
jQuery = function( selector, context ) { jQuery = function( selector, context ) {
...@@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli ...@@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli
}); });
function isArraylike( obj ) { function isArraylike( obj ) {
var length = obj.length,
// Support: iOS 8.2 (not reproducible in simulator)
// `in` check used to prevent JIT error (gh-2145)
// hasOwn isn't used here due to false negatives
// regarding Nodelist length in IE
var length = "length" in obj && obj.length,
type = jQuery.type( obj ); type = jQuery.type( obj );
if ( type === "function" || jQuery.isWindow( obj ) ) { if ( type === "function" || jQuery.isWindow( obj ) ) {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
// Backbone.js 1.1.2
// (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(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 the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create local references to array methods we'll want to use later.
var array = [];
var push = array.push;
var slice = array.slice;
var splice = array.splice;
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.1.2';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Backbone.Events
// ---------------
// A module that can be mixed in to *any object* in order to provide it with
// custom events. You may bind with `on` or remove with `off` callback
// functions to an event; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
var Events = Backbone.Events = {
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on: function(name, callback, context) {
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
this._events || (this._events = {});
var events = this._events[name] || (this._events[name] = []);
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, it will be removed.
once: function(name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.on(name, once, context);
},
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
off: function(name, callback, context) {
var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) {
this._events = void 0;
return this;
}
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (events = this._events[name]) {
this._events[name] = retain = [];
if (callback || context) {
for (j = 0, k = events.length; j < k; j++) {
ev = events[j];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
retain.push(ev);
}
}
}
if (!retain.length) delete this._events[name];
}
}
return this;
},
// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(name) {
if (!this._events) return this;
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this;
},
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function(obj, name, callback) {
var 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;
}
};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
}
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
}
return true;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
}
};
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
if (!callback && typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);
// Backbone.Model
// --------------
// Backbone **Models** are the basic data object in the framework --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId('c');
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// A hash of attributes whose current and previous value differ.
changed: null,
// The value returned during the last failed validation.
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Return a copy of the model's `attributes` object.
toJSON: function(options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Get the value of an attribute.
get: function(attr) {
return this.attributes[attr];
},
// Get the HTML-escaped value of an attribute.
escape: function(attr) {
return _.escape(this.get(attr));
},
// Returns `true` if the attribute contains a value that is not null
// or undefined.
has: function(attr) {
return this.get(attr) != null;
},
// Set a hash of model attributes on the object, firing `"change"`. This is
// the core primitive operation of a model, updating the data and notifying
// anyone who needs to know about the change in state. The heart of the beast.
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// Run validation.
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
unset = options.unset;
silent = options.silent;
changes = [];
changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
current = this.attributes, prev = this._previousAttributes;
// Check for changes of `id`.
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
// For each `set` attribute, update or delete the current value.
for (attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
// Trigger all relevant attribute changes.
if (!silent) {
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);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
// Remove an attribute from the model, firing `"change"`. `unset` is a noop
// if the attribute doesn't exist.
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// Clear all attributes on the model, firing `"change"`.
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// Determine if the model has changed since the last `"change"` event.
// If you specify an attribute name, determine if that attribute has changed.
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// Return an object containing all the attributes that have changed, or
// false if there are no changed attributes. Useful for determining what
// parts of a view need to be updated and/or what attributes need to be
// persisted to the server. Unset attributes will be set to undefined.
// You can also pass an attributes object to diff against the model,
// determining if there *would be* a change.
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var val, changed = false;
var old = this._changing ? this._previousAttributes : this.attributes;
for (var attr in diff) {
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
},
// Get the previous value of an attribute, recorded at the time the last
// `"change"` event was fired.
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// Get all of the attributes of the model at the time of the previous
// `"change"` event.
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Fetch the model from the server. If the server's representation of the
// model differs from its current attributes, they will be overridden,
// triggering a `"change"` event.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
save: function(key, val, options) {
var attrs, method, xhr, attributes = this.attributes;
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({validate: true}, options);
// 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) {
this.attributes = _.extend({}, attributes, attrs);
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
var serverAttrs = model.parse(resp, options);
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
return false;
}
if (success) success(model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
if (method === 'patch') options.attrs = attrs;
xhr = this.sync(method, this, options);
// Restore attributes.
if (attrs && options.wait) this.attributes = attributes;
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var destroy = function() {
model.trigger('destroy', model, model.collection, options);
};
options.success = function(resp) {
if (options.wait || model.isNew()) destroy();
if (success) success(model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
if (this.isNew()) {
options.success();
return false;
}
wrapError(this, options);
var xhr = this.sync('delete', this, options);
if (!options.wait) destroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// 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();
if (this.isNew()) return base;
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id);
},
// **parse** converts a response into the hash of attributes to be `set` on
// the model. The default implementation is just to pass the response along.
parse: function(resp, options) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
isNew: function() {
return !this.has(this.idAttribute);
},
// Check if the model is currently in a valid state.
isValid: function(options) {
return this._validate({}, _.extend(options || {}, { validate: true }));
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false;
}
});
// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];
// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
Model.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.attributes);
return _[method].apply(_, args);
};
});
// Backbone.Collection
// -------------------
// If models tend to represent a single row of data, a Backbone Collection is
// more analagous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain
// indexes of their models, both in order, and for lookup by `id`.
// Create a new **Collection**, perhaps to contain a specific type of `model`.
// If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options));
};
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// The default model for a collection is just a **Backbone.Model**.
// This should be overridden in most cases.
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// The JSON representation of a Collection is an array of the
// models' attributes.
toJSON: function(options) {
return this.map(function(model){ return model.toJSON(options); });
},
// Proxy `Backbone.sync` by default.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set.
add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
},
// Remove a model, or a list of models from the set.
remove: function(models, options) {
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 = models[i] = this.get(models[i]);
if (!model) continue;
delete this._byId[model.id];
delete this._byId[model.cid];
index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
this._removeReference(model, options);
}
return singular ? models[0] : models;
},
// Update a collection by `set`-ing a new list of models, adding new ones,
// removing models that are no longer present, and merging models that
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
set: function(models, options) {
options = _.defaults({}, options, setOptions);
if (options.parse) models = this.parse(models, options);
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++) {
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(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;
// 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);
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 (remove) {
for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
if (toRemove.length) this.remove(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
if (toAdd.length || (order && order.length)) {
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
for (i = 0, l = toAdd.length; i < l; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (i = 0, l = orderedModels.length; i < l; i++) {
this.models.push(orderedModels[i]);
}
}
}
// Silently sort the collection if appropriate.
if (sort) this.sort({silent: true});
// 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);
}
// 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,
// you can reset the entire set with a new list of models, without firing
// any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations.
reset: function(models, options) {
options || (options = {});
for (var i = 0, l = this.models.length; i < l; i++) {
this._removeReference(this.models[i], options);
}
options.previousModels = this.models;
this._reset();
models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return models;
},
// Add a model to the end of the collection.
push: function(model, options) {
return this.add(model, _.extend({at: this.length}, options));
},
// Remove a model from the end of the collection.
pop: function(options) {
var model = this.at(this.length - 1);
this.remove(model, options);
return model;
},
// Add a model to the beginning of the collection.
unshift: function(model, options) {
return this.add(model, _.extend({at: 0}, options));
},
// Remove a model from the beginning of the collection.
shift: function(options) {
var model = this.at(0);
this.remove(model, options);
return model;
},
// Slice out a sub-array of models from the collection.
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] || this._byId[obj.id] || this._byId[obj.cid];
},
// Get the model at the given index.
at: function(index) {
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of
// `filter`.
where: function(attrs, first) {
if (_.isEmpty(attrs)) return first ? void 0 : [];
return this[first ? 'find' : 'filter'](function(model) {
for (var key in attrs) {
if (attrs[key] !== model.get(key)) return false;
}
return true;
});
},
// Return the first model with matching attributes. Useful for simple cases
// of `find`.
findWhere: function(attrs) {
return this.where(attrs, true);
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
sort: function(options) {
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
// Run sort based on type of `comparator`.
if (_.isString(this.comparator) || this.comparator.length === 1) {
this.models = this.sortBy(this.comparator, this);
} else {
this.models.sort(_.bind(this.comparator, this));
}
if (!options.silent) this.trigger('sort', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
pluck: function(attr) {
return _.invoke(this.models, 'get', attr);
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`.
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var success = options.success;
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options);
if (success) success(collection, resp, options);
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
create: function(model, options) {
options = options ? _.clone(options) : {};
if (!(model = this._prepareModel(model, options))) return false;
if (!options.wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(model, resp) {
if (options.wait) collection.add(model, options);
if (success) success(model, resp, options);
};
model.save(null, options);
return model;
},
// **parse** converts a response into a list of models to be added to the
// collection. The default implementation is just to pass it through.
parse: function(resp, options) {
return resp;
},
// Create a new collection with an identical list of models as this one.
clone: function() {
return new this.constructor(this.models);
},
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
this.length = 0;
this.models = [];
this._byId = {};
},
// Prepare a hash of attributes (or other model) to be added to this
// collection.
_prepareModel: function(attrs, options) {
if (attrs instanceof Model) return attrs;
options = options ? _.clone(options) : {};
options.collection = this;
var model = new this.model(attrs, options);
if (!model.validationError) return model;
this.trigger('invalid', this, model.validationError, options);
return false;
},
// 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, options) {
if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this);
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
_onModelEvent: function(event, model, collection, options) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (model && event === 'change:' + model.idAttribute) {
delete this._byId[model.previous(model.idAttribute)];
if (model.id != null) this._byId[model.id] = model;
}
this.trigger.apply(this, arguments);
}
});
// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
'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) {
Collection.prototype[method] = function() {
var args = slice.call(arguments);
args.unshift(this.models);
return _[method].apply(_, args);
};
});
// Underscore methods that take a property name as an argument.
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
// Use attributes instead of properties.
_.each(attributeMethods, function(method) {
Collection.prototype[method] = function(value, context) {
var iterator = _.isFunction(value) ? value : function(model) {
return model.get(value);
};
return _[method](this.models, iterator, context);
};
});
// Backbone.View
// -------------
// Backbone Views are almost more convention than they are actual code. A View
// is simply a JavaScript object that represents a logical chunk of UI in the
// DOM. This might be a single item, an entire list, a sidebar or panel, or
// even the surrounding frame which wraps your whole app. Defining a chunk of
// UI as a **View** allows you to define your DOM events declaratively, without
// having to worry about render order ... and makes it easy for the view to
// react to specific changes in the state of your models.
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be merged as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be preferred to global lookups where possible.
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
// Change the view's element (`this.el` property), including event
// re-delegation.
setElement: function(element, delegate) {
if (this.$el) this.undelegateEvents();
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element);
this.el = this.$el[0];
if (delegate !== false) this.delegateEvents();
return this;
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save',
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// This only works for delegate-able events: not `focus`, `blur`, and
// not `change`, `submit`, and `reset` in Internet Explorer.
delegateEvents: function(events) {
if (!(events || (events = _.result(this, 'events')))) return this;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;
var match = key.match(delegateEventSplitter);
var eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateEvents' + this.cid;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
return this;
},
// Clears all callbacks previously bound to the view with `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
undelegateEvents: function() {
this.$el.off('.delegateEvents' + this.cid);
return this;
},
// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
_ensureElement: function() {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs);
this.setElement($el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
}
});
// Backbone.sync
// -------------
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// Default JSON-request options.
var params = {type: type, dataType: 'json'};
// Ensure that we have a URL.
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// For older servers, emulate JSON by encoding the request into an HTML-form.
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
// Don't process data on a non-GET request.
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
// If we're sending a `PATCH` request, and we're in an old Internet Explorer
// that still has ActiveX enabled by default, override jQuery to use that
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8.
if (params.type === 'PATCH' && noXhrPatch) {
params.xhr = function() {
return new ActiveXObject("Microsoft.XMLHTTP");
};
}
// Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
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',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
// Override this if you'd like to use a different library.
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
// Backbone.Router
// ---------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
router.execute(callback, args);
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
});
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);
return this;
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
// order of the routes here to support behavior where the most general
// routes can be defined at the bottom of the route map.
_bindRoutes: function() {
if (!this.routes) return;
this.routes = _.result(this, 'routes');
var route, routes = _.keys(this.routes);
while ((route = routes.pop()) != null) {
this.route(route, this.routes[route]);
}
},
// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional) {
return optional ? match : '([^/?]+)';
})
.replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1);
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;
});
}
});
// Backbone.History
// ----------------
// Handles cross-browser history management, based on either
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
// and URL fragments. If the browser supports neither (old IE, natch),
// falls back to polling.
var History = Backbone.History = function() {
this.handlers = [];
_.bindAll(this, 'checkUrl');
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
this.location = window.location;
this.history = window.history;
}
};
// Cached regex for stripping a leading hash/slash and trailing space.
var routeStripper = /^[#\/]|\s+$/g;
// Cached regex for stripping leading and trailing slashes.
var rootStripper = /^\/+|\/+$/g;
// Cached regex for detecting MSIE.
var isExplorer = /msie [\w.]+/;
// Cached regex for removing a trailing slash.
var trailingSlash = /\/$/;
// Cached regex for stripping urls of hash.
var pathStripper = /#.*$/;
// Has the history handling already been started?
History.started = false;
// Set up all inheritable **Backbone.History** properties and methods.
_.extend(History.prototype, Events, {
// The default interval to poll for hash changes, if necessary, is
// 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) {
var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the cross-browser normalized URL fragment, either from the URL,
// the hash, or the override.
getFragment: function(fragment, forcePushState) {
if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange || forcePushState) {
fragment = decodeURI(this.location.pathname + this.location.search);
var root = this.root.replace(trailingSlash, '');
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length);
} else {
fragment = this.getHash();
}
}
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
start: function(options) {
if (History.started) throw new Error("Backbone.history has already been started");
History.started = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
var fragment = this.getFragment();
var docMode = document.documentMode;
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
// Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
if (oldIE && this._wantsHashChange) {
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">');
this.iframe = frame.hide().appendTo('body')[0].contentWindow;
this.navigate(fragment);
}
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
Backbone.$(window).on('popstate', this.checkUrl);
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
Backbone.$(window).on('hashchange', this.checkUrl);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// Determine if we need to change the base url, for a pushState link
// opened by a non-pushState browser.
this.fragment = fragment;
var loc = this.location;
// Transition from hashChange to pushState or vice versa if both are
// 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.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._hasPushState && this.atRoot() && loc.hash) {
this.fragment = this.getHash().replace(routeStripper, '');
this.history.replaceState({}, document.title, this.root + this.fragment);
}
}
if (!this.options.silent) return this.loadUrl();
},
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl);
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false;
},
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
if (current === this.fragment && this.iframe) {
current = this.getFragment(this.getHash(this.iframe));
}
if (current === this.fragment) return false;
if (this.iframe) this.navigate(current);
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(fragment) {
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// 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};
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;
// 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) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) {
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if(!options.replace) this.iframe.document.open().close();
this._updateHash(this.iframe.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
if (options.trigger) return this.loadUrl(fragment);
},
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment);
} else {
// Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment;
}
}
});
// Create the default Backbone.history.
Backbone.history = new History;
// Helpers
// -------
// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var extend = function(protoProps, staticProps) {
var parent = this;
var child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) _.extend(child.prototype, protoProps);
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
return child;
};
// Set up inheritance for the model, collection, router, view and history.
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
// Throw an error when a URL is needed, and none is supplied.
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
// Wrap an optional error callback with a fallback error event.
var wrapError = function(model, options) {
var error = options.error;
options.error = function(resp) {
if (error) error(model, resp, options);
model.trigger('error', model, resp, options);
};
};
return Backbone;
}));
/*! /*!
* Knockout JavaScript library v3.2.0 * Knockout JavaScript library v3.3.0
* (c) Steven Sanderson - http://knockoutjs.com/ * (c) Steven Sanderson - http://knockoutjs.com/
* License: MIT (http://www.opensource.org/licenses/mit-license.php) * License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/ */
...@@ -16,18 +16,17 @@ var DEBUG=true; ...@@ -16,18 +16,17 @@ var DEBUG=true;
JSON = window["JSON"]; JSON = window["JSON"];
(function(factory) { (function(factory) {
// Support three module loading scenarios // Support three module loading scenarios
if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { if (typeof define === 'function' && define['amd']) {
// [1] CommonJS/Node.js // [1] AMD anonymous module
var target = module['exports'] || exports; // module.exports is for Node.js
factory(target, require);
} else if (typeof define === 'function' && define['amd']) {
// [2] AMD anonymous module
define(['exports', 'require'], factory); define(['exports', 'require'], factory);
} else if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {
// [2] CommonJS/Node.js
factory(module['exports'] || exports); // module.exports is for Node.js
} else { } else {
// [3] No module loader (plain <script> tag) - put directly in global namespace // [3] No module loader (plain <script> tag) - put directly in global namespace
factory(window['ko'] = {}); factory(window['ko'] = {});
} }
}(function(koExports, require){ }(function(koExports, amdRequire){
// Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler). // Internally, all KO objects are attached to koExports (even the non-exported ones whose names will be minified by the closure compiler).
// In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable. // In the future, the following "ko" variable may be made distinct from "koExports" so that private objects are not externally reachable.
var ko = typeof koExports !== 'undefined' ? koExports : {}; var ko = typeof koExports !== 'undefined' ? koExports : {};
...@@ -46,7 +45,7 @@ ko.exportSymbol = function(koPath, object) { ...@@ -46,7 +45,7 @@ ko.exportSymbol = function(koPath, object) {
ko.exportProperty = function(owner, publicName, object) { ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object; owner[publicName] = object;
}; };
ko.version = "3.2.0"; ko.version = "3.3.0";
ko.exportSymbol('version', ko.version); ko.exportSymbol('version', ko.version);
ko.utils = (function () { ko.utils = (function () {
...@@ -113,6 +112,37 @@ ko.utils = (function () { ...@@ -113,6 +112,37 @@ ko.utils = (function () {
return (inputType == "checkbox") || (inputType == "radio"); return (inputType == "checkbox") || (inputType == "radio");
} }
// For details on the pattern for changing node classes
// see: https://github.com/knockout/knockout/issues/1597
var cssClassNameRegex = /\S+/g;
function toggleDomNodeCssClass(node, classNames, shouldHaveClass) {
var addOrRemoveFn;
if (classNames) {
if (typeof node.classList === 'object') {
addOrRemoveFn = node.classList[shouldHaveClass ? 'add' : 'remove'];
ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
addOrRemoveFn.call(node.classList, className);
});
} else if (typeof node.className['baseVal'] === 'string') {
// SVG tag .classNames is an SVGAnimatedString instance
toggleObjectClassPropertyString(node.className, 'baseVal', classNames, shouldHaveClass);
} else {
// node.className ought to be a string.
toggleObjectClassPropertyString(node, 'className', classNames, shouldHaveClass);
}
}
}
function toggleObjectClassPropertyString(obj, prop, classNames, shouldHaveClass) {
// obj/prop is either a node/'className' or a SVGAnimatedString/'baseVal'.
var currentClassNames = obj[prop].match(cssClassNameRegex) || [];
ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
ko.utils.addOrRemoveItem(currentClassNames, className, shouldHaveClass);
});
obj[prop] = currentClassNames.join(" ");
}
return { return {
fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/], fieldsIncludedWithJsonPost: ['authenticity_token', /^__RequestVerificationToken(_.*)?$/],
...@@ -226,8 +256,9 @@ ko.utils = (function () { ...@@ -226,8 +256,9 @@ ko.utils = (function () {
// Ensure it's a real array, as we're about to reparent the nodes and // Ensure it's a real array, as we're about to reparent the nodes and
// we don't want the underlying collection to change while we're doing that. // we don't want the underlying collection to change while we're doing that.
var nodesArray = ko.utils.makeArray(nodes); var nodesArray = ko.utils.makeArray(nodes);
var templateDocument = (nodesArray[0] && nodesArray[0].ownerDocument) || document;
var container = document.createElement('div'); var container = templateDocument.createElement('div');
for (var i = 0, j = nodesArray.length; i < j; i++) { for (var i = 0, j = nodesArray.length; i < j; i++) {
container.appendChild(ko.cleanNode(nodesArray[i])); container.appendChild(ko.cleanNode(nodesArray[i]));
} }
...@@ -283,7 +314,7 @@ ko.utils = (function () { ...@@ -283,7 +314,7 @@ ko.utils = (function () {
// Rule [A] // Rule [A]
while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode) while (continuousNodeArray.length && continuousNodeArray[0].parentNode !== parentNode)
continuousNodeArray.shift(); continuousNodeArray.splice(0, 1);
// Rule [B] // Rule [B]
if (continuousNodeArray.length > 1) { if (continuousNodeArray.length > 1) {
...@@ -412,16 +443,7 @@ ko.utils = (function () { ...@@ -412,16 +443,7 @@ ko.utils = (function () {
return ko.isObservable(value) ? value.peek() : value; return ko.isObservable(value) ? value.peek() : value;
}, },
toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) { toggleDomNodeCssClass: toggleDomNodeCssClass,
if (classNames) {
var cssClassNameRegex = /\S+/g,
currentClassNames = node.className.match(cssClassNameRegex) || [];
ko.utils.arrayForEach(classNames.match(cssClassNameRegex), function(className) {
ko.utils.addOrRemoveItem(currentClassNames, className, shouldHaveClass);
});
node.className = currentClassNames.join(" ");
}
},
setTextContent: function(element, textContent) { setTextContent: function(element, textContent) {
var value = ko.utils.unwrapObservable(textContent); var value = ko.utils.unwrapObservable(textContent);
...@@ -595,16 +617,26 @@ ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent); ...@@ -595,16 +617,26 @@ ko.exportSymbol('utils.triggerEvent', ko.utils.triggerEvent);
ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable); ko.exportSymbol('utils.unwrapObservable', ko.utils.unwrapObservable);
ko.exportSymbol('utils.objectForEach', ko.utils.objectForEach); ko.exportSymbol('utils.objectForEach', ko.utils.objectForEach);
ko.exportSymbol('utils.addOrRemoveItem', ko.utils.addOrRemoveItem); ko.exportSymbol('utils.addOrRemoveItem', ko.utils.addOrRemoveItem);
ko.exportSymbol('utils.setTextContent', ko.utils.setTextContent);
ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly
if (!Function.prototype['bind']) { if (!Function.prototype['bind']) {
// Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf) // Function.prototype.bind is a standard part of ECMAScript 5th Edition (December 2009, http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-262.pdf)
// In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js // In case the browser doesn't implement it natively, provide a JavaScript implementation. This implementation is based on the one in prototype.js
Function.prototype['bind'] = function (object) { Function.prototype['bind'] = function (object) {
var originalFunction = this, args = Array.prototype.slice.call(arguments), object = args.shift(); var originalFunction = this;
if (arguments.length === 1) {
return function () {
return originalFunction.apply(object, arguments);
};
} else {
var partialArgs = Array.prototype.slice.call(arguments, 1);
return function () { return function () {
return originalFunction.apply(object, args.concat(Array.prototype.slice.call(arguments))); var args = partialArgs.slice(0);
args.push.apply(args, arguments);
return originalFunction.apply(object, args);
}; };
}
}; };
} }
...@@ -751,7 +783,7 @@ ko.utils.domNodeDisposal = new (function () { ...@@ -751,7 +783,7 @@ ko.utils.domNodeDisposal = new (function () {
if (jQueryInstance && (typeof jQueryInstance['cleanData'] == "function")) if (jQueryInstance && (typeof jQueryInstance['cleanData'] == "function"))
jQueryInstance['cleanData']([node]); jQueryInstance['cleanData']([node]);
} }
} };
})(); })();
ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for convenience
ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience
...@@ -763,7 +795,10 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -763,7 +795,10 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
(function () { (function () {
var leadingCommentRegex = /^(\s*)<!--(.*?)-->/; var leadingCommentRegex = /^(\s*)<!--(.*?)-->/;
function simpleHtmlParse(html) { function simpleHtmlParse(html, documentContext) {
documentContext || (documentContext = document);
var windowContext = documentContext['parentWindow'] || documentContext['defaultView'] || window;
// Based on jQuery's "clean" function, but only accounting for table-related elements. // Based on jQuery's "clean" function, but only accounting for table-related elements.
// If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly // If you have referenced jQuery, this won't be used anyway - KO will use jQuery's "clean" function directly
...@@ -773,7 +808,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -773,7 +808,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
// (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present. // (possibly a text node) in front of the comment. So, KO does not attempt to workaround this IE issue automatically at present.
// Trim whitespace, otherwise indexOf won't work as expected // Trim whitespace, otherwise indexOf won't work as expected
var tags = ko.utils.stringTrim(html).toLowerCase(), div = document.createElement("div"); var tags = ko.utils.stringTrim(html).toLowerCase(), div = documentContext.createElement("div");
// Finds the first match from the left column, and returns the corresponding "wrap" data from the right column // Finds the first match from the left column, and returns the corresponding "wrap" data from the right column
var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "<table>", "</table>"] || var wrap = tags.match(/^<(thead|tbody|tfoot)/) && [1, "<table>", "</table>"] ||
...@@ -784,8 +819,8 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -784,8 +819,8 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
// Go to html and back, then peel off extra wrappers // Go to html and back, then peel off extra wrappers
// Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness. // Note that we always prefix with some dummy text, because otherwise, IE<9 will strip out leading comment nodes in descendants. Total madness.
var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>"; var markup = "ignored<div>" + wrap[1] + html + wrap[2] + "</div>";
if (typeof window['innerShiv'] == "function") { if (typeof windowContext['innerShiv'] == "function") {
div.appendChild(window['innerShiv'](markup)); div.appendChild(windowContext['innerShiv'](markup));
} else { } else {
div.innerHTML = markup; div.innerHTML = markup;
} }
...@@ -797,13 +832,13 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -797,13 +832,13 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
return ko.utils.makeArray(div.lastChild.childNodes); return ko.utils.makeArray(div.lastChild.childNodes);
} }
function jQueryHtmlParse(html) { function jQueryHtmlParse(html, documentContext) {
// jQuery's "parseHTML" function was introduced in jQuery 1.8.0 and is a documented public API. // jQuery's "parseHTML" function was introduced in jQuery 1.8.0 and is a documented public API.
if (jQueryInstance['parseHTML']) { if (jQueryInstance['parseHTML']) {
return jQueryInstance['parseHTML'](html) || []; // Ensure we always return an array and never null return jQueryInstance['parseHTML'](html, documentContext) || []; // Ensure we always return an array and never null
} else { } else {
// For jQuery < 1.8.0, we fall back on the undocumented internal "clean" function. // For jQuery < 1.8.0, we fall back on the undocumented internal "clean" function.
var elems = jQueryInstance['clean']([html]); var elems = jQueryInstance['clean']([html], documentContext);
// As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment. // As of jQuery 1.7.1, jQuery parses the HTML by appending it to some dummy parent nodes held in an in-memory document fragment.
// Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time. // Unfortunately, it never clears the dummy parent nodes from the document fragment, so it leaks memory over time.
...@@ -822,9 +857,9 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -822,9 +857,9 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
} }
} }
ko.utils.parseHtmlFragment = function(html) { ko.utils.parseHtmlFragment = function(html, documentContext) {
return jQueryInstance ? jQueryHtmlParse(html) // As below, benefit from jQuery's optimisations where possible return jQueryInstance ? jQueryHtmlParse(html, documentContext) // As below, benefit from jQuery's optimisations where possible
: simpleHtmlParse(html); // ... otherwise, this simple logic will do in most common cases. : simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases.
}; };
ko.utils.setHtml = function(node, html) { ko.utils.setHtml = function(node, html) {
...@@ -844,7 +879,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD ...@@ -844,7 +879,7 @@ ko.exportSymbol('utils.domNodeDisposal.removeDisposeCallback', ko.utils.domNodeD
jQueryInstance(node)['html'](html); jQueryInstance(node)['html'](html);
} else { } else {
// ... otherwise, use KO's own parsing logic. // ... otherwise, use KO's own parsing logic.
var parsedNodes = ko.utils.parseHtmlFragment(html); var parsedNodes = ko.utils.parseHtmlFragment(html, node.ownerDocument);
for (var i = 0; i < parsedNodes.length; i++) for (var i = 0; i < parsedNodes.length; i++)
node.appendChild(parsedNodes[i]); node.appendChild(parsedNodes[i]);
} }
...@@ -1011,7 +1046,7 @@ function applyExtenders(requestedExtenders) { ...@@ -1011,7 +1046,7 @@ function applyExtenders(requestedExtenders) {
ko.exportSymbol('extenders', ko.extenders); ko.exportSymbol('extenders', ko.extenders);
ko.subscription = function (target, callback, disposeCallback) { ko.subscription = function (target, callback, disposeCallback) {
this.target = target; this._target = target;
this.callback = callback; this.callback = callback;
this.disposeCallback = disposeCallback; this.disposeCallback = disposeCallback;
this.isDisposed = false; this.isDisposed = false;
...@@ -1025,6 +1060,7 @@ ko.subscription.prototype.dispose = function () { ...@@ -1025,6 +1060,7 @@ ko.subscription.prototype.dispose = function () {
ko.subscribable = function () { ko.subscribable = function () {
ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']); ko.utils.setPrototypeOfOrExtend(this, ko.subscribable['fn']);
this._subscriptions = {}; this._subscriptions = {};
this._versionNumber = 1;
} }
var defaultEvent = "change"; var defaultEvent = "change";
...@@ -1054,6 +1090,9 @@ var ko_subscribable_fn = { ...@@ -1054,6 +1090,9 @@ var ko_subscribable_fn = {
"notifySubscribers": function (valueToNotify, event) { "notifySubscribers": function (valueToNotify, event) {
event = event || defaultEvent; event = event || defaultEvent;
if (event === defaultEvent) {
this.updateVersion();
}
if (this.hasSubscriptionsForEvent(event)) { if (this.hasSubscriptionsForEvent(event)) {
try { try {
ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined) ko.dependencyDetection.begin(); // Begin suppressing dependency detection (by setting the top frame to undefined)
...@@ -1069,6 +1108,18 @@ var ko_subscribable_fn = { ...@@ -1069,6 +1108,18 @@ var ko_subscribable_fn = {
} }
}, },
getVersion: function () {
return this._versionNumber;
},
hasChanged: function (versionToCheck) {
return this.getVersion() !== versionToCheck;
},
updateVersion: function () {
++this._versionNumber;
},
limit: function(limitFunction) { limit: function(limitFunction) {
var self = this, selfIsObservable = ko.isObservable(self), var self = this, selfIsObservable = ko.isObservable(self),
isPending, previousValue, pendingValue, beforeChange = 'beforeChange'; isPending, previousValue, pendingValue, beforeChange = 'beforeChange';
...@@ -1115,12 +1166,16 @@ var ko_subscribable_fn = { ...@@ -1115,12 +1166,16 @@ var ko_subscribable_fn = {
return this._subscriptions[event] && this._subscriptions[event].length; return this._subscriptions[event] && this._subscriptions[event].length;
}, },
getSubscriptionsCount: function () { getSubscriptionsCount: function (event) {
if (event) {
return this._subscriptions[event] && this._subscriptions[event].length || 0;
} else {
var total = 0; var total = 0;
ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) { ko.utils.objectForEach(this._subscriptions, function(eventName, subscriptions) {
total += subscriptions.length; total += subscriptions.length;
}); });
return total; return total;
}
}, },
isDifferent: function(oldValue, newValue) { isDifferent: function(oldValue, newValue) {
...@@ -1213,6 +1268,8 @@ ko.exportSymbol('computedContext', ko.computedContext); ...@@ -1213,6 +1268,8 @@ ko.exportSymbol('computedContext', ko.computedContext);
ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount); ko.exportSymbol('computedContext.getDependenciesCount', ko.computedContext.getDependenciesCount);
ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial); ko.exportSymbol('computedContext.isInitial', ko.computedContext.isInitial);
ko.exportSymbol('computedContext.isSleeping', ko.computedContext.isSleeping); ko.exportSymbol('computedContext.isSleeping', ko.computedContext.isSleeping);
ko.exportSymbol('ignoreDependencies', ko.ignoreDependencies = ko.dependencyDetection.ignore);
ko.observable = function (initialValue) { ko.observable = function (initialValue) {
var _latestValue = initialValue; var _latestValue = initialValue;
...@@ -1418,15 +1475,27 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1418,15 +1475,27 @@ ko.extenders['trackArrayChanges'] = function(target) {
} }
var trackingChanges = false, var trackingChanges = false,
cachedDiff = null, cachedDiff = null,
arrayChangeSubscription,
pendingNotifications = 0, pendingNotifications = 0,
underlyingSubscribeFunction = target.subscribe; underlyingBeforeSubscriptionAddFunction = target.beforeSubscriptionAdd,
underlyingAfterSubscriptionRemoveFunction = target.afterSubscriptionRemove;
// Intercept "subscribe" calls, and for array change events, ensure change tracking is enabled // Watch "subscribe" calls, and for array change events, ensure change tracking is enabled
target.subscribe = target['subscribe'] = function(callback, callbackTarget, event) { target.beforeSubscriptionAdd = function (event) {
if (underlyingBeforeSubscriptionAddFunction)
underlyingBeforeSubscriptionAddFunction.call(target, event);
if (event === arrayChangeEventName) { if (event === arrayChangeEventName) {
trackChanges(); trackChanges();
} }
return underlyingSubscribeFunction.apply(this, arguments); };
// Watch "dispose" calls, and for array change events, ensure change tracking is disabled when all are disposed
target.afterSubscriptionRemove = function (event) {
if (underlyingAfterSubscriptionRemoveFunction)
underlyingAfterSubscriptionRemoveFunction.call(target, event);
if (event === arrayChangeEventName && !target.hasSubscriptionsForEvent(arrayChangeEventName)) {
arrayChangeSubscription.dispose();
trackingChanges = false;
}
}; };
function trackChanges() { function trackChanges() {
...@@ -1450,22 +1519,23 @@ ko.extenders['trackArrayChanges'] = function(target) { ...@@ -1450,22 +1519,23 @@ ko.extenders['trackArrayChanges'] = function(target) {
// change it's possible to produce a diff // change it's possible to produce a diff
var previousContents = [].concat(target.peek() || []); var previousContents = [].concat(target.peek() || []);
cachedDiff = null; cachedDiff = null;
target.subscribe(function(currentContents) { arrayChangeSubscription = target.subscribe(function(currentContents) {
// Make a copy of the current contents and ensure it's an array // Make a copy of the current contents and ensure it's an array
currentContents = [].concat(currentContents || []); currentContents = [].concat(currentContents || []);
// Compute the diff and issue notifications, but only if someone is listening // Compute the diff and issue notifications, but only if someone is listening
if (target.hasSubscriptionsForEvent(arrayChangeEventName)) { if (target.hasSubscriptionsForEvent(arrayChangeEventName)) {
var changes = getChanges(previousContents, currentContents); var changes = getChanges(previousContents, currentContents);
if (changes.length) {
target['notifySubscribers'](changes, arrayChangeEventName);
}
} }
// Eliminate references to the old, removed items, so they can be GCed // Eliminate references to the old, removed items, so they can be GCed
previousContents = currentContents; previousContents = currentContents;
cachedDiff = null; cachedDiff = null;
pendingNotifications = 0; pendingNotifications = 0;
if (changes && changes.length) {
target['notifySubscribers'](changes, arrayChangeEventName);
}
}); });
} }
...@@ -1558,44 +1628,58 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1558,44 +1628,58 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
if (typeof readFunction != "function") if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed"); throw new Error("Pass a function that returns the value of the ko.computed");
function addSubscriptionToDependency(subscribable, id) { function addDependencyTracking(id, target, trackingObj) {
if (!_subscriptionsToDependencies[id]) { if (pure && target === dependentObservable) {
_subscriptionsToDependencies[id] = subscribable.subscribe(evaluatePossiblyAsync); throw Error("A 'pure' computed must not be called recursively");
++_dependenciesCount;
} }
dependencyTracking[id] = trackingObj;
trackingObj._order = _dependenciesCount++;
trackingObj._version = target.getVersion();
} }
function disposeAllSubscriptionsToDependencies() { function haveDependenciesChanged() {
ko.utils.objectForEach(_subscriptionsToDependencies, function (id, subscription) { var id, dependency;
subscription.dispose(); for (id in dependencyTracking) {
}); if (dependencyTracking.hasOwnProperty(id)) {
_subscriptionsToDependencies = {}; dependency = dependencyTracking[id];
if (dependency._target.hasChanged(dependency._version)) {
return true;
}
}
}
} }
function disposeComputed() { function disposeComputed() {
disposeAllSubscriptionsToDependencies(); if (!isSleeping && dependencyTracking) {
ko.utils.objectForEach(dependencyTracking, function (id, dependency) {
if (dependency.dispose)
dependency.dispose();
});
}
dependencyTracking = null;
_dependenciesCount = 0; _dependenciesCount = 0;
_isDisposed = true; _isDisposed = true;
_needsEvaluation = false; _needsEvaluation = false;
isSleeping = false;
} }
function evaluatePossiblyAsync() { function evaluatePossiblyAsync() {
var throttleEvaluationTimeout = dependentObservable['throttleEvaluation']; var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) { if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
clearTimeout(evaluationTimeoutInstance); clearTimeout(evaluationTimeoutInstance);
evaluationTimeoutInstance = setTimeout(evaluateImmediate, throttleEvaluationTimeout); evaluationTimeoutInstance = setTimeout(function () {
evaluateImmediate(true /*notifyChange*/);
}, throttleEvaluationTimeout);
} else if (dependentObservable._evalRateLimited) { } else if (dependentObservable._evalRateLimited) {
dependentObservable._evalRateLimited(); dependentObservable._evalRateLimited();
} else { } else {
evaluateImmediate(); evaluateImmediate(true /*notifyChange*/);
} }
} }
function evaluateImmediate(suppressChangeNotification) { function evaluateImmediate(notifyChange) {
if (_isBeingEvaluated) { if (_isBeingEvaluated) {
if (pure) {
throw Error("A 'pure' computed must not be called recursively");
}
// If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation. // If the evaluation of a ko.computed causes side effects, it's possible that it will trigger its own re-evaluation.
// This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost // This is not desirable (it's hard for a developer to realise a chain of dependencies might cause this, and they almost
// certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing // certainly didn't intend infinite re-evaluations). So, for predictability, we simply prevent ko.computeds from causing
...@@ -1621,51 +1705,32 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1621,51 +1705,32 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
_isBeingEvaluated = true; _isBeingEvaluated = true;
// When sleeping, recalculate the value and return.
if (isSleeping) {
try {
var dependencyTracking = {};
ko.dependencyDetection.begin({
callback: function (subscribable, id) {
if (!dependencyTracking[id]) {
dependencyTracking[id] = 1;
++_dependenciesCount;
}
},
computed: dependentObservable,
isInitial: undefined
});
_dependenciesCount = 0;
_latestValue = readFunction.call(evaluatorFunctionTarget);
} finally {
ko.dependencyDetection.end();
_isBeingEvaluated = false;
}
} else {
try { try {
// Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal). // Initially, we assume that none of the subscriptions are still being used (i.e., all are candidates for disposal).
// Then, during evaluation, we cross off any that are in fact still being used. // Then, during evaluation, we cross off any that are in fact still being used.
var disposalCandidates = _subscriptionsToDependencies, disposalCount = _dependenciesCount; var disposalCandidates = dependencyTracking,
disposalCount = _dependenciesCount,
isInitial = pure ? undefined : !_dependenciesCount; // If we're evaluating when there are no previous dependencies, it must be the first time
ko.dependencyDetection.begin({ ko.dependencyDetection.begin({
callback: function(subscribable, id) { callback: function(subscribable, id) {
if (!_isDisposed) { if (!_isDisposed) {
if (disposalCount && disposalCandidates[id]) { if (disposalCount && disposalCandidates[id]) {
// Don't want to dispose this subscription, as it's still being used // Don't want to dispose this subscription, as it's still being used
_subscriptionsToDependencies[id] = disposalCandidates[id]; addDependencyTracking(id, subscribable, disposalCandidates[id]);
++_dependenciesCount;
delete disposalCandidates[id]; delete disposalCandidates[id];
--disposalCount; --disposalCount;
} else { } else if (!dependencyTracking[id]) {
// Brand new subscription - add it // Brand new subscription - add it
addSubscriptionToDependency(subscribable, id); addDependencyTracking(id, subscribable, isSleeping ? { _target: subscribable } : subscribable.subscribe(evaluatePossiblyAsync));
} }
} }
}, },
computed: dependentObservable, computed: dependentObservable,
isInitial: pure ? undefined : !_dependenciesCount // If we're evaluating when there are no previous dependencies, it must be the first time isInitial: isInitial
}); });
_subscriptionsToDependencies = {}; dependencyTracking = {};
_dependenciesCount = 0; _dependenciesCount = 0;
try { try {
...@@ -1675,8 +1740,9 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1675,8 +1740,9 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
ko.dependencyDetection.end(); ko.dependencyDetection.end();
// For each subscription no longer being used, remove it from the active subscriptions list and dispose it // For each subscription no longer being used, remove it from the active subscriptions list and dispose it
if (disposalCount) { if (disposalCount && !isSleeping) {
ko.utils.objectForEach(disposalCandidates, function(id, toDispose) { ko.utils.objectForEach(disposalCandidates, function(id, toDispose) {
if (toDispose.dispose)
toDispose.dispose(); toDispose.dispose();
}); });
} }
...@@ -1685,19 +1751,26 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1685,19 +1751,26 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
} }
if (dependentObservable.isDifferent(_latestValue, newValue)) { if (dependentObservable.isDifferent(_latestValue, newValue)) {
dependentObservable["notifySubscribers"](_latestValue, "beforeChange"); if (!isSleeping) {
notify(_latestValue, "beforeChange");
}
_latestValue = newValue; _latestValue = newValue;
if (DEBUG) dependentObservable._latestValue = _latestValue; if (DEBUG) dependentObservable._latestValue = _latestValue;
if (suppressChangeNotification !== true) { // Check for strict true value since setTimeout in Firefox passes a numeric value to the function if (isSleeping) {
dependentObservable["notifySubscribers"](_latestValue); dependentObservable.updateVersion();
} else if (notifyChange) {
notify(_latestValue);
}
} }
if (isInitial) {
notify(_latestValue, "awake");
} }
} finally { } finally {
_isBeingEvaluated = false; _isBeingEvaluated = false;
} }
}
if (!_dependenciesCount) if (!_dependenciesCount)
dispose(); dispose();
...@@ -1715,17 +1788,18 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1715,17 +1788,18 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
} else { } else {
// Reading the value // Reading the value
ko.dependencyDetection.registerDependency(dependentObservable); ko.dependencyDetection.registerDependency(dependentObservable);
if (_needsEvaluation) if (_needsEvaluation || (isSleeping && haveDependenciesChanged())) {
evaluateImmediate(true /* suppressChangeNotification */); evaluateImmediate();
}
return _latestValue; return _latestValue;
} }
} }
function peek() { function peek() {
// Peek won't re-evaluate, except to get the initial value when "deferEvaluation" is set, or while the computed is sleeping. // Peek won't re-evaluate, except while the computed is sleeping or to get the initial value when "deferEvaluation" is set.
// Those are the only times that both of these conditions will be satisfied. if ((_needsEvaluation && !_dependenciesCount) || (isSleeping && haveDependenciesChanged())) {
if (_needsEvaluation && !_dependenciesCount) evaluateImmediate();
evaluateImmediate(true /* suppressChangeNotification */); }
return _latestValue; return _latestValue;
} }
...@@ -1733,13 +1807,17 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1733,13 +1807,17 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
return _needsEvaluation || _dependenciesCount > 0; return _needsEvaluation || _dependenciesCount > 0;
} }
function notify(value, event) {
dependentObservable["notifySubscribers"](value, event);
}
// By here, "options" is always non-null // By here, "options" is always non-null
var writeFunction = options["write"], var writeFunction = options["write"],
disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null, disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
disposeWhenOption = options["disposeWhen"] || options.disposeWhen, disposeWhenOption = options["disposeWhen"] || options.disposeWhen,
disposeWhen = disposeWhenOption, disposeWhen = disposeWhenOption,
dispose = disposeComputed, dispose = disposeComputed,
_subscriptionsToDependencies = {}, dependencyTracking = {},
_dependenciesCount = 0, _dependenciesCount = 0,
evaluationTimeoutInstance = null; evaluationTimeoutInstance = null;
...@@ -1751,7 +1829,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1751,7 +1829,7 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
dependentObservable.peek = peek; dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _dependenciesCount; }; dependentObservable.getDependenciesCount = function () { return _dependenciesCount; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function"; dependentObservable.hasWriteFunction = typeof writeFunction === "function";
dependentObservable.dispose = function () { dispose(); }; dependentObservable.dispose = function () { dispose(); };
dependentObservable.isActive = isActive; dependentObservable.isActive = isActive;
...@@ -1773,24 +1851,69 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva ...@@ -1773,24 +1851,69 @@ ko.computed = ko.dependentObservable = function (evaluatorFunctionOrOptions, eva
if (options['pure']) { if (options['pure']) {
pure = true; pure = true;
isSleeping = true; // Starts off sleeping; will awake on the first subscription isSleeping = true; // Starts off sleeping; will awake on the first subscription
dependentObservable.beforeSubscriptionAdd = function () { dependentObservable.beforeSubscriptionAdd = function (event) {
// If asleep, wake up the computed and evaluate to register any dependencies. // If asleep, wake up the computed by subscribing to any dependencies.
if (isSleeping) { if (!_isDisposed && isSleeping && event == 'change') {
isSleeping = false; isSleeping = false;
evaluateImmediate(true /* suppressChangeNotification */); if (_needsEvaluation || haveDependenciesChanged()) {
dependencyTracking = null;
_dependenciesCount = 0;
_needsEvaluation = true;
evaluateImmediate();
} else {
// First put the dependencies in order
var dependeciesOrder = [];
ko.utils.objectForEach(dependencyTracking, function (id, dependency) {
dependeciesOrder[dependency._order] = id;
});
// Next, subscribe to each one
ko.utils.arrayForEach(dependeciesOrder, function(id, order) {
var dependency = dependencyTracking[id],
subscription = dependency._target.subscribe(evaluatePossiblyAsync);
subscription._order = order;
subscription._version = dependency._version;
dependencyTracking[id] = subscription;
});
}
if (!_isDisposed) { // test since evaluating could trigger disposal
notify(_latestValue, "awake");
}
} }
};
dependentObservable.afterSubscriptionRemove = function (event) {
if (!_isDisposed && event == 'change' && !dependentObservable.hasSubscriptionsForEvent('change')) {
ko.utils.objectForEach(dependencyTracking, function (id, dependency) {
if (dependency.dispose) {
dependencyTracking[id] = {
_target: dependency._target,
_order: dependency._order,
_version: dependency._version
};
dependency.dispose();
} }
dependentObservable.afterSubscriptionRemove = function () { });
if (!dependentObservable.getSubscriptionsCount()) { isSleeping = true;
disposeAllSubscriptionsToDependencies(); notify(undefined, "asleep");
isSleeping = _needsEvaluation = true;
} }
};
// Because a pure computed is not automatically updated while it is sleeping, we can't
// simply return the version number. Instead, we check if any of the dependencies have
// changed and conditionally re-evaluate the computed observable.
dependentObservable._originalGetVersion = dependentObservable.getVersion;
dependentObservable.getVersion = function () {
if (isSleeping && (_needsEvaluation || haveDependenciesChanged())) {
evaluateImmediate();
} }
return dependentObservable._originalGetVersion();
};
} else if (options['deferEvaluation']) { } else if (options['deferEvaluation']) {
// This will force a computed with deferEvaluation to evaluate when the first subscriptions is registered. // This will force a computed with deferEvaluation to evaluate when the first subscriptions is registered.
dependentObservable.beforeSubscriptionAdd = function () { dependentObservable.beforeSubscriptionAdd = function (event) {
if (event == 'change' || event == 'beforeChange') {
peek(); peek();
delete dependentObservable.beforeSubscriptionAdd; }
} }
} }
...@@ -2085,7 +2208,7 @@ ko.expressionRewriting = (function () { ...@@ -2085,7 +2208,7 @@ ko.expressionRewriting = (function () {
if (str.charCodeAt(0) === 123) str = str.slice(1, -1); if (str.charCodeAt(0) === 123) str = str.slice(1, -1);
// Split into tokens // Split into tokens
var result = [], toks = str.match(bindingToken), key, values, depth = 0; var result = [], toks = str.match(bindingToken), key, values = [], depth = 0;
if (toks) { if (toks) {
// Append a comma so that we don't need a separate code block to deal with the last item // Append a comma so that we don't need a separate code block to deal with the last item
...@@ -2096,15 +2219,17 @@ ko.expressionRewriting = (function () { ...@@ -2096,15 +2219,17 @@ ko.expressionRewriting = (function () {
// A comma signals the end of a key/value pair if depth is zero // A comma signals the end of a key/value pair if depth is zero
if (c === 44) { // "," if (c === 44) { // ","
if (depth <= 0) { if (depth <= 0) {
if (key) result.push((key && values.length) ? {key: key, value: values.join('')} : {'unknown': key || values.join('')});
result.push(values ? {key: key, value: values.join('')} : {'unknown': key}); key = depth = 0;
key = values = depth = 0; values = [];
continue; continue;
} }
// Simply skip the colon that separates the name and value // Simply skip the colon that separates the name and value
} else if (c === 58) { // ":" } else if (c === 58) { // ":"
if (!values) if (!depth && !key && values.length === 1) {
key = values.pop();
continue; continue;
}
// A set of slashes is initially matched as a regular expression, but could be division // A set of slashes is initially matched as a regular expression, but could be division
} else if (c === 47 && i && tok.length > 1) { // "/" } else if (c === 47 && i && tok.length > 1) { // "/"
// Look at the end of the previous token to determine if the slash is actually division // Look at the end of the previous token to determine if the slash is actually division
...@@ -2123,15 +2248,11 @@ ko.expressionRewriting = (function () { ...@@ -2123,15 +2248,11 @@ ko.expressionRewriting = (function () {
++depth; ++depth;
} else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']' } else if (c === 41 || c === 125 || c === 93) { // ')', '}', ']'
--depth; --depth;
// The key must be a single token; if it's a string, trim the quotes // The key will be the first token; if it's a string, trim the quotes
} else if (!key && !values) { } else if (!key && !values.length && (c === 34 || c === 39)) { // '"', "'"
key = (c === 34 || c === 39) /* '"', "'" */ ? tok.slice(1, -1) : tok; tok = tok.slice(1, -1);
continue;
} }
if (values)
values.push(tok); values.push(tok);
else
values = [tok];
} }
} }
return result; return result;
...@@ -2512,9 +2633,10 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2512,9 +2633,10 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// may consider adding <template> to this list, because such elements' contents are always // may consider adding <template> to this list, because such elements' contents are always
// intended to be bound in a different context from where they appear in the document. // intended to be bound in a different context from where they appear in the document.
var bindingDoesNotRecurseIntoElementTypes = { var bindingDoesNotRecurseIntoElementTypes = {
// Don't want bindings that operate on text nodes to mutate <script> contents, // Don't want bindings that operate on text nodes to mutate <script> and <textarea> contents,
// because it's unexpected and a potential XSS issue // because it's unexpected and a potential XSS issue
'script': true 'script': true,
'textarea': true
}; };
// Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers // Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
...@@ -2978,8 +3100,15 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -2978,8 +3100,15 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
var cachedDefinition = getObjectOwnProperty(loadedDefinitionsCache, componentName); var cachedDefinition = getObjectOwnProperty(loadedDefinitionsCache, componentName);
if (cachedDefinition) { if (cachedDefinition) {
// It's already loaded and cached. Reuse the same definition object. // It's already loaded and cached. Reuse the same definition object.
// Note that for API consistency, even cache hits complete asynchronously. // Note that for API consistency, even cache hits complete asynchronously by default.
setTimeout(function() { callback(cachedDefinition) }, 0); // You can bypass this by putting synchronous:true on your component config.
if (cachedDefinition.isSynchronousComponent) {
ko.dependencyDetection.ignore(function() { // See comment in loaderRegistryBehaviors.js for reasoning
callback(cachedDefinition.definition);
});
} else {
setTimeout(function() { callback(cachedDefinition.definition); }, 0);
}
} else { } else {
// Join the loading process that is already underway, or start a new one. // Join the loading process that is already underway, or start a new one.
loadComponentAndNotify(componentName, callback); loadComponentAndNotify(componentName, callback);
...@@ -3003,14 +3132,22 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3003,14 +3132,22 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
if (!subscribable) { if (!subscribable) {
// It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache. // It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache.
subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable(); subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable();
beginLoadingComponent(componentName, function(definition) { subscribable.subscribe(callback);
loadedDefinitionsCache[componentName] = definition;
beginLoadingComponent(componentName, function(definition, config) {
var isSynchronousComponent = !!(config && config['synchronous']);
loadedDefinitionsCache[componentName] = { definition: definition, isSynchronousComponent: isSynchronousComponent };
delete loadingSubscribablesCache[componentName]; delete loadingSubscribablesCache[componentName];
// For API consistency, all loads complete asynchronously. However we want to avoid // For API consistency, all loads complete asynchronously. However we want to avoid
// adding an extra setTimeout if it's unnecessary (i.e., the completion is already // adding an extra setTimeout if it's unnecessary (i.e., the completion is already
// async) since setTimeout(..., 0) still takes about 16ms or more on most browsers. // async) since setTimeout(..., 0) still takes about 16ms or more on most browsers.
if (completedAsync) { //
// You can bypass the 'always synchronous' feature by putting the synchronous:true
// flag on your component configuration when you register it.
if (completedAsync || isSynchronousComponent) {
// Note that notifySubscribers ignores any dependencies read within the callback.
// See comment in loaderRegistryBehaviors.js for reasoning
subscribable['notifySubscribers'](definition); subscribable['notifySubscribers'](definition);
} else { } else {
setTimeout(function() { setTimeout(function() {
...@@ -3019,23 +3156,24 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3019,23 +3156,24 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
} }
}); });
completedAsync = true; completedAsync = true;
} } else {
subscribable.subscribe(callback); subscribable.subscribe(callback);
} }
}
function beginLoadingComponent(componentName, callback) { function beginLoadingComponent(componentName, callback) {
getFirstResultFromLoaders('getConfig', [componentName], function(config) { getFirstResultFromLoaders('getConfig', [componentName], function(config) {
if (config) { if (config) {
// We have a config, so now load its definition // We have a config, so now load its definition
getFirstResultFromLoaders('loadComponent', [componentName, config], function(definition) { getFirstResultFromLoaders('loadComponent', [componentName, config], function(definition) {
callback(definition); callback(definition, config);
}); });
} else { } else {
// The component has no config - it's unknown to all the loaders. // The component has no config - it's unknown to all the loaders.
// Note that this is not an error (e.g., a module loading error) - that would abort the // Note that this is not an error (e.g., a module loading error) - that would abort the
// process and this callback would not run. For this callback to run, all loaders must // process and this callback would not run. For this callback to run, all loaders must
// have confirmed they don't know about this component. // have confirmed they don't know about this component.
callback(null); callback(null, null);
} }
}); });
} }
...@@ -3291,8 +3429,8 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3291,8 +3429,8 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
function possiblyGetConfigFromAmd(errorCallback, config, callback) { function possiblyGetConfigFromAmd(errorCallback, config, callback) {
if (typeof config['require'] === 'string') { if (typeof config['require'] === 'string') {
// The config is the value of an AMD module // The config is the value of an AMD module
if (require || window['require']) { if (amdRequire || window['require']) {
(require || window['require'])([config['require']], callback); (amdRequire || window['require'])([config['require']], callback);
} else { } else {
errorCallback('Uses require, but no AMD loader is present'); errorCallback('Uses require, but no AMD loader is present');
} }
...@@ -3364,18 +3502,26 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3364,18 +3502,26 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem }); return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem });
}), }),
result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) { result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) {
var paramValue = paramValueComputed.peek();
// Does the evaluation of the parameter value unwrap any observables? // Does the evaluation of the parameter value unwrap any observables?
if (!paramValueComputed.isActive()) { if (!paramValueComputed.isActive()) {
// No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly. // No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly.
// Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed) // Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed)
return paramValueComputed.peek(); return paramValue;
} else { } else {
// Yes it does. Supply a computed property that unwraps both the outer (binding expression) // Yes it does. Supply a computed property that unwraps both the outer (binding expression)
// level of observability, and any inner (resulting model value) level of observability. // level of observability, and any inner (resulting model value) level of observability.
// This means the component doesn't have to worry about multiple unwrapping. // This means the component doesn't have to worry about multiple unwrapping. If the value is a
return ko.computed(function() { // writable observable, the computed will also be writable and pass the value on to the observable.
return ko.computed({
'read': function() {
return ko.utils.unwrapObservable(paramValueComputed()); return ko.utils.unwrapObservable(paramValueComputed());
}, null, { disposeWhenNodeIsRemoved: elem }); },
'write': ko.isWriteableObservable(paramValue) && function(value) {
paramValueComputed()(value);
},
disposeWhenNodeIsRemoved: elem
});
} }
}); });
...@@ -3438,7 +3584,8 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3438,7 +3584,8 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion // Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null; currentLoadingOperationId = null;
}; },
originalChildNodes = ko.utils.makeArray(ko.virtualElements.childNodes(element));
ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel); ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);
...@@ -3472,8 +3619,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3472,8 +3619,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
throw new Error('Unknown component \'' + componentName + '\''); throw new Error('Unknown component \'' + componentName + '\'');
} }
cloneTemplateIntoElement(componentName, componentDefinition, element); cloneTemplateIntoElement(componentName, componentDefinition, element);
var componentViewModel = createViewModel(componentDefinition, element, componentParams), var componentViewModel = createViewModel(componentDefinition, element, originalChildNodes, componentParams),
childBindingContext = bindingContext['createChildContext'](componentViewModel); childBindingContext = bindingContext['createChildContext'](componentViewModel, /* dataItemAlias */ undefined, function(ctx) {
ctx['$component'] = componentViewModel;
ctx['$componentTemplateNodes'] = originalChildNodes;
});
currentViewModel = componentViewModel; currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element); ko.applyBindingsToDescendants(childBindingContext, element);
}); });
...@@ -3495,10 +3645,10 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider); ...@@ -3495,10 +3645,10 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.virtualElements.setDomNodeChildren(element, clonedNodesArray); ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
} }
function createViewModel(componentDefinition, element, componentParams) { function createViewModel(componentDefinition, element, originalChildNodes, componentParams) {
var componentViewModelFactory = componentDefinition['createViewModel']; var componentViewModelFactory = componentDefinition['createViewModel'];
return componentViewModelFactory return componentViewModelFactory
? componentViewModelFactory.call(componentDefinition, componentParams, { element: element }) ? componentViewModelFactory.call(componentDefinition, componentParams, { 'element': element, 'templateNodes': originalChildNodes })
: componentParams; // Template-only component : componentParams; // Template-only component
} }
...@@ -3651,7 +3801,7 @@ ko.bindingHandlers['checkedValue'] = { ...@@ -3651,7 +3801,7 @@ ko.bindingHandlers['checkedValue'] = {
ko.bindingHandlers['css'] = { ko.bindingHandlers['css'] = {
'update': function (element, valueAccessor) { 'update': function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()); var value = ko.utils.unwrapObservable(valueAccessor());
if (typeof value == "object") { if (value !== null && typeof value == "object") {
ko.utils.objectForEach(value, function(className, shouldHaveClass) { ko.utils.objectForEach(value, function(className, shouldHaveClass) {
shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass); shouldHaveClass = ko.utils.unwrapObservable(shouldHaveClass);
ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass); ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
...@@ -3893,19 +4043,23 @@ ko.bindingHandlers['options'] = { ...@@ -3893,19 +4043,23 @@ ko.bindingHandlers['options'] = {
return ko.utils.arrayFilter(element.options, function (node) { return node.selected; }); return ko.utils.arrayFilter(element.options, function (node) { return node.selected; });
} }
var selectWasPreviouslyEmpty = element.length == 0; var selectWasPreviouslyEmpty = element.length == 0,
var previousScrollTop = (!selectWasPreviouslyEmpty && element.multiple) ? element.scrollTop : null; multiple = element.multiple,
var unwrappedArray = ko.utils.unwrapObservable(valueAccessor()); previousScrollTop = (!selectWasPreviouslyEmpty && multiple) ? element.scrollTop : null,
var includeDestroyed = allBindings.get('optionsIncludeDestroyed'); unwrappedArray = ko.utils.unwrapObservable(valueAccessor()),
var arrayToDomNodeChildrenOptions = {}; valueAllowUnset = allBindings.get('valueAllowUnset') && allBindings['has']('value'),
var captionValue; includeDestroyed = allBindings.get('optionsIncludeDestroyed'),
var filteredArray; arrayToDomNodeChildrenOptions = {},
var previousSelectedValues; captionValue,
filteredArray,
previousSelectedValues = [];
if (element.multiple) { if (!valueAllowUnset) {
if (multiple) {
previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue); previousSelectedValues = ko.utils.arrayMap(selectedOptions(), ko.selectExtensions.readValue);
} else { } else if (element.selectedIndex >= 0) {
previousSelectedValues = element.selectedIndex >= 0 ? [ ko.selectExtensions.readValue(element.options[element.selectedIndex]) ] : []; previousSelectedValues.push(ko.selectExtensions.readValue(element.options[element.selectedIndex]));
}
} }
if (unwrappedArray) { if (unwrappedArray) {
...@@ -3946,7 +4100,7 @@ ko.bindingHandlers['options'] = { ...@@ -3946,7 +4100,7 @@ ko.bindingHandlers['options'] = {
var itemUpdate = false; var itemUpdate = false;
function optionForArrayItem(arrayEntry, index, oldOptions) { function optionForArrayItem(arrayEntry, index, oldOptions) {
if (oldOptions.length) { if (oldOptions.length) {
previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : []; previousSelectedValues = !valueAllowUnset && oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
itemUpdate = true; itemUpdate = true;
} }
var option = element.ownerDocument.createElement("option"); var option = element.ownerDocument.createElement("option");
...@@ -3973,20 +4127,25 @@ ko.bindingHandlers['options'] = { ...@@ -3973,20 +4127,25 @@ ko.bindingHandlers['options'] = {
}; };
function setSelectionCallback(arrayEntry, newOptions) { function setSelectionCallback(arrayEntry, newOptions) {
if (itemUpdate && valueAllowUnset) {
// The model value is authoritative, so make sure its value is the one selected
// There is no need to use dependencyDetection.ignore since setDomNodeChildrenFromArrayMapping does so already.
ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
} else if (previousSelectedValues.length) {
// IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document. // IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
// That's why we first added them without selection. Now it's time to set the selection. // That's why we first added them without selection. Now it's time to set the selection.
if (previousSelectedValues.length) {
var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0; var isSelected = ko.utils.arrayIndexOf(previousSelectedValues, ko.selectExtensions.readValue(newOptions[0])) >= 0;
ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected); ko.utils.setOptionNodeSelectionState(newOptions[0], isSelected);
// If this option was changed from being selected during a single-item update, notify the change // If this option was changed from being selected during a single-item update, notify the change
if (itemUpdate && !isSelected) if (itemUpdate && !isSelected) {
ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]); ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
} }
} }
}
var callback = setSelectionCallback; var callback = setSelectionCallback;
if (allBindings['has']('optionsAfterRender')) { if (allBindings['has']('optionsAfterRender') && typeof allBindings.get('optionsAfterRender') == "function") {
callback = function(arrayEntry, newOptions) { callback = function(arrayEntry, newOptions) {
setSelectionCallback(arrayEntry, newOptions); setSelectionCallback(arrayEntry, newOptions);
ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]); ko.dependencyDetection.ignore(allBindings.get('optionsAfterRender'), null, [newOptions[0], arrayEntry !== captionPlaceholder ? arrayEntry : undefined]);
...@@ -3996,13 +4155,13 @@ ko.bindingHandlers['options'] = { ...@@ -3996,13 +4155,13 @@ ko.bindingHandlers['options'] = {
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback); ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
ko.dependencyDetection.ignore(function () { ko.dependencyDetection.ignore(function () {
if (allBindings.get('valueAllowUnset') && allBindings['has']('value')) { if (valueAllowUnset) {
// The model value is authoritative, so make sure its value is the one selected // The model value is authoritative, so make sure its value is the one selected
ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */); ko.selectExtensions.writeValue(element, ko.utils.unwrapObservable(allBindings.get('value')), true /* allowUnset */);
} else { } else {
// Determine if the selection has changed as a result of updating the options list // Determine if the selection has changed as a result of updating the options list
var selectionChanged; var selectionChanged;
if (element.multiple) { if (multiple) {
// For a multiple-select box, compare the new selection count to the previous one // For a multiple-select box, compare the new selection count to the previous one
// But if nothing was selected before, the selection can't have changed // But if nothing was selected before, the selection can't have changed
selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length; selectionChanged = previousSelectedValues.length && selectedOptions().length < previousSelectedValues.length;
...@@ -4417,6 +4576,7 @@ makeEventHandlerShortcut('click'); ...@@ -4417,6 +4576,7 @@ makeEventHandlerShortcut('click');
// // - you might also want to make bindingContext.$parent, bindingContext.$parents, // // - you might also want to make bindingContext.$parent, bindingContext.$parents,
// // and bindingContext.$root available in the template too // // and bindingContext.$root available in the template too
// // - options gives you access to any other properties set on "data-bind: { template: options }" // // - options gives you access to any other properties set on "data-bind: { template: options }"
// // - templateDocument is the document object of the template
// // // //
// // Return value: an array of DOM nodes // // Return value: an array of DOM nodes
// } // }
...@@ -4434,7 +4594,7 @@ makeEventHandlerShortcut('click'); ...@@ -4434,7 +4594,7 @@ makeEventHandlerShortcut('click');
ko.templateEngine = function () { }; ko.templateEngine = function () { };
ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) { ko.templateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options, templateDocument) {
throw new Error("Override renderTemplateSource"); throw new Error("Override renderTemplateSource");
}; };
...@@ -4459,7 +4619,7 @@ ko.templateEngine.prototype['makeTemplateSource'] = function(template, templateD ...@@ -4459,7 +4619,7 @@ ko.templateEngine.prototype['makeTemplateSource'] = function(template, templateD
ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options, templateDocument) { ko.templateEngine.prototype['renderTemplate'] = function (template, bindingContext, options, templateDocument) {
var templateSource = this['makeTemplateSource'](template, templateDocument); var templateSource = this['makeTemplateSource'](template, templateDocument);
return this['renderTemplateSource'](templateSource, bindingContext, options); return this['renderTemplateSource'](templateSource, bindingContext, options, templateDocument);
}; };
ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templateDocument) { ko.templateEngine.prototype['isTemplateRewritten'] = function (template, templateDocument) {
...@@ -4479,7 +4639,7 @@ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCal ...@@ -4479,7 +4639,7 @@ ko.templateEngine.prototype['rewriteTemplate'] = function (template, rewriterCal
ko.exportSymbol('templateEngine', ko.templateEngine); ko.exportSymbol('templateEngine', ko.templateEngine);
ko.templateRewriting = (function () { ko.templateRewriting = (function () {
var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi; var memoizeDataBindingAttributeSyntaxRegex = /(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'|[^>]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi;
var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g; var memoizeVirtualContainerBindingSyntaxRegex = /<!--\s*ko\b\s*([\s\S]*?)\s*-->/g;
function validateDataBindValuesForRewriting(keyValueArray) { function validateDataBindValuesForRewriting(keyValueArray) {
...@@ -4720,7 +4880,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -4720,7 +4880,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) { function executeTemplate(targetNodeOrNodeArray, renderMode, template, bindingContext, options) {
options = options || {}; options = options || {};
var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray); var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
var templateDocument = firstTargetNode && firstTargetNode.ownerDocument; var templateDocument = (firstTargetNode || template || {}).ownerDocument;
var templateEngineToUse = (options['templateEngine'] || _templateEngine); var templateEngineToUse = (options['templateEngine'] || _templateEngine);
ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument); ko.templateRewriting.ensureTemplateIsRewritten(template, templateEngineToUse, templateDocument);
var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument); var renderedNodesArray = templateEngineToUse['renderTemplate'](template, bindingContext, options, templateDocument);
...@@ -4826,6 +4986,10 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -4826,6 +4986,10 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext); activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
if (options['afterRender']) if (options['afterRender'])
options['afterRender'](addedNodesArray, arrayValue); options['afterRender'](addedNodesArray, arrayValue);
// release the "cache" variable, so that it can be collected by
// the GC when its value isn't used from within the bindings anymore.
arrayItemContext = null;
}; };
return ko.dependentObservable(function () { return ko.dependentObservable(function () {
...@@ -4860,6 +5024,17 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS ...@@ -4860,6 +5024,17 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
if (typeof bindingValue == "string" || bindingValue['name']) { if (typeof bindingValue == "string" || bindingValue['name']) {
// It's a named template - clear the element // It's a named template - clear the element
ko.virtualElements.emptyNode(element); ko.virtualElements.emptyNode(element);
} else if ('nodes' in bindingValue) {
// We've been given an array of DOM nodes. Save them as the template source.
// There is no known use case for the node array being an observable array (if the output
// varies, put that behavior *into* your template - that's what templates are for), and
// the implementation would be a mess, so assert that it's not observable.
var nodes = bindingValue['nodes'] || [];
if (ko.isObservable(nodes)) {
throw new Error('The "nodes" option must be a plain, non-observable array.');
}
var container = ko.utils.moveCleanedNodesToContainerElement(nodes); // This also removes the nodes from their current parent
new ko.templateSources.anonymousTemplate(element)['nodes'](container);
} else { } else {
// It's an anonymous template - store the element contents, then clear the element // It's an anonymous template - store the element contents, then clear the element
var templateNodes = ko.virtualElements.childNodes(element), var templateNodes = ko.virtualElements.childNodes(element),
...@@ -5194,7 +5369,7 @@ ko.nativeTemplateEngine = function () { ...@@ -5194,7 +5369,7 @@ ko.nativeTemplateEngine = function () {
ko.nativeTemplateEngine.prototype = new ko.templateEngine(); ko.nativeTemplateEngine.prototype = new ko.templateEngine();
ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine; ko.nativeTemplateEngine.prototype.constructor = ko.nativeTemplateEngine;
ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options) { ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSource, bindingContext, options, templateDocument) {
var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly var useNodesIfAvailable = !(ko.utils.ieVersion < 9), // IE<9 cloneNode doesn't work properly
templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null, templateNodesFunc = useNodesIfAvailable ? templateSource['nodes'] : null,
templateNodes = templateNodesFunc ? templateSource['nodes']() : null; templateNodes = templateNodesFunc ? templateSource['nodes']() : null;
...@@ -5203,7 +5378,7 @@ ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSo ...@@ -5203,7 +5378,7 @@ ko.nativeTemplateEngine.prototype['renderTemplateSource'] = function (templateSo
return ko.utils.makeArray(templateNodes.cloneNode(true).childNodes); return ko.utils.makeArray(templateNodes.cloneNode(true).childNodes);
} else { } else {
var templateText = templateSource['text'](); var templateText = templateSource['text']();
return ko.utils.parseHtmlFragment(templateText); return ko.utils.parseHtmlFragment(templateText, templateDocument);
} }
}; };
...@@ -5240,7 +5415,8 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine); ...@@ -5240,7 +5415,8 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
return jQueryInstance['tmpl'](compiledTemplate, data, jQueryTemplateOptions); return jQueryInstance['tmpl'](compiledTemplate, data, jQueryTemplateOptions);
} }
this['renderTemplateSource'] = function(templateSource, bindingContext, options) { this['renderTemplateSource'] = function(templateSource, bindingContext, options, templateDocument) {
templateDocument = templateDocument || document;
options = options || {}; options = options || {};
ensureHasReferencedJQueryTemplates(); ensureHasReferencedJQueryTemplates();
...@@ -5259,7 +5435,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine); ...@@ -5259,7 +5435,7 @@ ko.exportSymbol('nativeTemplateEngine', ko.nativeTemplateEngine);
var jQueryTemplateOptions = jQueryInstance['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']); var jQueryTemplateOptions = jQueryInstance['extend']({ 'koBindingContext': bindingContext }, options['templateOptions']);
var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions); var resultNodes = executeTemplate(precompiled, data, jQueryTemplateOptions);
resultNodes['appendTo'](document.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work resultNodes['appendTo'](templateDocument.createElement("div")); // Using "appendTo" forces jQuery/jQuery.tmpl to perform necessary cleanup work
jQueryInstance['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders jQueryInstance['fragments'] = {}; // Clear jQuery's fragment cache to avoid a memory leak after a large number of template renders
return resultNodes; return resultNodes;
......
...@@ -44,7 +44,7 @@ input[type="checkbox"] { ...@@ -44,7 +44,7 @@ input[type="checkbox"] {
display: none; display: none;
} }
#todoapp { .todoapp {
background: #fff; background: #fff;
margin: 130px 0 40px 0; margin: 130px 0 40px 0;
position: relative; position: relative;
...@@ -52,25 +52,25 @@ input[type="checkbox"] { ...@@ -52,25 +52,25 @@ input[type="checkbox"] {
0 25px 50px 0 rgba(0, 0, 0, 0.1); 0 25px 50px 0 rgba(0, 0, 0, 0.1);
} }
#todoapp input::-webkit-input-placeholder { .todoapp input::-webkit-input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp input::-moz-placeholder { .todoapp input::-moz-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp input::input-placeholder { .todoapp input::input-placeholder {
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
color: #e6e6e6; color: #e6e6e6;
} }
#todoapp h1 { .todoapp h1 {
position: absolute; position: absolute;
top: -155px; top: -155px;
width: 100%; width: 100%;
...@@ -83,7 +83,7 @@ input[type="checkbox"] { ...@@ -83,7 +83,7 @@ input[type="checkbox"] {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
#new-todo, .new-todo,
.edit { .edit {
position: relative; position: relative;
margin: 0; margin: 0;
...@@ -104,14 +104,14 @@ input[type="checkbox"] { ...@@ -104,14 +104,14 @@ input[type="checkbox"] {
font-smoothing: antialiased; font-smoothing: antialiased;
} }
#new-todo { .new-todo {
padding: 16px 16px 16px 60px; padding: 16px 16px 16px 60px;
border: none; border: none;
background: rgba(0, 0, 0, 0.003); background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
} }
#main { .main {
position: relative; position: relative;
z-index: 2; z-index: 2;
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
...@@ -121,7 +121,7 @@ label[for='toggle-all'] { ...@@ -121,7 +121,7 @@ label[for='toggle-all'] {
display: none; display: none;
} }
#toggle-all { .toggle-all {
position: absolute; position: absolute;
top: -55px; top: -55px;
left: -12px; left: -12px;
...@@ -131,50 +131,50 @@ label[for='toggle-all'] { ...@@ -131,50 +131,50 @@ label[for='toggle-all'] {
border: none; /* Mobile Safari */ border: none; /* Mobile Safari */
} }
#toggle-all:before { .toggle-all:before {
content: '❯'; content: '❯';
font-size: 22px; font-size: 22px;
color: #e6e6e6; color: #e6e6e6;
padding: 10px 27px 10px 27px; padding: 10px 27px 10px 27px;
} }
#toggle-all:checked:before { .toggle-all:checked:before {
color: #737373; color: #737373;
} }
#todo-list { .todo-list {
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
#todo-list li { .todo-list li {
position: relative; position: relative;
font-size: 24px; font-size: 24px;
border-bottom: 1px solid #ededed; border-bottom: 1px solid #ededed;
} }
#todo-list li:last-child { .todo-list li:last-child {
border-bottom: none; border-bottom: none;
} }
#todo-list li.editing { .todo-list li.editing {
border-bottom: none; border-bottom: none;
padding: 0; padding: 0;
} }
#todo-list li.editing .edit { .todo-list li.editing .edit {
display: block; display: block;
width: 506px; width: 506px;
padding: 13px 17px 12px 17px; padding: 13px 17px 12px 17px;
margin: 0 0 0 43px; margin: 0 0 0 43px;
} }
#todo-list li.editing .view { .todo-list li.editing .view {
display: none; display: none;
} }
#todo-list li .toggle { .todo-list li .toggle {
text-align: center; text-align: center;
width: 40px; width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */ /* auto, since non-WebKit browsers doesn't support input styling */
...@@ -188,15 +188,15 @@ label[for='toggle-all'] { ...@@ -188,15 +188,15 @@ label[for='toggle-all'] {
appearance: none; appearance: none;
} }
#todo-list li .toggle:after { .todo-list li .toggle:after {
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>'); 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 { .todo-list li .toggle:checked:after {
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>'); 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 { .todo-list li label {
white-space: pre; white-space: pre;
word-break: break-word; word-break: break-word;
padding: 15px 60px 15px 15px; padding: 15px 60px 15px 15px;
...@@ -206,12 +206,12 @@ label[for='toggle-all'] { ...@@ -206,12 +206,12 @@ label[for='toggle-all'] {
transition: color 0.4s; transition: color 0.4s;
} }
#todo-list li.completed label { .todo-list li.completed label {
color: #d9d9d9; color: #d9d9d9;
text-decoration: line-through; text-decoration: line-through;
} }
#todo-list li .destroy { .todo-list li .destroy {
display: none; display: none;
position: absolute; position: absolute;
top: 0; top: 0;
...@@ -226,27 +226,27 @@ label[for='toggle-all'] { ...@@ -226,27 +226,27 @@ label[for='toggle-all'] {
transition: color 0.2s ease-out; transition: color 0.2s ease-out;
} }
#todo-list li .destroy:hover { .todo-list li .destroy:hover {
color: #af5b5e; color: #af5b5e;
} }
#todo-list li .destroy:after { .todo-list li .destroy:after {
content: '×'; content: '×';
} }
#todo-list li:hover .destroy { .todo-list li:hover .destroy {
display: block; display: block;
} }
#todo-list li .edit { .todo-list li .edit {
display: none; display: none;
} }
#todo-list li.editing:last-child { .todo-list li.editing:last-child {
margin-bottom: -1px; margin-bottom: -1px;
} }
#footer { .footer {
color: #777; color: #777;
padding: 10px 15px; padding: 10px 15px;
height: 20px; height: 20px;
...@@ -254,7 +254,7 @@ label[for='toggle-all'] { ...@@ -254,7 +254,7 @@ label[for='toggle-all'] {
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
} }
#footer:before { .footer:before {
content: ''; content: '';
position: absolute; position: absolute;
right: 0; right: 0;
...@@ -269,16 +269,16 @@ label[for='toggle-all'] { ...@@ -269,16 +269,16 @@ label[for='toggle-all'] {
0 17px 2px -6px rgba(0, 0, 0, 0.2); 0 17px 2px -6px rgba(0, 0, 0, 0.2);
} }
#todo-count { .todo-count {
float: left; float: left;
text-align: left; text-align: left;
} }
#todo-count strong { .todo-count strong {
font-weight: 300; font-weight: 300;
} }
#filters { .filters {
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
...@@ -287,11 +287,11 @@ label[for='toggle-all'] { ...@@ -287,11 +287,11 @@ label[for='toggle-all'] {
left: 0; left: 0;
} }
#filters li { .filters li {
display: inline; display: inline;
} }
#filters li a { .filters li a {
color: inherit; color: inherit;
margin: 3px; margin: 3px;
padding: 3px 7px; padding: 3px 7px;
...@@ -300,17 +300,17 @@ label[for='toggle-all'] { ...@@ -300,17 +300,17 @@ label[for='toggle-all'] {
border-radius: 3px; border-radius: 3px;
} }
#filters li a.selected, .filters li a.selected,
#filters li a:hover { .filters li a:hover {
border-color: rgba(175, 47, 47, 0.1); border-color: rgba(175, 47, 47, 0.1);
} }
#filters li a.selected { .filters li a.selected {
border-color: rgba(175, 47, 47, 0.2); border-color: rgba(175, 47, 47, 0.2);
} }
#clear-completed, .clear-completed,
html #clear-completed:active { html .clear-completed:active {
float: right; float: right;
position: relative; position: relative;
line-height: 20px; line-height: 20px;
...@@ -319,11 +319,11 @@ html #clear-completed:active { ...@@ -319,11 +319,11 @@ html #clear-completed:active {
position: relative; position: relative;
} }
#clear-completed:hover { .clear-completed:hover {
text-decoration: underline; text-decoration: underline;
} }
#info { .info {
margin: 65px auto 0; margin: 65px auto 0;
color: #bfbfbf; color: #bfbfbf;
font-size: 10px; font-size: 10px;
...@@ -331,17 +331,17 @@ html #clear-completed:active { ...@@ -331,17 +331,17 @@ html #clear-completed:active {
text-align: center; text-align: center;
} }
#info p { .info p {
line-height: 1; line-height: 1;
} }
#info a { .info a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
font-weight: 400; font-weight: 400;
} }
#info a:hover { .info a:hover {
text-decoration: underline; text-decoration: underline;
} }
...@@ -350,16 +350,16 @@ html #clear-completed:active { ...@@ -350,16 +350,16 @@ html #clear-completed:active {
Can't use it globally since it destroys checkboxes in Firefox Can't use it globally since it destroys checkboxes in Firefox
*/ */
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all, .toggle-all,
#todo-list li .toggle { .todo-list li .toggle {
background: none; background: none;
} }
#todo-list li .toggle { .todo-list li .toggle {
height: 40px; height: 40px;
} }
#toggle-all { .toggle-all {
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
transform: rotate(90deg); transform: rotate(90deg);
-webkit-appearance: none; -webkit-appearance: none;
...@@ -368,11 +368,11 @@ html #clear-completed:active { ...@@ -368,11 +368,11 @@ html #clear-completed:active {
} }
@media (max-width: 430px) { @media (max-width: 430px) {
#footer { .footer {
height: 50px; height: 50px;
} }
#filters { .filters {
bottom: 10px; bottom: 10px;
} }
} }
...@@ -114,7 +114,12 @@ ...@@ -114,7 +114,12 @@
})({}); })({});
if (location.hostname === 'todomvc.com') { if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script')); (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-31081062-1', 'auto');
ga('send', 'pageview');
} }
/* jshint ignore:end */ /* jshint ignore:end */
...@@ -228,7 +233,7 @@ ...@@ -228,7 +233,7 @@
xhr.onload = function (e) { xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText); var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) { if (parsedResponse instanceof Array) {
var count = parsedResponse.length var count = parsedResponse.length;
if (count !== 0) { if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues'; issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline'; document.getElementById('issue-count').style.display = 'inline';
......
// Underscore.js 1.7.0 // Underscore.js 1.8.3
// http://underscorejs.org // http://underscorejs.org
// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license. // Underscore may be freely distributed under the MIT license.
(function() { (function() {
...@@ -21,7 +21,6 @@ ...@@ -21,7 +21,6 @@
var var
push = ArrayProto.push, push = ArrayProto.push,
slice = ArrayProto.slice, slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString, toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty; hasOwnProperty = ObjProto.hasOwnProperty;
...@@ -30,7 +29,11 @@ ...@@ -30,7 +29,11 @@
var var
nativeIsArray = Array.isArray, nativeIsArray = Array.isArray,
nativeKeys = Object.keys, nativeKeys = Object.keys,
nativeBind = FuncProto.bind; nativeBind = FuncProto.bind,
nativeCreate = Object.create;
// Naked function reference for surrogate-prototype-swapping.
var Ctor = function(){};
// Create a safe reference to the Underscore object for use below. // Create a safe reference to the Underscore object for use below.
var _ = function(obj) { var _ = function(obj) {
...@@ -52,12 +55,12 @@ ...@@ -52,12 +55,12 @@
} }
// Current version. // Current version.
_.VERSION = '1.7.0'; _.VERSION = '1.8.3';
// Internal function that returns an efficient (for current engines) version // Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other Underscore // of the passed-in callback, to be repeatedly applied in other Underscore
// functions. // functions.
var createCallback = function(func, context, argCount) { var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func; if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) { switch (argCount == null ? 3 : argCount) {
case 1: return function(value) { case 1: return function(value) {
...@@ -81,12 +84,60 @@ ...@@ -81,12 +84,60 @@
// A mostly-internal function to generate callbacks that can be applied // A mostly-internal function to generate callbacks that can be applied
// to each element in a collection, returning the desired result — either // to each element in a collection, returning the desired result — either
// identity, an arbitrary callback, a property matcher, or a property accessor. // identity, an arbitrary callback, a property matcher, or a property accessor.
_.iteratee = function(value, context, argCount) { var cb = function(value, context, argCount) {
if (value == null) return _.identity; if (value == null) return _.identity;
if (_.isFunction(value)) return createCallback(value, context, argCount); if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value)) return _.matches(value); if (_.isObject(value)) return _.matcher(value);
return _.property(value); return _.property(value);
}; };
_.iteratee = function(value, context) {
return cb(value, context, Infinity);
};
// An internal function for creating assigner functions.
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};
// An internal function for creating a new object that inherits from another.
var baseCreate = function(prototype) {
if (!_.isObject(prototype)) return {};
if (nativeCreate) return nativeCreate(prototype);
Ctor.prototype = prototype;
var result = new Ctor;
Ctor.prototype = null;
return result;
};
var property = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
// Helper for collection methods to determine whether a collection
// should be iterated as an array or as an object
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = property('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Collection Functions // Collection Functions
// -------------------- // --------------------
...@@ -95,11 +146,10 @@ ...@@ -95,11 +146,10 @@
// Handles raw objects in addition to array-likes. Treats all // Handles raw objects in addition to array-likes. Treats all
// sparse array-likes as if they were dense. // sparse array-likes as if they were dense.
_.each = _.forEach = function(obj, iteratee, context) { _.each = _.forEach = function(obj, iteratee, context) {
if (obj == null) return obj; iteratee = optimizeCb(iteratee, context);
iteratee = createCallback(iteratee, context); var i, length;
var i, length = obj.length; if (isArrayLike(obj)) {
if (length === +length) { for (i = 0, length = obj.length; i < length; i++) {
for (i = 0; i < length; i++) {
iteratee(obj[i], i, obj); iteratee(obj[i], i, obj);
} }
} else { } else {
...@@ -113,77 +163,66 @@ ...@@ -113,77 +163,66 @@
// Return the results of applying the iteratee to each element. // Return the results of applying the iteratee to each element.
_.map = _.collect = function(obj, iteratee, context) { _.map = _.collect = function(obj, iteratee, context) {
if (obj == null) return []; iteratee = cb(iteratee, context);
iteratee = _.iteratee(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj),
length = (keys || obj).length, length = (keys || obj).length,
results = Array(length), results = Array(length);
currentKey;
for (var index = 0; index < length; index++) { for (var index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index; var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj); results[index] = iteratee(obj[currentKey], currentKey, obj);
} }
return results; return results;
}; };
var reduceError = 'Reduce of empty array with no initial value'; // Create a reducing function iterating left or right.
function createReduce(dir) {
// **Reduce** builds up a single result from a list of values, aka `inject`, // Optimized iterator function as using arguments.length
// or `foldl`. // in the main function will deoptimize the, see #1991.
_.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { function iterator(obj, iteratee, memo, keys, index, length) {
if (obj == null) obj = []; for (; index >= 0 && index < length; index += dir) {
iteratee = createCallback(iteratee, context, 4); var currentKey = keys ? keys[index] : index;
var keys = obj.length !== +obj.length && _.keys(obj),
length = (keys || obj).length,
index = 0, currentKey;
if (arguments.length < 3) {
if (!length) throw new TypeError(reduceError);
memo = obj[keys ? keys[index++] : index++];
}
for (; index < length; index++) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj); memo = iteratee(memo, obj[currentKey], currentKey, obj);
} }
return memo; return memo;
}; }
// The right-associative version of reduce, also known as `foldr`. return function(obj, iteratee, memo, context) {
_.reduceRight = _.foldr = function(obj, iteratee, memo, context) { iteratee = optimizeCb(iteratee, context, 4);
if (obj == null) obj = []; var keys = !isArrayLike(obj) && _.keys(obj),
iteratee = createCallback(iteratee, context, 4); length = (keys || obj).length,
var keys = obj.length !== + obj.length && _.keys(obj), index = dir > 0 ? 0 : length - 1;
index = (keys || obj).length, // Determine the initial value if none is provided.
currentKey;
if (arguments.length < 3) { if (arguments.length < 3) {
if (!index) throw new TypeError(reduceError); memo = obj[keys ? keys[index] : index];
memo = obj[keys ? keys[--index] : --index]; index += dir;
}
while (index--) {
currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
} }
return memo; return iterator(obj, iteratee, memo, keys, index, length);
}; };
}
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`.
_.reduce = _.foldl = _.inject = createReduce(1);
// The right-associative version of reduce, also known as `foldr`.
_.reduceRight = _.foldr = createReduce(-1);
// Return the first value which passes a truth test. Aliased as `detect`. // Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, predicate, context) { _.find = _.detect = function(obj, predicate, context) {
var result; var key;
predicate = _.iteratee(predicate, context); if (isArrayLike(obj)) {
_.some(obj, function(value, index, list) { key = _.findIndex(obj, predicate, context);
if (predicate(value, index, list)) { } else {
result = value; key = _.findKey(obj, predicate, context);
return true;
} }
}); if (key !== void 0 && key !== -1) return obj[key];
return result;
}; };
// Return all the elements that pass a truth test. // Return all the elements that pass a truth test.
// Aliased as `select`. // Aliased as `select`.
_.filter = _.select = function(obj, predicate, context) { _.filter = _.select = function(obj, predicate, context) {
var results = []; var results = [];
if (obj == null) return results; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value); if (predicate(value, index, list)) results.push(value);
}); });
...@@ -192,19 +231,17 @@ ...@@ -192,19 +231,17 @@
// Return all the elements for which a truth test fails. // Return all the elements for which a truth test fails.
_.reject = function(obj, predicate, context) { _.reject = function(obj, predicate, context) {
return _.filter(obj, _.negate(_.iteratee(predicate)), context); return _.filter(obj, _.negate(cb(predicate)), context);
}; };
// Determine whether all of the elements match a truth test. // Determine whether all of the elements match a truth test.
// Aliased as `all`. // Aliased as `all`.
_.every = _.all = function(obj, predicate, context) { _.every = _.all = function(obj, predicate, context) {
if (obj == null) return true; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length;
length = (keys || obj).length, for (var index = 0; index < length; index++) {
index, currentKey; var currentKey = keys ? keys[index] : index;
for (index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
if (!predicate(obj[currentKey], currentKey, obj)) return false; if (!predicate(obj[currentKey], currentKey, obj)) return false;
} }
return true; return true;
...@@ -213,24 +250,22 @@ ...@@ -213,24 +250,22 @@
// Determine if at least one element in the object matches a truth test. // Determine if at least one element in the object matches a truth test.
// Aliased as `any`. // Aliased as `any`.
_.some = _.any = function(obj, predicate, context) { _.some = _.any = function(obj, predicate, context) {
if (obj == null) return false; predicate = cb(predicate, context);
predicate = _.iteratee(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj),
var keys = obj.length !== +obj.length && _.keys(obj), length = (keys || obj).length;
length = (keys || obj).length, for (var index = 0; index < length; index++) {
index, currentKey; var currentKey = keys ? keys[index] : index;
for (index = 0; index < length; index++) {
currentKey = keys ? keys[index] : index;
if (predicate(obj[currentKey], currentKey, obj)) return true; if (predicate(obj[currentKey], currentKey, obj)) return true;
} }
return false; return false;
}; };
// Determine if the array or object contains a given value (using `===`). // Determine if the array or object contains a given item (using `===`).
// Aliased as `include`. // Aliased as `includes` and `include`.
_.contains = _.include = function(obj, target) { _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
if (obj == null) return false; if (!isArrayLike(obj)) obj = _.values(obj);
if (obj.length !== +obj.length) obj = _.values(obj); if (typeof fromIndex != 'number' || guard) fromIndex = 0;
return _.indexOf(obj, target) >= 0; return _.indexOf(obj, item, fromIndex) >= 0;
}; };
// Invoke a method (with arguments) on every item in a collection. // Invoke a method (with arguments) on every item in a collection.
...@@ -238,7 +273,8 @@ ...@@ -238,7 +273,8 @@
var args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
var isFunc = _.isFunction(method); var isFunc = _.isFunction(method);
return _.map(obj, function(value) { return _.map(obj, function(value) {
return (isFunc ? method : value[method]).apply(value, args); var func = isFunc ? method : value[method];
return func == null ? func : func.apply(value, args);
}); });
}; };
...@@ -250,13 +286,13 @@ ...@@ -250,13 +286,13 @@
// Convenience version of a common use case of `filter`: selecting only objects // Convenience version of a common use case of `filter`: selecting only objects
// containing specific `key:value` pairs. // containing specific `key:value` pairs.
_.where = function(obj, attrs) { _.where = function(obj, attrs) {
return _.filter(obj, _.matches(attrs)); return _.filter(obj, _.matcher(attrs));
}; };
// Convenience version of a common use case of `find`: getting the first object // Convenience version of a common use case of `find`: getting the first object
// containing specific `key:value` pairs. // containing specific `key:value` pairs.
_.findWhere = function(obj, attrs) { _.findWhere = function(obj, attrs) {
return _.find(obj, _.matches(attrs)); return _.find(obj, _.matcher(attrs));
}; };
// Return the maximum element (or element-based computation). // Return the maximum element (or element-based computation).
...@@ -264,7 +300,7 @@ ...@@ -264,7 +300,7 @@
var result = -Infinity, lastComputed = -Infinity, var result = -Infinity, lastComputed = -Infinity,
value, computed; value, computed;
if (iteratee == null && obj != null) { if (iteratee == null && obj != null) {
obj = obj.length === +obj.length ? obj : _.values(obj); obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) { for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i]; value = obj[i];
if (value > result) { if (value > result) {
...@@ -272,7 +308,7 @@ ...@@ -272,7 +308,7 @@
} }
} }
} else { } else {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
computed = iteratee(value, index, list); computed = iteratee(value, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) { if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
...@@ -289,7 +325,7 @@ ...@@ -289,7 +325,7 @@
var result = Infinity, lastComputed = Infinity, var result = Infinity, lastComputed = Infinity,
value, computed; value, computed;
if (iteratee == null && obj != null) { if (iteratee == null && obj != null) {
obj = obj.length === +obj.length ? obj : _.values(obj); obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) { for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i]; value = obj[i];
if (value < result) { if (value < result) {
...@@ -297,7 +333,7 @@ ...@@ -297,7 +333,7 @@
} }
} }
} else { } else {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index, list) { _.each(obj, function(value, index, list) {
computed = iteratee(value, index, list); computed = iteratee(value, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) { if (computed < lastComputed || computed === Infinity && result === Infinity) {
...@@ -312,7 +348,7 @@ ...@@ -312,7 +348,7 @@
// Shuffle a collection, using the modern version of the // Shuffle a collection, using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
_.shuffle = function(obj) { _.shuffle = function(obj) {
var set = obj && obj.length === +obj.length ? obj : _.values(obj); var set = isArrayLike(obj) ? obj : _.values(obj);
var length = set.length; var length = set.length;
var shuffled = Array(length); var shuffled = Array(length);
for (var index = 0, rand; index < length; index++) { for (var index = 0, rand; index < length; index++) {
...@@ -328,7 +364,7 @@ ...@@ -328,7 +364,7 @@
// The internal `guard` argument allows it to work with `map`. // The internal `guard` argument allows it to work with `map`.
_.sample = function(obj, n, guard) { _.sample = function(obj, n, guard) {
if (n == null || guard) { if (n == null || guard) {
if (obj.length !== +obj.length) obj = _.values(obj); if (!isArrayLike(obj)) obj = _.values(obj);
return obj[_.random(obj.length - 1)]; return obj[_.random(obj.length - 1)];
} }
return _.shuffle(obj).slice(0, Math.max(0, n)); return _.shuffle(obj).slice(0, Math.max(0, n));
...@@ -336,7 +372,7 @@ ...@@ -336,7 +372,7 @@
// Sort the object's values by a criterion produced by an iteratee. // Sort the object's values by a criterion produced by an iteratee.
_.sortBy = function(obj, iteratee, context) { _.sortBy = function(obj, iteratee, context) {
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
return _.pluck(_.map(obj, function(value, index, list) { return _.pluck(_.map(obj, function(value, index, list) {
return { return {
value: value, value: value,
...@@ -358,7 +394,7 @@ ...@@ -358,7 +394,7 @@
var group = function(behavior) { var group = function(behavior) {
return function(obj, iteratee, context) { return function(obj, iteratee, context) {
var result = {}; var result = {};
iteratee = _.iteratee(iteratee, context); iteratee = cb(iteratee, context);
_.each(obj, function(value, index) { _.each(obj, function(value, index) {
var key = iteratee(value, index, obj); var key = iteratee(value, index, obj);
behavior(result, value, key); behavior(result, value, key);
...@@ -386,37 +422,24 @@ ...@@ -386,37 +422,24 @@
if (_.has(result, key)) result[key]++; else result[key] = 1; if (_.has(result, key)) result[key]++; else result[key] = 1;
}); });
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iteratee, context) {
iteratee = _.iteratee(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = array.length;
while (low < high) {
var mid = low + high >>> 1;
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
// Safely create a real, live array from anything iterable. // Safely create a real, live array from anything iterable.
_.toArray = function(obj) { _.toArray = function(obj) {
if (!obj) return []; if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj); if (_.isArray(obj)) return slice.call(obj);
if (obj.length === +obj.length) return _.map(obj, _.identity); if (isArrayLike(obj)) return _.map(obj, _.identity);
return _.values(obj); return _.values(obj);
}; };
// Return the number of elements in an object. // Return the number of elements in an object.
_.size = function(obj) { _.size = function(obj) {
if (obj == null) return 0; if (obj == null) return 0;
return obj.length === +obj.length ? obj.length : _.keys(obj).length; return isArrayLike(obj) ? obj.length : _.keys(obj).length;
}; };
// Split a collection into two arrays: one whose elements all satisfy the given // Split a collection into two arrays: one whose elements all satisfy the given
// predicate, and one whose elements all do not satisfy the predicate. // predicate, and one whose elements all do not satisfy the predicate.
_.partition = function(obj, predicate, context) { _.partition = function(obj, predicate, context) {
predicate = _.iteratee(predicate, context); predicate = cb(predicate, context);
var pass = [], fail = []; var pass = [], fail = [];
_.each(obj, function(value, key, obj) { _.each(obj, function(value, key, obj) {
(predicate(value, key, obj) ? pass : fail).push(value); (predicate(value, key, obj) ? pass : fail).push(value);
...@@ -433,30 +456,27 @@ ...@@ -433,30 +456,27 @@
_.first = _.head = _.take = function(array, n, guard) { _.first = _.head = _.take = function(array, n, guard) {
if (array == null) return void 0; if (array == null) return void 0;
if (n == null || guard) return array[0]; if (n == null || guard) return array[0];
if (n < 0) return []; return _.initial(array, array.length - n);
return slice.call(array, 0, n);
}; };
// Returns everything but the last entry of the array. Especially useful on // Returns everything but the last entry of the array. Especially useful on
// the arguments object. Passing **n** will return all the values in // the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with // the array, excluding the last N.
// `_.map`.
_.initial = function(array, n, guard) { _.initial = function(array, n, guard) {
return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
}; };
// Get the last element of an array. Passing **n** will return the last N // Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`. // values in the array.
_.last = function(array, n, guard) { _.last = function(array, n, guard) {
if (array == null) return void 0; if (array == null) return void 0;
if (n == null || guard) return array[array.length - 1]; if (n == null || guard) return array[array.length - 1];
return slice.call(array, Math.max(array.length - n, 0)); return _.rest(array, Math.max(0, array.length - n));
}; };
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
// Especially useful on the arguments object. Passing an **n** will return // Especially useful on the arguments object. Passing an **n** will return
// the rest N values in the array. The **guard** // the rest N values in the array.
// check allows it to work with `_.map`.
_.rest = _.tail = _.drop = function(array, n, guard) { _.rest = _.tail = _.drop = function(array, n, guard) {
return slice.call(array, n == null || guard ? 1 : n); return slice.call(array, n == null || guard ? 1 : n);
}; };
...@@ -467,18 +487,20 @@ ...@@ -467,18 +487,20 @@
}; };
// Internal implementation of a recursive `flatten` function. // Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, strict, output) { var flatten = function(input, shallow, strict, startIndex) {
if (shallow && _.every(input, _.isArray)) { var output = [], idx = 0;
return concat.apply(output, input); for (var i = startIndex || 0, length = getLength(input); i < length; i++) {
}
for (var i = 0, length = input.length; i < length; i++) {
var value = input[i]; var value = input[i];
if (!_.isArray(value) && !_.isArguments(value)) { if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
if (!strict) output.push(value); //flatten current level of array or arguments object
} else if (shallow) { if (!shallow) value = flatten(value, shallow, strict);
push.apply(output, value); var j = 0, len = value.length;
} else { output.length += len;
flatten(value, shallow, strict, output); while (j < len) {
output[idx++] = value[j++];
}
} else if (!strict) {
output[idx++] = value;
} }
} }
return output; return output;
...@@ -486,7 +508,7 @@ ...@@ -486,7 +508,7 @@
// Flatten out an array, either recursively (by default), or just one level. // Flatten out an array, either recursively (by default), or just one level.
_.flatten = function(array, shallow) { _.flatten = function(array, shallow) {
return flatten(array, shallow, false, []); return flatten(array, shallow, false);
}; };
// Return a version of the array that does not contain the specified value(s). // Return a version of the array that does not contain the specified value(s).
...@@ -498,27 +520,26 @@ ...@@ -498,27 +520,26 @@
// been sorted, you have the option of using a faster algorithm. // been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`. // Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iteratee, context) { _.uniq = _.unique = function(array, isSorted, iteratee, context) {
if (array == null) return [];
if (!_.isBoolean(isSorted)) { if (!_.isBoolean(isSorted)) {
context = iteratee; context = iteratee;
iteratee = isSorted; iteratee = isSorted;
isSorted = false; isSorted = false;
} }
if (iteratee != null) iteratee = _.iteratee(iteratee, context); if (iteratee != null) iteratee = cb(iteratee, context);
var result = []; var result = [];
var seen = []; var seen = [];
for (var i = 0, length = array.length; i < length; i++) { for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i]; var value = array[i],
computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted) { if (isSorted) {
if (!i || seen !== value) result.push(value); if (!i || seen !== computed) result.push(value);
seen = value; seen = computed;
} else if (iteratee) { } else if (iteratee) {
var computed = iteratee(value, i, array); if (!_.contains(seen, computed)) {
if (_.indexOf(seen, computed) < 0) {
seen.push(computed); seen.push(computed);
result.push(value); result.push(value);
} }
} else if (_.indexOf(result, value) < 0) { } else if (!_.contains(result, value)) {
result.push(value); result.push(value);
} }
} }
...@@ -528,16 +549,15 @@ ...@@ -528,16 +549,15 @@
// Produce an array that contains the union: each distinct element from all of // Produce an array that contains the union: each distinct element from all of
// the passed-in arrays. // the passed-in arrays.
_.union = function() { _.union = function() {
return _.uniq(flatten(arguments, true, true, [])); return _.uniq(flatten(arguments, true, true));
}; };
// Produce an array that contains every item shared between all the // Produce an array that contains every item shared between all the
// passed-in arrays. // passed-in arrays.
_.intersection = function(array) { _.intersection = function(array) {
if (array == null) return [];
var result = []; var result = [];
var argsLength = arguments.length; var argsLength = arguments.length;
for (var i = 0, length = array.length; i < length; i++) { for (var i = 0, length = getLength(array); i < length; i++) {
var item = array[i]; var item = array[i];
if (_.contains(result, item)) continue; if (_.contains(result, item)) continue;
for (var j = 1; j < argsLength; j++) { for (var j = 1; j < argsLength; j++) {
...@@ -551,7 +571,7 @@ ...@@ -551,7 +571,7 @@
// Take the difference between one array and a number of other arrays. // Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain. // Only the elements present in just the first array will remain.
_.difference = function(array) { _.difference = function(array) {
var rest = flatten(slice.call(arguments, 1), true, true, []); var rest = flatten(arguments, true, true, 1);
return _.filter(array, function(value){ return _.filter(array, function(value){
return !_.contains(rest, value); return !_.contains(rest, value);
}); });
...@@ -559,23 +579,28 @@ ...@@ -559,23 +579,28 @@
// Zip together multiple lists into a single array -- elements that share // Zip together multiple lists into a single array -- elements that share
// an index go together. // an index go together.
_.zip = function(array) { _.zip = function() {
if (array == null) return []; return _.unzip(arguments);
var length = _.max(arguments, 'length').length; };
var results = Array(length);
for (var i = 0; i < length; i++) { // Complement of _.zip. Unzip accepts an array of arrays and groups
results[i] = _.pluck(arguments, i); // each array's elements on shared indices
_.unzip = function(array) {
var length = array && _.max(array, getLength).length || 0;
var result = Array(length);
for (var index = 0; index < length; index++) {
result[index] = _.pluck(array, index);
} }
return results; return result;
}; };
// Converts lists into objects. Pass either a single array of `[key, value]` // Converts lists into objects. Pass either a single array of `[key, value]`
// pairs, or two parallel arrays of the same length -- one of keys, and one of // pairs, or two parallel arrays of the same length -- one of keys, and one of
// the corresponding values. // the corresponding values.
_.object = function(list, values) { _.object = function(list, values) {
if (list == null) return {};
var result = {}; var result = {};
for (var i = 0, length = list.length; i < length; i++) { for (var i = 0, length = getLength(list); i < length; i++) {
if (values) { if (values) {
result[list[i]] = values[i]; result[list[i]] = values[i];
} else { } else {
...@@ -585,40 +610,73 @@ ...@@ -585,40 +610,73 @@
return result; return result;
}; };
// Return the position of the first occurrence of an item in an array, // Generator function to create the findIndex and findLastIndex functions
// or -1 if the item is not included in the array. function createPredicateIndexFinder(dir) {
// If the array is large and already in sort order, pass `true` return function(array, predicate, context) {
// for **isSorted** to use binary search. predicate = cb(predicate, context);
_.indexOf = function(array, item, isSorted) { var length = getLength(array);
if (array == null) return -1; var index = dir > 0 ? 0 : length - 1;
var i = 0, length = array.length; for (; index >= 0 && index < length; index += dir) {
if (isSorted) { if (predicate(array[index], index, array)) return index;
if (typeof isSorted == 'number') {
i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted;
} else {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
} }
for (; i < length; i++) if (array[i] === item) return i;
return -1; return -1;
}; };
}
// Returns the first index on an array-like that passes a predicate test
_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iteratee, context) {
iteratee = cb(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
_.lastIndexOf = function(array, item, from) { // Generator function to create the indexOf and lastIndexOf functions
if (array == null) return -1; function createIndexFinder(dir, predicateFind, sortedIndex) {
var idx = array.length; return function(array, item, idx) {
if (typeof from == 'number') { var i = 0, length = getLength(array);
idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
} }
while (--idx >= 0) if (array[idx] === item) return idx;
return -1; return -1;
}; };
}
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
// Generate an integer Array containing an arithmetic progression. A port of // Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See // the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range). // [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) { _.range = function(start, stop, step) {
if (arguments.length <= 1) { if (stop == null) {
stop = start || 0; stop = start || 0;
start = 0; start = 0;
} }
...@@ -637,25 +695,25 @@ ...@@ -637,25 +695,25 @@
// Function (ahem) Functions // Function (ahem) Functions
// ------------------ // ------------------
// Reusable constructor function for prototype setting. // Determines whether to execute a function as a constructor
var Ctor = function(){}; // or a normal function with the provided arguments
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) return result;
return self;
};
// Create a function bound to a given object (assigning `this`, and arguments, // Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available. // available.
_.bind = function(func, context) { _.bind = function(func, context) {
var args, bound;
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
args = slice.call(arguments, 2); var args = slice.call(arguments, 2);
bound = function() { var bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
Ctor.prototype = func.prototype;
var self = new Ctor;
Ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (_.isObject(result)) return result;
return self;
}; };
return bound; return bound;
}; };
...@@ -665,15 +723,16 @@ ...@@ -665,15 +723,16 @@
// as a placeholder, allowing any combination of arguments to be pre-filled. // as a placeholder, allowing any combination of arguments to be pre-filled.
_.partial = function(func) { _.partial = function(func) {
var boundArgs = slice.call(arguments, 1); var boundArgs = slice.call(arguments, 1);
return function() { var bound = function() {
var position = 0; var position = 0, length = boundArgs.length;
var args = boundArgs.slice(); var args = Array(length);
for (var i = 0, length = args.length; i < length; i++) { for (var i = 0; i < length; i++) {
if (args[i] === _) args[i] = arguments[position++]; args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i];
} }
while (position < arguments.length) args.push(arguments[position++]); while (position < arguments.length) args.push(arguments[position++]);
return func.apply(this, args); return executeBound(func, bound, this, this, args);
}; };
return bound;
}; };
// Bind a number of an object's methods to that object. Remaining arguments // Bind a number of an object's methods to that object. Remaining arguments
...@@ -693,7 +752,7 @@ ...@@ -693,7 +752,7 @@
_.memoize = function(func, hasher) { _.memoize = function(func, hasher) {
var memoize = function(key) { var memoize = function(key) {
var cache = memoize.cache; var cache = memoize.cache;
var address = hasher ? hasher.apply(this, arguments) : key; var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
return cache[address]; return cache[address];
}; };
...@@ -712,9 +771,7 @@ ...@@ -712,9 +771,7 @@
// Defers a function, scheduling it to run after the current call stack has // Defers a function, scheduling it to run after the current call stack has
// cleared. // cleared.
_.defer = function(func) { _.defer = _.partial(_.delay, _, 1);
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once // Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run // during a given window of time. Normally, the throttled function will run
...@@ -739,8 +796,10 @@ ...@@ -739,8 +796,10 @@
context = this; context = this;
args = arguments; args = arguments;
if (remaining <= 0 || remaining > wait) { if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = null; timeout = null;
}
previous = now; previous = now;
result = func.apply(context, args); result = func.apply(context, args);
if (!timeout) context = args = null; if (!timeout) context = args = null;
...@@ -761,7 +820,7 @@ ...@@ -761,7 +820,7 @@
var later = function() { var later = function() {
var last = _.now() - timestamp; var last = _.now() - timestamp;
if (last < wait && last > 0) { if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last); timeout = setTimeout(later, wait - last);
} else { } else {
timeout = null; timeout = null;
...@@ -814,7 +873,7 @@ ...@@ -814,7 +873,7 @@
}; };
}; };
// Returns a function that will only be executed after being called N times. // Returns a function that will only be executed on and after the Nth call.
_.after = function(times, func) { _.after = function(times, func) {
return function() { return function() {
if (--times < 1) { if (--times < 1) {
...@@ -823,15 +882,14 @@ ...@@ -823,15 +882,14 @@
}; };
}; };
// Returns a function that will only be executed before being called N times. // Returns a function that will only be executed up to (but not including) the Nth call.
_.before = function(times, func) { _.before = function(times, func) {
var memo; var memo;
return function() { return function() {
if (--times > 0) { if (--times > 0) {
memo = func.apply(this, arguments); memo = func.apply(this, arguments);
} else {
func = null;
} }
if (times <= 1) func = null;
return memo; return memo;
}; };
}; };
...@@ -843,13 +901,47 @@ ...@@ -843,13 +901,47 @@
// Object Functions // Object Functions
// ---------------- // ----------------
// Retrieve the names of an object's properties. // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
function collectNonEnumProps(obj, keys) {
var nonEnumIdx = nonEnumerableProps.length;
var constructor = obj.constructor;
var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto;
// Constructor is a special case.
var prop = 'constructor';
if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
while (nonEnumIdx--) {
prop = nonEnumerableProps[nonEnumIdx];
if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
keys.push(prop);
}
}
}
// Retrieve the names of an object's own properties.
// Delegates to **ECMAScript 5**'s native `Object.keys` // Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = function(obj) { _.keys = function(obj) {
if (!_.isObject(obj)) return []; if (!_.isObject(obj)) return [];
if (nativeKeys) return nativeKeys(obj); if (nativeKeys) return nativeKeys(obj);
var keys = []; var keys = [];
for (var key in obj) if (_.has(obj, key)) keys.push(key); for (var key in obj) if (_.has(obj, key)) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};
// Retrieve all the property names of an object.
_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys; return keys;
}; };
...@@ -864,6 +956,21 @@ ...@@ -864,6 +956,21 @@
return values; return values;
}; };
// Returns the results of applying the iteratee to each element of the object
// In contrast to _.map it returns an object
_.mapObject = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = _.keys(obj),
length = keys.length,
results = {},
currentKey;
for (var index = 0; index < length; index++) {
currentKey = keys[index];
results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
// Convert an object into a list of `[key, value]` pairs. // Convert an object into a list of `[key, value]` pairs.
_.pairs = function(obj) { _.pairs = function(obj) {
var keys = _.keys(obj); var keys = _.keys(obj);
...@@ -896,37 +1003,38 @@ ...@@ -896,37 +1003,38 @@
}; };
// Extend a given object with all the properties in passed-in object(s). // Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) { _.extend = createAssigner(_.allKeys);
if (!_.isObject(obj)) return obj;
var source, prop; // Assigns a given object with all the own properties in the passed-in object(s)
for (var i = 1, length = arguments.length; i < length; i++) { // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
source = arguments[i]; _.extendOwn = _.assign = createAssigner(_.keys);
for (prop in source) {
if (hasOwnProperty.call(source, prop)) { // Returns the first key on an object that passes a predicate test
obj[prop] = source[prop]; _.findKey = function(obj, predicate, context) {
} predicate = cb(predicate, context);
} var keys = _.keys(obj), key;
for (var i = 0, length = keys.length; i < length; i++) {
key = keys[i];
if (predicate(obj[key], key, obj)) return key;
} }
return obj;
}; };
// Return a copy of the object only containing the whitelisted properties. // Return a copy of the object only containing the whitelisted properties.
_.pick = function(obj, iteratee, context) { _.pick = function(object, oiteratee, context) {
var result = {}, key; var result = {}, obj = object, iteratee, keys;
if (obj == null) return result; if (obj == null) return result;
if (_.isFunction(iteratee)) { if (_.isFunction(oiteratee)) {
iteratee = createCallback(iteratee, context); keys = _.allKeys(obj);
for (key in obj) { iteratee = optimizeCb(oiteratee, context);
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
}
} else { } else {
var keys = concat.apply([], slice.call(arguments, 1)); keys = flatten(arguments, false, false, 1);
obj = new Object(obj); iteratee = function(value, key, obj) { return key in obj; };
for (var i = 0, length = keys.length; i < length; i++) { obj = Object(obj);
key = keys[i];
if (key in obj) result[key] = obj[key];
} }
for (var i = 0, length = keys.length; i < length; i++) {
var key = keys[i];
var value = obj[key];
if (iteratee(value, key, obj)) result[key] = value;
} }
return result; return result;
}; };
...@@ -936,7 +1044,7 @@ ...@@ -936,7 +1044,7 @@
if (_.isFunction(iteratee)) { if (_.isFunction(iteratee)) {
iteratee = _.negate(iteratee); iteratee = _.negate(iteratee);
} else { } else {
var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); var keys = _.map(flatten(arguments, false, false, 1), String);
iteratee = function(value, key) { iteratee = function(value, key) {
return !_.contains(keys, key); return !_.contains(keys, key);
}; };
...@@ -945,15 +1053,15 @@ ...@@ -945,15 +1053,15 @@
}; };
// Fill in a given object with default properties. // Fill in a given object with default properties.
_.defaults = function(obj) { _.defaults = createAssigner(_.allKeys, true);
if (!_.isObject(obj)) return obj;
for (var i = 1, length = arguments.length; i < length; i++) { // Creates an object that inherits from the given prototype object.
var source = arguments[i]; // If additional properties are provided then they will be added to the
for (var prop in source) { // created object.
if (obj[prop] === void 0) obj[prop] = source[prop]; _.create = function(prototype, props) {
} var result = baseCreate(prototype);
} if (props) _.extendOwn(result, props);
return obj; return result;
}; };
// Create a (shallow-cloned) duplicate of an object. // Create a (shallow-cloned) duplicate of an object.
...@@ -970,6 +1078,19 @@ ...@@ -970,6 +1078,19 @@
return obj; return obj;
}; };
// Returns whether an object has a given set of `key:value` pairs.
_.isMatch = function(object, attrs) {
var keys = _.keys(attrs), length = keys.length;
if (object == null) return !length;
var obj = Object(object);
for (var i = 0; i < length; i++) {
var key = keys[i];
if (attrs[key] !== obj[key] || !(key in obj)) return false;
}
return true;
};
// Internal recursive comparison function for `isEqual`. // Internal recursive comparison function for `isEqual`.
var eq = function(a, b, aStack, bStack) { var eq = function(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical. // Identical objects are equal. `0 === -0`, but they aren't identical.
...@@ -1004,74 +1125,76 @@ ...@@ -1004,74 +1125,76 @@
// of `NaN` are not equivalent. // of `NaN` are not equivalent.
return +a === +b; return +a === +b;
} }
var areArrays = className === '[object Array]';
if (!areArrays) {
if (typeof a != 'object' || typeof b != 'object') return false; if (typeof a != 'object' || typeof b != 'object') return false;
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
_.isFunction(bCtor) && bCtor instanceof bCtor)
&& ('constructor' in a && 'constructor' in b)) {
return false;
}
}
// Assume equality for cyclic structures. The algorithm for detecting cyclic // Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length; var length = aStack.length;
while (length--) { while (length--) {
// Linear search. Performance is inversely proportional to the number of // Linear search. Performance is inversely proportional to the number of
// unique nested structures. // unique nested structures.
if (aStack[length] === a) return bStack[length] === b; if (aStack[length] === a) return bStack[length] === b;
} }
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (
aCtor !== bCtor &&
// Handle Object.create(x) cases
'constructor' in a && 'constructor' in b &&
!(_.isFunction(aCtor) && aCtor instanceof aCtor &&
_.isFunction(bCtor) && bCtor instanceof bCtor)
) {
return false;
}
// Add the first object to the stack of traversed objects. // Add the first object to the stack of traversed objects.
aStack.push(a); aStack.push(a);
bStack.push(b); bStack.push(b);
var size, result;
// Recursively compare objects and arrays. // Recursively compare objects and arrays.
if (className === '[object Array]') { if (areArrays) {
// Compare array lengths to determine if a deep comparison is necessary. // Compare array lengths to determine if a deep comparison is necessary.
size = a.length; length = a.length;
result = size === b.length; if (length !== b.length) return false;
if (result) {
// Deep compare the contents, ignoring non-numeric properties. // Deep compare the contents, ignoring non-numeric properties.
while (size--) { while (length--) {
if (!(result = eq(a[size], b[size], aStack, bStack))) break; if (!eq(a[length], b[length], aStack, bStack)) return false;
}
} }
} else { } else {
// Deep compare objects. // Deep compare objects.
var keys = _.keys(a), key; var keys = _.keys(a), key;
size = keys.length; length = keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality. // Ensure that both objects contain the same number of properties before comparing deep equality.
result = _.keys(b).length === size; if (_.keys(b).length !== length) return false;
if (result) { while (length--) {
while (size--) {
// Deep compare each member // Deep compare each member
key = keys[size]; key = keys[length];
if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
}
} }
} }
// Remove the first object from the stack of traversed objects. // Remove the first object from the stack of traversed objects.
aStack.pop(); aStack.pop();
bStack.pop(); bStack.pop();
return result; return true;
}; };
// Perform a deep comparison to check if two objects are equal. // Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) { _.isEqual = function(a, b) {
return eq(a, b, [], []); return eq(a, b);
}; };
// Is a given array, string, or object empty? // Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties. // An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) { _.isEmpty = function(obj) {
if (obj == null) return true; if (obj == null) return true;
if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false; return _.keys(obj).length === 0;
return true;
}; };
// Is a given value a DOM element? // Is a given value a DOM element?
...@@ -1091,14 +1214,14 @@ ...@@ -1091,14 +1214,14 @@
return type === 'function' || type === 'object' && !!obj; return type === 'function' || type === 'object' && !!obj;
}; };
// Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
_.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
_['is' + name] = function(obj) { _['is' + name] = function(obj) {
return toString.call(obj) === '[object ' + name + ']'; return toString.call(obj) === '[object ' + name + ']';
}; };
}); });
// Define a fallback version of the method in browsers (ahem, IE), where // Define a fallback version of the method in browsers (ahem, IE < 9), where
// there isn't any inspectable "Arguments" type. // there isn't any inspectable "Arguments" type.
if (!_.isArguments(arguments)) { if (!_.isArguments(arguments)) {
_.isArguments = function(obj) { _.isArguments = function(obj) {
...@@ -1106,8 +1229,9 @@ ...@@ -1106,8 +1229,9 @@
}; };
} }
// Optimize `isFunction` if appropriate. Work around an IE 11 bug. // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
if (typeof /./ !== 'function') { // IE 11 (#1621), and in Safari 8 (#1929).
if (typeof /./ != 'function' && typeof Int8Array != 'object') {
_.isFunction = function(obj) { _.isFunction = function(obj) {
return typeof obj == 'function' || false; return typeof obj == 'function' || false;
}; };
...@@ -1159,6 +1283,7 @@ ...@@ -1159,6 +1283,7 @@
return value; return value;
}; };
// Predicate-generating functions. Often useful outside of Underscore.
_.constant = function(value) { _.constant = function(value) {
return function() { return function() {
return value; return value;
...@@ -1167,30 +1292,28 @@ ...@@ -1167,30 +1292,28 @@
_.noop = function(){}; _.noop = function(){};
_.property = function(key) { _.property = property;
return function(obj) {
// Generates a function for a given object that returns a given property.
_.propertyOf = function(obj) {
return obj == null ? function(){} : function(key) {
return obj[key]; return obj[key];
}; };
}; };
// Returns a predicate for checking whether an object has a given set of `key:value` pairs. // Returns a predicate for checking whether an object has a given set of
_.matches = function(attrs) { // `key:value` pairs.
var pairs = _.pairs(attrs), length = pairs.length; _.matcher = _.matches = function(attrs) {
attrs = _.extendOwn({}, attrs);
return function(obj) { return function(obj) {
if (obj == null) return !length; return _.isMatch(obj, attrs);
obj = new Object(obj);
for (var i = 0; i < length; i++) {
var pair = pairs[i], key = pair[0];
if (pair[1] !== obj[key] || !(key in obj)) return false;
}
return true;
}; };
}; };
// Run a function **n** times. // Run a function **n** times.
_.times = function(n, iteratee, context) { _.times = function(n, iteratee, context) {
var accum = Array(Math.max(0, n)); var accum = Array(Math.max(0, n));
iteratee = createCallback(iteratee, context, 1); iteratee = optimizeCb(iteratee, context, 1);
for (var i = 0; i < n; i++) accum[i] = iteratee(i); for (var i = 0; i < n; i++) accum[i] = iteratee(i);
return accum; return accum;
}; };
...@@ -1239,10 +1362,12 @@ ...@@ -1239,10 +1362,12 @@
// If the value of the named `property` is a function then invoke it with the // If the value of the named `property` is a function then invoke it with the
// `object` as context; otherwise, return it. // `object` as context; otherwise, return it.
_.result = function(object, property) { _.result = function(object, property, fallback) {
if (object == null) return void 0; var value = object == null ? void 0 : object[property];
var value = object[property]; if (value === void 0) {
return _.isFunction(value) ? object[property]() : value; value = fallback;
}
return _.isFunction(value) ? value.call(object) : value;
}; };
// Generate a unique integer id (unique within the entire client session). // Generate a unique integer id (unique within the entire client session).
...@@ -1357,8 +1482,8 @@ ...@@ -1357,8 +1482,8 @@
// underscore functions. Wrapped objects may be chained. // underscore functions. Wrapped objects may be chained.
// Helper function to continue chaining intermediate results. // Helper function to continue chaining intermediate results.
var result = function(obj) { var result = function(instance, obj) {
return this._chain ? _(obj).chain() : obj; return instance._chain ? _(obj).chain() : obj;
}; };
// Add your own custom functions to the Underscore object. // Add your own custom functions to the Underscore object.
...@@ -1368,7 +1493,7 @@ ...@@ -1368,7 +1493,7 @@
_.prototype[name] = function() { _.prototype[name] = function() {
var args = [this._wrapped]; var args = [this._wrapped];
push.apply(args, arguments); push.apply(args, arguments);
return result.call(this, func.apply(_, args)); return result(this, func.apply(_, args));
}; };
}); });
}; };
...@@ -1383,7 +1508,7 @@ ...@@ -1383,7 +1508,7 @@
var obj = this._wrapped; var obj = this._wrapped;
method.apply(obj, arguments); method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
return result.call(this, obj); return result(this, obj);
}; };
}); });
...@@ -1391,7 +1516,7 @@ ...@@ -1391,7 +1516,7 @@
_.each(['concat', 'join', 'slice'], function(name) { _.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name]; var method = ArrayProto[name];
_.prototype[name] = function() { _.prototype[name] = function() {
return result.call(this, method.apply(this._wrapped, arguments)); return result(this, method.apply(this._wrapped, arguments));
}; };
}); });
...@@ -1400,6 +1525,14 @@ ...@@ -1400,6 +1525,14 @@
return this._wrapped; return this._wrapped;
}; };
// Provide unwrapping proxy for some methods used in engine operations
// such as arithmetic and JSON stringification.
_.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
_.prototype.toString = function() {
return '' + this._wrapped;
};
// AMD registration happens at the end for compatibility with AMD loaders // AMD registration happens at the end for compatibility with AMD loaders
// that may not enforce next-turn semantics on modules. Even though general // that may not enforce next-turn semantics on modules. Even though general
// practice for AMD registration is to be anonymous, underscore registers // practice for AMD registration is to be anonymous, underscore registers
......
{ {
"private": true, "private": true,
"dependencies": { "dependencies": {
"backbone.localStorage": "^1.1.3", "backbone": "^1.2.3",
"jquery": "^2.0.0", "backbone.localstorage": "^1.1.16",
"knockback": "^0.20.5", "jquery": "^2.1.4",
"todomvc-app-css": "^1.0.0", "knockback": "^1.0.0",
"todomvc-common": "^1.0.1" "knockout": "^3.3.0",
"todomvc-app-css": "^2.0.1",
"todomvc-common": "^1.0.2",
"underscore": "^1.8.3"
} }
} }
# Add custom handlers to Knockout.js - adapted from Knockout.js Todos app: https://github.com/ashish01/knockoutjs-todos
ko.bindingHandlers.dblclick =
init: (element, value_accessor) -> $(element).dblclick(ko.utils.unwrapObservable(value_accessor()))
ko.bindingHandlers.block =
update: (element, value_accessor) -> element.style.display = if ko.utils.unwrapObservable(value_accessor()) then 'block' else 'none'
ko.bindingHandlers.selectAndFocus =
init: (element, value_accessor, all_bindings_accessor) ->
ko.bindingHandlers.hasfocus.init(element, value_accessor, all_bindings_accessor)
ko.utils.registerEventHandler(element, 'focus', -> element.select())
update: (element, value_accessor) ->
ko.utils.unwrapObservable(value_accessor()) # create dependency
_.defer -> ko.bindingHandlers.hasfocus.update(element, value_accessor)
\ No newline at end of file
class window.Todo extends Backbone.Model
completed: (completed) ->
return !!@get('completed') if arguments.length is 0 # getter
@save({completed: if completed then new Date() else null}) # setter
\ No newline at end of file
class window.TodoCollection extends Backbone.Collection class window.TodoCollection extends Backbone.Collection
localStorage: new Store('todos-knockback') # Save all of the todos under the "todos-knockback" namespace. localStorage: new Store('todos-knockback') # Save all of the todos under the "todos-knockback" namespace.
model: Todo
completedCount: -> @models.reduce(((prev,cur)-> return prev + if cur.completed() then 1 else 0), 0)
remainingCount: -> @models.length - @completedCount()
completeAll: (completed) -> @each((todo) -> todo.completed(completed))
destroyCompleted: ->
completed_tasks = @filter((todo) -> return todo.completed())
model.destroy() for model in completed_tasks
return
\ No newline at end of file
ENTER_KEY = 13 ENTER_KEY = 13
window.AppViewModel = -> class window.AppViewModel
############################# constructor: ->
# Shared
#############################
# collections
@collections =
todos: new TodoCollection()
@collections.todos.fetch()
# shared observables
@list_filter_mode = ko.observable('') @list_filter_mode = ko.observable('')
filter_fn = ko.computed => filter_fn = ko.computed(=>
switch @list_filter_mode() switch @list_filter_mode()
when 'active' then return (model) -> return not model.completed() when 'active' then return ((model) -> return !model.get('completed'))
when 'completed' then return (model) -> return model.completed() when 'completed' then return ((model) -> return model.get('completed'))
else return -> return true else return (-> return true)
)
@todos = kb.collectionObservable(new TodoCollection(), TodoViewModel, {filters: filter_fn})
@todos.collection().fetch()
@todos = kb.collectionObservable(@collections.todos, {view_model: TodoViewModel, filters: filter_fn}) # Note: collectionObservables only track additions and removals so model attribute changes need to be manually triggered
@todos_changed = kb.triggeredObservable(@collections.todos, 'change add remove') @_todos_changed = kb.triggeredObservable(@todos.collection(), 'change add remove')
@tasks_exist = ko.computed(=> @todos_changed(); return !!@collections.todos.length) @tasks_exist = ko.computed(=> @_todos_changed(); return !!@todos.collection().models.length)
#############################
# Header Section
#############################
@title = ko.observable('') @title = ko.observable('')
@onAddTodo = (view_model, event) =>
return true if not $.trim(@title()) or (event.keyCode != ENTER_KEY)
# Create task and reset UI
@collections.todos.create({title: $.trim(@title())})
@title('')
############################# @completed_count = ko.computed(=> @_todos_changed(); return @todos.collection().filter((model) -> return model.get('completed')).length)
# Main Section @remaining_count = ko.computed(=> @_todos_changed(); return @todos.collection().length - @completed_count())
############################# @all_completed = ko.computed({
@remaining_count = ko.computed => @todos_changed(); return @collections.todos.remainingCount()
@completed_count = ko.computed => @todos_changed(); return @collections.todos.completedCount()
@all_completed = ko.computed
read: => return not @remaining_count() read: => return not @remaining_count()
write: (completed) => @collections.todos.completeAll(completed) write: (completed) => @todos.collection().each((model) -> model.save({completed}))
})
#############################
# Footer Section
#############################
@onDestroyCompleted = => @collections.todos.destroyCompleted()
#############################
# Localization
#############################
@loc = @loc =
remaining_message: ko.computed(=> return "<strong>#{@remaining_count()}</strong> #{if @remaining_count() == 1 then 'item' else 'items'} left") remaining_message: ko.computed(=> return "<strong>#{@remaining_count()}</strong> #{if @remaining_count() == 1 then 'item' else 'items'} left")
clear_message: ko.computed(=> return if (count = @completed_count()) then "Clear completed (#{count})" else '') clear_message: ko.computed(=> return if @completed_count() then 'Clear completed' else '')
#############################
# Routing
#############################
router = new Backbone.Router router = new Backbone.Router
router.route('', null, => @list_filter_mode('')) for route in ['', 'active', 'completed'] then do (route) =>
router.route('active', null, => @list_filter_mode('active')) router.route(route, null, => @list_filter_mode(route))
router.route('completed', null, => @list_filter_mode('completed'))
Backbone.history.start() Backbone.history.start()
return onAddTodo: (vm, event) =>
\ No newline at end of file return if not $.trim(@title()) or (event.keyCode isnt ENTER_KEY)
@todos.collection().create({title: $.trim(@title())})
@title('')
onClearCompleted: => model.destroy() for model in @todos.collection().filter((model) -> return model.get('completed'))
window.TodoViewModel = (model) -> ENTER_KEY = 13
ENTER_KEY = 13 ESCAPE_KEY = 27
ESCAPE_KEY = 27
class window.TodoViewModel extends kb.ViewModel
constructor: (model, options) ->
super(model, {requires: ['title', 'completed']}, options)
@completed.subscribe((completed) => @model().save({completed}))
@edit_title = ko.observable()
@editing = ko.observable(false) @editing = ko.observable(false)
@completed = kb.observable(model, {key: 'completed', read: (-> return model.completed()), write: ((completed) -> model.completed(completed)) }, @)
@title = kb.observable(model, { onDestroy: => @model().destroy()
key: 'title'
write: ((title) =>
if $.trim(title) then model.save(title: $.trim(title)) else _.defer(->model.destroy())
@editing(false)
)
}, @)
@onDestroyTodo = => model.destroy() onCheckEditBegin: =>
return if @editing()
@onCheckEditBegin = => @edit_title(@title())
if not @editing()
@editing(true) @editing(true)
$('.todo-input').focus() $('.todo-input').focus()
@onCheckEditEnd = (view_model, event) => onCheckEditEnd: (vm, event) =>
if (event.keyCode == ESCAPE_KEY) return unless @editing()
if (event.keyCode is ESCAPE_KEY)
@editing(false) @editing(false)
if (event.keyCode == ENTER_KEY) or (event.type == 'blur')
if (event.keyCode is ENTER_KEY) or (event.type is 'blur')
$('.todo-input').blur() $('.todo-input').blur()
title = @edit_title()
if $.trim(title) then @model().set({title}).save({title: $.trim(title)}) else _.defer(=>@model().destroy())
@editing(false) @editing(false)
return
\ No newline at end of file
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