UNPKG

todomvc

Version:

> Helping you select an MV\* framework

1,510 lines (1,330 loc) 239 kB
(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.