From a7d314628c5f140953f98a2c403d713acf724905 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Tue, 9 Oct 2012 18:13:48 -0400
Subject: [PATCH 1/4] Added backbone.js and underscore.js files through
 hook_library. The edit library now depends on them.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 edit.module        |   24 +
 libs/backbone.js   | 1431 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 libs/underscore.js | 1200 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 2655 insertions(+)
 create mode 100644 libs/backbone.js
 create mode 100644 libs/underscore.js

diff --git a/edit.module b/edit.module
index c6f0704..aefbe18 100644
--- a/edit.module
+++ b/edit.module
@@ -162,6 +162,7 @@ function edit_library() {
       array('system', 'jquery.form'),
       array('system', 'drupal.form'),
       array('system', 'drupal.ajax'),
+      array('edit', 'edit.backbone'),
     ),
   );
 
@@ -170,6 +171,29 @@ function edit_library() {
     $libraries['edit']['dependencies'][] = array($wysiwyg_module, "$wysiwyg_module.edit");
   }
 
+  // Register underscore as a library.
+  $libraries['edit.underscore'] = array(
+    'title' => 'Underscore.js',
+    'website' => 'http://underscorejs.org',
+    'version' => '1.4.2',
+    'js' => array(
+      $path . '/libs/underscore.js' => array(),
+    ),
+  );
+
+  // Register backbone as a library.
+  $libraries['edit.backbone'] = array(
+    'title' => 'Backbone.js',
+    'website' => 'http://backbonejs.org',
+    'version' => '0.9.2',
+    'js' => array(
+      $path . '/libs/backbone.js' => array(),
+    ),
+    'dependencies' => array(
+      array('edit', 'edit.underscore'),
+    ),
+  );
+
   return $libraries;
 }
 
diff --git a/libs/backbone.js b/libs/backbone.js
new file mode 100644
index 0000000..3373c95
--- /dev/null
+++ b/libs/backbone.js
@@ -0,0 +1,1431 @@
+//     Backbone.js 0.9.2
+
+//     (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
+//     Backbone may be freely distributed under the MIT license.
+//     For all details and documentation:
+//     http://backbonejs.org
+
+(function(){
+
+  // Initial Setup
+  // -------------
+
+  // Save a reference to the global object (`window` in the browser, `global`
+  // on the server).
+  var root = this;
+
+  // Save the previous value of the `Backbone` variable, so that it can be
+  // restored later on, if `noConflict` is used.
+  var previousBackbone = root.Backbone;
+
+  // Create a local reference to slice/splice.
+  var slice = Array.prototype.slice;
+  var splice = Array.prototype.splice;
+
+  // 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 = root.Backbone = {};
+  }
+
+  // Current version of the library. Keep in sync with `package.json`.
+  Backbone.VERSION = '0.9.2';
+
+  // Require Underscore, if we're on the server, and it's not already present.
+  var _ = root._;
+  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');
+
+  // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
+  var $ = root.jQuery || root.Zepto || root.ender;
+
+  // Set the JavaScript library that will be used for DOM manipulation and
+  // Ajax calls (a.k.a. the `$` variable). By default Backbone will use: jQuery,
+  // Zepto, or Ender; but the `setDomLibrary()` method lets you inject an
+  // alternate JavaScript library (or a mock library for testing your views
+  // outside of a browser).
+  Backbone.setDomLibrary = function(lib) {
+    $ = lib;
+  };
+
+  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+  // to its previous owner. Returns a reference to this Backbone object.
+  Backbone.noConflict = function() {
+    root.Backbone = previousBackbone;
+    return this;
+  };
+
+  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+  // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+  // set a `X-Http-Method-Override` header.
+  Backbone.emulateHTTP = false;
+
+  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+  // `application/json` requests ... will encode the body as
+  // `application/x-www-form-urlencoded` instead and will send the model in a
+  // form param named `model`.
+  Backbone.emulateJSON = false;
+
+  // Backbone.Events
+  // -----------------
+
+  // Regular expression used to split event strings
+  var eventSplitter = /\s+/;
+
+  // A module that can be mixed in to *any object* in order to provide it with
+  // custom events. You may bind with `on` or remove with `off` callback functions
+  // to an event; trigger`-ing an event fires all callbacks in succession.
+  //
+  //     var object = {};
+  //     _.extend(object, Backbone.Events);
+  //     object.on('expand', function(){ alert('expanded'); });
+  //     object.trigger('expand');
+  //
+  var Events = Backbone.Events = {
+
+    // Bind one or more space separated events, `events`, to a `callback`
+    // function. Passing `"all"` will bind the callback to all events fired.
+    on: function(events, callback, context) {
+
+      var calls, event, node, tail, list;
+      if (!callback) return this;
+      events = events.split(eventSplitter);
+      calls = this._callbacks || (this._callbacks = {});
+
+      // Create an immutable callback list, allowing traversal during
+      // modification.  The tail is an empty object that will always be used
+      // as the next node.
+      while (event = events.shift()) {
+        list = calls[event];
+        node = list ? list.tail : {};
+        node.next = tail = {};
+        node.context = context;
+        node.callback = callback;
+        calls[event] = {tail: tail, next: list ? list.next : node};
+      }
+
+      return this;
+    },
+
+    // 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 `events` is null, removes all bound callbacks for all events.
+    off: function(events, callback, context) {
+      var event, calls, node, tail, cb, ctx;
+
+      // No events, or removing *all* events.
+      if (!(calls = this._callbacks)) return;
+      if (!(events || callback || context)) {
+        delete this._callbacks;
+        return this;
+      }
+
+      // Loop through the listed events and contexts, splicing them out of the
+      // linked list of callbacks if appropriate.
+      events = events ? events.split(eventSplitter) : _.keys(calls);
+      while (event = events.shift()) {
+        node = calls[event];
+        delete calls[event];
+        if (!node || !(callback || context)) continue;
+        // Create a new list, omitting the indicated callbacks.
+        tail = node.tail;
+        while ((node = node.next) !== tail) {
+          cb = node.callback;
+          ctx = node.context;
+          if ((callback && cb !== callback) || (context && ctx !== context)) {
+            this.on(event, cb, ctx);
+          }
+        }
+      }
+
+      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(events) {
+      var event, node, calls, tail, args, all, rest;
+      if (!(calls = this._callbacks)) return this;
+      all = calls.all;
+      events = events.split(eventSplitter);
+      rest = slice.call(arguments, 1);
+
+      // For each event, walk through the linked list of callbacks twice,
+      // first to trigger the event, then to trigger any `"all"` callbacks.
+      while (event = events.shift()) {
+        if (node = calls[event]) {
+          tail = node.tail;
+          while ((node = node.next) !== tail) {
+            node.callback.apply(node.context || this, rest);
+          }
+        }
+        if (node = all) {
+          tail = node.tail;
+          args = [event].concat(rest);
+          while ((node = node.next) !== tail) {
+            node.callback.apply(node.context || this, args);
+          }
+        }
+      }
+
+      return this;
+    }
+
+  };
+
+  // Aliases for backwards compatibility.
+  Events.bind   = Events.on;
+  Events.unbind = Events.off;
+
+  // Backbone.Model
+  // --------------
+
+  // Create a new model, with defined attributes. A client id (`cid`)
+  // is automatically generated and assigned for you.
+  var Model = Backbone.Model = function(attributes, options) {
+    var defaults;
+    attributes || (attributes = {});
+    if (options && options.parse) attributes = this.parse(attributes);
+    if (defaults = getValue(this, 'defaults')) {
+      attributes = _.extend({}, defaults, attributes);
+    }
+    if (options && options.collection) this.collection = options.collection;
+    this.attributes = {};
+    this._escapedAttributes = {};
+    this.cid = _.uniqueId('c');
+    this.changed = {};
+    this._silent = {};
+    this._pending = {};
+    this.set(attributes, {silent: true});
+    // Reset change tracking.
+    this.changed = {};
+    this._silent = {};
+    this._pending = {};
+    this._previousAttributes = _.clone(this.attributes);
+    this.initialize.apply(this, arguments);
+  };
+
+  // Attach all inheritable methods to the Model prototype.
+  _.extend(Model.prototype, Events, {
+
+    // A hash of attributes whose current and previous value differ.
+    changed: null,
+
+    // A hash of attributes that have silently changed since the last time
+    // `change` was called.  Will become pending attributes on the next call.
+    _silent: null,
+
+    // A hash of attributes that have changed since the last `'change'` event
+    // began.
+    _pending: null,
+
+    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+    // CouchDB users may want to set this to `"_id"`.
+    idAttribute: 'id',
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // Return a copy of the model's `attributes` object.
+    toJSON: function(options) {
+      return _.clone(this.attributes);
+    },
+
+    // Get the value of an attribute.
+    get: function(attr) {
+      return this.attributes[attr];
+    },
+
+    // Get the HTML-escaped value of an attribute.
+    escape: function(attr) {
+      var html;
+      if (html = this._escapedAttributes[attr]) return html;
+      var val = this.get(attr);
+      return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
+    },
+
+    // Returns `true` if the attribute contains a value that is not null
+    // or undefined.
+    has: function(attr) {
+      return this.get(attr) != null;
+    },
+
+    // Set a hash of model attributes on the object, firing `"change"` unless
+    // you choose to silence it.
+    set: function(key, value, options) {
+      var attrs, attr, val;
+
+      // Handle both `"key", value` and `{key: value}` -style arguments.
+      if (_.isObject(key) || key == null) {
+        attrs = key;
+        options = value;
+      } else {
+        attrs = {};
+        attrs[key] = value;
+      }
+
+      // Extract attributes and options.
+      options || (options = {});
+      if (!attrs) return this;
+      if (attrs instanceof Model) attrs = attrs.attributes;
+      if (options.unset) for (attr in attrs) attrs[attr] = void 0;
+
+      // Run validation.
+      if (!this._validate(attrs, options)) return false;
+
+      // Check for changes of `id`.
+      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+      var changes = options.changes = {};
+      var now = this.attributes;
+      var escaped = this._escapedAttributes;
+      var prev = this._previousAttributes || {};
+
+      // For each `set` attribute...
+      for (attr in attrs) {
+        val = attrs[attr];
+
+        // If the new and current value differ, record the change.
+        if (!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {
+          delete escaped[attr];
+          (options.silent ? this._silent : changes)[attr] = true;
+        }
+
+        // Update or delete the current value.
+        options.unset ? delete now[attr] : now[attr] = val;
+
+        // If the new and previous value differ, record the change.  If not,
+        // then remove changes for this attribute.
+        if (!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {
+          this.changed[attr] = val;
+          if (!options.silent) this._pending[attr] = true;
+        } else {
+          delete this.changed[attr];
+          delete this._pending[attr];
+        }
+      }
+
+      // Fire the `"change"` events.
+      if (!options.silent) this.change(options);
+      return this;
+    },
+
+    // Remove an attribute from the model, firing `"change"` unless you choose
+    // to silence it. `unset` is a noop if the attribute doesn't exist.
+    unset: function(attr, options) {
+      (options || (options = {})).unset = true;
+      return this.set(attr, null, options);
+    },
+
+    // Clear all attributes on the model, firing `"change"` unless you choose
+    // to silence it.
+    clear: function(options) {
+      (options || (options = {})).unset = true;
+      return this.set(_.clone(this.attributes), options);
+    },
+
+    // Fetch the model from the server. If the server's representation of the
+    // model differs from its current attributes, they will be overriden,
+    // triggering a `"change"` event.
+    fetch: function(options) {
+      options = options ? _.clone(options) : {};
+      var model = this;
+      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);
+      };
+      options.error = Backbone.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.
+    // If the server returns an attributes hash that differs, the model's
+    // state will be `set` again.
+    save: function(key, value, options) {
+      var attrs, current;
+
+      // Handle both `("key", value)` and `({key: value})` -style calls.
+      if (_.isObject(key) || key == null) {
+        attrs = key;
+        options = value;
+      } else {
+        attrs = {};
+        attrs[key] = value;
+      }
+      options = options ? _.clone(options) : {};
+
+      // If we're "wait"-ing to set changed attributes, validate early.
+      if (options.wait) {
+        if (!this._validate(attrs, options)) return false;
+        current = _.clone(this.attributes);
+      }
+
+      // Regular saves `set` attributes before persisting to the server.
+      var silentOptions = _.extend({}, options, {silent: true});
+      if (attrs && !this.set(attrs, options.wait ? silentOptions : options)) {
+        return false;
+      }
+
+      // After a successful server-side save, the client is (optionally)
+      // updated with the server-side state.
+      var model = this;
+      var success = options.success;
+      options.success = function(resp, status, xhr) {
+        var serverAttrs = model.parse(resp, xhr);
+        if (options.wait) {
+          delete options.wait;
+          serverAttrs = _.extend(attrs || {}, serverAttrs);
+        }
+        if (!model.set(serverAttrs, options)) return false;
+        if (success) {
+          success(model, resp);
+        } else {
+          model.trigger('sync', model, resp, options);
+        }
+      };
+
+      // Finish configuring and sending the Ajax request.
+      options.error = Backbone.wrapError(options.error, model, options);
+      var method = this.isNew() ? 'create' : 'update';
+      var xhr = (this.sync || Backbone.sync).call(this, method, this, options);
+      if (options.wait) this.set(current, silentOptions);
+      return xhr;
+    },
+
+    // Destroy this model on the server if it was already persisted.
+    // Optimistically removes the model from its collection, if it has one.
+    // If `wait: true` is passed, waits for the server to respond before removal.
+    destroy: function(options) {
+      options = options ? _.clone(options) : {};
+      var model = this;
+      var success = options.success;
+
+      var triggerDestroy = function() {
+        model.trigger('destroy', model, model.collection, options);
+      };
+
+      if (this.isNew()) {
+        triggerDestroy();
+        return false;
+      }
+
+      options.success = function(resp) {
+        if (options.wait) triggerDestroy();
+        if (success) {
+          success(model, resp);
+        } else {
+          model.trigger('sync', model, resp, options);
+        }
+      };
+
+      options.error = Backbone.wrapError(options.error, model, options);
+      var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);
+      if (!options.wait) triggerDestroy();
+      return xhr;
+    },
+
+    // Default URL for the model's representation on the server -- if you're
+    // using Backbone's restful methods, override this to change the endpoint
+    // that will be called.
+    url: function() {
+      var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
+      if (this.isNew()) return base;
+      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, xhr) {
+      return resp;
+    },
+
+    // Create a new model with identical attributes to this one.
+    clone: function() {
+      return new this.constructor(this.attributes);
+    },
+
+    // A model is new if it has never been saved to the server, and lacks an id.
+    isNew: function() {
+      return this.id == null;
+    },
+
+    // Call this method to manually fire a `"change"` event for this model and
+    // a `"change:attribute"` event for each changed attribute.
+    // Calling this will cause all objects observing the model to update.
+    change: function(options) {
+      options || (options = {});
+      var changing = this._changing;
+      this._changing = true;
+
+      // Silent changes become pending changes.
+      for (var attr in this._silent) this._pending[attr] = true;
+
+      // Silent changes are triggered.
+      var changes = _.extend({}, options.changes, this._silent);
+      this._silent = {};
+      for (var attr in changes) {
+        this.trigger('change:' + attr, this, this.get(attr), options);
+      }
+      if (changing) return this;
+
+      // Continue firing `"change"` events while there are pending changes.
+      while (!_.isEmpty(this._pending)) {
+        this._pending = {};
+        this.trigger('change', this, options);
+        // Pending and silent changes still remain.
+        for (var attr in this.changed) {
+          if (this._pending[attr] || this._silent[attr]) continue;
+          delete this.changed[attr];
+        }
+        this._previousAttributes = _.clone(this.attributes);
+      }
+
+      this._changing = false;
+      return this;
+    },
+
+    // Determine if the model has changed since the last `"change"` event.
+    // If you specify an attribute name, determine if that attribute has changed.
+    hasChanged: function(attr) {
+      if (!arguments.length) return !_.isEmpty(this.changed);
+      return _.has(this.changed, attr);
+    },
+
+    // Return an object containing all the attributes that have changed, or
+    // false if there are no changed attributes. Useful for determining what
+    // parts of a view need to be updated and/or what attributes need to be
+    // persisted to the server. Unset attributes will be set to undefined.
+    // You can also pass an attributes object to diff against the model,
+    // determining if there *would be* a change.
+    changedAttributes: function(diff) {
+      if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+      var val, changed = false, old = this._previousAttributes;
+      for (var attr in diff) {
+        if (_.isEqual(old[attr], (val = diff[attr]))) continue;
+        (changed || (changed = {}))[attr] = val;
+      }
+      return changed;
+    },
+
+    // Get the previous value of an attribute, recorded at the time the last
+    // `"change"` event was fired.
+    previous: function(attr) {
+      if (!arguments.length || !this._previousAttributes) return null;
+      return this._previousAttributes[attr];
+    },
+
+    // Get all of the attributes of the model at the time of the previous
+    // `"change"` event.
+    previousAttributes: function() {
+      return _.clone(this._previousAttributes);
+    },
+
+    // Check if the model is currently in a valid state. It's only possible to
+    // get into an *invalid* state if you're using silent changes.
+    isValid: function() {
+      return !this.validate(this.attributes);
+    },
+
+    // Run validation against the next complete set of model attributes,
+    // returning `true` if all is well. If a specific `error` callback has
+    // been passed, call that instead of firing the general `"error"` event.
+    _validate: function(attrs, options) {
+      if (options.silent || !this.validate) return true;
+      attrs = _.extend({}, this.attributes, attrs);
+      var error = this.validate(attrs, options);
+      if (!error) return true;
+      if (options && options.error) {
+        options.error(this, error, options);
+      } else {
+        this.trigger('error', this, error, options);
+      }
+      return false;
+    }
+
+  });
+
+  // Backbone.Collection
+  // -------------------
+
+  // Provides a standard collection class for our sets of models, ordered
+  // or unordered. If a `comparator` is specified, the Collection will maintain
+  // its models in sort order, as they're added and removed.
+  var Collection = Backbone.Collection = function(models, options) {
+    options || (options = {});
+    if (options.model) this.model = options.model;
+    if (options.comparator) this.comparator = options.comparator;
+    this._reset();
+    this.initialize.apply(this, arguments);
+    if (models) this.reset(models, {silent: true, parse: options.parse});
+  };
+
+  // Define the Collection's inheritable methods.
+  _.extend(Collection.prototype, Events, {
+
+    // The default model for a collection is just a **Backbone.Model**.
+    // This should be overridden in most cases.
+    model: Model,
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // The JSON representation of a Collection is an array of the
+    // models' attributes.
+    toJSON: function(options) {
+      return this.map(function(model){ return model.toJSON(options); });
+    },
+
+    // Add a model, or list of models to the set. Pass **silent** to avoid
+    // firing the `add` event for every new model.
+    add: function(models, options) {
+      var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];
+      options || (options = {});
+      models = _.isArray(models) ? models.slice() : [models];
+
+      // Begin by turning bare objects into model references, and preventing
+      // invalid models or duplicate models from being added.
+      for (i = 0, length = models.length; i < length; i++) {
+        if (!(model = models[i] = this._prepareModel(models[i], options))) {
+          throw new Error("Can't add an invalid model to a collection");
+        }
+        cid = model.cid;
+        id = model.id;
+        if (cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {
+          dups.push(i);
+          continue;
+        }
+        cids[cid] = ids[id] = model;
+      }
+
+      // Remove duplicates.
+      i = dups.length;
+      while (i--) {
+        models.splice(dups[i], 1);
+      }
+
+      // Listen to added models' events, and index models for lookup by
+      // `id` and by `cid`.
+      for (i = 0, length = models.length; i < length; i++) {
+        (model = models[i]).on('all', this._onModelEvent, this);
+        this._byCid[model.cid] = model;
+        if (model.id != null) this._byId[model.id] = model;
+      }
+
+      // Insert models into the collection, re-sorting if needed, and triggering
+      // `add` events unless silenced.
+      this.length += length;
+      index = options.at != null ? options.at : this.models.length;
+      splice.apply(this.models, [index, 0].concat(models));
+      if (this.comparator) this.sort({silent: true});
+      if (options.silent) return this;
+      for (i = 0, length = this.models.length; i < length; i++) {
+        if (!cids[(model = this.models[i]).cid]) continue;
+        options.index = i;
+        model.trigger('add', model, this, options);
+      }
+      return this;
+    },
+
+    // Remove a model, or a list of models from the set. Pass silent to avoid
+    // firing the `remove` event for every model removed.
+    remove: function(models, options) {
+      var i, l, index, model;
+      options || (options = {});
+      models = _.isArray(models) ? models.slice() : [models];
+      for (i = 0, l = models.length; i < l; i++) {
+        model = this.getByCid(models[i]) || this.get(models[i]);
+        if (!model) continue;
+        delete this._byId[model.id];
+        delete this._byCid[model.cid];
+        index = this.indexOf(model);
+        this.models.splice(index, 1);
+        this.length--;
+        if (!options.silent) {
+          options.index = index;
+          model.trigger('remove', model, this, options);
+        }
+        this._removeReference(model);
+      }
+      return this;
+    },
+
+    // Add a model to the end of the collection.
+    push: function(model, options) {
+      model = this._prepareModel(model, options);
+      this.add(model, options);
+      return model;
+    },
+
+    // Remove a model from the end of the collection.
+    pop: function(options) {
+      var model = this.at(this.length - 1);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Add a model to the beginning of the collection.
+    unshift: function(model, options) {
+      model = this._prepareModel(model, options);
+      this.add(model, _.extend({at: 0}, options));
+      return model;
+    },
+
+    // Remove a model from the beginning of the collection.
+    shift: function(options) {
+      var model = this.at(0);
+      this.remove(model, options);
+      return model;
+    },
+
+    // Get a model from the set by id.
+    get: function(id) {
+      if (id == null) return void 0;
+      return this._byId[id.id != null ? id.id : id];
+    },
+
+    // Get a model from the set by client id.
+    getByCid: function(cid) {
+      return cid && this._byCid[cid.cid || cid];
+    },
+
+    // Get the model at the given index.
+    at: function(index) {
+      return this.models[index];
+    },
+
+    // Return models with matching attributes. Useful for simple cases of `filter`.
+    where: function(attrs) {
+      if (_.isEmpty(attrs)) return [];
+      return this.filter(function(model) {
+        for (var key in attrs) {
+          if (attrs[key] !== model.get(key)) return false;
+        }
+        return true;
+      });
+    },
+
+    // Force the collection to re-sort itself. You don't need to call this under
+    // normal circumstances, as the set will maintain sort order as each item
+    // is added.
+    sort: function(options) {
+      options || (options = {});
+      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+      var boundComparator = _.bind(this.comparator, this);
+      if (this.comparator.length == 1) {
+        this.models = this.sortBy(boundComparator);
+      } else {
+        this.models.sort(boundComparator);
+      }
+      if (!options.silent) this.trigger('reset', this, options);
+      return this;
+    },
+
+    // Pluck an attribute from each model in the collection.
+    pluck: function(attr) {
+      return _.map(this.models, function(model){ return model.get(attr); });
+    },
+
+    // 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 `add` or `remove` events. Fires `reset` when finished.
+    reset: function(models, options) {
+      models  || (models = []);
+      options || (options = {});
+      for (var i = 0, l = this.models.length; i < l; i++) {
+        this._removeReference(this.models[i]);
+      }
+      this._reset();
+      this.add(models, _.extend({silent: true}, options));
+      if (!options.silent) this.trigger('reset', this, options);
+      return this;
+    },
+
+    // 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 ? _.clone(options) : {};
+      if (options.parse === undefined) options.parse = true;
+      var collection = this;
+      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);
+      };
+      options.error = Backbone.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. Add the model to the
+    // collection immediately, unless `wait: true` is passed, in which case we
+    // wait for the server to agree.
+    create: function(model, options) {
+      var coll = this;
+      options = options ? _.clone(options) : {};
+      model = this._prepareModel(model, options);
+      if (!model) return false;
+      if (!options.wait) coll.add(model, options);
+      var success = options.success;
+      options.success = function(nextModel, resp, xhr) {
+        if (options.wait) coll.add(nextModel, options);
+        if (success) {
+          success(nextModel, resp);
+        } else {
+          nextModel.trigger('sync', model, resp, options);
+        }
+      };
+      model.save(null, options);
+      return model;
+    },
+
+    // **parse** converts a response into a list of models to be added to the
+    // collection. The default implementation is just to pass it through.
+    parse: function(resp, xhr) {
+      return resp;
+    },
+
+    // Proxy to _'s chain. Can't be proxied the same way the rest of the
+    // underscore methods are proxied because it relies on the underscore
+    // constructor.
+    chain: function () {
+      return _(this.models).chain();
+    },
+
+    // Reset all internal state. Called when the collection is reset.
+    _reset: function(options) {
+      this.length = 0;
+      this.models = [];
+      this._byId  = {};
+      this._byCid = {};
+    },
+
+    // Prepare a model or hash of attributes to be added to this collection.
+    _prepareModel: function(model, options) {
+      options || (options = {});
+      if (!(model instanceof Model)) {
+        var attrs = model;
+        options.collection = this;
+        model = new this.model(attrs, options);
+        if (!model._validate(model.attributes, options)) model = false;
+      } else if (!model.collection) {
+        model.collection = this;
+      }
+      return model;
+    },
+
+    // Internal method to remove a model's ties to a collection.
+    _removeReference: function(model) {
+      if (this == model.collection) {
+        delete model.collection;
+      }
+      model.off('all', this._onModelEvent, this);
+    },
+
+    // Internal method called every time a model in the set fires an event.
+    // Sets need to update their indexes when models change ids. All other
+    // events simply proxy through. "add" and "remove" events that originate
+    // in other collections are ignored.
+    _onModelEvent: function(event, model, collection, options) {
+      if ((event == 'add' || event == 'remove') && collection != this) return;
+      if (event == 'destroy') {
+        this.remove(model, options);
+      }
+      if (model && event === 'change:' + model.idAttribute) {
+        delete this._byId[model.previous(model.idAttribute)];
+        this._byId[model.id] = model;
+      }
+      this.trigger.apply(this, arguments);
+    }
+
+  });
+
+  // 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', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex',
+    'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf',
+    'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];
+
+  // Mix in each Underscore method as a proxy to `Collection#models`.
+  _.each(methods, function(method) {
+    Collection.prototype[method] = function() {
+      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
+    };
+  });
+
+  // Backbone.Router
+  // -------------------
+
+  // Routers map faux-URLs to actions, and fire events when routes are
+  // matched. Creating a new one sets its `routes` hash, if not set statically.
+  var Router = Backbone.Router = function(options) {
+    options || (options = {});
+    if (options.routes) this.routes = options.routes;
+    this._bindRoutes();
+    this.initialize.apply(this, arguments);
+  };
+
+  // Cached regular expressions for matching named param parts and splatted
+  // parts of route strings.
+  var 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) {
+      Backbone.history || (Backbone.history = new History);
+      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+      if (!callback) callback = this[name];
+      Backbone.history.route(route, _.bind(function(fragment) {
+        var args = this._extractParameters(route, fragment);
+        callback && callback.apply(this, args);
+        this.trigger.apply(this, ['route:' + name].concat(args));
+        Backbone.history.trigger('route', this, name, args);
+      }, this));
+      return this;
+    },
+
+    // Simple proxy to `Backbone.history` to save a fragment into the history.
+    navigate: function(fragment, options) {
+      Backbone.history.navigate(fragment, options);
+    },
+
+    // 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) {
+        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 hash.
+    _routeToRegExp: function(route) {
+      route = route.replace(escapeRegExp, '\\$&')
+                   .replace(namedParam, '([^\/]+)')
+                   .replace(splatParam, '(.*?)');
+      return new RegExp('^' + route + '$');
+    },
+
+    // Given a route, and a URL fragment that it matches, return the array of
+    // extracted parameters.
+    _extractParameters: function(route, fragment) {
+      return route.exec(fragment).slice(1);
+    }
+
+  });
+
+  // Backbone.History
+  // ----------------
+
+  // Handles cross-browser history management, based on URL fragments. If the
+  // browser does not support `onhashchange`, falls back to polling.
+  var History = Backbone.History = function() {
+    this.handlers = [];
+    _.bindAll(this, 'checkUrl');
+  };
+
+  // Cached regex for cleaning leading hashes and slashes .
+  var routeStripper = /^[#\/]/;
+
+  // Cached regex for detecting MSIE.
+  var isExplorer = /msie [\w.]+/;
+
+  // 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,
+
+    // Gets the true hash value. Cannot use location.hash directly due to bug
+    // in Firefox where location.hash will always be decoded.
+    getHash: function(windowOverride) {
+      var loc = windowOverride ? windowOverride.location : window.location;
+      var match = loc.href.match(/#(.*)$/);
+      return match ? match[1] : '';
+    },
+
+    // Get the cross-browser normalized URL fragment, either from the URL,
+    // the hash, or the override.
+    getFragment: function(fragment, forcePushState) {
+      if (fragment == null) {
+        if (this._hasPushState || forcePushState) {
+          fragment = window.location.pathname;
+          var search = window.location.search;
+          if (search) fragment += search;
+        } else {
+          fragment = this.getHash();
+        }
+      }
+      if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
+      return fragment.replace(routeStripper, '');
+    },
+
+    // Start the hash change handling, returning `true` if the current URL matches
+    // an existing route, and `false` otherwise.
+    start: function(options) {
+      if (History.started) throw new Error("Backbone.history has already been started");
+      History.started = true;
+
+      // Figure out the initial configuration. Do we need an iframe?
+      // Is pushState desired ... is it available?
+      this.options          = _.extend({}, {root: '/'}, this.options, options);
+      this._wantsHashChange = this.options.hashChange !== false;
+      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 = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
+        this.navigate(fragment);
+      }
+
+      // Depending on whether we're using pushState or hashes, and whether
+      // 'onhashchange' is supported, determine how we check the URL state.
+      if (this._hasPushState) {
+        $(window).bind('popstate', this.checkUrl);
+      } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) {
+        $(window).bind('hashchange', this.checkUrl);
+      } else if (this._wantsHashChange) {
+        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+      }
+
+      // Determine if we need to change the base url, for a pushState link
+      // opened by a non-pushState browser.
+      this.fragment = fragment;
+      var loc = window.location;
+      var atRoot  = loc.pathname == this.options.root;
+
+      // If we've started off with a route from a `pushState`-enabled browser,
+      // but we're currently in a browser that doesn't support it...
+      if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {
+        this.fragment = this.getFragment(null, true);
+        window.location.replace(this.options.root + '#' + this.fragment);
+        // Return immediately as browser will do redirect to new url
+        return true;
+
+      // Or if we've started out with a hash-based route, but we're currently
+      // in a browser where it could be `pushState`-based instead...
+      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
+        this.fragment = this.getHash().replace(routeStripper, '');
+        window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
+      }
+
+      if (!this.options.silent) {
+        return this.loadUrl();
+      }
+    },
+
+    // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+    // but possibly useful for unit testing Routers.
+    stop: function() {
+      $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);
+      clearInterval(this._checkUrlInterval);
+      History.started = false;
+    },
+
+    // Add a route to be tested when the fragment changes. Routes added later
+    // may override previous routes.
+    route: function(route, callback) {
+      this.handlers.unshift({route: route, callback: callback});
+    },
+
+    // Checks the current URL to see if it has changed, and if it has,
+    // calls `loadUrl`, normalizing across the hidden iframe.
+    checkUrl: function(e) {
+      var current = this.getFragment();
+      if (current == this.fragment && this.iframe) current = this.getFragment(this.getHash(this.iframe));
+      if (current == this.fragment) return false;
+      if (this.iframe) this.navigate(current);
+      this.loadUrl() || this.loadUrl(this.getHash());
+    },
+
+    // Attempt to load the current URL fragment. If a route succeeds with a
+    // match, returns `true`. If no defined routes matches the fragment,
+    // returns `false`.
+    loadUrl: function(fragmentOverride) {
+      var fragment = this.fragment = this.getFragment(fragmentOverride);
+      var matched = _.any(this.handlers, function(handler) {
+        if (handler.route.test(fragment)) {
+          handler.callback(fragment);
+          return true;
+        }
+      });
+      return matched;
+    },
+
+    // Save a fragment into the hash history, or replace the URL state if the
+    // '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 frag = (fragment || '').replace(routeStripper, '');
+      if (this.fragment == frag) return;
+
+      // If pushState is available, we use it to set the fragment as a real URL.
+      if (this._hasPushState) {
+        if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
+        this.fragment = frag;
+        window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);
+
+      // If hash changes haven't been explicitly disabled, update the hash
+      // fragment to store history.
+      } else if (this._wantsHashChange) {
+        this.fragment = frag;
+        this._updateHash(window.location, frag, options.replace);
+        if (this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {
+          // Opening and closing the iframe tricks IE7 and earlier to push a history entry on hash-tag change.
+          // When replace is true, we don't want this.
+          if(!options.replace) this.iframe.document.open().close();
+          this._updateHash(this.iframe.location, frag, options.replace);
+        }
+
+      // If you've told us that you explicitly don't want fallback hashchange-
+      // based history, then `navigate` becomes a page refresh.
+      } else {
+        window.location.assign(this.options.root + fragment);
+      }
+      if (options.trigger) 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) {
+        location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);
+      } else {
+        location.hash = fragment;
+      }
+    }
+  });
+
+  // Backbone.View
+  // -------------
+
+  // Creating a Backbone.View creates its initial element outside of the DOM,
+  // if an existing element is not provided...
+  var View = Backbone.View = function(options) {
+    this.cid = _.uniqueId('view');
+    this._configure(options || {});
+    this._ensureElement();
+    this.initialize.apply(this, arguments);
+    this.delegateEvents();
+  };
+
+  // Cached regex to split keys for `delegate`.
+  var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+  // List of view options to be merged as properties.
+  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
+
+  // Set up all inheritable **Backbone.View** properties and methods.
+  _.extend(View.prototype, Events, {
+
+    // The default `tagName` of a View's element is `"div"`.
+    tagName: 'div',
+
+    // jQuery delegate for element lookup, scoped to DOM elements within the
+    // current view. This should be prefered to global lookups where possible.
+    $: function(selector) {
+      return this.$el.find(selector);
+    },
+
+    // Initialize is an empty function by default. Override it with your own
+    // initialization logic.
+    initialize: function(){},
+
+    // **render** is the core function that your view should override, in order
+    // to populate its element (`this.el`), with the appropriate HTML. The
+    // convention is for **render** to always return `this`.
+    render: function() {
+      return this;
+    },
+
+    // Remove this view from the DOM. Note that the view isn't present in the
+    // DOM by default, so calling this method may be a no-op.
+    remove: function() {
+      this.$el.remove();
+      return this;
+    },
+
+    // For small amounts of DOM Elements, where a full-blown template isn't
+    // needed, use **make** to manufacture elements, one at a time.
+    //
+    //     var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
+    //
+    make: function(tagName, attributes, content) {
+      var el = document.createElement(tagName);
+      if (attributes) $(el).attr(attributes);
+      if (content) $(el).html(content);
+      return el;
+    },
+
+    // Change the view's element (`this.el` property), including event
+    // re-delegation.
+    setElement: function(element, delegate) {
+      if (this.$el) this.undelegateEvents();
+      this.$el = (element instanceof $) ? element : $(element);
+      this.el = this.$el[0];
+      if (delegate !== false) this.delegateEvents();
+      return this;
+    },
+
+    // Set callbacks, where `this.events` is a hash of
+    //
+    // *{"event selector": "callback"}*
+    //
+    //     {
+    //       'mousedown .title':  'edit',
+    //       'click .button':     'save'
+    //       'click .open':       function(e) { ... }
+    //     }
+    //
+    // pairs. Callbacks will be bound to the view, with `this` set properly.
+    // Uses event delegation for efficiency.
+    // Omitting the selector binds the event to `this.el`.
+    // This only works for delegate-able events: not `focus`, `blur`, and
+    // not `change`, `submit`, and `reset` in Internet Explorer.
+    delegateEvents: function(events) {
+      if (!(events || (events = getValue(this, 'events')))) return;
+      this.undelegateEvents();
+      for (var key in events) {
+        var method = events[key];
+        if (!_.isFunction(method)) method = this[events[key]];
+        if (!method) throw new Error('Method "' + events[key] + '" does not exist');
+        var match = key.match(delegateEventSplitter);
+        var eventName = match[1], selector = match[2];
+        method = _.bind(method, this);
+        eventName += '.delegateEvents' + this.cid;
+        if (selector === '') {
+          this.$el.bind(eventName, method);
+        } else {
+          this.$el.delegate(selector, eventName, method);
+        }
+      }
+    },
+
+    // Clears all callbacks previously bound to the view with `delegateEvents`.
+    // You usually don't need to use this, but may wish to if you have multiple
+    // Backbone views attached to the same DOM element.
+    undelegateEvents: function() {
+      this.$el.unbind('.delegateEvents' + this.cid);
+    },
+
+    // Performs the initial configuration of a View with a set of options.
+    // Keys with special meaning *(model, collection, id, className)*, are
+    // attached directly to the view.
+    _configure: function(options) {
+      if (this.options) options = _.extend({}, this.options, options);
+      for (var i = 0, l = viewOptions.length; i < l; i++) {
+        var attr = viewOptions[i];
+        if (options[attr]) this[attr] = options[attr];
+      }
+      this.options = options;
+    },
+
+    // Ensure that the View has a DOM element to render into.
+    // If `this.el` is a string, pass it through `$()`, take the first
+    // matching element, and re-assign it to `el`. Otherwise, create
+    // an element from the `id`, `className` and `tagName` properties.
+    _ensureElement: function() {
+      if (!this.el) {
+        var attrs = getValue(this, 'attributes') || {};
+        if (this.id) attrs.id = this.id;
+        if (this.className) attrs['class'] = this.className;
+        this.setElement(this.make(this.tagName, attrs), false);
+      } else {
+        this.setElement(this.el, false);
+      }
+    }
+
+  });
+
+  // The self-propagating extend function that Backbone classes use.
+  var extend = function (protoProps, classProps) {
+    var child = inherits(this, protoProps, classProps);
+    child.extend = this.extend;
+    return child;
+  };
+
+  // Set up inheritance for the model, collection, and view.
+  Model.extend = Collection.extend = Router.extend = View.extend = extend;
+
+  // Backbone.sync
+  // -------------
+
+  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+  var methodMap = {
+    'create': 'POST',
+    'update': 'PUT',
+    'delete': 'DELETE',
+    'read':   'GET'
+  };
+
+  // Override this function to change the manner in which Backbone persists
+  // models to the server. You will be passed the type of request, and the
+  // model in question. By default, makes a RESTful Ajax request
+  // to the model's `url()`. Some possible customizations could be:
+  //
+  // * Use `setTimeout` to batch rapid-fire updates into a single request.
+  // * Send up the models as XML instead of JSON.
+  // * Persist models via WebSockets instead of Ajax.
+  //
+  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+  // as `POST`, with a `_method` parameter containing the true HTTP method,
+  // as well as all requests with the body as `application/x-www-form-urlencoded`
+  // instead of `application/json` with the model in a param named `model`.
+  // Useful when interfacing with server-side languages like **PHP** that make
+  // it difficult to read the body of `PUT` requests.
+  Backbone.sync = function(method, model, options) {
+    var type = methodMap[method];
+
+    // Default options, unless specified.
+    options || (options = {});
+
+    // Default JSON-request options.
+    var params = {type: type, dataType: 'json'};
+
+    // Ensure that we have a URL.
+    if (!options.url) {
+      params.url = getValue(model, 'url') || urlError();
+    }
+
+    // Ensure that we have the appropriate request data.
+    if (!options.data && model && (method == 'create' || method == 'update')) {
+      params.contentType = 'application/json';
+      params.data = JSON.stringify(model.toJSON());
+    }
+
+    // For older servers, emulate JSON by encoding the request into an HTML-form.
+    if (Backbone.emulateJSON) {
+      params.contentType = 'application/x-www-form-urlencoded';
+      params.data = params.data ? {model: params.data} : {};
+    }
+
+    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+    // And an `X-HTTP-Method-Override` header.
+    if (Backbone.emulateHTTP) {
+      if (type === 'PUT' || type === 'DELETE') {
+        if (Backbone.emulateJSON) params.data._method = type;
+        params.type = 'POST';
+        params.beforeSend = function(xhr) {
+          xhr.setRequestHeader('X-HTTP-Method-Override', type);
+        };
+      }
+    }
+
+    // Don't process data on a non-GET request.
+    if (params.type !== 'GET' && !Backbone.emulateJSON) {
+      params.processData = false;
+    }
+
+    // Make the request, allowing the user to override any Ajax options.
+    return $.ajax(_.extend(params, options));
+  };
+
+  // Wrap an optional error callback with a fallback error event.
+  Backbone.wrapError = function(onError, originalModel, options) {
+    return function(model, resp) {
+      resp = model === originalModel ? resp : model;
+      if (onError) {
+        onError(originalModel, resp, options);
+      } else {
+        originalModel.trigger('error', originalModel, resp, options);
+      }
+    };
+  };
+
+  // Helpers
+  // -------
+
+  // Shared empty constructor function to aid in prototype-chain creation.
+  var ctor = function(){};
+
+  // 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 inherits = function(parent, protoProps, staticProps) {
+    var child;
+
+    // The constructor function for the new subclass is either defined by you
+    // (the "constructor" property in your `extend` definition), or defaulted
+    // by us to simply call the parent's constructor.
+    if (protoProps && protoProps.hasOwnProperty('constructor')) {
+      child = protoProps.constructor;
+    } else {
+      child = function(){ parent.apply(this, arguments); };
+    }
+
+    // Inherit class (static) properties from parent.
+    _.extend(child, parent);
+
+    // Set the prototype chain to inherit from `parent`, without calling
+    // `parent`'s constructor function.
+    ctor.prototype = parent.prototype;
+    child.prototype = new ctor();
+
+    // Add prototype properties (instance properties) to the subclass,
+    // if supplied.
+    if (protoProps) _.extend(child.prototype, protoProps);
+
+    // Add static properties to the constructor function, if supplied.
+    if (staticProps) _.extend(child, staticProps);
+
+    // Correctly set child's `prototype.constructor`.
+    child.prototype.constructor = child;
+
+    // Set a convenience property in case the parent's prototype is needed later.
+    child.__super__ = parent.prototype;
+
+    return child;
+  };
+
+  // Helper function to get a value from a Backbone object as a property
+  // or as a function.
+  var getValue = function(object, prop) {
+    if (!(object && object[prop])) return null;
+    return _.isFunction(object[prop]) ? object[prop]() : object[prop];
+  };
+
+  // Throw an error when a URL is needed, and none is supplied.
+  var urlError = function() {
+    throw new Error('A "url" property or function must be specified');
+  };
+
+}).call(this);
diff --git a/libs/underscore.js b/libs/underscore.js
new file mode 100644
index 0000000..1ebe267
--- /dev/null
+++ b/libs/underscore.js
@@ -0,0 +1,1200 @@
+//     Underscore.js 1.4.2
+//     http://underscorejs.org
+//     (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` in the browser, or `global` on the server.
+  var root = this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Establish the object that gets returned to break out of a loop iteration.
+  var breaker = {};
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var push             = ArrayProto.push,
+      slice            = ArrayProto.slice,
+      concat           = ArrayProto.concat,
+      unshift          = ArrayProto.unshift,
+      toString         = ObjProto.toString,
+      hasOwnProperty   = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeForEach      = ArrayProto.forEach,
+    nativeMap          = ArrayProto.map,
+    nativeReduce       = ArrayProto.reduce,
+    nativeReduceRight  = ArrayProto.reduceRight,
+    nativeFilter       = ArrayProto.filter,
+    nativeEvery        = ArrayProto.every,
+    nativeSome         = ArrayProto.some,
+    nativeIndexOf      = ArrayProto.indexOf,
+    nativeLastIndexOf  = ArrayProto.lastIndexOf,
+    nativeIsArray      = Array.isArray,
+    nativeKeys         = Object.keys,
+    nativeBind         = FuncProto.bind;
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for the old `require()` API. If we're in
+  // the browser, add `_` as a global object via a string identifier,
+  // for Closure Compiler "advanced" mode.
+  if (typeof exports !== 'undefined') {
+    if (typeof module !== 'undefined' && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root['_'] = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.4.2';
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles objects with the built-in `forEach`, arrays, and raw objects.
+  // Delegates to **ECMAScript 5**'s native `forEach` if available.
+  var each = _.each = _.forEach = function(obj, iterator, context) {
+    if (obj == null) return;
+    if (nativeForEach && obj.forEach === nativeForEach) {
+      obj.forEach(iterator, context);
+    } else if (obj.length === +obj.length) {
+      for (var i = 0, l = obj.length; i < l; i++) {
+        if (iterator.call(context, obj[i], i, obj) === breaker) return;
+      }
+    } else {
+      for (var key in obj) {
+        if (_.has(obj, key)) {
+          if (iterator.call(context, obj[key], key, obj) === breaker) return;
+        }
+      }
+    }
+  };
+
+  // Return the results of applying the iterator to each element.
+  // Delegates to **ECMAScript 5**'s native `map` if available.
+  _.map = _.collect = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
+    each(obj, function(value, index, list) {
+      results[results.length] = iterator.call(context, value, index, list);
+    });
+    return results;
+  };
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
+  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
+    var initial = arguments.length > 2;
+    if (obj == null) obj = [];
+    if (nativeReduce && obj.reduce === nativeReduce) {
+      if (context) iterator = _.bind(iterator, context);
+      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
+    }
+    each(obj, function(value, index, list) {
+      if (!initial) {
+        memo = value;
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, value, index, list);
+      }
+    });
+    if (!initial) throw new TypeError('Reduce of empty array with no initial value');
+    return memo;
+  };
+
+  // The right-associative version of reduce, also known as `foldr`.
+  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
+  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+    var initial = arguments.length > 2;
+    if (obj == null) obj = [];
+    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
+      if (context) iterator = _.bind(iterator, context);
+      return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+    }
+    var length = obj.length;
+    if (length !== +length) {
+      var keys = _.keys(obj);
+      length = keys.length;
+    }
+    each(obj, function(value, index, list) {
+      index = keys ? keys[--length] : --length;
+      if (!initial) {
+        memo = obj[index];
+        initial = true;
+      } else {
+        memo = iterator.call(context, memo, obj[index], index, list);
+      }
+    });
+    if (!initial) throw new TypeError('Reduce of empty array with no initial value');
+    return memo;
+  };
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, iterator, context) {
+    var result;
+    any(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) {
+        result = value;
+        return true;
+      }
+    });
+    return result;
+  };
+
+  // Return all the elements that pass a truth test.
+  // Delegates to **ECMAScript 5**'s native `filter` if available.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
+    each(obj, function(value, index, list) {
+      if (iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, iterator, context) {
+    var results = [];
+    if (obj == null) return results;
+    each(obj, function(value, index, list) {
+      if (!iterator.call(context, value, index, list)) results[results.length] = value;
+    });
+    return results;
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Delegates to **ECMAScript 5**'s native `every` if available.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, iterator, context) {
+    iterator || (iterator = _.identity);
+    var result = true;
+    if (obj == null) return result;
+    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
+    each(obj, function(value, index, list) {
+      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
+    });
+    return !!result;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Delegates to **ECMAScript 5**'s native `some` if available.
+  // Aliased as `any`.
+  var any = _.some = _.any = function(obj, iterator, context) {
+    iterator || (iterator = _.identity);
+    var result = false;
+    if (obj == null) return result;
+    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
+    each(obj, function(value, index, list) {
+      if (result || (result = iterator.call(context, value, index, list))) return breaker;
+    });
+    return !!result;
+  };
+
+  // Determine if the array or object contains a given value (using `===`).
+  // Aliased as `include`.
+  _.contains = _.include = function(obj, target) {
+    var found = false;
+    if (obj == null) return found;
+    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
+    found = any(obj, function(value) {
+      return value === target;
+    });
+    return found;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = function(obj, method) {
+    var args = slice.call(arguments, 2);
+    return _.map(obj, function(value) {
+      return (_.isFunction(method) ? method : value[method]).apply(value, args);
+    });
+  };
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, function(value){ return value[key]; });
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // with specific `key:value` pairs.
+  _.where = function(obj, attrs) {
+    if (_.isEmpty(attrs)) return [];
+    return _.filter(obj, function(value) {
+      for (var key in attrs) {
+        if (attrs[key] !== value[key]) return false;
+      }
+      return true;
+    });
+  };
+
+  // Return the maximum element or (element-based computation).
+  // Can't optimize arrays of integers longer than 65,535 elements.
+  // See: https://bugs.webkit.org/show_bug.cgi?id=80797
+  _.max = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+      return Math.max.apply(Math, obj);
+    }
+    if (!iterator && _.isEmpty(obj)) return -Infinity;
+    var result = {computed : -Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed >= result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iterator, context) {
+    if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
+      return Math.min.apply(Math, obj);
+    }
+    if (!iterator && _.isEmpty(obj)) return Infinity;
+    var result = {computed : Infinity};
+    each(obj, function(value, index, list) {
+      var computed = iterator ? iterator.call(context, value, index, list) : value;
+      computed < result.computed && (result = {value : value, computed : computed});
+    });
+    return result.value;
+  };
+
+  // Shuffle an array.
+  _.shuffle = function(obj) {
+    var rand;
+    var index = 0;
+    var shuffled = [];
+    each(obj, function(value) {
+      rand = _.random(index++);
+      shuffled[index - 1] = shuffled[rand];
+      shuffled[rand] = value;
+    });
+    return shuffled;
+  };
+
+  // An internal function to generate lookup iterators.
+  var lookupIterator = function(value) {
+    return _.isFunction(value) ? value : function(obj){ return obj[value]; };
+  };
+
+  // Sort the object's values by a criterion produced by an iterator.
+  _.sortBy = function(obj, value, context) {
+    var iterator = lookupIterator(value);
+    return _.pluck(_.map(obj, function(value, index, list) {
+      return {
+        value : value,
+        index : index,
+        criteria : iterator.call(context, value, index, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index < right.index ? -1 : 1;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(obj, value, context, behavior) {
+    var result = {};
+    var iterator = lookupIterator(value);
+    each(obj, function(value, index) {
+      var key = iterator.call(context, value, index, obj);
+      behavior(result, key, value);
+    });
+    return result;
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = function(obj, value, context) {
+    return group(obj, value, context, function(result, key, value) {
+      (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
+    });
+  };
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = function(obj, value, context) {
+    return group(obj, value, context, function(result, key, value) {
+      if (!_.has(result, key)) result[key] = 0;
+      result[key]++;
+    });
+  };
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iterator, context) {
+    iterator = iterator == null ? _.identity : lookupIterator(iterator);
+    var value = iterator.call(context, obj);
+    var low = 0, high = array.length;
+    while (low < high) {
+      var mid = (low + high) >>> 1;
+      iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
+    }
+    return low;
+  };
+
+  // Safely convert anything iterable into a real, live array.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (obj.length === +obj.length) return slice.call(obj);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
+  };
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N. The **guard** check allows it to work with
+  // `_.map`.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array. The **guard** check allows it to work with `_.map`.
+  _.last = function(array, n, guard) {
+    if ((n != null) && !guard) {
+      return slice.call(array, Math.max(array.length - n, 0));
+    } else {
+      return array[array.length - 1];
+    }
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array. The **guard**
+  // check allows it to work with `_.map`.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, (n == null) || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, function(value){ return !!value; });
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, output) {
+    each(input, function(value) {
+      if (_.isArray(value)) {
+        shallow ? push.apply(output, value) : flatten(value, shallow, output);
+      } else {
+        output.push(value);
+      }
+    });
+    return output;
+  };
+
+  // Return a completely flattened version of an array.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, []);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = function(array) {
+    return _.difference(array, slice.call(arguments, 1));
+  };
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iterator, context) {
+    var initial = iterator ? _.map(array, iterator, context) : array;
+    var results = [];
+    var seen = [];
+    each(initial, function(value, index) {
+      if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
+        seen.push(value);
+        results.push(array[index]);
+      }
+    });
+    return results;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = function() {
+    return _.uniq(concat.apply(ArrayProto, arguments));
+  };
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    var rest = slice.call(arguments, 1);
+    return _.filter(_.uniq(array), function(item) {
+      return _.every(rest, function(other) {
+        return _.indexOf(other, item) >= 0;
+      });
+    });
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = function(array) {
+    var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
+    return _.filter(array, function(value){ return !_.contains(rest, value); });
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = function() {
+    var args = slice.call(arguments);
+    var length = _.max(_.pluck(args, 'length'));
+    var results = new Array(length);
+    for (var i = 0; i < length; i++) {
+      results[i] = _.pluck(args, "" + i);
+    }
+    return results;
+  };
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    var result = {};
+    for (var i = 0, l = list.length; i < l; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
+  // we need this function. Return the position of the first occurrence of an
+  // item in an array, or -1 if the item is not included in the array.
+  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = function(array, item, isSorted) {
+    if (array == null) return -1;
+    var i = 0, l = array.length;
+    if (isSorted) {
+      if (typeof isSorted == 'number') {
+        i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
+      } else {
+        i = _.sortedIndex(array, item);
+        return array[i] === item ? i : -1;
+      }
+    }
+    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
+    for (; i < l; i++) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
+  _.lastIndexOf = function(array, item, from) {
+    if (array == null) return -1;
+    var hasIndex = from != null;
+    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
+      return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
+    }
+    var i = (hasIndex ? from : array.length);
+    while (i--) if (array[i] === item) return i;
+    return -1;
+  };
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (arguments.length <= 1) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = arguments[2] || 1;
+
+    var len = Math.max(Math.ceil((stop - start) / step), 0);
+    var idx = 0;
+    var range = new Array(len);
+
+    while(idx < len) {
+      range[idx++] = start;
+      start += step;
+    }
+
+    return range;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Reusable constructor function for prototype setting.
+  var ctor = function(){};
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Binding with arguments is also known as `curry`.
+  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.
+  // We check for `func.bind` first, to fail fast when `func` is undefined.
+  _.bind = function bind(func, context) {
+    var bound, args;
+    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
+    if (!_.isFunction(func)) throw new TypeError;
+    args = slice.call(arguments, 2);
+    return bound = function() {
+      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
+      ctor.prototype = func.prototype;
+      var self = new ctor;
+      var result = func.apply(self, args.concat(slice.call(arguments)));
+      if (Object(result) === result) return result;
+      return self;
+    };
+  };
+
+  // Bind all of an object's methods to that object. Useful for ensuring that
+  // all callbacks defined on an object belong to it.
+  _.bindAll = function(obj) {
+    var funcs = slice.call(arguments, 1);
+    if (funcs.length == 0) funcs = _.functions(obj);
+    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
+    return obj;
+  };
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memo = {};
+    hasher || (hasher = _.identity);
+    return function() {
+      var key = hasher.apply(this, arguments);
+      return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
+    };
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = function(func, wait) {
+    var args = slice.call(arguments, 2);
+    return setTimeout(function(){ return func.apply(null, args); }, wait);
+  };
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = function(func) {
+    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
+  };
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time.
+  _.throttle = function(func, wait) {
+    var context, args, timeout, throttling, more, result;
+    var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
+    return function() {
+      context = this; args = arguments;
+      var later = function() {
+        timeout = null;
+        if (more) {
+          result = func.apply(context, args);
+        }
+        whenDone();
+      };
+      if (!timeout) timeout = setTimeout(later, wait);
+      if (throttling) {
+        more = true;
+      } else {
+        throttling = true;
+        result = func.apply(context, args);
+      }
+      whenDone();
+      return result;
+    };
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, result;
+    return function() {
+      var context = this, args = arguments;
+      var later = function() {
+        timeout = null;
+        if (!immediate) result = func.apply(context, args);
+      };
+      var callNow = immediate && !timeout;
+      clearTimeout(timeout);
+      timeout = setTimeout(later, wait);
+      if (callNow) result = func.apply(context, args);
+      return result;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = function(func) {
+    var ran = false, memo;
+    return function() {
+      if (ran) return memo;
+      ran = true;
+      memo = func.apply(this, arguments);
+      func = null;
+      return memo;
+    };
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return function() {
+      var args = [func];
+      push.apply(args, arguments);
+      return wrapper.apply(this, args);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var funcs = arguments;
+    return function() {
+      var args = arguments;
+      for (var i = funcs.length - 1; i >= 0; i--) {
+        args = [funcs[i].apply(this, args)];
+      }
+      return args[0];
+    };
+  };
+
+  // Returns a function that will only be executed after being called N times.
+  _.after = function(times, func) {
+    if (times <= 0) return func();
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Object Functions
+  // ----------------
+
+  // Retrieve the names of an object's properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = nativeKeys || function(obj) {
+    if (obj !== Object(obj)) throw new TypeError('Invalid object');
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var values = [];
+    for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
+    return values;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var pairs = [];
+    for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) {
+        obj[prop] = source[prop];
+      }
+    });
+    return obj;
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = function(obj) {
+    var copy = {};
+    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+    each(keys, function(key) {
+      if (key in obj) copy[key] = obj[key];
+    });
+    return copy;
+  };
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = function(obj) {
+    var copy = {};
+    var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
+    for (var key in obj) {
+      if (!_.contains(keys, key)) copy[key] = obj[key];
+    }
+    return copy;
+  };
+
+  // Fill in a given object with default properties.
+  _.defaults = function(obj) {
+    each(slice.call(arguments, 1), function(source) {
+      for (var prop in source) {
+        if (obj[prop] == null) obj[prop] = source[prop];
+      }
+    });
+    return obj;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
+    if (a === b) return a !== 0 || 1 / a == 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className != toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, dates, and booleans are compared by value.
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return a == String(b);
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+        // other numeric values.
+        return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a == +b;
+      // RegExps are compared by their source patterns and flags.
+      case '[object RegExp]':
+        return a.source == b.source &&
+               a.global == b.global &&
+               a.multiline == b.multiline &&
+               a.ignoreCase == b.ignoreCase;
+    }
+    if (typeof a != 'object' || typeof b != 'object') return false;
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] == a) return bStack[length] == b;
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size = 0, result = true;
+    // Recursively compare objects and arrays.
+    if (className == '[object Array]') {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      size = a.length;
+      result = size == b.length;
+      if (result) {
+        // Deep compare the contents, ignoring non-numeric properties.
+        while (size--) {
+          if (!(result = eq(a[size], b[size], aStack, bStack))) break;
+        }
+      }
+    } else {
+      // Objects with different constructors are not equivalent, but `Object`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
+                               _.isFunction(bCtor) && (bCtor instanceof bCtor))) {
+        return false;
+      }
+      // Deep compare objects.
+      for (var key in a) {
+        if (_.has(a, key)) {
+          // Count the expected number of properties.
+          size++;
+          // Deep compare each member.
+          if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
+        }
+      }
+      // Ensure that both objects contain the same number of properties.
+      if (result) {
+        for (key in b) {
+          if (_.has(b, key) && !(size--)) break;
+        }
+        result = !size;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return result;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b, [], []);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
+    for (var key in obj) if (_.has(obj, key)) return false;
+    return true;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) == '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    return obj === Object(obj);
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
+  each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) == '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return !!(obj && _.has(obj, 'callee'));
+    };
+  }
+
+  // Optimize `isFunction` if appropriate.
+  if (typeof (/./) !== 'function') {
+    _.isFunction = function(obj) {
+      return typeof obj === 'function';
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return _.isNumber(obj) && isFinite(obj);
+  };
+
+  // Is the given value `NaN`? (NaN is the only number which does not equal itself).
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && obj != +obj;
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iterators.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iterator, context) {
+    for (var i = 0; i < n; i++) iterator.call(context, i);
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + (0 | Math.random() * (max - min + 1));
+  };
+
+  // List of HTML entities for escaping.
+  var entityMap = {
+    escape: {
+      '&': '&amp;',
+      '<': '&lt;',
+      '>': '&gt;',
+      '"': '&quot;',
+      "'": '&#x27;',
+      '/': '&#x2F;'
+    }
+  };
+  entityMap.unescape = _.invert(entityMap.escape);
+
+  // Regexes containing the keys and values listed immediately above.
+  var entityRegexes = {
+    escape:   new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
+    unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
+  };
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  _.each(['escape', 'unescape'], function(method) {
+    _[method] = function(string) {
+      if (string == null) return '';
+      return ('' + string).replace(entityRegexes[method], function(match) {
+        return entityMap[method][match];
+      });
+    };
+  });
+
+  // If the value of the named property is a function then invoke it;
+  // otherwise, return it.
+  _.result = function(object, property) {
+    if (object == null) return null;
+    var value = object[property];
+    return _.isFunction(value) ? value.call(object) : value;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    each(_.functions(obj), function(name){
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return result.call(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = idCounter++;
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate    : /<%([\s\S]+?)%>/g,
+    interpolate : /<%=([\s\S]+?)%>/g,
+    escape      : /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'":      "'",
+    '\\':     '\\',
+    '\r':     'r',
+    '\n':     'n',
+    '\t':     't',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  _.template = function(text, data, settings) {
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = new RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset)
+        .replace(escaper, function(match) { return '\\' + escapes[match]; });
+      source +=
+        escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" :
+        interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" :
+        evaluate ? "';\n" + evaluate + "\n__p+='" : '';
+      index = offset + match.length;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + "return __p;\n";
+
+    try {
+      var render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    if (data) return render(data, _);
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled function source as a convenience for precompilation.
+    template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function, which will delegate to the wrapper.
+  _.chain = function(obj) {
+    return _(obj).chain();
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var result = function(obj) {
+    return this._chain ? _(obj).chain() : obj;
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
+      return result.call(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return result.call(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  _.extend(_.prototype, {
+
+    // Start chaining a wrapped Underscore object.
+    chain: function() {
+      this._chain = true;
+      return this;
+    },
+
+    // Extracts the result from a wrapped and chained object.
+    value: function() {
+      return this._wrapped;
+    }
+
+  });
+
+}).call(this);
-- 
1.7.10.4


From bf659a584eca88626191fef2538d54c33b65aaf9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Thu, 11 Oct 2012 17:13:34 -0400
Subject: [PATCH 2/4] Issue #1808076 by jessebeach: Intermediate commit before
 integrating frega's work.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 js/edit.js |  253 +++++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 158 insertions(+), 95 deletions(-)

diff --git a/js/edit.js b/js/edit.js
index 30c3f75..968b8c1 100644
--- a/js/edit.js
+++ b/js/edit.js
@@ -1,4 +1,6 @@
-(function ($) {
+(function ($, _, Backbone) {
+
+  "use strict";
 
 Drupal.edit = Drupal.edit || {};
 Drupal.edit.wysiwyg = Drupal.edit.wysiwyg || {};
@@ -8,109 +10,171 @@ Drupal.edit.wysiwyg = Drupal.edit.wysiwyg || {};
  */
 Drupal.behaviors.edit = {
   attach: function(context) {
+    var editManager = new Edit();
     $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init);
     $('#edit_view-edit-toggles').once('edit-toggle', Drupal.edit.toggle.render);
-
-    // TODO: remove this; this is to make the current prototype somewhat usable.
-    $('a.edit_view-edit-toggle').click(function() {
-      $(this).trigger('click.edit');
-    });
   }
 };
 
 Drupal.edit.const = {};
 Drupal.edit.const.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit";
 
-Drupal.edit.init = function() {
-  Drupal.edit.state = {};
-  // We always begin in view mode.
-  Drupal.edit.state.isViewing = true;
-  Drupal.edit.state.fieldBeingHighlighted = [];
-  Drupal.edit.state.fieldBeingEdited = [];
-  Drupal.edit.state.higlightedEditable = null;
-  Drupal.edit.state.editedEditable = null;
-  Drupal.edit.state.queues = {};
-  Drupal.edit.state.wysiwygReady = false;
-
-  // Build inventory.
-  var IDMapper = function() { return Drupal.edit.getID($(this)); };
-  Drupal.edit.state.fields = Drupal.edit.findEditableFields().map(IDMapper);
-  console.log('Fields:', Drupal.edit.state.fields.length, ';', Drupal.edit.state.fields);
-
-  // Form preloader.
-  Drupal.edit.state.queues.preload = Drupal.edit.findEditableFields().filter('.edit-type-form').map(IDMapper);
-  console.log('Fields with (server-generated) forms:', Drupal.edit.state.queues.preload);
-
-  // Initialize WYSIWYG, if any.
-  if (Drupal.settings.edit.wysiwyg) {
-    $(document).bind('edit-wysiwyg-ready.edit', function() {
-      Drupal.edit.state.wysiwygReady = true;
-      console.log('edit: WYSIWYG ready');
-    });
-    Drupal.edit.wysiwyg[Drupal.settings.edit.wysiwyg].init();
+/**
+ * Custom log function to prevent calls to undefined console object.
+ */
+function log (message, type) {
+  if ('console' in window) {
+    var type = type || 'log';
+    if (type in console) {
+      console[type](message);
+    }
   }
+}
 
-  // Create a backstage area.
-  $(Drupal.theme('editBackstage', {})).appendTo('body');
-
-  // Transition between view/edit states.
-  $("a.edit_view-edit-toggle").bind('click.edit', function() {
-    var wasViewing = Drupal.edit.state.isViewing;
-    var isViewing  = Drupal.edit.state.isViewing = $(this).hasClass('edit-view');
-    // Swap active class among the two links.
-    $('a.edit_view-edit-toggle').removeClass('active');
-    $('a.edit_view-edit-toggle').parent().removeClass('active');
-    $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).addClass('active');
-    $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).parent().addClass('active');
-
-    if (wasViewing && !isViewing) {
-      $(Drupal.theme('editOverlay', {}))
-      .appendTo('body')
-      .addClass('edit-animate-slow edit-animate-invisible')
-      .bind('click.edit', Drupal.edit.clickOverlay);;
-
-      var $f = Drupal.edit.findEditableFields();
-      Drupal.edit.startEditableFields($f);
-
-      // TODO: preload forms. We could do one request per form, but that's more
-      // RTTs than needed. Instead, the server should support batch requests.
-      console.log('Preloading forms that we might need!', Drupal.edit.state.queues.preload);
-
-      // Animations. Integrate with both navbar and toolbar.
-      $('#edit_overlay').css('top', $('#navbar, #toolbar').outerHeight());
-      $('#edit_overlay').removeClass('edit-animate-invisible');
-
-      // Disable contextual links in edit mode.
-      $('.contextual-links-region')
-      .addClass('edit-contextual-links-region')
-      .removeClass('contextual-links-region');
-    }
-    else if (!wasViewing && isViewing) {
-      // Animations.
-      $('#edit_overlay')
-      .addClass('edit-animate-invisible')
-      .bind(Drupal.edit.const.transitionEnd, function(e) {
-        $('#edit_overlay, .edit-form-container, .edit-toolbar-container, #edit_modal, #edit_backstage').remove();
-      });
+/**
+ * Define a Backbone model for an editable.
+ */
+var Editable = Backbone.Model.extend({
+  defaults: {
 
-      var $f = Drupal.edit.findEditableFields();
-      Drupal.edit.stopEditableFields($f);
+  },
+  initialize: function () {
 
-      // Re-enable contextual links in view mode.
-      $('.edit-contextual-links-region')
-      .addClass('contextual-links-region')
-      .removeClass('edit-contextual-links-region');
-    }
-    else {
-      // No state change.
+  }
+});
+
+/**
+ * Define a Backbone Collection for editable item instances.
+ */
+var EditableFields = Backbone.Collection.extend({
+  model: Editable,
+  getFields: function () {
+    return this;
+  },
+  getModel: function () {
+    return this.models;
+  }
+});
+
+/**
+ * The in-place-edit controller.
+ *
+ * The Edit controller is a singleton.
+ */
+function Edit () {
+  // Check for an existing instance of Edit and return it.
+  if (Edit.prototype._singletonInstance) {
+    return Edit.prototype._singletonInstance;
+  }
+  // Create a singleton reference.
+  var EditModel = Backbone.Model.extend({
+    defaults: {
+      context: 'body',
+      isViewing: true,
+      entityBeingHighlighted: [],
+      fieldBeingHighlighted: [],
+      fieldBeingEdited: [],
+      highlightedEditable: null,
+      editedEditable: null,
+      editedFieldView: null,
+      queues: [],
+      wysiwygReady: false,
+      fields: []
+    },
+    initialize: function () {
+      // Build inventory.
+      var mapper = $.proxy(this, 'IDMapper');
+      this.set('fields', this.findEditableFields().map(mapper));
+      log('Fields:', this.get('fields').length, ';', this.get('fields'));
+
+      // Form preloader.
+      this.set('queues', this.findEditableFields().filter('.edit-type-form').map(mapper));
+      log('Fields with (server-generated) forms:', this.get('queues'));
+
+      // Initialize WYSIWYG, if any.
+      if (this.get('wysiwyg')) {
+        $(document)
+          .on('edit-wysiwyg-ready.edit', $.proxy(this, 'wysiwygReadyHandler'));
+        Drupal.edit.wysiwyg[this.get('wysiwyg')].init();
+      }
+
+      // Create a backstage area.
+      $(Drupal.theme('editBackstage', {})).appendTo('body');
+
+      // Transition between view/edit states.
+      $("a.edit_view-edit-toggle").on('click.edit', $.proxy(this, 'toggleEditState'));
+    },
+    IDMapper: function (index, domElement) {
+      return this.getID($(domElement));
+    },
+    getID: function ($domElement) {
+      return $domElement.data('edit-id');
+    },
+    findEditableFields: function(context) {
+      return $('.edit-field.edit-allowed', context || this.context);
+    },
+    wysiwygReadyHandler: function (event) {
+      this.set('wysiwygReady', true);
+      log('edit: WYSIWYG ready');
+    },
+    toggleEditState: function (event) {
+      event.preventDefault();
+      var $toggle = $(event.target);
+      var wasViewing = this.get('isViewing');
+      var isViewing = $(this).hasClass('edit-view');
+      this.set('isViewing', isViewing);
+      // Swap active class among the two links.
+      $('a.edit_view-edit-toggle').removeClass('active');
+      $('a.edit_view-edit-toggle').parent().removeClass('active');
+      $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).addClass('active');
+      $('a.edit_view-edit-toggle.edit-' + (isViewing ? 'view' : 'edit')).parent().addClass('active');
+
+      if (wasViewing && !isViewing) {
+        $(Drupal.theme('editOverlay', {}))
+        .appendTo('body')
+        .addClass('edit-animate-slow edit-animate-invisible')
+        .bind('click.edit', Drupal.edit.clickOverlay);;
+
+        var $f = Drupal.edit.findEditableFields();
+        Drupal.edit.startEditableFields($f);
+
+        // TODO: preload forms. We could do one request per form, but that's more
+        // RTTs than needed. Instead, the server should support batch requests.
+        console.log('Preloading forms that we might need!', Drupal.edit.state.queues.preload);
+
+        // Animations. Integrate with both navbar and toolbar.
+        $('#edit_overlay').css('top', $('#navbar, #toolbar').outerHeight());
+        $('#edit_overlay').removeClass('edit-animate-invisible');
+
+        // Disable contextual links in edit mode.
+        $('.contextual-links-region')
+        .addClass('edit-contextual-links-region')
+        .removeClass('contextual-links-region');
+      }
+      else if (!wasViewing && isViewing) {
+        // Animations.
+        $('#edit_overlay')
+        .addClass('edit-animate-invisible')
+        .bind(Drupal.edit.const.transitionEnd, function(e) {
+          $('#edit_overlay, .edit-form-container, .edit-toolbar-container, #edit_modal, #edit_backstage').remove();
+        });
+
+        var $f = Drupal.edit.findEditableFields();
+        Drupal.edit.stopEditableFields($f);
+
+        // Re-enable contextual links in view mode.
+        $('.edit-contextual-links-region')
+        .addClass('contextual-links-region')
+        .removeClass('edit-contextual-links-region');
+      }
+      else {
+        // No state change.
+      }
     }
-    return false;
   });
-};
-
-Drupal.edit.findEditableFields = function(context) {
-  return $('.edit-field.edit-allowed', context || Drupal.settings.edit.context);
-};
+  // Return an instance of the Edit model.
+  return Edit.prototype._singletonInstance = new EditModel(Drupal.settings.edit);
+}
 
 /*
  * findEditableFields() just looks for fields that are editable, i.e. for the
@@ -137,10 +201,6 @@ Drupal.edit.findEditablesForFields = function($fields) {
   return $editables;
 };
 
-Drupal.edit.getID = function($field) {
-  return $field.data('edit-id');
-};
-
 Drupal.edit.findFieldForID = function(id, context) {
   return $('[data-edit-id="' + id + '"]', context || $('#content'));
 };
@@ -683,4 +743,7 @@ Drupal.edit.editables = {
   }
 };
 
-})(jQuery);
+// Assign the Edit singleton to the global Drupal namespace.
+Drupal.Edit = Edit;
+
+})(jQuery, _, Backbone);
-- 
1.7.10.4


From 87b0a293d11351eb743e3cc9afebab917ded4464 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Thu, 11 Oct 2012 17:24:39 -0400
Subject: [PATCH 3/4] Merging in frega's changes from ui-editables.js and
 views.js made in
 git://github.com/frega/edit-createjs.git#7.x-1.x
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 js/ui-editables.js |  341 +++++++++++++++---------
 js/views.js        |  755 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 971 insertions(+), 125 deletions(-)
 create mode 100644 js/views.js

diff --git a/js/ui-editables.js b/js/ui-editables.js
index 874a224..47f97ff 100644
--- a/js/ui-editables.js
+++ b/js/ui-editables.js
@@ -8,127 +8,30 @@
 
 Drupal.edit = Drupal.edit || {};
 
+Drupal.edit.form = {
+  create: function($editable, cb) {
+    // @todo: refactor Drupal.edit.form into *two* separate objects/classes:
+    // direct editables and form-based ones.
 
-Drupal.edit.toolbar = {
-  create: function($editable) {
-    if (Drupal.edit.toolbar.get($editable).length > 0) {
-      return false;
-    }
-    else {
-      // Render toolbar.
-      var $toolbar = $(Drupal.theme('editToolbarContainer', {
-        id: this._id($editable)
-      }));
-
-      // Insert in DOM.
-      if ($editable.css('display') == 'inline') {
-        $toolbar.prependTo($editable.offsetParent());
-
-        var pos = $editable.position();
-        Drupal.edit.toolbar.get($editable)
-        .css('left', pos.left).css('top', pos.top);
-      }
-      else {
-        $toolbar.insertBefore($editable);
+    var entity = Drupal.edit.vie.entities.get(Drupal.edit.util.getElementSubject($editable));
+    var predicate = Drupal.edit.util.getElementPredicate($editable);
+    var edit_id = entity.getSubjectUri() + ':' + predicate;
+    var $field = Drupal.edit.util.findFieldForEditable($editable);
+    // @todo: this needs to be refactored, we should have access to the view
+    // rather than the $editable/$el of the view.
+    // moreover, we should probably be de-coupling this and trigger events
+    // instead of mucking with the toolbar from here.
+    var toolbarView = Drupal.edit.state.get('editedFieldView').getToolbarView();
+    var $toolbar = toolbarView.getToolbarElement();
+    // We only create a placeholder-div/form for the form-based instances.
+    if ($field.hasClass('edit-type-form')) {
+      // Check whether this form has already been loaded.
+      if (Drupal.edit.form.get($editable).length > 0) {
+        return false;
       }
-
-      // Animate the toolbar into visibility.
-      setTimeout(function() {
-        $toolbar.removeClass('edit-animate-invisible');
-      }, 0);
-
-      // Remove any and all existing toolbars, except for any that are for a
-      // currently being edited field.
-      $('.edit-toolbar-container:not(:has(.edit-editing))')
-      .trigger('edit-toolbar-remove.edit');
-
-      // Event bindings.
-      $toolbar
-      .bind('mouseenter.edit', function(e) {
-        // Prevent triggering the entity's mouse enter event.
-        e.stopPropagation();
-      })
-      .bind('mouseleave.edit', function(e) {
-        var el = $editable[0];
-        if (e.relatedTarget != el && !jQuery.contains(el, e.relatedTarget)) {
-          console.log('triggering mouseleave on ', $editable);
-          $editable.trigger('mouseleave.edit');
-        }
-        // Prevent triggering the entity's mouse leave event.
-        e.stopPropagation();
-      })
-      // Immediate removal whenever requested.
-      // (This is necessary when showing many toolbars in rapid succession: we
-      // don't want all of them to show up!)
-      .bind('edit-toolbar-remove.edit', function(e) {
-        $toolbar.remove();
-      })
-      .delegate('.edit-toolbar, .edit-toolgroup', 'click.edit mousedown.edit', function(e) {
-        if (!$(e.target).is(':input')) {
-          return false;
-        }
-      });
-
-      return true;
-    }
-  },
-
-  get: function($editable) {
-    return ($editable.length == 0)
-      ? $([])
-      : $('#' + this._id($editable));
-  },
-
-  remove: function($editable) {
-    var $toolbar = Drupal.edit.toolbar.get($editable);
-
-    // Remove after animation.
-    $toolbar
-    .addClass('edit-animate-invisible')
-    // Prevent this toolbar from being detected *while* it is being removed.
-    .removeAttr('id')
-    .find('.edit-toolbar .edit-toolgroup')
-    .addClass('edit-animate-invisible')
-    .bind(Drupal.edit.const.transitionEnd, function(e) {
-      $toolbar.remove();
-    });
-  },
-
-  // Animate into view.
-  show: function($editable, toolgroup) {
-    this._find($editable, toolgroup).removeClass('edit-animate-invisible');
-  },
-
-  addClass: function($editable, toolgroup, classes) {
-    this._find($editable, toolgroup).addClass(classes);
-  },
-
-  removeClass: function($editable, toolgroup, classes) {
-    this._find($editable, toolgroup).removeClass(classes);
-  },
-
-  _find: function($editable, toolgroup) {
-    return Drupal.edit.toolbar.get($editable)
-           .find('.edit-toolbar .edit-toolgroup.' + toolgroup);
-  },
-
-  _id: function($editable) {
-    var edit_id = Drupal.edit.getID(Drupal.edit.findFieldForEditable($editable));
-    return 'edit-toolbar-for-' + edit_id.split(':').join('_');
-  }
-};
-
-
-Drupal.edit.form = {
-  create: function($editable) {
-    if (Drupal.edit.form.get($editable).length > 0) {
-      return false;
-    }
-    else {
-      // Indicate in the 'info' toolgroup that the form is loading. Animated.
-      setTimeout(function() {
-        Drupal.edit.toolbar.addClass($editable, 'info', 'loading');
-      }, 0);
+      // Indicate in the 'info' toolgroup that the form is loading.
+      // Drupal.edit.toolbar.addClass($editable, 'primary', 'info', 'loading');
+      toolbarView.addClass('info', 'loading');
 
       // Render form container.
       var $form = $(Drupal.theme('editFormContainer', {
@@ -144,7 +47,8 @@ Drupal.edit.form = {
         $form.css('left', pos.left).css('top', pos.top);
         // Reset the toolbar's positioning because it'll be moved inside the
         // form container.
-        Drupal.edit.toolbar.get($editable).css('left', '').css('top', '');
+        // Drupal.edit.toolbar.get($editable).css('left', '').css('top', '');
+        $toolbar.css('left', '').css('top', '');
       }
       else {
         $form.insertBefore($editable);
@@ -152,10 +56,73 @@ Drupal.edit.form = {
 
       // Move  toolbar inside .edit-form-container, to let it snap to the width
       // of the form instead of the field formatter.
-      Drupal.edit.toolbar.get($editable).detach().prependTo('.edit-form')
+      // Drupal.edit.toolbar.get($editable).detach().prependTo('.edit-form');
+      $toolbar.detach().prependTo('.edit-form');
 
-      return true;
     }
+
+    var onLoadCallback = function(status, $form) {
+      // @todo: re-factor
+      if ($field.hasClass('edit-type-form')) {
+        var formWrapperId = Drupal.edit.form._id($editable);
+        // $form.wrap('<div>');
+        $('#' + formWrapperId + ' .placeholder').replaceWith($form);
+
+        // Indicate in the 'info' toolgroup that the form has loaded.
+        // Drupal.edit.toolbar.removeClass($editable, 'primary', 'info', 'loading');
+        toolbarView.removeClass('info', 'loading');
+
+        // Detect changes in this form.
+        Drupal.edit.form.get($editable)
+          .delegate(':input', 'formUpdated.edit', function () {
+            $editable
+              .data('edit-content-changed', true)
+              .trigger('edit-content-changed.edit');
+          })
+          .delegate('input', 'keypress.edit', function (event) {
+            if (event.keyCode == 13) {
+              return false;
+            }
+          });
+        var $submit = Drupal.edit.form.get($editable).find('.edit-form-submit');
+      } else if ($field.hasClass('edit-type-direct')) {
+        // Direct forms are stuffed into #edit_backstage, apparently.
+        $('#edit_backstage').append($form);
+        var $submit = $form.find('.edit-form-submit');
+      }
+      Drupal.edit.form._setupAjaxForm($editable, $field, $submit);
+
+      // Animations.
+      // Drupal.edit.toolbar.show($editable, 'ops');
+      toolbarView.show('ops');
+      $editable.trigger('edit-form-loaded.edit');
+
+      // callback to be able to decorate / continue with the editable...
+      cb($editable, $field);
+    };
+
+    this.loadForm(entity, predicate, onLoadCallback);
+  },
+  // @todo: complete refactoring.
+  _setupAjaxForm: function($editable, $field, $submit) {
+    // Re-wire the form to handle submit.
+    var element_settings = {
+      url: $submit.closest('form').attr('action'),
+      setClick: true,
+      event: 'click.edit',
+      progress: {type:'throbber'},
+      // IPE-specific settings.
+      $editable: $editable,
+      $field: $field,
+      submit: { nocssjs : ($field.hasClass('edit-type-direct')) }
+    };
+    var base = $submit.attr('id');
+    // Removing existing Drupal.ajax-thingy.
+    if (Drupal.ajax.hasOwnProperty(base)) {
+      delete Drupal.ajax[base];
+      $editable.unbind('edit-internal.edit');
+    }
+    Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings);
   },
 
   get: function($editable) {
@@ -165,14 +132,138 @@ Drupal.edit.form = {
   },
 
   remove: function($editable) {
+    console.log('Drupal.edit.form.remove', Drupal.edit.form.get($editable));
     Drupal.edit.form.get($editable).remove();
   },
 
   _id: function($editable) {
-    var edit_id = ($editable.hasClass('edit-entity'))
-      ? Drupal.edit.getID($editable)
-      : Drupal.edit.getID(Drupal.edit.findFieldForEditable($editable));
+    var edit_id = Drupal.edit.util.getID($editable);
     return 'edit-form-for-' + edit_id.split(':').join('_');
+  },
+
+  /**
+   * Loads a drupal form for a given vieEntity
+   *
+   * @todo: we need to use this for non-form-based FieldView-instances as well.
+   * @todo: error handling etc.
+   *
+   * @param vieEntity object vieEntity
+   * @param predicate string predicate ("field name")
+   * @param callback callback callback once form is loaded callback(status, $form)
+   */
+  loadForm: function (vieEntity, predicate, callback) {
+    var edit_id = vieEntity.getSubjectUri() + ':' + predicate;
+    var $field = Drupal.edit.util.findFieldForID(edit_id);
+    var $editable = Drupal.edit.util.findEditablesForFields($field);
+    var element_settings = {
+      url      : Drupal.edit.util.calcFormURLForField(edit_id),
+      event    : 'edit-internal.edit',
+      $field   : $field,
+      $editable: $editable,
+      submit   : { nocssjs : ($field.hasClass('edit-type-direct')) },
+      progress : { type : null } // No progress indicator.
+    };
+    // Removing existing Drupal.ajax-thingy.
+    if (Drupal.ajax.hasOwnProperty(edit_id)) {
+      delete Drupal.ajax[edit_id];
+      $editable.unbind('edit-internal.edit');
+    }
+    Drupal.ajax[edit_id] = new Drupal.ajax(edit_id, $editable, element_settings);
+    // Some form of closure.
+    Drupal.ajax[edit_id].commands.edit_field_form = function(ajax, response, status) {
+      callback(status, $(response.data));
+    }
+    $editable.trigger('edit-internal.edit');
+  },
+  /**
+   * Saves (trigger submit) a edit.module form that has been loaded via
+   * Drupal.edit.form.loadForm method. Replaces the old editable with the
+   * response.
+   *
+   * @todo: validation handling.
+   *
+   * @param vieEntity object vieEntity
+   * @param predicate string  predicate ("field name")
+   * @param $editable object  editable object
+   * @param value object? form value - ignored in form-based
+   * @param callback  callback after form has been submitted cb(error, $el);
+   */
+  saveForm: function (vieEntity, predicate, $editable, value, callback) {
+    var edit_id = vieEntity.getSubjectUri() + ':' + predicate;
+    var $field = Drupal.edit.util.findFieldForID(edit_id);
+    // Handle form FormEditableFieldView
+    if ($field.hasClass('edit-type-form')) {
+      // Figure out the submit button for this form.
+      var $submit = Drupal.edit.form.get($editable).find('.edit-form-submit');
+      var base = $submit.attr('id');
+
+      // handle the saveForm callback (edit_field_form_saved).
+      var formEditableFormSubmittedCallback = function(ajax, response, status) {
+        var edit_id = vieEntity.getSubjectUri() + ':' + predicate;
+        // response.data contains the updated rendering of the field, if any.
+        if (response.data) {
+          // Stop the editing.
+          var currentEditorView = Drupal.edit.state.get('editedFieldView');
+          if (currentEditorView) {
+            currentEditorView.disableEditor();
+          }
+          // this is different from edit.module. did not understand what the
+          // stuff was about.
+          $inner = $(response.data).html();
+          $editable.html($inner);
+        }
+        // Remove this Drupal.ajax[base]?
+        delete Drupal.ajax[base];
+        callback(null, $editable);
+      }
+      // Setup of a closure to handle the response at last minute.
+      Drupal.ajax[base].commands.edit_field_form_saved = formEditableFormSubmittedCallback;
+      $submit.trigger('click.edit');
+    } else {
+      // EditableFieldView (direct or wysiwyg).
+
+      var submitDirectForm = function(value) {
+        var $submit = $('#edit_backstage form .edit-form-submit');
+        var base = $submit.attr('id');
+
+        // Shove the value into any field that isn't hidden or a submit button.
+        $('#edit_backstage form').find(':input[type!="hidden"][type!="submit"]').val(value);
+        console.log('submitDirectForm', $submit, base, $('#edit_backstage form'));
+        // Set the callback.
+        Drupal.ajax[base].commands.edit_field_form_saved = function(ajax, response, status) {
+          // Direct forms are stuffed into #edit_backstage, apparently.
+          // that's why Drupal.edit.form.remove($editable); doesn't work.
+          $('#edit_backstage form').remove();
+          // Removing Drupal.ajax-thingy.
+          delete Drupal.ajax[base];
+          // @todo: title currently returns success but no response.data.
+          if (response.data) {
+            // Stop the editing.
+            var currentEditorView = Drupal.edit.state.get('editedFieldView');
+            if (currentEditorView) {
+              currentEditorView.disableEditor();
+            }
+            callback(null, $editable);
+          } else {
+            // @todo: handle errors, empty response?
+            console.log('no response data', response, status);
+            callback(true, $editable);
+          }
+        };
+        $submit.trigger('click.edit');
+      }
+
+      // If form doesn't already exist, create it and then submit.
+      if (!Drupal.edit.form.get($editable).length > 0) {
+        Drupal.edit.form.create($editable, function($editable, $field) {
+          // Submit the value.
+          submitDirectForm(value);
+        });
+      } else {
+        // Submit the value.
+        submitDirectForm(value);
+      }
+    }
   }
 };
 
diff --git a/js/views.js b/js/views.js
new file mode 100644
index 0000000..6ed8418
--- /dev/null
+++ b/js/views.js
@@ -0,0 +1,755 @@
+(function ($) {
+  Drupal.edit = Drupal.edit || {};
+  Drupal.edit.views = Drupal.edit.views || {};
+
+  Drupal.edit.views.ToolbarView = Backbone.View.extend({
+    fieldView:null,
+    // @todo: this should be the toolbar's $el.
+    $toolbar:null,
+    initialize:function (options) {
+      this.fieldView = options.fieldView;
+    },
+    getEditable:function () {
+      return this.fieldView.$el;
+    },
+    getToolbarElement:function () {
+      return $('#' + this._id() );
+    },
+    createToolbar:function () {
+      if (this.getToolbarElement().length) {
+        return true;
+      }
+      $editable = this.getEditable();
+      // Render toolbar.
+      var $toolbar = $(Drupal.theme('editToolbarContainer', {
+        id:this._id()
+      }));
+      // Insert in DOM.
+      if ($editable.css('display') == 'inline') {
+        $toolbar.prependTo($editable.offsetParent());
+        var pos = $editable.position();
+        $toolbar.css('left', pos.left).css('top', pos.top);
+      }
+      else {
+        $toolbar.insertBefore($editable);
+      }
+
+      // Animate the toolbar into visibility.
+      setTimeout(function () {
+        $toolbar.removeClass('edit-animate-invisible');
+      }, 0);
+
+      // Remove any and all existing toolbars, except for any that are for a
+      // currently being edited field.
+      $('.edit-toolbar-container:not(:has(.edit-editing))')
+        .trigger('edit-toolbar-remove.edit');
+
+      // Event bindings.
+      $toolbar
+        .bind('mouseenter.edit', function (e) {
+          // Prevent triggering the entity's mouse enter event.
+          e.stopPropagation();
+        })
+        .bind('mouseleave.edit', function (e) {
+          var el = $editable[0];
+          if (e.relatedTarget != el && !jQuery.contains(el, e.relatedTarget)) {
+            console.log('triggering mouseleave on ', $editable);
+            $editable.trigger('mouseleave.edit');
+          }
+          // Prevent triggering the entity's mouse leave event.
+          e.stopPropagation();
+        })
+        // Immediate removal whenever requested.
+        // (This is necessary when showing many toolbars in rapid succession: we
+        // don't want all of them to show up!)
+        .bind('edit-toolbar-remove.edit', function (e) {
+          $toolbar.remove();
+        })
+        .delegate('.edit-toolbar, .edit-toolgroup', 'click.edit mousedown.edit', function (e) {
+          if (!$(e.target).is(':input')) {
+            return false;
+          }
+        });
+
+      // We get the label to show from VIE's type system
+      var label = this.fieldView.predicate;
+      var attributeDef = this.fieldView.model.get('@type').attributes.get(this.fieldView.predicate);
+      if (attributeDef && attributeDef.metadata) {
+        label = attributeDef.metadata.label;
+      }
+      var self = this;
+
+      $toolbar.find('.edit-toolbar:not(:has(.edit-toolgroup.info))')
+        .append(Drupal.theme('editToolgroup', {
+        classes:'info',
+        buttons:[
+          {
+            url:'#',
+            label:label,
+            classes:'blank-button label',
+            hasButtonRole:false
+          }
+        ]
+      }))
+        .delegate('a.label', 'click.edit', function (event) {
+          self.fieldView.$el.trigger('click.edit');
+          event.stopPropagation();
+          event.preventDefault();
+        });
+      $toolbar
+        .addClass('edit-editing')
+        .find('.edit-toolbar:not(:has(.edit-toolgroup.ops))')
+        .append(Drupal.theme('editToolgroup', {
+        classes:'ops',
+        buttons:[
+          {
+            url:'#',
+            label:Drupal.t('Save'),
+            classes:'field-save save gray-button'
+          },
+          {
+            url:'#',
+            label:'<span class="close"></span>',
+            classes:'field-close close gray-button'
+          }
+        ]
+      }))
+        .delegate('a.field-save', 'click.edit', function (event) {
+          self.fieldView.saveClicked(event);
+        })
+        .delegate('a.field-close', 'click.edit', function (event) {
+          self.fieldView.closeClicked(event);
+        });
+
+      if ($editable.hasClass('edit-type-direct-with-wysiwyg')) {
+        $toolbar
+        .find('.edit-toolbar:not(:has(.edit-toolbar-wysiwyg-tabs))')
+        .append(Drupal.theme('editToolgroup', {
+          classes: 'wysiwyg-tabs',
+          buttons: []
+        }))
+        .end()
+        .find('.edit-toolbar:not(:has(.edit-toolgroup.wysiwyg))')
+        .append(Drupal.theme('editToolgroup', {
+          classes: 'wysiwyg',
+          buttons: []
+        }));
+        this.fieldView.$el.addClass('edit-wysiwyg-attached');
+      }
+      return true;
+    },
+    // @todo: proper Backbone.remove() and unbind all events above!
+    removeToolbar:function () {
+      var $toolbar  = this.getToolbarElement();
+      // Remove after animation.
+      $toolbar
+        .addClass('edit-animate-invisible')
+        // Prevent this toolbar from being detected *while* it is being removed.
+        .removeAttr('id')
+        .find('.edit-toolbar .edit-toolgroup')
+        .addClass('edit-animate-invisible')
+        .bind(Drupal.edit.const.transitionEnd, function (e) {
+          $toolbar.remove();
+        });
+    },
+    // Animate into view.
+    show:function (toolgroup) {
+      this._find(toolgroup).removeClass('edit-animate-invisible');
+    },
+    hide:function (toolgroup) {
+      this._find(toolgroup).addClass('edit-animate-invisible');
+    },
+    addClass:function (toolgroup, classes) {
+      this._find(toolgroup).addClass(classes);
+    },
+    removeClass:function (toolgroup, classes) {
+      this._find(toolgroup).removeClass(classes);
+    },
+    _find:function (toolgroup) {
+      return this.getToolbarElement().find('.edit-toolbar .edit-toolgroup.' + toolgroup);
+    },
+    _id:function () {
+      var edit_id = Drupal.edit.util.getID(this.getEditable());
+      return 'edit-toolbar-for-' + edit_id.split(':').join('_');
+    }
+  });
+
+  Drupal.edit.views.OverlayView = Backbone.View.extend({
+    state: null,
+
+    events: {
+      'click': 'escapeEditor'
+    },
+
+    initialize: function (options) {
+      this.state = options.state;
+      _.bindAll(this, 'stateChange', 'escapeEditor');
+      this.state.bind('change:isViewing', this.stateChange);
+    },
+
+    stateChange: function () {
+      if (this.state.get('isViewing')) {
+        this.hideOverlay();
+        return;
+      }
+      this.showOverlay();
+    },
+
+    showOverlay: function () {
+      $(Drupal.theme('editOverlay', {}))
+      .appendTo('body')
+      .addClass('edit-animate-slow edit-animate-invisible')
+
+      // Animations
+      $('#edit_overlay').css('top', $('#navbar').outerHeight());
+      $('#edit_overlay').removeClass('edit-animate-invisible');
+
+      // Disable contextual links in edit mode.
+      $('.contextual-links-region')
+      .addClass('edit-contextual-links-region')
+      .removeClass('contextual-links-region');
+    },
+
+    hideOverlay: function () {
+      $('#edit_overlay')
+      .addClass('edit-animate-invisible')
+      .bind(Drupal.edit.const.transitionEnd, function (event) {
+        $('#edit_overlay, .edit-form-container, .edit-toolbar-container, #edit_modal, .edit-curtain').remove();
+      });
+
+      // Enable contextual links in edit mode.
+      $('.edit-contextual-links-region')
+      .addClass('contextual-links-region')
+      .removeClass('edit-contextual-links-region');
+    },
+
+    escapeEditor: function () {
+      var editor = this.state.get('fieldBeingEdited');
+      if (Drupal.edit.modal.get().length > 0 || editor.length === 0) {
+        return;
+      }
+
+      var editedFieldView = this.state.get('editedFieldView');
+      // No modals open and user is in edit state, close editor by
+      // triggering a click to the cancel button
+      editedFieldView.getToolbarElement().find('a.close').trigger('click.edit');
+    }
+  })
+
+  // ## EditableView
+  //
+  // This view wraps a editable DOM element, and connects it
+  // with the VIE Entity instance. Whenever the particular
+  // attribute (predicate) of the instance changes, whether
+  // due to user interaction or some AJAX call, the contents
+  // of the DOM element will be automatically updated.
+  Drupal.edit.views.EditableView = Backbone.View.extend({
+    predicate: null,
+
+    initialize: function (options) {
+      this.predicate = '<http://viejs.org/ns/' + options.predicate + '>';
+      _.bindAll(this, 'render');
+      this.model.bind('change:' + this.predicate, this.render);
+    },
+
+    render: function () {
+      this.$el.html(this.model.get(this.predicate));
+      return this;
+    }
+  });
+
+  // ## FieldView
+  //
+  // This view wraps a field, and connects it with the state of
+  // the Spark Edit module. When state changes to `edit`, the view
+  // decorates the view with the necessary DOM and classes to provide
+  // the editing tools
+  Drupal.edit.views.FieldView = Backbone.View.extend({
+    predicate: null,
+    state: null,
+    editable: false,
+    editing: false,
+    vie: null,
+    editableViews: [],
+    toolbarView: null,
+
+    events: {
+      'mouseenter': 'mouseEnter',
+      'mouseleave': 'mouseLeave'
+    },
+
+    initialize: function (options) {
+      this.state = this.options.state;
+      this.predicate = this.options.predicate;
+      this.vie = this.options.vie;
+
+      _.bindAll(this, 'stateChange', 'mouseEnter', 'mouseLeave', 'checkHighlight');
+
+      this.state.on('change:isViewing', this.stateChange);
+      this.state.on('change:fieldBeingHighlighted', this.checkHighlight);
+
+    },
+
+    buildEditableView: function () {
+      var self = this;
+      Drupal.edit.util.findEditablesForFields(this.$el).each(function () {
+        self.editableViews.push(new Drupal.edit.views.EditableView({
+          model: self.model,
+          el: this,
+          predicate: self.predicate
+        }));
+      });
+    },
+
+    stateChange: function () {
+      if (this.state.get('isViewing')) {
+        this.editable = false;
+        this.undecorate();
+        return;
+      }
+      this.editable = true;
+      this.decorate();
+    },
+
+    decorate: function () {
+      this.$el
+      .addClass('edit-animate-fast')
+      .addClass('edit-candidate edit-editable')
+      .data('edit-background-color', Drupal.edit.util.getBgColor(this.$el));
+    },
+
+    undecorate: function () {
+      // @todo: clarify: undecorating shouldn't remove edit-editable?
+      this.$el
+        .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay');
+    },
+
+    mouseEnter: function (event) {
+      if (!this.editable) {
+        return;
+      }
+      if (this.state.get('editedFieldView')) {
+        // Some field is being edited, ignore
+        return;
+      }
+      var self = this;
+      Drupal.edit.util.ignoreHoveringVia(event, '.edit-toolbar-container', function () {
+        if (!self.editing) {
+          console.log('field:mouseenter', self.model.id, self.predicate);
+          self.startHighlight();
+        }
+        event.stopPropagation();
+      });
+    },
+
+    mouseLeave: function (event) {
+      if (!this.editable) {
+        return;
+      }
+      if (this.state.get('editedFieldView')) {
+        // Some field is being edited, ignore
+        return;
+      }
+      var self = this;
+      Drupal.edit.util.ignoreHoveringVia(event, '.edit-toolbar-container', function () {
+        if (!self.editing) {
+          console.log('field:mouseleave', self.model.id, self.predicate);
+          self.stopHighlight();
+        }
+        event.stopPropagation();
+      });
+    },
+
+    startHighlight: function () {
+      console.log('startHighlight', this.model.id, this.predicate);
+
+      // Animations.
+      var self = this;
+      setTimeout(function () {
+        self.$el.addClass('edit-highlighted');
+        self.getToolbarView().show('info');
+      }, 0);
+
+      this.state.set('fieldBeingHighlighted', this.$el);
+      this.state.set('highlightedEditable', this.model.id + ':' + this.predicate);
+    },
+
+    stopHighlight: function () {
+      console.log('stopHighlight', this.model.id, this.predicate);
+      // Animations
+      this.$el.removeClass('edit-highlighted');
+      this.state.set('fieldBeingHighlighted', []);
+      this.state.set('highlightedEditable', null);
+      // hide info
+      this.disableToolbar();
+
+    },
+
+    checkHighlight: function () {
+      return;
+      if (this.state.get('fieldBeingHighlighted') === this.$el) {
+        return;
+      }
+      this.stopHighlight();
+    },
+    enableToolbar: function () {
+      if (!this.toolbarView) {
+        this.toolbarView = new Drupal.edit.views.ToolbarView({
+          fieldView: this
+        });
+      }
+      this.toolbarView.createToolbar();
+    },
+    disableToolbar: function() {
+      if (this.toolbarView) {
+        this.toolbarView.removeToolbar();
+        this.toolbarView.remove();
+        // @todo: make sure everything is unbound.
+        delete this.toolbarView;
+      }
+    },
+    getToolbarView: function() {
+      if (!this.toolbarView) {
+        this.enableToolbar();
+      }
+      return this.toolbarView;
+    },
+    getToolbarElement: function() {
+      return this.getToolbarView().getToolbarElement();
+    }
+  });
+
+  // ## EditableFieldView
+  //
+  // This element is a subtype of the FieldView that adds the controlling
+  // needed for direct editables (as provided by Create.js editable widget)
+  // to the FieldView
+  Drupal.edit.views.EditableFieldView = Drupal.edit.views.FieldView.extend({
+
+    events: {
+      'mouseenter': 'mouseEnter',
+      'mouseleave': 'mouseLeave',
+      'click':      'enableEditor',
+      'createeditableenable': 'editorEnabled',
+      'createeditabledisable': 'editorDisabled',
+      'createeditablechanged': 'contentChanged'
+    },
+
+    initialize: function (options) {
+      this.state = this.options.state;
+      this.predicate = this.options.predicate;
+      this.vie = this.options.vie;
+
+      _.bindAll(this, 'stateChange', 'mouseEnter', 'mouseLeave', 'checkHighlight', 'enableEditor', 'editorEnabled', 'editorDisabled', 'contentChanged');
+
+      this.state.on('change:isViewing', this.stateChange);
+      this.state.on('change:fieldBeingHighlighted', this.checkHighlight);
+    },
+
+    stateChange: function () {
+      if (this.state.get('isViewing')) {
+        this.stopEditable();
+        return;
+      }
+      this.startEditable();
+    },
+
+    // Entered edit state
+    startEditable: function () {
+      this.editable = true;
+
+       this.$el.createEditable({
+          model: this.model,
+          vie: this.vie,
+          disabled: true
+        });
+
+      this.decorate();
+    },
+
+    // Left edit state
+    stopEditable: function () {
+      if (!this.editable) {
+        return;
+      }
+
+      this.editable = false;
+
+      this.disableEditor();
+      this.undecorate();
+    },
+
+    enableEditor: function (event) {
+      if (!this.editable) {
+        // Not in edit state, ignore
+        return;
+      }
+
+      if (this.editing) {
+        // Already editing, ignore
+        return;
+      }
+
+      if (event) {
+        event.stopPropagation();
+        event.preventDefault();
+      }
+
+      this.startHighlight();
+
+      this.$el
+      .addClass('edit-editing')
+      .css('background-color', this.$el.data('edit-background-color'));
+
+      // Ensure others are not editable when we are
+      if (this.state.get('editedFieldView')) {
+        this.state.get('editedFieldView').disableEditor();
+      }
+      // @todo: we currently need to set this to access the current FieldView
+      // in ui-editable.js which is horrible.
+      this.state.set('editedFieldView', this);
+      // Start the Create.js editable widget
+      this.enableEditableWidget();
+      // Enable the toolbar with the save and close buttons
+      this.enableToolbar();
+
+      this.state.set('fieldBeingEdited', this.$el);
+      this.state.set('editedEditable', Drupal.edit.util.getID(this.$el));
+      this.state.set('editedFieldView', this);
+    },
+
+    enableEditableWidget: function () {
+      this.$el.createEditable({
+        vie: this.vie,
+        disabled: false
+      });
+    },
+
+    disableEditor: function () {
+      console.log('disableEditor', this.model.id, this.predicate);
+
+      this.$el
+      .removeClass('edit-editing')
+      .css('background-color', '');
+
+      // TODO: Restore curtain height
+
+      // Stop the Create.js editable widget
+      this.disableEditableWidget();
+      this.disableToolbar();
+
+      $('#edit_backstage form').remove();
+
+      this.state.set('fieldBeingEdited', []);
+      this.state.set('editedEditable', null);
+      this.state.set('editedFieldView', null);
+    },
+
+    disableEditableWidget: function () {
+      this.$el.createEditable({
+        vie: this.vie,
+        disabled: true
+      });
+    },
+
+    editorEnabled: function () {
+      console.log("editorenabled", this.model.id, this.predicate);
+      // Avoid re-"padding" of editable.
+      if (!this.editing) {
+        this.padEditable();
+      }
+
+      this.getToolbarView().show('wysiwyg-tabs');
+      this.getToolbarView().show('wysiwyg');
+      // Show the ops (save, close) as well.
+      this.getToolbarView().show('ops');
+      // hmm, why in the DOM?
+      this.$el.data('edit-content-changed', false);
+      this.$el.trigger('edit-form-loaded.edit');
+      this.editing = true;
+    },
+
+    saveClicked: function (event) {
+      this.$el.blur();
+      if (event) {
+        event.stopPropagation();
+        event.preventDefault();
+      }
+      // Find entity and predicate.
+      var entity = Drupal.edit.vie.entities.get(Drupal.edit.util.getElementSubject(this.$el));
+      var predicate = this.predicate;
+      // Drupal.edit.form.saveForm loads and saves form if necessary.
+      Drupal.edit.form.saveForm(entity, predicate, this.$el, this.model.get(this.predicate), function() {
+        // Editable has been saved.
+      });
+    },
+
+    closeClicked: function (event) {
+      event.stopPropagation();
+      event.preventDefault();
+      // @TODO - handle dirty state.
+      // Disable the editor for the time being, but allow the editable to be
+      // re-enabled on click if needed.
+      this.disableEditor();
+    },
+
+    padEditable: function () {
+      var self = this;
+      // Add 5px padding for readability. This means we'll freeze the current
+      // width and *then* add 5px padding, hence ensuring the padding is added "on
+      // the outside".
+      // 1) Freeze the width (if it's not already set); don't use animations.
+      if (this.$el[0].style.width === "") {
+        this.$el
+        .data('edit-width-empty', true)
+        .addClass('edit-animate-disable-width')
+        .css('width', this.$el.width());
+      }
+
+      // 2) Add padding; use animations.
+      var posProp = Drupal.edit.util.getPositionProperties(this.$el);
+      var $toolbar = this.getToolbarElement();
+      setTimeout(function() {
+        // Re-enable width animations (padding changes affect width too!).
+        self.$el.removeClass('edit-animate-disable-width');
+
+        // The whole toolbar must move to the top when it's an inline editable.
+        if (self.$el.css('display') == 'inline') {
+          $toolbar.css('top', parseFloat($toolbar.css('top')) - 5 + 'px');
+        }
+
+        // @todo: adjust this according to the new
+        // Drupal.theme.prototype.editToolbarContainer
+        // The toolbar must move to the top and the left.
+        var $hf = $toolbar.find('.edit-toolbar-heightfaker');
+        $hf.css({ bottom: '6px', left: '-5px' });
+        // When using a WYSIWYG editor, the width of the toolbar must match the
+        // width of the editable.
+        if (self.$el.hasClass('edit-type-direct-with-wysiwyg')) {
+          $hf.css({ width: self.$el.width() + 10 });
+        }
+
+        // Pad the editable.
+        self.$el
+        .css({
+          'position': 'relative',
+          'top':  posProp['top']  - 5 + 'px',
+          'left': posProp['left'] - 5 + 'px',
+          'padding-top'   : posProp['padding-top']    + 5 + 'px',
+          'padding-left'  : posProp['padding-left']   + 5 + 'px',
+          'padding-right' : posProp['padding-right']  + 5 + 'px',
+          'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
+          'margin-bottom':  posProp['margin-bottom'] - 10 + 'px'
+        });
+      }, 0);
+    },
+
+    unpadEditable: function () {
+      var self = this;
+
+      // 1) Set the empty width again.
+      if (this.$el.data('edit-width-empty') === true) {
+        console.log('restoring width');
+        this.$el
+        .addClass('edit-animate-disable-width')
+        .css('width', '');
+      }
+
+      // 2) Remove padding; use animations (these will run simultaneously with)
+      // the fading out of the toolbar as its gets removed).
+      var posProp = Drupal.edit.util.getPositionProperties(this.$el);
+      var $toolbar = this.getToolbarElement();
+
+      setTimeout(function() {
+        // Re-enable width animations (padding changes affect width too!).
+        self.$el.removeClass('edit-animate-disable-width');
+
+        // Move the toolbar back to its original position.
+        var $hf = $toolbar.find('.edit-toolbar-heightfaker');
+        $hf.css({ bottom: '1px', left: '' });
+        // When using a WYSIWYG editor, restore the width of the toolbar.
+        if (self.$el.hasClass('edit-type-direct-with-wysiwyg')) {
+          $hf.css({ width: '' });
+        }
+        // Undo our changes to the clipping (to prevent the bottom box-shadow).
+        $toolbar
+        .undelegate('.edit-toolbar', Drupal.edit.const.transitionEnd)
+        .find('.edit-toolbar').css('clip', '');
+
+        // Unpad the editable.
+        self.$el
+        .css({
+          'position': 'relative',
+          'top':  posProp['top']  + 5 + 'px',
+          'left': posProp['left'] + 5 + 'px',
+          'padding-top'   : posProp['padding-top']    - 5 + 'px',
+          'padding-left'  : posProp['padding-left']   - 5 + 'px',
+          'padding-right' : posProp['padding-right']  - 5 + 'px',
+          'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
+          'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
+        });
+      }, 0);
+    },
+
+    editorDisabled: function () {
+      // Avoid re-"unpadding" of editable.
+      if (this.editing) {
+        this.unpadEditable();
+      }
+      this.$el.removeClass('ui-state-disabled');
+      this.$el.removeClass('edit-wysiwyg-attached');
+
+      this.editing = false;
+    },
+
+    contentChanged: function () {
+      this.$el.data('edit-content-changed', true);
+      this.$el.trigger('edit-content-changed.edit');
+
+      this.getToolbarElement()
+      .find('a.save')
+      .addClass('blue-button')
+      .removeClass('gray-button')
+    }
+  });
+
+  // ## FormEditableFieldView
+  //
+  // This view is a subtype of the FieldView that is used for the
+  // elements Spark edits via regular Drupal forms.
+  Drupal.edit.views.FormEditableFieldView = Drupal.edit.views.EditableFieldView.extend({
+
+    enableEditableWidget: function () {
+      this.$el.createEditable({
+        vie: this.vie,
+        disabled: false
+      });
+    },
+
+    disableEditableWidget: function () {
+      this.$el.createEditable({
+        vie: this.vie,
+        disabled: true
+      });
+    },
+
+    saveClicked: function (event) {
+      // Stop events.
+      if (event) {
+        event.stopPropagation();
+        event.preventDefault();
+      }
+
+      var value = this.model.get(this.predicate);
+      var entity = Drupal.edit.vie.entities.get(Drupal.edit.util.getElementSubject(this.$el));
+      var that = this;
+
+      Drupal.edit.form.saveForm(entity, this.predicate, this.$el, null, function(error, $el) {
+        // Restart the editable.
+        that.startEditable();
+      });
+    }
+
+  });
+
+})(jQuery);
-- 
1.7.10.4


From ea78fe76da6900f8cb8247e7aa31e811c54d06f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Thu, 11 Oct 2012 19:06:57 -0400
Subject: [PATCH 4/4] Completed the initial port of the Drupal.edit object to
 a Drupal.Edit API framework. The edit application is
 Backbone-ified to the point that we can now start
 incorporating frega's editables and views work.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 js/edit.js |  283 +++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 156 insertions(+), 127 deletions(-)

diff --git a/js/edit.js b/js/edit.js
index 968b8c1..3d9b5c5 100644
--- a/js/edit.js
+++ b/js/edit.js
@@ -1,73 +1,65 @@
-(function ($, _, Backbone) {
+(function ($, _, Backbone, undefined) {
 
   "use strict";
 
-Drupal.edit = Drupal.edit || {};
-Drupal.edit.wysiwyg = Drupal.edit.wysiwyg || {};
-
-/**
- * Attach toggling behavior and in-place editing.
- */
-Drupal.behaviors.edit = {
-  attach: function(context) {
-    var editManager = new Edit();
-    $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init);
-    $('#edit_view-edit-toggles').once('edit-toggle', Drupal.edit.toggle.render);
-  }
-};
-
-Drupal.edit.const = {};
-Drupal.edit.const.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit";
-
-/**
- * Custom log function to prevent calls to undefined console object.
- */
-function log (message, type) {
-  if ('console' in window) {
-    var type = type || 'log';
-    if (type in console) {
-      console[type](message);
+  Drupal.edit = Drupal.edit || {};
+  Drupal.edit.wysiwyg = Drupal.edit.wysiwyg || {};
+
+  /**
+   * Attach toggling behavior and in-place editing.
+   */
+  Drupal.behaviors.edit = {
+    attach: function(context) {
+      var app = Edit.getApplicationInstance(Drupal.settings.edit);
+      $('#edit_view-edit-toggles').once('edit-init', Drupal.edit.init);
+      $('#edit_view-edit-toggles').once('edit-toggle', Drupal.edit.toggle.render);
+    }
+  };
+
+  Drupal.edit.const = {};
+  Drupal.edit.const.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit";
+
+  /**
+   * Custom log function to prevent calls to undefined console object.
+   */
+  function log (message, type) {
+    if ('console' in window) {
+      var type = type || 'log';
+      if (type in console) {
+        console[type](message);
+      }
     }
   }
-}
 
-/**
- * Define a Backbone model for an editable.
- */
-var Editable = Backbone.Model.extend({
-  defaults: {
+  /**
+   * Define a Backbone model for an editable.
+   */
+  var Editable = Backbone.Model.extend({
+    defaults: {
 
-  },
-  initialize: function () {
+    },
+    initialize: function () {
 
-  }
-});
+    }
+  });
 
-/**
- * Define a Backbone Collection for editable item instances.
- */
-var EditableFields = Backbone.Collection.extend({
-  model: Editable,
-  getFields: function () {
-    return this;
-  },
-  getModel: function () {
-    return this.models;
-  }
-});
+  /**
+   * Define a Backbone Collection for editable item instances.
+   */
+  var EditableFields = Backbone.Collection.extend({
+    model: Editable,
+    getFields: function () {
+      return this;
+    },
+    getModel: function () {
+      return this.models;
+    }
+  });
 
-/**
- * The in-place-edit controller.
- *
- * The Edit controller is a singleton.
- */
-function Edit () {
-  // Check for an existing instance of Edit and return it.
-  if (Edit.prototype._singletonInstance) {
-    return Edit.prototype._singletonInstance;
-  }
-  // Create a singleton reference.
-  var EditModel = Backbone.Model.extend({
+  /**
+   * The Edit application Model.
+   */
+  var EditApp = Backbone.Model.extend({
     defaults: {
       context: 'body',
       isViewing: true,
@@ -113,6 +105,42 @@ function Edit () {
     findEditableFields: function(context) {
       return $('.edit-field.edit-allowed', context || this.context);
     },
+    startEditableFields: function($fields) {
+      var $fields = $fields.once('edit');
+      // Ignore fields that need a WYSIWYG editor if no WYSIWYG editor is present
+      if (!this.get('wysiwyg')) {
+        $fields = $fields.filter(':not(.edit-type-direct-with-wysiwyg)');
+      }
+      this
+      .findEditablesForFields($fields)
+      .addClass('edit-animate-fast')
+      .addClass('edit-candidate edit-editable')
+      .bind('mouseenter.edit', function(e) {
+        var $editable = $(this);
+        Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() {
+          log('field:mouseenter');
+          if (!$editable.hasClass('edit-editing')) {
+            Drupal.edit.editables.startHighlight($editable);
+          }
+        });
+      })
+      .bind('mouseleave.edit', function(e) {
+        var $editable = $(this);
+        Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() {
+          log('field:mouseleave');
+          if (!$editable.hasClass('edit-editing')) {
+            Drupal.edit.editables.stopHighlight($editable);
+          }
+        });
+      })
+      .bind('click.edit', function() {
+        Drupal.edit.editables.startEdit($(this)); return false;
+      })
+      // Some transformations are editable-specific.
+      .map(function() {
+        $(this).data('edit-background-color', Drupal.edit.util.getBgColor($(this)));
+      });
+    },
     wysiwygReadyHandler: function (event) {
       this.set('wysiwygReady', true);
       log('edit: WYSIWYG ready');
@@ -135,12 +163,12 @@ function Edit () {
         .addClass('edit-animate-slow edit-animate-invisible')
         .bind('click.edit', Drupal.edit.clickOverlay);;
 
-        var $f = Drupal.edit.findEditableFields();
-        Drupal.edit.startEditableFields($f);
+        var $f = this.findEditableFields();
+        this.startEditableFields($f);
 
         // TODO: preload forms. We could do one request per form, but that's more
         // RTTs than needed. Instead, the server should support batch requests.
-        console.log('Preloading forms that we might need!', Drupal.edit.state.queues.preload);
+        log('Preloading forms that we might need!', this.get('queues'));
 
         // Animations. Integrate with both navbar and toolbar.
         $('#edit_overlay').css('top', $('#navbar, #toolbar').outerHeight());
@@ -170,36 +198,75 @@ function Edit () {
       else {
         // No state change.
       }
+    },
+    /*
+     * findEditableFields() just looks for fields that are editable, i.e. for the
+     * field *wrappers*. Depending on the field, however, either the whole field wrapper
+     * will be marked as editable (in this case, an inline form will be used for editing),
+     * *or* a specific (field-specific even!) DOM element within that field wrapper will be
+     * marked as editable.
+     * This function is for finding the *editables* themselves, given the *editable fields*.
+     */
+    findEditablesForFields: function($fields) {
+      var $editables = $();
+
+      // type = form
+      $editables = $editables.add($fields.filter('.edit-type-form'));
+
+      // type = direct
+      var $direct = $fields.filter('.edit-type-direct');
+      $editables = $editables.add($direct.find('.field-item'));
+      // Edge case: "title" pseudofield on pages with lists of nodes.
+      $editables = $editables.add($direct.filter('h2').find('a'));
+      // Edge case: "title" pseudofield on node pages.
+      $editables = $editables.add($direct.find('h1'));
+
+      return $editables;
     }
   });
-  // Return an instance of the Edit model.
-  return Edit.prototype._singletonInstance = new EditModel(Drupal.settings.edit);
-}
 
-/*
- * findEditableFields() just looks for fields that are editable, i.e. for the
- * field *wrappers*. Depending on the field, however, either the whole field wrapper
- * will be marked as editable (in this case, an inline form will be used for editing),
- * *or* a specific (field-specific even!) DOM element within that field wrapper will be
- * marked as editable.
- * This function is for finding the *editables* themselves, given the *editable fields*.
- */
-Drupal.edit.findEditablesForFields = function($fields) {
-  var $editables = $();
-
-  // type = form
-  $editables = $editables.add($fields.filter('.edit-type-form'));
-
-  // type = direct
-  var $direct = $fields.filter('.edit-type-direct');
-  $editables = $editables.add($direct.find('.field-item'));
-  // Edge case: "title" pseudofield on pages with lists of nodes.
-  $editables = $editables.add($direct.filter('h2').find('a'));
-  // Edge case: "title" pseudofield on node pages.
-  $editables = $editables.add($direct.find('h1'));
+  // Define a variable to hold the application instance. This is kept out of
+  // scope of the Edit object so that it is immutable once invoked.
+  var application;
+  /**
+   * The in-place-edit controller.
+   */
+  var Edit = {
+    /**
+     * The application is a singleton.
+     */
+    getApplicationInstance: function (options) {
+      // Check for an existing instance of Edit.
+      if (application !== undefined) {
+        // Set additional options on the instance.
+        if (options !== undefined && typeof options === 'object') {
+          var opt;
+          for (opt in options) {
+            if (options.hasOwnProperty(opt)) {
+              application.set(opt, options[opt]);
+            }
+          }
+        }
+        return application;
+      }
+      // Create a singleton reference EditApp model.
+      return application = new EditApp(options || {});
+    },
+    /**
+     * Add properties to the application instance.
+     */
+    extend: function (extensions) {
+      if (application !== undefined && extensions !== undefined && typeof extensions === 'object') {
+          var ext;
+          for (ext in extensions) {
+            if (extensions.hasOwnProperty(ext)) {
+              EditApp.prototype[ext] = extensions[ext];
+            }
+          }
+        }
+    }
+  }
 
-  return $editables;
-};
 
 Drupal.edit.findFieldForID = function(id, context) {
   return $('[data-edit-id="' + id + '"]', context || $('#content'));
@@ -209,44 +276,6 @@ Drupal.edit.findFieldForEditable = function($editable) {
   return $editable.filter('.edit-type-form').length ? $editable : $editable.closest('.edit-type-direct');
 };
 
-Drupal.edit.startEditableFields = function($fields) {
-  var $fields = $fields.once('edit');
-  // Ignore fields that need a WYSIWYG editor if no WYSIWYG editor is present
-  if (!Drupal.settings.edit.wysiwyg) {
-    $fields = $fields.filter(':not(.edit-type-direct-with-wysiwyg)');
-  }
-  var $editables = Drupal.edit.findEditablesForFields($fields);
-
-  $editables
-  .addClass('edit-animate-fast')
-  .addClass('edit-candidate edit-editable')
-  .bind('mouseenter.edit', function(e) {
-    var $editable = $(this);
-    Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() {
-      console.log('field:mouseenter');
-      if (!$editable.hasClass('edit-editing')) {
-        Drupal.edit.editables.startHighlight($editable);
-      }
-    });
-  })
-  .bind('mouseleave.edit', function(e) {
-    var $editable = $(this);
-    Drupal.edit.util.ignoreHoveringVia(e, '.edit-toolbar-container', function() {
-      console.log('field:mouseleave');
-      if (!$editable.hasClass('edit-editing')) {
-        Drupal.edit.editables.stopHighlight($editable);
-      }
-    });
-  })
-  .bind('click.edit', function() {
-    Drupal.edit.editables.startEdit($(this)); return false;
-  })
-  // Some transformations are editable-specific.
-  .map(function() {
-    $(this).data('edit-background-color', Drupal.edit.util.getBgColor($(this)));
-  });
-};
-
 Drupal.edit.stopEditableFields = function($fields) {
   var $editables = Drupal.edit.findEditablesForFields($fields);
 
-- 
1.7.10.4

