From 7678aa3e161eb6014d4fad65341b5544fd337746 Mon Sep 17 00:00:00 2001 From: "Elf M. Sternberg" Date: Sun, 7 Aug 2011 12:33:07 -0700 Subject: [PATCH] Updated to modern standards. --- backbone-localstorage.js | 84 - backbone.js | 525 ++- backbonestore.nw | 538 ++- data/items.json | 6 + index.html | 130 +- jquery-1.5.js | 8176 -------------------------------------- jquery-1.6.2.min.js | 18 + jquery.tmpl.min.js | 1 - jquery.validjson.js | 283 -- store.js | 241 +- underscore-min.js | 24 - underscore.js | 285 +- 12 files changed, 1185 insertions(+), 9126 deletions(-) delete mode 100644 backbone-localstorage.js delete mode 100644 jquery-1.5.js create mode 100644 jquery-1.6.2.min.js delete mode 100644 jquery.tmpl.min.js delete mode 100644 jquery.validjson.js delete mode 100644 underscore-min.js diff --git a/backbone-localstorage.js b/backbone-localstorage.js deleted file mode 100644 index add3cf7..0000000 --- a/backbone-localstorage.js +++ /dev/null @@ -1,84 +0,0 @@ -// A simple module to replace `Backbone.sync` with *localStorage*-based -// persistence. Models are given GUIDS, and saved into a JSON object. Simple -// as that. - -// Generate four random hex digits. -function S4() { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); -}; - -// Generate a pseudo-GUID by concatenating random hexadecimal. -function guid() { - return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); -}; - -// Our Store is represented by a single JS object in *localStorage*. Create it -// with a meaningful name, like the name you'd give a table. -var Store = function(name) { - this.name = name; - var store = localStorage.getItem(this.name); - this.data = (store && JSON.parse(store)) || {}; -}; - -_.extend(Store.prototype, { - - // Save the current state of the **Store** to *localStorage*. - save: function() { - localStorage.setItem(this.name, JSON.stringify(this.data)); - }, - - // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already - // have an id of it's own. - create: function(model) { - if (!model.id) model.id = model.attributes.id = guid(); - this.data[model.id] = model; - this.save(); - return model; - }, - - // Update a model by replacing its copy in `this.data`. - update: function(model) { - this.data[model.id] = model; - this.save(); - return model; - }, - - // Retrieve a model from `this.data` by id. - find: function(model) { - return this.data[model.id]; - }, - - // Return the array of all models currently in storage. - findAll: function() { - return _.values(this.data); - }, - - // Delete a model from `this.data`, returning it. - destroy: function(model) { - delete this.data[model.id]; - this.save(); - return model; - } - -}); - -// Override `Backbone.sync` to use delegate to the model or collection's -// *localStorage* property, which should be an instance of `Store`. -Backbone.sync = function(method, model, success, error) { - - var resp; - var store = model.localStorage || model.collection.localStorage; - - switch (method) { - case "read": resp = model.id ? store.find(model) : store.findAll(); break; - case "create": resp = store.create(model); break; - case "update": resp = store.update(model); break; - case "delete": resp = store.destroy(model); break; - } - - if (resp) { - success(resp); - } else { - error("Record not found"); - } -}; \ No newline at end of file diff --git a/backbone.js b/backbone.js index 04fe46e..62da3bd 100644 --- a/backbone.js +++ b/backbone.js @@ -1,4 +1,4 @@ -// Backbone.js 0.3.3 +// Backbone.js 0.5.2 // (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: @@ -9,24 +9,37 @@ // Initial Setup // ------------- + // Save a reference to the global object. + var root = this; + + // Save the previous value of the `Backbone` variable. + var previousBackbone = root.Backbone; + // The top-level namespace. All public Backbone classes and modules will // be attached to this. Exported for both CommonJS and the browser. var Backbone; if (typeof exports !== 'undefined') { Backbone = exports; } else { - Backbone = this.Backbone = {}; + Backbone = root.Backbone = {}; } // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.3.3'; + Backbone.VERSION = '0.5.2'; // Require Underscore, if we're on the server, and it's not already present. - var _ = this._; - if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; - // For Backbone's purposes, either jQuery or Zepto owns the `$` variable. - var $ = this.jQuery || this.Zepto; + // For Backbone's purposes, jQuery or Zepto owns the `$` variable. + var $ = root.jQuery || root.Zepto; + + // 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 use support legacy HTTP servers. Setting this option will // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a @@ -55,10 +68,10 @@ // Bind an event, specified by a string name, `ev`, to a `callback` function. // Passing `"all"` will bind the callback to all events fired. - bind : function(ev, callback) { + bind : function(ev, callback, context) { var calls = this._callbacks || (this._callbacks = {}); - var list = this._callbacks[ev] || (this._callbacks[ev] = []); - list.push(callback); + var list = calls[ev] || (calls[ev] = []); + list.push([callback, context]); return this; }, @@ -76,8 +89,8 @@ var list = calls[ev]; if (!list) return this; for (var i = 0, l = list.length; i < l; i++) { - if (callback === list[i]) { - list.splice(i, 1); + if (list[i] && callback === list[i][0]) { + list[i] = null; break; } } @@ -89,17 +102,21 @@ // Trigger an event, firing all bound callbacks. Callbacks are passed the // same arguments as `trigger` is, apart from the event name. // Listening for `"all"` passes the true event name as the first argument. - trigger : function(ev) { - var list, calls, i, l; + trigger : function(eventName) { + var list, calls, ev, callback, args; + var both = 2; if (!(calls = this._callbacks)) return this; - if (list = calls[ev]) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - if (list = calls['all']) { - for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, arguments); + while (both--) { + ev = both ? eventName : 'all'; + if (list = calls[ev]) { + for (var i = 0, l = list.length; i < l; i++) { + if (!(callback = list[i])) { + list.splice(i, 1); i--; l--; + } else { + args = both ? Array.prototype.slice.call(arguments, 1) : arguments; + callback[0].apply(callback[1] || this, args); + } + } } } return this; @@ -113,12 +130,17 @@ // Create a new model, with defined attributes. A client id (`cid`) // is automatically generated and assigned for you. Backbone.Model = function(attributes, options) { + var defaults; attributes || (attributes = {}); - if (this.defaults) attributes = _.extend({}, this.defaults, attributes); + if (defaults = this.defaults) { + if (_.isFunction(defaults)) defaults = defaults.call(this); + attributes = _.extend({}, defaults, attributes); + } this.attributes = {}; this._escapedAttributes = {}; this.cid = _.uniqueId('c'); this.set(attributes, {silent : true}); + this._changed = false; this._previousAttributes = _.clone(this.attributes); if (options && options.collection) this.collection = options.collection; this.initialize(attributes, options); @@ -134,6 +156,10 @@ // Has the item been changed since the last `"change"` event? _changed : false, + // 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(){}, @@ -153,7 +179,13 @@ var html; if (html = this._escapedAttributes[attr]) return html; var val = this.attributes[attr]; - return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : val); + return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has : function(attr) { + return this.attributes[attr] != null; }, // Set a hash of model attributes on the object, firing `"change"` unless you @@ -170,7 +202,11 @@ if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; // Check for changes of `id`. - if ('id' in attrs) this.id = attrs.id; + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // We're about to start triggering change events. + var alreadyChanging = this._changing; + this._changing = true; // Update attributes. for (var attr in attrs) { @@ -178,21 +214,21 @@ if (!_.isEqual(now[attr], val)) { now[attr] = val; delete escaped[attr]; - if (!options.silent) { - this._changed = true; - this.trigger('change:' + attr, this, val, options); - } + this._changed = true; + if (!options.silent) this.trigger('change:' + attr, this, val, options); } } // Fire the `"change"` event, if the model has been changed. - if (!options.silent && this._changed) this.change(options); + if (!alreadyChanging && !options.silent && this._changed) this.change(options); + this._changing = false; return this; }, // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. + // to silence it. `unset` is a noop if the attribute doesn't exist. unset : function(attr, options) { + if (!(attr in this.attributes)) return this; options || (options = {}); var value = this.attributes[attr]; @@ -204,8 +240,9 @@ // Remove the attribute. delete this.attributes[attr]; delete this._escapedAttributes[attr]; + if (attr == this.idAttribute) delete this.id; + this._changed = true; if (!options.silent) { - this._changed = true; this.trigger('change:' + attr, this, void 0, options); this.change(options); } @@ -216,6 +253,7 @@ // to silence it. clear : function(options) { options || (options = {}); + var attr; var old = this.attributes; // Run validation. @@ -225,8 +263,8 @@ this.attributes = {}; this._escapedAttributes = {}; + this._changed = true; if (!options.silent) { - this._changed = true; for (attr in old) { this.trigger('change:' + attr, this, void 0, options); } @@ -241,13 +279,13 @@ fetch : function(options) { options || (options = {}); var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp); }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; + options.error = wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); }, // Set a hash of model attributes, and sync the model to the server. @@ -257,42 +295,43 @@ options || (options = {}); if (attrs && !this.set(attrs, options)) return false; var model = this; - var success = function(resp) { - if (!model.set(model.parse(resp), options)) return false; - if (options.success) options.success(model, resp); + var success = options.success; + options.success = function(resp, status, xhr) { + if (!model.set(model.parse(resp, xhr), options)) return false; + if (success) success(model, resp, xhr); }; - var error = wrapError(options.error, model, options); + options.error = wrapError(options.error, model, options); var method = this.isNew() ? 'create' : 'update'; - (this.sync || Backbone.sync)(method, this, success, error); - return this; + return (this.sync || Backbone.sync).call(this, method, this, options); }, - // Destroy this model on the server. Upon success, the model is removed + // Destroy this model on the server if it was already persisted. Upon success, the model is removed // from its collection, if it has one. destroy : function(options) { options || (options = {}); + if (this.isNew()) return this.trigger('destroy', this, this.collection, options); var model = this; - var success = function(resp) { - if (model.collection) model.collection.remove(model); - if (options.success) options.success(model, resp); + var success = options.success; + options.success = function(resp) { + model.trigger('destroy', model, model.collection, options); + if (success) success(model, resp); }; - var error = wrapError(options.error, model, options); - (this.sync || Backbone.sync)('delete', this, success, error); - return this; + options.error = wrapError(options.error, model, options); + return (this.sync || Backbone.sync).call(this, 'delete', this, options); }, // 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 = getUrl(this.collection); + var base = getUrl(this.collection) || this.urlRoot || urlError(); if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id; + return base + (base.charAt(base.length - 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) { + parse : function(resp, xhr) { return resp; }, @@ -301,10 +340,9 @@ return new this.constructor(this); }, - // A model is new if it has never been saved to the server, and has a negative - // ID. + // A model is new if it has never been saved to the server, and lacks an id. isNew : function() { - return !this.id; + return this.id == null; }, // Call this method to manually fire a `change` event for this model. @@ -359,7 +397,7 @@ var error = this.validate(attrs); if (error) { if (options.error) { - options.error(this, error); + options.error(this, error, options); } else { this.trigger('error', this, error, options); } @@ -378,14 +416,11 @@ // its models in sort order, as they're added and removed. Backbone.Collection = function(models, options) { options || (options = {}); - if (options.comparator) { - this.comparator = options.comparator; - delete options.comparator; - } - this._boundOnModelEvent = _.bind(this._onModelEvent, this); + if (options.comparator) this.comparator = options.comparator; + _.bindAll(this, '_onModelEvent', '_removeReference'); this._reset(); - if (models) this.refresh(models, {silent: true}); - this.initialize(models, options); + if (models) this.reset(models, {silent: true}); + this.initialize.apply(this, arguments); }; // Define the Collection's inheritable methods. @@ -453,7 +488,7 @@ options || (options = {}); if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, @@ -463,51 +498,53 @@ }, // When you have more items than you want to add or remove individually, - // you can refresh the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `refresh` when finished. - refresh : function(models, options) { + // you can reset the entire set with a new list of models, without firing + // any `added` or `removed` events. Fires `reset` when finished. + reset : function(models, options) { models || (models = []); options || (options = {}); + this.each(this._removeReference); this._reset(); this.add(models, {silent: true}); - if (!options.silent) this.trigger('refresh', this, options); + if (!options.silent) this.trigger('reset', this, options); return this; }, - // Fetch the default set of models for this collection, refreshing the - // collection when they arrive. + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `add: true` is passed, appends the + // models to the collection instead of resetting. fetch : function(options) { options || (options = {}); var collection = this; - var success = function(resp) { - collection.refresh(collection.parse(resp)); - if (options.success) options.success(collection, resp); + var success = options.success; + options.success = function(resp, status, xhr) { + collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); + if (success) success(collection, resp); }; - var error = wrapError(options.error, collection, options); - (this.sync || Backbone.sync)('read', this, success, error); - return this; + options.error = wrapError(options.error, collection, options); + return (this.sync || Backbone.sync).call(this, 'read', this, options); }, // Create a new instance of a model in this collection. After the model // has been created on the server, it will be added to the collection. + // Returns the model, or 'false' if validation on a new model fails. create : function(model, options) { var coll = this; options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: coll}); - } else { - model.collection = coll; - } - var success = function(nextModel, resp) { - coll.add(nextModel); - if (options.success) options.success(nextModel, resp); + model = this._prepareModel(model, options); + if (!model) return false; + var success = options.success; + options.success = function(nextModel, resp, xhr) { + coll.add(nextModel, options); + if (success) success(nextModel, resp, xhr); }; - return model.save(null, {success : success, error : options.error}); + 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) { + parse : function(resp, xhr) { return resp; }, @@ -518,7 +555,7 @@ return _(this.models).chain(); }, - // Reset all internal state. Called when the collection is refreshed. + // Reset all internal state. Called when the collection is reset. _reset : function(options) { this.length = 0; this.models = []; @@ -526,21 +563,34 @@ this._byCid = {}; }, + // Prepare a model to be added to this collection + _prepareModel: function(model, options) { + if (!(model instanceof Backbone.Model)) { + var attrs = model; + model = new this.model(attrs, {collection: this}); + if (model.validate && !model._performValidation(attrs, options)) model = false; + } else if (!model.collection) { + model.collection = this; + } + return model; + }, + // Internal implementation of adding a single model to the set, updating // hash indexes for `id` and `cid` lookups. + // Returns the model, or 'false' if validation on a new model fails. _add : function(model, options) { options || (options = {}); - if (!(model instanceof Backbone.Model)) { - model = new this.model(model, {collection: this}); - } + model = this._prepareModel(model, options); + if (!model) return false; var already = this.getByCid(model); if (already) throw new Error(["Can't add the same model to a set twice", already.id]); this._byId[model.id] = model; this._byCid[model.cid] = model; - model.collection = this; - var index = this.comparator ? this.sortedIndex(model, this.comparator) : this.length; + var index = options.at != null ? options.at : + this.comparator ? this.sortedIndex(model, this.comparator) : + this.length; this.models.splice(index, 0, model); - model.bind('all', this._boundOnModelEvent); + model.bind('all', this._onModelEvent); this.length++; if (!options.silent) model.trigger('add', model, this, options); return model; @@ -554,20 +604,32 @@ if (!model) return null; delete this._byId[model.id]; delete this._byCid[model.cid]; - delete model.collection; this.models.splice(this.indexOf(model), 1); this.length--; if (!options.silent) model.trigger('remove', model, this, options); - model.unbind('all', this._boundOnModelEvent); + this._removeReference(model); return model; }, + // Internal method to remove a model's ties to a collection. + _removeReference : function(model) { + if (this == model.collection) { + delete model.collection; + } + model.unbind('all', this._onModelEvent); + }, + // 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. - _onModelEvent : function(ev, model) { - if (ev === 'change:id') { - delete this._byId[model.previous('id')]; + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent : function(ev, model, collection, options) { + if ((ev == 'add' || ev == 'remove') && collection != this) return; + if (ev == 'destroy') { + this._remove(model, options); + } + if (model && ev === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; this._byId[model.id] = model; } this.trigger.apply(this, arguments); @@ -578,7 +640,7 @@ // Underscore methods that we want to implement on the Collection. var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', + 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty']; // Mix in each Underscore method as a proxy to `Collection#models`. @@ -588,25 +650,26 @@ }; }); - // Backbone.Controller + // Backbone.Router // ------------------- - // Controllers map faux-URLs to actions, and fire events when routes are + // 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. - Backbone.Controller = function(options) { + Backbone.Router = function(options) { options || (options = {}); if (options.routes) this.routes = options.routes; this._bindRoutes(); - this.initialize(options); + this.initialize.apply(this, arguments); }; // Cached regular expressions for matching named param parts and splatted // parts of route strings. - var namedParam = /:([\w\d]+)/g; - var splatParam = /\*([\w\d]+)/g; + var namedParam = /:([\w\d]+)/g; + var splatParam = /\*([\w\d]+)/g; + var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; - // Set up all inheritable **Backbone.Controller** properties and methods. - _.extend(Backbone.Controller.prototype, Backbone.Events, { + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Backbone.Router.prototype, Backbone.Events, { // Initialize is an empty function by default. Override it with your own // initialization logic. @@ -628,25 +691,31 @@ }, this)); }, - // Simple proxy to `Backbone.history` to save a fragment into the history, - // without triggering routes. - saveLocation : function(fragment) { - Backbone.history.saveLocation(fragment); + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate : function(fragment, triggerRoute) { + Backbone.history.navigate(fragment, triggerRoute); }, - // Bind all defined routes to `Backbone.history`. + // 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; + var routes = []; for (var route in this.routes) { - var name = this.routes[route]; - this.route(route, name, this[name]); + routes.unshift([route, this.routes[route]]); + } + for (var i = 0, l = routes.length; i < l; i++) { + this.route(routes[i][0], routes[i][1], this[routes[i][1]]); } }, // Convert a route string into a regular expression, suitable for matching - // against the current location fragment. + // against the current location hash. _routeToRegExp : function(route) { - route = route.replace(namedParam, "([^\/]*)").replace(splatParam, "(.*?)"); + route = route.replace(escapeRegExp, "\\$&") + .replace(namedParam, "([^\/]*)") + .replace(splatParam, "(.*?)"); return new RegExp('^' + route + '$'); }, @@ -661,17 +730,22 @@ // Backbone.History // ---------------- - // Handles cross-browser history management, based on URL hashes. If the + // Handles cross-browser history management, based on URL fragments. If the // browser does not support `onhashchange`, falls back to polling. Backbone.History = function() { this.handlers = []; - this.fragment = this.getFragment(); _.bindAll(this, 'checkUrl'); }; // Cached regex for cleaning hashes. var hashStrip = /^#*/; + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Has the history handling already been started? + var historyStarted = false; + // Set up all inheritable **Backbone.History** properties and methods. _.extend(Backbone.History.prototype, { @@ -679,53 +753,89 @@ // twenty times a second. interval: 50, - // Get the cross-browser normalized URL fragment. - getFragment : function(loc) { - return (loc || window.location).hash.replace(hashStrip, ''); + // 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 || forcePushState) { + fragment = window.location.pathname; + var search = window.location.search; + if (search) fragment += search; + if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); + } else { + fragment = window.location.hash; + } + } + return fragment.replace(hashStrip, ''); }, // Start the hash change handling, returning `true` if the current URL matches // an existing route, and `false` otherwise. - start : function() { - var docMode = document.documentMode; - var oldIE = ($.browser.msie && (!docMode || docMode <= 7)); + start : function(options) { + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + if (historyStarted) throw new Error("Backbone.history has already been started"); + this.options = _.extend({}, {root: '/'}, this.options, options); + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); if (oldIE) { this.iframe = $('