todomvc
Version:
> Helping you select an MV\* framework
1,510 lines (1,330 loc) • 239 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
var MainView = require('./views/main');
var Me = require('./models/me');
var Router = require('./router');
window.app = {
init: function () {
// Model representing state for
// user using the app. Calling it
// 'me' is a bit of convention but
// it's basically 'app state'.
this.me = new Me();
// Our main view
this.view = new MainView({
el: document.body,
model: this.me
});
// Create and fire up the router
this.router = new Router();
this.router.history.start();
}
};
window.app.init();
},{"./models/me":2,"./router":5,"./views/main":7}],2:[function(require,module,exports){
// typically we us a 'me' model to represent state for the
// user of the app. So in an app where you have a logged in
// user this is where we'd store username, etc.
// We also use it to store session properties, which is the
// non-persisted state that we use to track application
// state for this user in this session.
'use strict';
var State = require('ampersand-state');
var Todos = require('./todos');
module.exports = State.extend({
initialize: function () {
// Listen to changes to the todos collection that will
// affect lengths we want to calculate.
this.listenTo(this.todos, 'change:completed change:title add remove', this.handleTodosUpdate);
// We also want to calculate these values once on init
this.handleTodosUpdate();
// Listen for changes to `mode` so we can update
// the collection mode.
this.on('change:mode', this.handleModeChange, this);
},
collections: {
todos: Todos
},
// We used only session properties here because there's
// no API or persistance layer for these in this app.
session: {
activeCount: {
type: 'number',
default: 0
},
completedCount: {
type: 'number',
default: 0
},
totalCount:{
type: 'number',
default: 0
},
allCompleted: {
type: 'boolean',
default: false
},
mode: {
type: 'string',
values: [
'all',
'completed',
'active'
],
default: 'all'
}
},
derived: {
// We produce this as an HTML snippet here
// for convenience since it also has to be
// pluralized it was easier this way.
itemsLeftHtml: {
deps: ['activeCount'],
fn: function () {
var plural = (this.activeCount === 1) ? '' : 's';
return '<strong>' + this.activeCount + '</strong> item' + plural + ' left';
}
}
},
// Calculate and set various lengths we're
// tracking. We set them as session properties
// so they're easy to listen to and bind to DOM
// where needed.
handleTodosUpdate: function () {
var completed = 0;
var todos = this.todos;
todos.each(function (todo) {
if (todo.completed) {
completed++;
}
});
this.set({
completedCount: completed,
activeCount: todos.length - completed,
totalCount: todos.length,
allCompleted: todos.length === completed
});
},
handleModeChange: function () {
this.todos.setMode(this.mode);
}
});
},{"./todos":4,"ampersand-state":22}],3:[function(require,module,exports){
'use strict';
// We're using 'ampersand-state' here instead of 'ampersand-model'
// because we don't need any of the RESTful
// methods for this app.
var State = require('ampersand-state');
module.exports = State.extend({
// Properties this model will store
props: {
title: {
type: 'string',
default: ''
},
completed: {
type: 'boolean',
default: false
}
},
// session properties work the same way as `props`
// but will not be included when serializing.
session: {
editing: {
type: 'boolean',
default: false
}
},
destroy: function () {
if (this.collection) {
this.collection.remove(this);
}
}
});
},{"ampersand-state":22}],4:[function(require,module,exports){
'use strict';
var Collection = require('ampersand-collection');
var SubCollection = require('ampersand-subcollection');
var debounce = require('debounce');
var Todo = require('./todo');
var STORAGE_KEY = 'todos-ampersand';
module.exports = Collection.extend({
model: Todo,
initialize: function () {
// Attempt to read from localStorage
this.readFromLocalStorage();
// This is what we'll actually render
// it's a subcollection of the whole todo collection
// that we'll add/remove filters to accordingly.
this.subset = new SubCollection(this);
// We put a slight debounce on this since it could possibly
// be called in rapid succession.
this.writeToLocalStorage = debounce(this.writeToLocalStorage, 100);
// Listen for storage events on the window to keep multiple
// tabs in sync
window.addEventListener('storage', this.handleStorageEvent.bind(this));
// We listen for changes to the collection
// and persist on change
this.on('all', this.writeToLocalStorage, this);
},
// Helper for removing all completed items
clearCompleted: function () {
var toRemove = this.filter(function (todo) {
return todo.completed;
});
this.remove(toRemove);
},
// Updates the collection to the appropriate mode.
// mode can 'all', 'completed', or 'active'
setMode: function (mode) {
if (mode === 'all') {
this.subset.clearFilters();
} else {
this.subset.configure({
where: {
completed: mode === 'completed'
}
}, true);
}
},
// The following two methods are all we need in order
// to add persistance to localStorage
writeToLocalStorage: function () {
localStorage[STORAGE_KEY] = JSON.stringify(this);
},
readFromLocalStorage: function () {
var existingData = localStorage[STORAGE_KEY];
if (existingData) {
this.set(JSON.parse(existingData));
}
},
// Handles events from localStorage. Browsers will fire
// this event in other tabs on the same domain.
handleStorageEvent: function (e) {
if (e.key === STORAGE_KEY) {
this.readFromLocalStorage();
}
}
});
},{"./todo":3,"ampersand-collection":9,"ampersand-subcollection":28,"debounce":53}],5:[function(require,module,exports){
'use strict';
/*global app */
var Router = require('ampersand-router');
module.exports = Router.extend({
routes: {
'*filter': 'setFilter'
},
setFilter: function (arg) {
app.me.mode = arg || 'all';
}
});
},{"ampersand-router":16}],6:[function(require,module,exports){
var jade = require("jade/runtime");
module.exports = function template(locals) {
var buf = [];
var jade_mixins = {};
var jade_interp;
buf.push("<li><div class=\"view\"><input type=\"checkbox\" data-hook=\"checkbox\" class=\"toggle\"/><label data-hook=\"title\"></label><button data-hook=\"action-delete\" class=\"destroy\"></button></div><input data-hook=\"input\" class=\"edit\"/></li>");;return buf.join("");
};
},{"jade/runtime":55}],7:[function(require,module,exports){
'use strict';
/*global app */
var View = require('ampersand-view');
var TodoView = require('./todo');
var ENTER_KEY = 13;
module.exports = View.extend({
events: {
'keypress [data-hook~=todo-input]': 'handleMainInput',
'click [data-hook~=mark-all]': 'handleMarkAllClick',
'click [data-hook~=clear-completed]': 'handleClearClick'
},
// Declaratively bind all our data to the template.
// This means only changed data in the DOM is updated
// with this approach we *only* ever touch the DOM with
// appropriate dom methods. Not just `innerHTML` which
// makes it about as fast as possible.
// These get re-applied if the view's element is replaced
// or if the model isn't there yet, etc.
// Binding type reference:
// http://ampersandjs.com/docs#ampersand-dom-bindings-binding-types
bindings: {
// Show hide main and footer
// based on truthiness of totalCount
'model.totalCount': {
type: 'toggle',
selector: '#main, #footer'
},
'model.completedCount': [
// Hides when there are none
{
type: 'toggle',
hook: 'clear-completed'
},
// Inserts completed count
{
type: 'text',
hook: 'completed-count'
}
],
// Inserts HTML from model that also
// does pluralizing.
'model.itemsLeftHtml': {
type: 'innerHTML',
hook: 'todo-count'
},
// Add 'selected' to right
// element
'model.mode': {
type: 'switchClass',
name: 'selected',
cases: {
'all': '[data-hook=all-mode]',
'active': '[data-hook=active-mode]',
'completed': '[data-hook=completed-mode]',
}
},
// Bind 'checked' state of checkbox
'model.allCompleted': {
type: 'booleanAttribute',
name: 'checked',
hook: 'mark-all'
}
},
// cache
initialize: function () {
this.mainInput = this.queryByHook('todo-input');
this.renderCollection(app.me.todos.subset, TodoView, this.queryByHook('todo-container'));
},
// handles DOM event from main input
handleMainInput: function (e) {
var val = this.mainInput.value.trim();
if (e.which === ENTER_KEY && val) {
app.me.todos.add({title: val});
this.mainInput.value = '';
}
},
// Here we set all to state provided.
handleMarkAllClick: function () {
var targetState = !app.me.allCompleted;
app.me.todos.each(function (todo) {
todo.completed = targetState;
});
},
// Handler for clear click
handleClearClick: function () {
app.me.todos.clearCompleted();
}
});
},{"./todo":8,"ampersand-view":35}],8:[function(require,module,exports){
'use strict';
var View = require('ampersand-view');
var todoTemplate = require('../templates/todo.jade');
var ENTER_KEY = 13;
var ESC_KEY = 27;
module.exports = View.extend({
// note that Ampersand is extrememly flexible with templating.
// This template property can be:
// 1. A plain HTML string
// 2. A function that returns an HTML string
// 3. A function that returns a DOM element
//
// Here we're using a jade template. A browserify transform
// called 'jadeify' lets us require a ".jade" file as if
// it were a module and it will compile it to a function
// for us. This function returns HTML as per #2 above.
template: todoTemplate,
// Events work like backbone they're all delegated to
// root element.
events: {
'change [data-hook=checkbox]': 'handleCheckboxChange',
'click [data-hook=action-delete]': 'handleDeleteClick',
'dblclick [data-hook=title]': 'handleDoubleClick',
'keyup [data-hook=input]': 'handleKeypress',
'blur [data-hook=input]': 'handleBlur'
},
// Declarative data bindings
bindings: {
'model.title': [
{
type: 'text',
hook: 'title'
},
{
type: 'value',
hook: 'input'
}
],
'model.editing': [
{
type: 'toggle',
yes: '[data-hook=input]',
no: '[data-hook=view]'
},
{
type: 'booleanClass'
}
],
'model.completed': [
{
type: 'booleanAttribute',
name: 'checked',
hook: 'checkbox'
},
{
type: 'booleanClass'
}
]
},
render: function () {
// Render this with template provided.
// Note that unlike backbone this includes the root element.
this.renderWithTemplate();
// cache reference to `input` for speed/convenience
this.input = this.queryByHook('input');
},
handleCheckboxChange: function (e) {
this.model.completed = e.target.checked;
},
handleDeleteClick: function () {
this.model.destroy();
},
// Just put us in edit mode and focus
handleDoubleClick: function () {
this.model.editing = true;
this.input.focus();
},
handleKeypress: function (e) {
if (e.which === ENTER_KEY) {
this.input.blur();
} else if (e.which === ESC_KEY) {
this.input.value = this.model.title;
this.input.blur();
}
},
// Since we always blur even in the other
// scenarios we use this as a 'save' point.
handleBlur: function () {
var val = this.input.value.trim();
if (val) {
this.model.set({
title: val,
editing: false
});
} else {
this.model.destroy();
}
}
});
},{"../templates/todo.jade":6,"ampersand-view":35}],9:[function(require,module,exports){
var BackboneEvents = require('backbone-events-standalone');
var classExtend = require('ampersand-class-extend');
var isArray = require('is-array');
var extend = require('extend-object');
var slice = [].slice;
function Collection(models, options) {
options || (options = {});
if (options.model) this.model = options.model;
if (options.comparator) this.comparator = options.comparator;
if (options.parent) this.parent = options.parent;
if (!this.mainIndex) {
var idAttribute = this.model && this.model.prototype && this.model.prototype.idAttribute;
this.mainIndex = idAttribute || 'id';
}
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, extend({silent: true}, options));
}
extend(Collection.prototype, BackboneEvents, {
initialize: function () {},
indexes: [],
isModel: function (model) {
return this.model && model instanceof this.model;
},
add: function (models, options) {
return this.set(models, extend({merge: false, add: true, remove: false}, options));
},
// overridable parse method
parse: function (res, options) {
return res;
},
// overridable serialize method
serialize: function () {
return this.map(function (model) {
if (model.serialize) {
return model.serialize();
} else {
var out = {};
extend(out, model);
delete out.collection;
return out;
}
});
},
toJSON: function () {
return this.serialize();
},
set: function (models, options) {
options = extend({add: true, remove: true, merge: true}, options);
if (options.parse) models = this.parse(models, options);
var singular = !isArray(models);
models = singular ? (models ? [models] : []) : models.slice();
var id, model, attrs, existing, sort, i, length;
var at = options.at;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = ('string' === typeof 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;
var targetProto = this.model && this.model.prototype || Object.prototype;
// Turn bare objects into model references, and prevent invalid models
// from being added.
for (i = 0, length = models.length; i < length; i++) {
attrs = models[i] || {};
if (this.isModel(attrs)) {
id = model = attrs;
} else if (targetProto.generateId) {
id = targetProto.generateId(attrs);
} else {
id = attrs[targetProto.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 || existing[this.mainIndex]] = true;
if (merge) {
attrs = attrs === model ? model.attributes : attrs;
if (options.parse) attrs = existing.parse(attrs, options);
// if this is model
if (existing.set) {
existing.set(attrs, options);
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
} else {
// if not just update the properties
extend(existing, attrs);
}
}
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 (!model) continue;
if (order && ((model.isNew && model.isNew() || !model[this.mainIndex]) || !modelMap[model.cid || model[this.mainIndex]])) order.push(model);
modelMap[model[this.mainIndex]] = true;
}
// Remove nonexistent models if appropriate.
if (remove) {
for (i = 0, length = this.length; i < length; i++) {
model = this.models[i];
if (!modelMap[model.cid || model[this.mainIndex]]) 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;
if (at != null) {
for (i = 0, length = toAdd.length; i < length; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
var orderedModels = order || toAdd;
for (i = 0, length = orderedModels.length; i < length; 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, length = toAdd.length; i < length; i++) {
model = toAdd[i];
if (model.trigger) {
model.trigger('add', model, this, options);
} else {
this.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;
},
get: function (query, indexName) {
if (!query) return;
var index = this._indexes[indexName || this.mainIndex];
return index[query] || index[query[this.mainIndex]] || this._indexes.cid[query.cid];
},
// Get the model at the given index.
at: function (index) {
return this.models[index];
},
remove: function (models, options) {
var singular = !isArray(models);
var i, length, model, index;
models = singular ? [models] : slice.call(models);
options || (options = {});
for (i = 0, length = models.length; i < length; i++) {
model = models[i] = this.get(models[i]);
if (!model) continue;
this._deIndex(model);
index = this.models.indexOf(model);
this.models.splice(index, 1);
if (!options.silent) {
options.index = index;
if (model.trigger) {
model.trigger('remove', model, this, options);
} else {
this.trigger('remove', model, this, options);
}
}
this._removeReference(model, options);
}
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, length = this.models.length; i < length; 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;
},
sort: function (options) {
var self = this;
if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
if (typeof this.comparator === 'string') {
this.models.sort(function (left, right) {
if (left.get) {
left = left.get(self.comparator);
right = right.get(self.comparator);
} else {
left = left[self.comparator];
right = right[self.comparator];
}
if (left > right || left === void 0) return 1;
if (left < right || right === void 0) return -1;
return 0;
});
} else if (this.comparator.length === 1) {
this.models.sort(function (left, right) {
left = self.comparator(left);
right = self.comparator(right);
if (left > right || left === void 0) return 1;
if (left < right || right === void 0) return -1;
return 0;
});
} else {
this.models.sort(this.comparator.bind(this));
}
if (!options.silent) this.trigger('sort', this, options);
return this;
},
// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function () {
var list = this.indexes || [];
var i = 0;
list.push(this.mainIndex);
list.push('cid');
var l = list.length;
this.models = [];
this._indexes = {};
for (; i < l; i++) {
this._indexes[list[i]] = {};
}
},
_prepareModel: function (attrs, options) {
// if we haven't defined a constructor, skip this
if (!this.model) return attrs;
if (this.isModel(attrs)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
} else {
options = options ? extend({}, 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;
}
},
_deIndex: function (model) {
for (var name in this._indexes) {
delete this._indexes[name][model[name] || (model.get && model.get(name))];
}
},
_index: function (model) {
for (var name in this._indexes) {
var indexVal = model[name] || (model.get && model.get(name));
if (indexVal) this._indexes[name][indexVal] = model;
}
},
// Internal method to create a model's ties to a collection.
_addReference: function (model, options) {
this._index(model);
if (!model.collection) model.collection = this;
if (model.on) 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;
this._deIndex(model);
if (model.off) model.off('all', this._onModelEvent, this);
},
_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:' + this.mainIndex) {
this._deIndex(model);
this._index(model);
}
this.trigger.apply(this, arguments);
}
});
Object.defineProperties(Collection.prototype, {
length: {
get: function () {
return this.models.length;
}
},
isCollection: {
value: true
}
});
var arrayMethods = [
'indexOf',
'lastIndexOf',
'every',
'some',
'forEach',
'map',
'filter',
'reduce',
'reduceRight'
];
arrayMethods.forEach(function (method) {
Collection.prototype[method] = function () {
return this.models[method].apply(this.models, arguments);
};
});
// alias each/forEach for maximum compatibility
Collection.prototype.each = Collection.prototype.forEach;
Collection.extend = classExtend;
module.exports = Collection;
},{"ampersand-class-extend":10,"backbone-events-standalone":12,"extend-object":13,"is-array":14}],10:[function(require,module,exports){
var objectExtend = require('extend-object');
/// Following code is largely pasted from Backbone.js
// 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) {
var parent = this;
var child;
var args = [].slice.call(arguments);
// 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 && protoProps.hasOwnProperty('constructor')) {
child = protoProps.constructor;
} else {
child = function () {
return parent.apply(this, arguments);
};
}
// Add static properties to the constructor function from parent
objectExtend(child, parent);
// 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();
// Mix in all prototype properties to the subclass if supplied.
if (protoProps) {
args.unshift(child.prototype);
objectExtend.apply(null, args);
}
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
return child;
};
// Expose the extend function
module.exports = extend;
},{"extend-object":13}],11:[function(require,module,exports){
/**
* Standalone extraction of Backbone.Events, no external dependency required.
* Degrades nicely when Backone/underscore are already available in the current
* global context.
*
* Note that docs suggest to use underscore's `_.extend()` method to add Events
* support to some given object. A `mixin()` method has been added to the Events
* prototype to avoid using underscore for that sole purpose:
*
* var myEventEmitter = BackboneEvents.mixin({});
*
* Or for a function constructor:
*
* function MyConstructor(){}
* MyConstructor.prototype.foo = function(){}
* BackboneEvents.mixin(MyConstructor.prototype);
*
* (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
* (c) 2013 Nicolas Perriault
*/
/* global exports:true, define, module */
(function() {
var root = this,
breaker = {},
nativeForEach = Array.prototype.forEach,
hasOwnProperty = Object.prototype.hasOwnProperty,
slice = Array.prototype.slice,
idCounter = 0;
// Returns a partial implementation matching the minimal API subset required
// by Backbone.Events
function miniscore() {
return {
keys: Object.keys,
uniqueId: function(prefix) {
var id = ++idCounter + '';
return prefix ? prefix + id : id;
},
has: function(obj, key) {
return hasOwnProperty.call(obj, key);
},
each: function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (this.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
},
once: function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
memo = func.apply(this, arguments);
func = null;
return memo;
};
}
};
}
var _ = miniscore(), Events;
// 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');
//
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 = {};
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 listeners = this._listeners;
if (!listeners) return this;
var deleteListener = !name && !callback;
if (typeof name === 'object') callback = this;
if (obj) (listeners = {})[obj._listenerId] = obj;
for (var id in listeners) {
listeners[id].off(name, callback, this);
if (deleteListener) delete this._listeners[id];
}
return this;
}
};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
}
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
}
return true;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
}
};
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = obj;
if (typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
// Aliases for backwards compatibility.
Events.bind = Events.on;
Events.unbind = Events.off;
// Mixin utility
Events.mixin = function(proto) {
var exports = ['on', 'once', 'off', 'trigger', 'stopListening', 'listenTo',
'listenToOnce', 'bind', 'unbind'];
_.each(exports, function(name) {
proto[name] = this[name];
}, this);
return proto;
};
// Export Events as BackboneEvents depending on current context
if (typeof define === "function") {
define(function() {
return Events;
});
} else if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = Events;
}
exports.BackboneEvents = Events;
} else {
root.BackboneEvents = Events;
}
})(this);
},{}],12:[function(require,module,exports){
module.exports = require('./backbone-events-standalone');
},{"./backbone-events-standalone":11}],13:[function(require,module,exports){
var arr = [];
var each = arr.forEach;
var slice = arr.slice;
module.exports = function(obj) {
each.call(slice.call(arguments, 1), function(source) {
if (source) {
for (var prop in source) {
obj[prop] = source[prop];
}
}
});
return obj;
};
},{}],14:[function(require,module,exports){
/**
* isArray
*/
var isArray = Array.isArray;
/**
* toString
*/
var str = Object.prototype.toString;
/**
* Whether or not the given `val`
* is an array.
*
* example:
*
* isArray([]);
* // > true
* isArray(arguments);
* // > false
* isArray('');
* // > false
*
* @param {mixed} val
* @return {bool}
*/
module.exports = isArray || function (val) {
return !! val && '[object Array]' == str.call(val);
};
},{}],15:[function(require,module,exports){
var Events = require('backbone-events-standalone');
var _ = require('underscore');
// 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.
var History = function () {
this.handlers = [];
this.checkUrl = _.bind(this.checkUrl, this);
// 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 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 () {
var path = this.location.pathname.replace(/[^\/]$/, '$&/');
return path === this.root && !this.location.search;
},
// 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 pathname and search params, without the root.
getPath: function () {
var path = decodeURI(this.location.pathname + this.location.search);
var root = this.root.slice(0, -1);
if (!path.indexOf(root)) path = path.slice(root.length);
return path.slice(1);
},
// Get the cross-browser normalized URL fragment from the path or hash.
getFragment: function (fragment) {
if (fragment == null) {
if (this._hasPushState || !this._wantsHashChange) {
fragment = this.getPath();
} 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.
// 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._hasHashChange = 'onhashchange' in window;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
this.fragment = this.getFragment();
// Add a cross-platform `addEventListener` shim for older browsers.
var addEventListener = window.addEventListener;
// Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
if (this._hasPushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// 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.location.replace(this.root + '#' + this.getPath());
// 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()) {
this.navigate(this.getHash(), {replace: true});
}
}
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 () {
// Add a cross-platform `removeEventListener` shim for older browsers.
var removeEventListener = window.removeEventListener;
// Remove window listeners.
if (this._hasPushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange) {
removeEventListener('hashchange', this.checkUrl, false);
}
// Some environments will throw when clearing an undefined interval.
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`.
checkUrl: function (e) {
var current = this.getFragment();
if (current === this.fragment) return false;
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 this.handlers.some(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 and decode for matching.
fragment = decodeURI(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 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;
}
}
});
module.exports = new History();
},{"backbone-events-standalone":20,"underscore":21}],16:[function(require,module,exports){
var classExtend = require('ampersand-class-extend');
var Events = require('backbone-events-standalone');
var ampHistory = require('./ampersand-history');
var _ = require('underscore');
// 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 = module.exports = function (options) {
options || (options = {});
this.history = options.history || ampHistory;
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;
this.history.route(route, function (fragment) {
var args = router._extractParameters(route, fragment);
if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
router.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, name) {
if (callback) callback.apply(this, args);
},
// Simple proxy to `ampHistory` to save a fragment into the history.
navigate: function (fragment, options) {
this.history.navigate(fragment, options);
return this;
},
// Helper for doing `internal` redirects without adding to history
// and thereby breaking backbutton functionality.
redirectTo: function (newUrl) {
this.navigate(newUrl, {replace: true, trigger: true});
},
// Bind all defined routes to `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 = Object.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 params.map(function (param, i) {
// Don't decode the search params.