Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / lib / backbone.js @ 91c7b930

History | View | Annotate | Download (43.7 kB)

1 8d08f18a Kostas Papadimitriou
//     Backbone.js 0.5.3
2 8d08f18a Kostas Papadimitriou
//     (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.
3 8d08f18a Kostas Papadimitriou
//     Backbone may be freely distributed under the MIT license.
4 8d08f18a Kostas Papadimitriou
//     For all details and documentation:
5 8d08f18a Kostas Papadimitriou
//     http://documentcloud.github.com/backbone
6 8d08f18a Kostas Papadimitriou
7 8d08f18a Kostas Papadimitriou
(function(){
8 8d08f18a Kostas Papadimitriou
9 8d08f18a Kostas Papadimitriou
  // Initial Setup
10 8d08f18a Kostas Papadimitriou
  // -------------
11 8d08f18a Kostas Papadimitriou
12 8d08f18a Kostas Papadimitriou
  // Save a reference to the global object.
13 8d08f18a Kostas Papadimitriou
  var root = this;
14 8d08f18a Kostas Papadimitriou
15 8d08f18a Kostas Papadimitriou
  // Save the previous value of the `Backbone` variable.
16 8d08f18a Kostas Papadimitriou
  var previousBackbone = root.Backbone;
17 8d08f18a Kostas Papadimitriou
18 2f834e7d Kostas Papadimitriou
  // Create a local reference to slice.
19 2f834e7d Kostas Papadimitriou
  var slice = Array.prototype.slice;
20 2f834e7d Kostas Papadimitriou
21 8d08f18a Kostas Papadimitriou
  // The top-level namespace. All public Backbone classes and modules will
22 8d08f18a Kostas Papadimitriou
  // be attached to this. Exported for both CommonJS and the browser.
23 8d08f18a Kostas Papadimitriou
  var Backbone;
24 8d08f18a Kostas Papadimitriou
  if (typeof exports !== 'undefined') {
25 8d08f18a Kostas Papadimitriou
    Backbone = exports;
26 8d08f18a Kostas Papadimitriou
  } else {
27 8d08f18a Kostas Papadimitriou
    Backbone = root.Backbone = {};
28 8d08f18a Kostas Papadimitriou
  }
29 8d08f18a Kostas Papadimitriou
30 8d08f18a Kostas Papadimitriou
  // Current version of the library. Keep in sync with `package.json`.
31 8d08f18a Kostas Papadimitriou
  Backbone.VERSION = '0.5.3';
32 8d08f18a Kostas Papadimitriou
33 8d08f18a Kostas Papadimitriou
  // Require Underscore, if we're on the server, and it's not already present.
34 8d08f18a Kostas Papadimitriou
  var _ = root._;
35 8d08f18a Kostas Papadimitriou
  if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;
36 8d08f18a Kostas Papadimitriou
37 8d08f18a Kostas Papadimitriou
  // For Backbone's purposes, jQuery, Zepto, or Ender owns the `$` variable.
38 8d08f18a Kostas Papadimitriou
  var $ = root.jQuery || root.Zepto || root.ender;
39 8d08f18a Kostas Papadimitriou
40 8d08f18a Kostas Papadimitriou
  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
41 8d08f18a Kostas Papadimitriou
  // to its previous owner. Returns a reference to this Backbone object.
42 8d08f18a Kostas Papadimitriou
  Backbone.noConflict = function() {
43 8d08f18a Kostas Papadimitriou
    root.Backbone = previousBackbone;
44 8d08f18a Kostas Papadimitriou
    return this;
45 8d08f18a Kostas Papadimitriou
  };
46 8d08f18a Kostas Papadimitriou
47 8d08f18a Kostas Papadimitriou
  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will
48 8d08f18a Kostas Papadimitriou
  // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a
49 8d08f18a Kostas Papadimitriou
  // `X-Http-Method-Override` header.
50 8d08f18a Kostas Papadimitriou
  Backbone.emulateHTTP = false;
51 8d08f18a Kostas Papadimitriou
52 8d08f18a Kostas Papadimitriou
  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
53 8d08f18a Kostas Papadimitriou
  // `application/json` requests ... will encode the body as
54 8d08f18a Kostas Papadimitriou
  // `application/x-www-form-urlencoded` instead and will send the model in a
55 8d08f18a Kostas Papadimitriou
  // form param named `model`.
56 8d08f18a Kostas Papadimitriou
  Backbone.emulateJSON = false;
57 8d08f18a Kostas Papadimitriou
58 8d08f18a Kostas Papadimitriou
  // Backbone.Events
59 8d08f18a Kostas Papadimitriou
  // -----------------
60 8d08f18a Kostas Papadimitriou
61 8d08f18a Kostas Papadimitriou
  // A module that can be mixed in to *any object* in order to provide it with
62 8d08f18a Kostas Papadimitriou
  // custom events. You may `bind` or `unbind` a callback function to an event;
63 8d08f18a Kostas Papadimitriou
  // `trigger`-ing an event fires all callbacks in succession.
64 8d08f18a Kostas Papadimitriou
  //
65 8d08f18a Kostas Papadimitriou
  //     var object = {};
66 8d08f18a Kostas Papadimitriou
  //     _.extend(object, Backbone.Events);
67 8d08f18a Kostas Papadimitriou
  //     object.bind('expand', function(){ alert('expanded'); });
68 8d08f18a Kostas Papadimitriou
  //     object.trigger('expand');
69 8d08f18a Kostas Papadimitriou
  //
70 8d08f18a Kostas Papadimitriou
  Backbone.Events = {
71 8d08f18a Kostas Papadimitriou
72 8d08f18a Kostas Papadimitriou
    // Bind an event, specified by a string name, `ev`, to a `callback` function.
73 8d08f18a Kostas Papadimitriou
    // Passing `"all"` will bind the callback to all events fired.
74 8d08f18a Kostas Papadimitriou
    bind : function(ev, callback, context) {
75 8d08f18a Kostas Papadimitriou
      var calls = this._callbacks || (this._callbacks = {});
76 2f834e7d Kostas Papadimitriou
      var list  = calls[ev] || (calls[ev] = {});
77 2f834e7d Kostas Papadimitriou
      var tail = list.tail || (list.tail = list.next = {});
78 2f834e7d Kostas Papadimitriou
      tail.callback = callback;
79 2f834e7d Kostas Papadimitriou
      tail.context = context;
80 2f834e7d Kostas Papadimitriou
      list.tail = tail.next = {};
81 8d08f18a Kostas Papadimitriou
      return this;
82 8d08f18a Kostas Papadimitriou
    },
83 8d08f18a Kostas Papadimitriou
84 8d08f18a Kostas Papadimitriou
    // Remove one or many callbacks. If `callback` is null, removes all
85 8d08f18a Kostas Papadimitriou
    // callbacks for the event. If `ev` is null, removes all bound callbacks
86 8d08f18a Kostas Papadimitriou
    // for all events.
87 8d08f18a Kostas Papadimitriou
    unbind : function(ev, callback) {
88 2f834e7d Kostas Papadimitriou
      var calls, node, prev;
89 8d08f18a Kostas Papadimitriou
      if (!ev) {
90 2f834e7d Kostas Papadimitriou
        this._callbacks = null;
91 8d08f18a Kostas Papadimitriou
      } else if (calls = this._callbacks) {
92 8d08f18a Kostas Papadimitriou
        if (!callback) {
93 2f834e7d Kostas Papadimitriou
          calls[ev] = {};
94 2f834e7d Kostas Papadimitriou
        } else if (node = calls[ev]) {
95 2f834e7d Kostas Papadimitriou
          while ((prev = node) && (node = node.next)) {
96 2f834e7d Kostas Papadimitriou
            if (node.callback !== callback) continue;
97 2f834e7d Kostas Papadimitriou
            prev.next = node.next;
98 2f834e7d Kostas Papadimitriou
            node.context = node.callback = null;
99 2f834e7d Kostas Papadimitriou
            break;
100 8d08f18a Kostas Papadimitriou
          }
101 8d08f18a Kostas Papadimitriou
        }
102 8d08f18a Kostas Papadimitriou
      }
103 8d08f18a Kostas Papadimitriou
      return this;
104 8d08f18a Kostas Papadimitriou
    },
105 8d08f18a Kostas Papadimitriou
106 8d08f18a Kostas Papadimitriou
    // Trigger an event, firing all bound callbacks. Callbacks are passed the
107 8d08f18a Kostas Papadimitriou
    // same arguments as `trigger` is, apart from the event name.
108 8d08f18a Kostas Papadimitriou
    // Listening for `"all"` passes the true event name as the first argument.
109 8d08f18a Kostas Papadimitriou
    trigger : function(eventName) {
110 2f834e7d Kostas Papadimitriou
      var node, calls, callback, args, ev, events = ['all', eventName];
111 8d08f18a Kostas Papadimitriou
      if (!(calls = this._callbacks)) return this;
112 2f834e7d Kostas Papadimitriou
      while (ev = events.pop()) {
113 2f834e7d Kostas Papadimitriou
        if (!(node = calls[ev])) continue;
114 2f834e7d Kostas Papadimitriou
        args = ev == 'all' ? arguments : slice.call(arguments, 1);
115 2f834e7d Kostas Papadimitriou
        while (node = node.next) if (callback = node.callback) callback.apply(node.context || this, args);
116 8d08f18a Kostas Papadimitriou
      }
117 8d08f18a Kostas Papadimitriou
      return this;
118 8d08f18a Kostas Papadimitriou
    }
119 8d08f18a Kostas Papadimitriou
120 8d08f18a Kostas Papadimitriou
  };
121 8d08f18a Kostas Papadimitriou
122 8d08f18a Kostas Papadimitriou
  // Backbone.Model
123 8d08f18a Kostas Papadimitriou
  // --------------
124 8d08f18a Kostas Papadimitriou
125 8d08f18a Kostas Papadimitriou
  // Create a new model, with defined attributes. A client id (`cid`)
126 8d08f18a Kostas Papadimitriou
  // is automatically generated and assigned for you.
127 8d08f18a Kostas Papadimitriou
  Backbone.Model = function(attributes, options) {
128 8d08f18a Kostas Papadimitriou
    var defaults;
129 8d08f18a Kostas Papadimitriou
    attributes || (attributes = {});
130 8d08f18a Kostas Papadimitriou
    if (defaults = this.defaults) {
131 8d08f18a Kostas Papadimitriou
      if (_.isFunction(defaults)) defaults = defaults.call(this);
132 8d08f18a Kostas Papadimitriou
      attributes = _.extend({}, defaults, attributes);
133 8d08f18a Kostas Papadimitriou
    }
134 8d08f18a Kostas Papadimitriou
    this.attributes = {};
135 8d08f18a Kostas Papadimitriou
    this._escapedAttributes = {};
136 8d08f18a Kostas Papadimitriou
    this.cid = _.uniqueId('c');
137 8d08f18a Kostas Papadimitriou
    this.set(attributes, {silent : true});
138 8d08f18a Kostas Papadimitriou
    this._changed = false;
139 8d08f18a Kostas Papadimitriou
    this._previousAttributes = _.clone(this.attributes);
140 8d08f18a Kostas Papadimitriou
    if (options && options.collection) this.collection = options.collection;
141 8d08f18a Kostas Papadimitriou
    this.initialize(attributes, options);
142 8d08f18a Kostas Papadimitriou
  };
143 8d08f18a Kostas Papadimitriou
144 8d08f18a Kostas Papadimitriou
  // Attach all inheritable methods to the Model prototype.
145 8d08f18a Kostas Papadimitriou
  _.extend(Backbone.Model.prototype, Backbone.Events, {
146 8d08f18a Kostas Papadimitriou
147 8d08f18a Kostas Papadimitriou
    // Has the item been changed since the last `"change"` event?
148 8d08f18a Kostas Papadimitriou
    _changed : false,
149 8d08f18a Kostas Papadimitriou
150 8d08f18a Kostas Papadimitriou
    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
151 8d08f18a Kostas Papadimitriou
    // CouchDB users may want to set this to `"_id"`.
152 8d08f18a Kostas Papadimitriou
    idAttribute : 'id',
153 8d08f18a Kostas Papadimitriou
154 8d08f18a Kostas Papadimitriou
    // Initialize is an empty function by default. Override it with your own
155 8d08f18a Kostas Papadimitriou
    // initialization logic.
156 8d08f18a Kostas Papadimitriou
    initialize : function(){},
157 8d08f18a Kostas Papadimitriou
158 8d08f18a Kostas Papadimitriou
    // Return a copy of the model's `attributes` object.
159 8d08f18a Kostas Papadimitriou
    toJSON : function() {
160 8d08f18a Kostas Papadimitriou
      return _.clone(this.attributes);
161 8d08f18a Kostas Papadimitriou
    },
162 8d08f18a Kostas Papadimitriou
163 8d08f18a Kostas Papadimitriou
    // Get the value of an attribute.
164 8d08f18a Kostas Papadimitriou
    get : function(attr) {
165 8d08f18a Kostas Papadimitriou
      return this.attributes[attr];
166 8d08f18a Kostas Papadimitriou
    },
167 8d08f18a Kostas Papadimitriou
168 8d08f18a Kostas Papadimitriou
    // Get the HTML-escaped value of an attribute.
169 8d08f18a Kostas Papadimitriou
    escape : function(attr) {
170 8d08f18a Kostas Papadimitriou
      var html;
171 8d08f18a Kostas Papadimitriou
      if (html = this._escapedAttributes[attr]) return html;
172 8d08f18a Kostas Papadimitriou
      var val = this.attributes[attr];
173 2f834e7d Kostas Papadimitriou
      return this._escapedAttributes[attr] = _.escape(val == null ? '' : '' + val);
174 8d08f18a Kostas Papadimitriou
    },
175 8d08f18a Kostas Papadimitriou
176 8d08f18a Kostas Papadimitriou
    // Returns `true` if the attribute contains a value that is not null
177 8d08f18a Kostas Papadimitriou
    // or undefined.
178 8d08f18a Kostas Papadimitriou
    has : function(attr) {
179 8d08f18a Kostas Papadimitriou
      return this.attributes[attr] != null;
180 8d08f18a Kostas Papadimitriou
    },
181 8d08f18a Kostas Papadimitriou
182 8d08f18a Kostas Papadimitriou
    // Set a hash of model attributes on the object, firing `"change"` unless you
183 8d08f18a Kostas Papadimitriou
    // choose to silence it.
184 8d08f18a Kostas Papadimitriou
    set : function(attrs, options) {
185 8d08f18a Kostas Papadimitriou
186 8d08f18a Kostas Papadimitriou
      // Extract attributes and options.
187 8d08f18a Kostas Papadimitriou
      options || (options = {});
188 8d08f18a Kostas Papadimitriou
      if (!attrs) return this;
189 8d08f18a Kostas Papadimitriou
      if (attrs.attributes) attrs = attrs.attributes;
190 8d08f18a Kostas Papadimitriou
      var now = this.attributes, escaped = this._escapedAttributes;
191 8d08f18a Kostas Papadimitriou
192 8d08f18a Kostas Papadimitriou
      // Run validation.
193 8d08f18a Kostas Papadimitriou
      if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;
194 8d08f18a Kostas Papadimitriou
195 8d08f18a Kostas Papadimitriou
      // Check for changes of `id`.
196 8d08f18a Kostas Papadimitriou
      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
197 8d08f18a Kostas Papadimitriou
198 8d08f18a Kostas Papadimitriou
      // We're about to start triggering change events.
199 8d08f18a Kostas Papadimitriou
      var alreadyChanging = this._changing;
200 8d08f18a Kostas Papadimitriou
      this._changing = true;
201 8d08f18a Kostas Papadimitriou
202 8d08f18a Kostas Papadimitriou
      // Update attributes.
203 8d08f18a Kostas Papadimitriou
      for (var attr in attrs) {
204 8d08f18a Kostas Papadimitriou
        var val = attrs[attr];
205 8d08f18a Kostas Papadimitriou
        if (!_.isEqual(now[attr], val)) {
206 8d08f18a Kostas Papadimitriou
          now[attr] = val;
207 8d08f18a Kostas Papadimitriou
          delete escaped[attr];
208 8d08f18a Kostas Papadimitriou
          this._changed = true;
209 8d08f18a Kostas Papadimitriou
          if (!options.silent) this.trigger('change:' + attr, this, val, options);
210 8d08f18a Kostas Papadimitriou
        }
211 8d08f18a Kostas Papadimitriou
      }
212 8d08f18a Kostas Papadimitriou
213 8d08f18a Kostas Papadimitriou
      // Fire the `"change"` event, if the model has been changed.
214 8d08f18a Kostas Papadimitriou
      if (!alreadyChanging && !options.silent && this._changed) this.change(options);
215 8d08f18a Kostas Papadimitriou
      this._changing = false;
216 8d08f18a Kostas Papadimitriou
      return this;
217 8d08f18a Kostas Papadimitriou
    },
218 8d08f18a Kostas Papadimitriou
219 8d08f18a Kostas Papadimitriou
    // Remove an attribute from the model, firing `"change"` unless you choose
220 8d08f18a Kostas Papadimitriou
    // to silence it. `unset` is a noop if the attribute doesn't exist.
221 8d08f18a Kostas Papadimitriou
    unset : function(attr, options) {
222 8d08f18a Kostas Papadimitriou
      if (!(attr in this.attributes)) return this;
223 8d08f18a Kostas Papadimitriou
      options || (options = {});
224 8d08f18a Kostas Papadimitriou
      var value = this.attributes[attr];
225 8d08f18a Kostas Papadimitriou
226 8d08f18a Kostas Papadimitriou
      // Run validation.
227 8d08f18a Kostas Papadimitriou
      var validObj = {};
228 8d08f18a Kostas Papadimitriou
      validObj[attr] = void 0;
229 8d08f18a Kostas Papadimitriou
      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
230 8d08f18a Kostas Papadimitriou
231 2f834e7d Kostas Papadimitriou
      // changedAttributes needs to know if an attribute has been unset.
232 2f834e7d Kostas Papadimitriou
      (this._unsetAttributes || (this._unsetAttributes = [])).push(attr);
233 2f834e7d Kostas Papadimitriou
234 8d08f18a Kostas Papadimitriou
      // Remove the attribute.
235 8d08f18a Kostas Papadimitriou
      delete this.attributes[attr];
236 8d08f18a Kostas Papadimitriou
      delete this._escapedAttributes[attr];
237 8d08f18a Kostas Papadimitriou
      if (attr == this.idAttribute) delete this.id;
238 8d08f18a Kostas Papadimitriou
      this._changed = true;
239 8d08f18a Kostas Papadimitriou
      if (!options.silent) {
240 8d08f18a Kostas Papadimitriou
        this.trigger('change:' + attr, this, void 0, options);
241 8d08f18a Kostas Papadimitriou
        this.change(options);
242 8d08f18a Kostas Papadimitriou
      }
243 8d08f18a Kostas Papadimitriou
      return this;
244 8d08f18a Kostas Papadimitriou
    },
245 8d08f18a Kostas Papadimitriou
246 8d08f18a Kostas Papadimitriou
    // Clear all attributes on the model, firing `"change"` unless you choose
247 8d08f18a Kostas Papadimitriou
    // to silence it.
248 8d08f18a Kostas Papadimitriou
    clear : function(options) {
249 8d08f18a Kostas Papadimitriou
      options || (options = {});
250 8d08f18a Kostas Papadimitriou
      var attr;
251 8d08f18a Kostas Papadimitriou
      var old = this.attributes;
252 8d08f18a Kostas Papadimitriou
253 8d08f18a Kostas Papadimitriou
      // Run validation.
254 8d08f18a Kostas Papadimitriou
      var validObj = {};
255 8d08f18a Kostas Papadimitriou
      for (attr in old) validObj[attr] = void 0;
256 8d08f18a Kostas Papadimitriou
      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;
257 8d08f18a Kostas Papadimitriou
258 8d08f18a Kostas Papadimitriou
      this.attributes = {};
259 8d08f18a Kostas Papadimitriou
      this._escapedAttributes = {};
260 8d08f18a Kostas Papadimitriou
      this._changed = true;
261 8d08f18a Kostas Papadimitriou
      if (!options.silent) {
262 8d08f18a Kostas Papadimitriou
        for (attr in old) {
263 8d08f18a Kostas Papadimitriou
          this.trigger('change:' + attr, this, void 0, options);
264 8d08f18a Kostas Papadimitriou
        }
265 8d08f18a Kostas Papadimitriou
        this.change(options);
266 8d08f18a Kostas Papadimitriou
      }
267 8d08f18a Kostas Papadimitriou
      return this;
268 8d08f18a Kostas Papadimitriou
    },
269 8d08f18a Kostas Papadimitriou
270 8d08f18a Kostas Papadimitriou
    // Fetch the model from the server. If the server's representation of the
271 8d08f18a Kostas Papadimitriou
    // model differs from its current attributes, they will be overriden,
272 8d08f18a Kostas Papadimitriou
    // triggering a `"change"` event.
273 8d08f18a Kostas Papadimitriou
    fetch : function(options) {
274 8d08f18a Kostas Papadimitriou
      options || (options = {});
275 8d08f18a Kostas Papadimitriou
      var model = this;
276 8d08f18a Kostas Papadimitriou
      var success = options.success;
277 8d08f18a Kostas Papadimitriou
      options.success = function(resp, status, xhr) {
278 8d08f18a Kostas Papadimitriou
        if (!model.set(model.parse(resp, xhr), options)) return false;
279 8d08f18a Kostas Papadimitriou
        if (success) success(model, resp);
280 8d08f18a Kostas Papadimitriou
      };
281 8d08f18a Kostas Papadimitriou
      options.error = wrapError(options.error, model, options);
282 8d08f18a Kostas Papadimitriou
      return (this.sync || Backbone.sync).call(this, 'read', this, options);
283 8d08f18a Kostas Papadimitriou
    },
284 8d08f18a Kostas Papadimitriou
285 8d08f18a Kostas Papadimitriou
    // Set a hash of model attributes, and sync the model to the server.
286 8d08f18a Kostas Papadimitriou
    // If the server returns an attributes hash that differs, the model's
287 8d08f18a Kostas Papadimitriou
    // state will be `set` again.
288 8d08f18a Kostas Papadimitriou
    save : function(attrs, options) {
289 8d08f18a Kostas Papadimitriou
      options || (options = {});
290 8d08f18a Kostas Papadimitriou
      if (attrs && !this.set(attrs, options)) return false;
291 8d08f18a Kostas Papadimitriou
      var model = this;
292 8d08f18a Kostas Papadimitriou
      var success = options.success;
293 8d08f18a Kostas Papadimitriou
      options.success = function(resp, status, xhr) {
294 8d08f18a Kostas Papadimitriou
        if (!model.set(model.parse(resp, xhr), options)) return false;
295 8d08f18a Kostas Papadimitriou
        if (success) success(model, resp, xhr);
296 8d08f18a Kostas Papadimitriou
      };
297 8d08f18a Kostas Papadimitriou
      options.error = wrapError(options.error, model, options);
298 8d08f18a Kostas Papadimitriou
      var method = this.isNew() ? 'create' : 'update';
299 8d08f18a Kostas Papadimitriou
      return (this.sync || Backbone.sync).call(this, method, this, options);
300 8d08f18a Kostas Papadimitriou
    },
301 8d08f18a Kostas Papadimitriou
302 8d08f18a Kostas Papadimitriou
    // Destroy this model on the server if it was already persisted. Upon success, the model is removed
303 8d08f18a Kostas Papadimitriou
    // from its collection, if it has one.
304 8d08f18a Kostas Papadimitriou
    destroy : function(options) {
305 8d08f18a Kostas Papadimitriou
      options || (options = {});
306 8d08f18a Kostas Papadimitriou
      if (this.isNew()) return this.trigger('destroy', this, this.collection, options);
307 8d08f18a Kostas Papadimitriou
      var model = this;
308 8d08f18a Kostas Papadimitriou
      var success = options.success;
309 8d08f18a Kostas Papadimitriou
      options.success = function(resp) {
310 7977bbc9 Kostas Papadimitriou
        if (!options.silent) {
311 7977bbc9 Kostas Papadimitriou
          model.trigger('destroy', model, model.collection, options);
312 7977bbc9 Kostas Papadimitriou
        }
313 8d08f18a Kostas Papadimitriou
        if (success) success(model, resp);
314 8d08f18a Kostas Papadimitriou
      };
315 8d08f18a Kostas Papadimitriou
      options.error = wrapError(options.error, model, options);
316 8d08f18a Kostas Papadimitriou
      return (this.sync || Backbone.sync).call(this, 'delete', this, options);
317 8d08f18a Kostas Papadimitriou
    },
318 8d08f18a Kostas Papadimitriou
319 8d08f18a Kostas Papadimitriou
    // Default URL for the model's representation on the server -- if you're
320 8d08f18a Kostas Papadimitriou
    // using Backbone's restful methods, override this to change the endpoint
321 8d08f18a Kostas Papadimitriou
    // that will be called.
322 8d08f18a Kostas Papadimitriou
    url : function(options) {
323 8d08f18a Kostas Papadimitriou
      var base = getUrl(this.collection, options) || this.urlRoot || urlError();
324 8d08f18a Kostas Papadimitriou
      if (this.isNew()) return base;
325 8d08f18a Kostas Papadimitriou
      return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
326 8d08f18a Kostas Papadimitriou
    },
327 8d08f18a Kostas Papadimitriou
328 8d08f18a Kostas Papadimitriou
    // **parse** converts a response into the hash of attributes to be `set` on
329 8d08f18a Kostas Papadimitriou
    // the model. The default implementation is just to pass the response along.
330 8d08f18a Kostas Papadimitriou
    parse : function(resp, xhr) {
331 8d08f18a Kostas Papadimitriou
      return resp;
332 8d08f18a Kostas Papadimitriou
    },
333 8d08f18a Kostas Papadimitriou
334 8d08f18a Kostas Papadimitriou
    // Create a new model with identical attributes to this one.
335 8d08f18a Kostas Papadimitriou
    clone : function() {
336 8d08f18a Kostas Papadimitriou
      return new this.constructor(this);
337 8d08f18a Kostas Papadimitriou
    },
338 8d08f18a Kostas Papadimitriou
339 8d08f18a Kostas Papadimitriou
    // A model is new if it has never been saved to the server, and lacks an id.
340 8d08f18a Kostas Papadimitriou
    isNew : function() {
341 8d08f18a Kostas Papadimitriou
      return this.id == null;
342 8d08f18a Kostas Papadimitriou
    },
343 8d08f18a Kostas Papadimitriou
344 8d08f18a Kostas Papadimitriou
    // Call this method to manually fire a `change` event for this model.
345 8d08f18a Kostas Papadimitriou
    // Calling this will cause all objects observing the model to update.
346 8d08f18a Kostas Papadimitriou
    change : function(options) {
347 8d08f18a Kostas Papadimitriou
      this.trigger('change', this, options);
348 8d08f18a Kostas Papadimitriou
      this._previousAttributes = _.clone(this.attributes);
349 2f834e7d Kostas Papadimitriou
      this._unsetAttributes = null;
350 8d08f18a Kostas Papadimitriou
      this._changed = false;
351 8d08f18a Kostas Papadimitriou
    },
352 8d08f18a Kostas Papadimitriou
353 8d08f18a Kostas Papadimitriou
    // Determine if the model has changed since the last `"change"` event.
354 8d08f18a Kostas Papadimitriou
    // If you specify an attribute name, determine if that attribute has changed.
355 8d08f18a Kostas Papadimitriou
    hasChanged : function(attr) {
356 8d08f18a Kostas Papadimitriou
      if (attr) return this._previousAttributes[attr] != this.attributes[attr];
357 8d08f18a Kostas Papadimitriou
      return this._changed;
358 8d08f18a Kostas Papadimitriou
    },
359 8d08f18a Kostas Papadimitriou
360 8d08f18a Kostas Papadimitriou
    // Return an object containing all the attributes that have changed, or false
361 8d08f18a Kostas Papadimitriou
    // if there are no changed attributes. Useful for determining what parts of a
362 8d08f18a Kostas Papadimitriou
    // view need to be updated and/or what attributes need to be persisted to
363 2f834e7d Kostas Papadimitriou
    // the server. Unset attributes will be set to undefined.
364 8d08f18a Kostas Papadimitriou
    changedAttributes : function(now) {
365 8d08f18a Kostas Papadimitriou
      now || (now = this.attributes);
366 2f834e7d Kostas Papadimitriou
      var old = this._previousAttributes, unset = this._unsetAttributes;
367 2f834e7d Kostas Papadimitriou
368 8d08f18a Kostas Papadimitriou
      var changed = false;
369 8d08f18a Kostas Papadimitriou
      for (var attr in now) {
370 8d08f18a Kostas Papadimitriou
        if (!_.isEqual(old[attr], now[attr])) {
371 2f834e7d Kostas Papadimitriou
          changed || (changed = {});
372 8d08f18a Kostas Papadimitriou
          changed[attr] = now[attr];
373 8d08f18a Kostas Papadimitriou
        }
374 8d08f18a Kostas Papadimitriou
      }
375 2f834e7d Kostas Papadimitriou
376 2f834e7d Kostas Papadimitriou
      if (unset) {
377 2f834e7d Kostas Papadimitriou
        changed || (changed = {});
378 2f834e7d Kostas Papadimitriou
        var len = unset.length;
379 2f834e7d Kostas Papadimitriou
        while (len--) changed[unset[len]] = void 0;
380 2f834e7d Kostas Papadimitriou
      }
381 2f834e7d Kostas Papadimitriou
382 8d08f18a Kostas Papadimitriou
      return changed;
383 8d08f18a Kostas Papadimitriou
    },
384 8d08f18a Kostas Papadimitriou
385 8d08f18a Kostas Papadimitriou
    // Get the previous value of an attribute, recorded at the time the last
386 8d08f18a Kostas Papadimitriou
    // `"change"` event was fired.
387 8d08f18a Kostas Papadimitriou
    previous : function(attr) {
388 8d08f18a Kostas Papadimitriou
      if (!attr || !this._previousAttributes) return null;
389 8d08f18a Kostas Papadimitriou
      return this._previousAttributes[attr];
390 8d08f18a Kostas Papadimitriou
    },
391 8d08f18a Kostas Papadimitriou
392 8d08f18a Kostas Papadimitriou
    // Get all of the attributes of the model at the time of the previous
393 8d08f18a Kostas Papadimitriou
    // `"change"` event.
394 8d08f18a Kostas Papadimitriou
    previousAttributes : function() {
395 8d08f18a Kostas Papadimitriou
      return _.clone(this._previousAttributes);
396 8d08f18a Kostas Papadimitriou
    },
397 8d08f18a Kostas Papadimitriou
398 8d08f18a Kostas Papadimitriou
    // Run validation against a set of incoming attributes, returning `true`
399 8d08f18a Kostas Papadimitriou
    // if all is well. If a specific `error` callback has been passed,
400 8d08f18a Kostas Papadimitriou
    // call that instead of firing the general `"error"` event.
401 8d08f18a Kostas Papadimitriou
    _performValidation : function(attrs, options) {
402 8d08f18a Kostas Papadimitriou
      var error = this.validate(attrs);
403 8d08f18a Kostas Papadimitriou
      if (error) {
404 8d08f18a Kostas Papadimitriou
        if (options.error) {
405 8d08f18a Kostas Papadimitriou
          options.error(this, error, options);
406 8d08f18a Kostas Papadimitriou
        } else {
407 8d08f18a Kostas Papadimitriou
          this.trigger('error', this, error, options);
408 8d08f18a Kostas Papadimitriou
        }
409 8d08f18a Kostas Papadimitriou
        return false;
410 8d08f18a Kostas Papadimitriou
      }
411 8d08f18a Kostas Papadimitriou
      return true;
412 8d08f18a Kostas Papadimitriou
    }
413 8d08f18a Kostas Papadimitriou
414 8d08f18a Kostas Papadimitriou
  });
415 8d08f18a Kostas Papadimitriou
416 8d08f18a Kostas Papadimitriou
  // Backbone.Collection
417 8d08f18a Kostas Papadimitriou
  // -------------------
418 8d08f18a Kostas Papadimitriou
419 8d08f18a Kostas Papadimitriou
  // Provides a standard collection class for our sets of models, ordered
420 8d08f18a Kostas Papadimitriou
  // or unordered. If a `comparator` is specified, the Collection will maintain
421 8d08f18a Kostas Papadimitriou
  // its models in sort order, as they're added and removed.
422 8d08f18a Kostas Papadimitriou
  Backbone.Collection = function(models, options) {
423 8d08f18a Kostas Papadimitriou
    options || (options = {});
424 8d08f18a Kostas Papadimitriou
    if (options.comparator) this.comparator = options.comparator;
425 8d08f18a Kostas Papadimitriou
    _.bindAll(this, '_onModelEvent', '_removeReference');
426 8d08f18a Kostas Papadimitriou
    this._reset();
427 8d08f18a Kostas Papadimitriou
    if (models) this.reset(models, {silent: true});
428 8d08f18a Kostas Papadimitriou
    this.initialize.apply(this, arguments);
429 8d08f18a Kostas Papadimitriou
  };
430 8d08f18a Kostas Papadimitriou
431 8d08f18a Kostas Papadimitriou
  // Define the Collection's inheritable methods.
432 8d08f18a Kostas Papadimitriou
  _.extend(Backbone.Collection.prototype, Backbone.Events, {
433 8d08f18a Kostas Papadimitriou
434 8d08f18a Kostas Papadimitriou
    // The default model for a collection is just a **Backbone.Model**.
435 8d08f18a Kostas Papadimitriou
    // This should be overridden in most cases.
436 8d08f18a Kostas Papadimitriou
    model : Backbone.Model,
437 8d08f18a Kostas Papadimitriou
438 8d08f18a Kostas Papadimitriou
    // Initialize is an empty function by default. Override it with your own
439 8d08f18a Kostas Papadimitriou
    // initialization logic.
440 8d08f18a Kostas Papadimitriou
    initialize : function(){},
441 8d08f18a Kostas Papadimitriou
442 8d08f18a Kostas Papadimitriou
    // The JSON representation of a Collection is an array of the
443 8d08f18a Kostas Papadimitriou
    // models' attributes.
444 8d08f18a Kostas Papadimitriou
    toJSON : function() {
445 8d08f18a Kostas Papadimitriou
      return this.map(function(model){ return model.toJSON(); });
446 8d08f18a Kostas Papadimitriou
    },
447 8d08f18a Kostas Papadimitriou
448 8d08f18a Kostas Papadimitriou
    // Add a model, or list of models to the set. Pass **silent** to avoid
449 8d08f18a Kostas Papadimitriou
    // firing the `added` event for every new model.
450 8d08f18a Kostas Papadimitriou
    add : function(models, options) {
451 8d08f18a Kostas Papadimitriou
      if (_.isArray(models)) {
452 8d08f18a Kostas Papadimitriou
        for (var i = 0, l = models.length; i < l; i++) {
453 8d08f18a Kostas Papadimitriou
          this._add(models[i], options);
454 8d08f18a Kostas Papadimitriou
        }
455 8d08f18a Kostas Papadimitriou
      } else {
456 8d08f18a Kostas Papadimitriou
        this._add(models, options);
457 8d08f18a Kostas Papadimitriou
      }
458 8d08f18a Kostas Papadimitriou
      return this;
459 8d08f18a Kostas Papadimitriou
    },
460 8d08f18a Kostas Papadimitriou
461 8d08f18a Kostas Papadimitriou
    // Remove a model, or a list of models from the set. Pass silent to avoid
462 8d08f18a Kostas Papadimitriou
    // firing the `removed` event for every model removed.
463 8d08f18a Kostas Papadimitriou
    remove : function(models, options) {
464 8d08f18a Kostas Papadimitriou
      if (_.isArray(models)) {
465 8d08f18a Kostas Papadimitriou
        for (var i = 0, l = models.length; i < l; i++) {
466 8d08f18a Kostas Papadimitriou
          this._remove(models[i], options);
467 8d08f18a Kostas Papadimitriou
        }
468 8d08f18a Kostas Papadimitriou
      } else {
469 8d08f18a Kostas Papadimitriou
        this._remove(models, options);
470 8d08f18a Kostas Papadimitriou
      }
471 8d08f18a Kostas Papadimitriou
      return this;
472 8d08f18a Kostas Papadimitriou
    },
473 8d08f18a Kostas Papadimitriou
474 8d08f18a Kostas Papadimitriou
    // Get a model from the set by id.
475 8d08f18a Kostas Papadimitriou
    get : function(id) {
476 8d08f18a Kostas Papadimitriou
      if (id == null) return null;
477 8d08f18a Kostas Papadimitriou
      return this._byId[id.id != null ? id.id : id];
478 8d08f18a Kostas Papadimitriou
    },
479 8d08f18a Kostas Papadimitriou
480 8d08f18a Kostas Papadimitriou
    // Get a model from the set by client id.
481 8d08f18a Kostas Papadimitriou
    getByCid : function(cid) {
482 8d08f18a Kostas Papadimitriou
      return cid && this._byCid[cid.cid || cid];
483 8d08f18a Kostas Papadimitriou
    },
484 8d08f18a Kostas Papadimitriou
485 8d08f18a Kostas Papadimitriou
    // Get the model at the given index.
486 2f834e7d Kostas Papadimitriou
    at : function(index) {
487 8d08f18a Kostas Papadimitriou
      return this.models[index];
488 8d08f18a Kostas Papadimitriou
    },
489 8d08f18a Kostas Papadimitriou
490 8d08f18a Kostas Papadimitriou
    // Force the collection to re-sort itself. You don't need to call this under normal
491 8d08f18a Kostas Papadimitriou
    // circumstances, as the set will maintain sort order as each item is added.
492 8d08f18a Kostas Papadimitriou
    sort : function(options) {
493 8d08f18a Kostas Papadimitriou
      options || (options = {});
494 8d08f18a Kostas Papadimitriou
      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
495 8d08f18a Kostas Papadimitriou
      this.models = this.sortBy(this.comparator);
496 8d08f18a Kostas Papadimitriou
      if (!options.silent) this.trigger('reset', this, options);
497 8d08f18a Kostas Papadimitriou
      return this;
498 8d08f18a Kostas Papadimitriou
    },
499 8d08f18a Kostas Papadimitriou
500 8d08f18a Kostas Papadimitriou
    // Pluck an attribute from each model in the collection.
501 8d08f18a Kostas Papadimitriou
    pluck : function(attr) {
502 8d08f18a Kostas Papadimitriou
      return _.map(this.models, function(model){ return model.get(attr); });
503 8d08f18a Kostas Papadimitriou
    },
504 8d08f18a Kostas Papadimitriou
505 8d08f18a Kostas Papadimitriou
    // When you have more items than you want to add or remove individually,
506 8d08f18a Kostas Papadimitriou
    // you can reset the entire set with a new list of models, without firing
507 8d08f18a Kostas Papadimitriou
    // any `added` or `removed` events. Fires `reset` when finished.
508 8d08f18a Kostas Papadimitriou
    reset : function(models, options) {
509 8d08f18a Kostas Papadimitriou
      models  || (models = []);
510 8d08f18a Kostas Papadimitriou
      options || (options = {});
511 8d08f18a Kostas Papadimitriou
      this.each(this._removeReference);
512 8d08f18a Kostas Papadimitriou
      this._reset();
513 8d08f18a Kostas Papadimitriou
      this.add(models, {silent: true});
514 8d08f18a Kostas Papadimitriou
      if (!options.silent) this.trigger('reset', this, options);
515 8d08f18a Kostas Papadimitriou
      return this;
516 8d08f18a Kostas Papadimitriou
    },
517 8d08f18a Kostas Papadimitriou
518 8d08f18a Kostas Papadimitriou
    // When you have an existing set of models in a collection, 
519 8d08f18a Kostas Papadimitriou
    // you can do in-place updates of these models, reusing existing instances.
520 8d08f18a Kostas Papadimitriou
    // - Items are matched against existing items in the collection by id
521 8d08f18a Kostas Papadimitriou
    // - New items are added
522 8d08f18a Kostas Papadimitriou
    // - matching models are updated using set(), triggers 'change'.
523 8d08f18a Kostas Papadimitriou
    // - existing models not present in the update are removed if 'removeMissing' is passed.
524 8d08f18a Kostas Papadimitriou
    // - a collection change event will be dispatched for each add() and remove()
525 8d08f18a Kostas Papadimitriou
    update : function(models, options) {
526 8d08f18a Kostas Papadimitriou
      models  || (models = []);
527 8d08f18a Kostas Papadimitriou
      options || (options = {});
528 8d08f18a Kostas Papadimitriou
529 8d08f18a Kostas Papadimitriou
      //keep track of the models we've updated, cause we're gunna delete the rest if 'removeMissing' is set.
530 8d08f18a Kostas Papadimitriou
      var updateMap = _.reduce(this.models, function(map, model){ map[model.id] = false; return map },{});
531 8d08f18a Kostas Papadimitriou
532 8d08f18a Kostas Papadimitriou
      _.each( models, function(model) {
533 8d08f18a Kostas Papadimitriou
534 8d08f18a Kostas Papadimitriou
        var idAttribute = this.model.prototype.idAttribute;
535 8d08f18a Kostas Papadimitriou
        var modelId = model[idAttribute];
536 8d08f18a Kostas Papadimitriou
537 8d08f18a Kostas Papadimitriou
        if ( modelId == undefined ) throw new Error("Can't update a model with no id attribute. Please use 'reset'.");
538 8d08f18a Kostas Papadimitriou
        
539 8d08f18a Kostas Papadimitriou
        if ( this._byId[modelId] ) {
540 8d08f18a Kostas Papadimitriou
          var attrs = (model instanceof Backbone.Model) ? _.clone(model.attributes) : _.clone(model);
541 8d08f18a Kostas Papadimitriou
          delete attrs[idAttribute];
542 8d08f18a Kostas Papadimitriou
          this._byId[modelId].set( attrs );
543 8d08f18a Kostas Papadimitriou
          updateMap[modelId] = true;
544 8d08f18a Kostas Papadimitriou
        }
545 8d08f18a Kostas Papadimitriou
        else {
546 8d08f18a Kostas Papadimitriou
          this.add( model );
547 8d08f18a Kostas Papadimitriou
        }
548 8d08f18a Kostas Papadimitriou
      }, this);
549 8d08f18a Kostas Papadimitriou
550 8d08f18a Kostas Papadimitriou
      if ( options.removeMissing ) {
551 8d08f18a Kostas Papadimitriou
        _.select(updateMap, function(updated, modelId){
552 8d08f18a Kostas Papadimitriou
          if (!updated) this.remove( modelId );
553 8d08f18a Kostas Papadimitriou
        }, this);
554 8d08f18a Kostas Papadimitriou
      }
555 8d08f18a Kostas Papadimitriou
556 8d08f18a Kostas Papadimitriou
      return this;
557 8d08f18a Kostas Papadimitriou
    },
558 8d08f18a Kostas Papadimitriou
559 8d08f18a Kostas Papadimitriou
    // Fetch the default set of models for this collection, resetting the
560 8d08f18a Kostas Papadimitriou
    // collection when they arrive. If `add: true` is passed, appends the
561 8d08f18a Kostas Papadimitriou
    // models to the collection instead of resetting.
562 8d08f18a Kostas Papadimitriou
    fetch : function(options) {
563 8d08f18a Kostas Papadimitriou
      options || (options = {});
564 8d08f18a Kostas Papadimitriou
      var collection = this;
565 8d08f18a Kostas Papadimitriou
      var success = options.success;
566 8d08f18a Kostas Papadimitriou
      options.success = function(resp, status, xhr) {
567 8d08f18a Kostas Papadimitriou
        collection[options.update ? 'update' : options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
568 8d08f18a Kostas Papadimitriou
        if (success) success(collection, resp);
569 8d08f18a Kostas Papadimitriou
      };
570 8d08f18a Kostas Papadimitriou
      options.error = wrapError(options.error, collection, options);
571 8d08f18a Kostas Papadimitriou
      return (this.sync || Backbone.sync).call(this, 'read', this, options);
572 8d08f18a Kostas Papadimitriou
    },
573 8d08f18a Kostas Papadimitriou
574 8d08f18a Kostas Papadimitriou
    // Create a new instance of a model in this collection. After the model
575 8d08f18a Kostas Papadimitriou
    // has been created on the server, it will be added to the collection.
576 8d08f18a Kostas Papadimitriou
    // Returns the model, or 'false' if validation on a new model fails.
577 8d08f18a Kostas Papadimitriou
    create : function(model, options) {
578 8d08f18a Kostas Papadimitriou
      var coll = this;
579 8d08f18a Kostas Papadimitriou
      options || (options = {});
580 8d08f18a Kostas Papadimitriou
      model = this._prepareModel(model, options);
581 8d08f18a Kostas Papadimitriou
      if (!model) return false;
582 8d08f18a Kostas Papadimitriou
      var success = options.success;
583 8d08f18a Kostas Papadimitriou
      options.success = function(nextModel, resp, xhr) {
584 8d08f18a Kostas Papadimitriou
        coll.add(nextModel, options);
585 8d08f18a Kostas Papadimitriou
        if (success) success(nextModel, resp, xhr);
586 8d08f18a Kostas Papadimitriou
      };
587 8d08f18a Kostas Papadimitriou
      model.save(null, options);
588 8d08f18a Kostas Papadimitriou
      return model;
589 8d08f18a Kostas Papadimitriou
    },
590 8d08f18a Kostas Papadimitriou
591 8d08f18a Kostas Papadimitriou
    // **parse** converts a response into a list of models to be added to the
592 8d08f18a Kostas Papadimitriou
    // collection. The default implementation is just to pass it through.
593 8d08f18a Kostas Papadimitriou
    parse : function(resp, xhr) {
594 8d08f18a Kostas Papadimitriou
      return resp;
595 8d08f18a Kostas Papadimitriou
    },
596 8d08f18a Kostas Papadimitriou
597 8d08f18a Kostas Papadimitriou
    // Proxy to _'s chain. Can't be proxied the same way the rest of the
598 8d08f18a Kostas Papadimitriou
    // underscore methods are proxied because it relies on the underscore
599 8d08f18a Kostas Papadimitriou
    // constructor.
600 2f834e7d Kostas Papadimitriou
    chain : function () {
601 8d08f18a Kostas Papadimitriou
      return _(this.models).chain();
602 8d08f18a Kostas Papadimitriou
    },
603 8d08f18a Kostas Papadimitriou
604 8d08f18a Kostas Papadimitriou
    // Reset all internal state. Called when the collection is reset.
605 8d08f18a Kostas Papadimitriou
    _reset : function(options) {
606 8d08f18a Kostas Papadimitriou
      this.length = 0;
607 8d08f18a Kostas Papadimitriou
      this.models = [];
608 8d08f18a Kostas Papadimitriou
      this._byId  = {};
609 8d08f18a Kostas Papadimitriou
      this._byCid = {};
610 8d08f18a Kostas Papadimitriou
    },
611 8d08f18a Kostas Papadimitriou
612 8d08f18a Kostas Papadimitriou
    // Prepare a model to be added to this collection
613 2f834e7d Kostas Papadimitriou
    _prepareModel : function(model, options) {
614 8d08f18a Kostas Papadimitriou
      if (!(model instanceof Backbone.Model)) {
615 8d08f18a Kostas Papadimitriou
        var attrs = model;
616 8d08f18a Kostas Papadimitriou
        model = new this.model(attrs, {collection: this});
617 2f834e7d Kostas Papadimitriou
        if (model.validate && !model._performValidation(model.attributes, options)) model = false;
618 8d08f18a Kostas Papadimitriou
      } else if (!model.collection) {
619 8d08f18a Kostas Papadimitriou
        model.collection = this;
620 8d08f18a Kostas Papadimitriou
      }
621 8d08f18a Kostas Papadimitriou
      return model;
622 8d08f18a Kostas Papadimitriou
    },
623 8d08f18a Kostas Papadimitriou
624 8d08f18a Kostas Papadimitriou
    // Internal implementation of adding a single model to the set, updating
625 8d08f18a Kostas Papadimitriou
    // hash indexes for `id` and `cid` lookups.
626 8d08f18a Kostas Papadimitriou
    // Returns the model, or 'false' if validation on a new model fails.
627 8d08f18a Kostas Papadimitriou
    _add : function(model, options) {
628 8d08f18a Kostas Papadimitriou
      options || (options = {});
629 8d08f18a Kostas Papadimitriou
      model = this._prepareModel(model, options);
630 8d08f18a Kostas Papadimitriou
      if (!model) return false;
631 8d08f18a Kostas Papadimitriou
      var already = this.getByCid(model);
632 8d08f18a Kostas Papadimitriou
      if (already) throw new Error(["Can't add the same model to a set twice", already.id]);
633 8d08f18a Kostas Papadimitriou
      this._byId[model.id] = model;
634 8d08f18a Kostas Papadimitriou
      this._byCid[model.cid] = model;
635 8d08f18a Kostas Papadimitriou
      var index = options.at != null ? options.at :
636 8d08f18a Kostas Papadimitriou
                  this.comparator ? this.sortedIndex(model, this.comparator) :
637 8d08f18a Kostas Papadimitriou
                  this.length;
638 8d08f18a Kostas Papadimitriou
      this.models.splice(index, 0, model);
639 8d08f18a Kostas Papadimitriou
      model.bind('all', this._onModelEvent);
640 8d08f18a Kostas Papadimitriou
      this.length++;
641 2f834e7d Kostas Papadimitriou
      options.index = index;
642 8d08f18a Kostas Papadimitriou
      if (!options.silent) model.trigger('add', model, this, options);
643 8d08f18a Kostas Papadimitriou
      return model;
644 8d08f18a Kostas Papadimitriou
    },
645 8d08f18a Kostas Papadimitriou
646 8d08f18a Kostas Papadimitriou
    // Internal implementation of removing a single model from the set, updating
647 8d08f18a Kostas Papadimitriou
    // hash indexes for `id` and `cid` lookups.
648 8d08f18a Kostas Papadimitriou
    _remove : function(model, options) {
649 8d08f18a Kostas Papadimitriou
      options || (options = {});
650 8d08f18a Kostas Papadimitriou
      model = this.getByCid(model) || this.get(model);
651 8d08f18a Kostas Papadimitriou
      if (!model) return null;
652 8d08f18a Kostas Papadimitriou
      delete this._byId[model.id];
653 8d08f18a Kostas Papadimitriou
      delete this._byCid[model.cid];
654 2f834e7d Kostas Papadimitriou
      var index = this.indexOf(model);
655 2f834e7d Kostas Papadimitriou
      this.models.splice(index, 1);
656 8d08f18a Kostas Papadimitriou
      this.length--;
657 2f834e7d Kostas Papadimitriou
      options.index = index;
658 8d08f18a Kostas Papadimitriou
      if (!options.silent) model.trigger('remove', model, this, options);
659 8d08f18a Kostas Papadimitriou
      this._removeReference(model);
660 8d08f18a Kostas Papadimitriou
      return model;
661 8d08f18a Kostas Papadimitriou
    },
662 8d08f18a Kostas Papadimitriou
663 8d08f18a Kostas Papadimitriou
    // Internal method to remove a model's ties to a collection.
664 8d08f18a Kostas Papadimitriou
    _removeReference : function(model) {
665 8d08f18a Kostas Papadimitriou
      if (this == model.collection) {
666 8d08f18a Kostas Papadimitriou
        delete model.collection;
667 8d08f18a Kostas Papadimitriou
      }
668 8d08f18a Kostas Papadimitriou
      model.unbind('all', this._onModelEvent);
669 8d08f18a Kostas Papadimitriou
    },
670 8d08f18a Kostas Papadimitriou
671 8d08f18a Kostas Papadimitriou
    // Internal method called every time a model in the set fires an event.
672 8d08f18a Kostas Papadimitriou
    // Sets need to update their indexes when models change ids. All other
673 8d08f18a Kostas Papadimitriou
    // events simply proxy through. "add" and "remove" events that originate
674 8d08f18a Kostas Papadimitriou
    // in other collections are ignored.
675 8d08f18a Kostas Papadimitriou
    _onModelEvent : function(ev, model, collection, options) {
676 8d08f18a Kostas Papadimitriou
      if ((ev == 'add' || ev == 'remove') && collection != this) return;
677 8d08f18a Kostas Papadimitriou
      if (ev == 'destroy') {
678 8d08f18a Kostas Papadimitriou
        this._remove(model, options);
679 8d08f18a Kostas Papadimitriou
      }
680 8d08f18a Kostas Papadimitriou
      if (model && ev === 'change:' + model.idAttribute) {
681 8d08f18a Kostas Papadimitriou
        delete this._byId[model.previous(model.idAttribute)];
682 8d08f18a Kostas Papadimitriou
        this._byId[model.id] = model;
683 8d08f18a Kostas Papadimitriou
      }
684 8d08f18a Kostas Papadimitriou
      this.trigger.apply(this, arguments);
685 8d08f18a Kostas Papadimitriou
    }
686 8d08f18a Kostas Papadimitriou
687 8d08f18a Kostas Papadimitriou
  });
688 8d08f18a Kostas Papadimitriou
689 8d08f18a Kostas Papadimitriou
  // Underscore methods that we want to implement on the Collection.
690 8d08f18a Kostas Papadimitriou
  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',
691 8d08f18a Kostas Papadimitriou
    'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',
692 8d08f18a Kostas Papadimitriou
    'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',
693 8d08f18a Kostas Papadimitriou
    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];
694 8d08f18a Kostas Papadimitriou
695 8d08f18a Kostas Papadimitriou
  // Mix in each Underscore method as a proxy to `Collection#models`.
696 8d08f18a Kostas Papadimitriou
  _.each(methods, function(method) {
697 8d08f18a Kostas Papadimitriou
    Backbone.Collection.prototype[method] = function() {
698 8d08f18a Kostas Papadimitriou
      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));
699 8d08f18a Kostas Papadimitriou
    };
700 8d08f18a Kostas Papadimitriou
  });
701 8d08f18a Kostas Papadimitriou
702 8d08f18a Kostas Papadimitriou
  // Backbone.Router
703 8d08f18a Kostas Papadimitriou
  // -------------------
704 8d08f18a Kostas Papadimitriou
705 8d08f18a Kostas Papadimitriou
  // Routers map faux-URLs to actions, and fire events when routes are
706 8d08f18a Kostas Papadimitriou
  // matched. Creating a new one sets its `routes` hash, if not set statically.
707 8d08f18a Kostas Papadimitriou
  Backbone.Router = function(options) {
708 8d08f18a Kostas Papadimitriou
    options || (options = {});
709 8d08f18a Kostas Papadimitriou
    if (options.routes) this.routes = options.routes;
710 8d08f18a Kostas Papadimitriou
    this._bindRoutes();
711 8d08f18a Kostas Papadimitriou
    this.initialize.apply(this, arguments);
712 8d08f18a Kostas Papadimitriou
  };
713 8d08f18a Kostas Papadimitriou
714 8d08f18a Kostas Papadimitriou
  // Cached regular expressions for matching named param parts and splatted
715 8d08f18a Kostas Papadimitriou
  // parts of route strings.
716 8d08f18a Kostas Papadimitriou
  var namedParam    = /:([\w\d]+)/g;
717 8d08f18a Kostas Papadimitriou
  var splatParam    = /\*([\w\d]+)/g;
718 8d08f18a Kostas Papadimitriou
  var escapeRegExp  = /[-[\]{}()+?.,\\^$|#\s]/g;
719 8d08f18a Kostas Papadimitriou
720 8d08f18a Kostas Papadimitriou
  // Set up all inheritable **Backbone.Router** properties and methods.
721 8d08f18a Kostas Papadimitriou
  _.extend(Backbone.Router.prototype, Backbone.Events, {
722 8d08f18a Kostas Papadimitriou
723 8d08f18a Kostas Papadimitriou
    // Initialize is an empty function by default. Override it with your own
724 8d08f18a Kostas Papadimitriou
    // initialization logic.
725 8d08f18a Kostas Papadimitriou
    initialize : function(){},
726 8d08f18a Kostas Papadimitriou
727 8d08f18a Kostas Papadimitriou
    // Manually bind a single named route to a callback. For example:
728 8d08f18a Kostas Papadimitriou
    //
729 8d08f18a Kostas Papadimitriou
    //     this.route('search/:query/p:num', 'search', function(query, num) {
730 8d08f18a Kostas Papadimitriou
    //       ...
731 8d08f18a Kostas Papadimitriou
    //     });
732 8d08f18a Kostas Papadimitriou
    //
733 8d08f18a Kostas Papadimitriou
    route : function(route, name, callback) {
734 8d08f18a Kostas Papadimitriou
      Backbone.history || (Backbone.history = new Backbone.History);
735 8d08f18a Kostas Papadimitriou
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
736 8d08f18a Kostas Papadimitriou
      Backbone.history.route(route, _.bind(function(fragment) {
737 8d08f18a Kostas Papadimitriou
        var args = this._extractParameters(route, fragment);
738 2f834e7d Kostas Papadimitriou
        callback && callback.apply(this, args);
739 8d08f18a Kostas Papadimitriou
        this.trigger.apply(this, ['route:' + name].concat(args));
740 8d08f18a Kostas Papadimitriou
      }, this));
741 8d08f18a Kostas Papadimitriou
    },
742 8d08f18a Kostas Papadimitriou
743 8d08f18a Kostas Papadimitriou
    // Simple proxy to `Backbone.history` to save a fragment into the history.
744 8d08f18a Kostas Papadimitriou
    navigate : function(fragment, triggerRoute) {
745 8d08f18a Kostas Papadimitriou
      Backbone.history.navigate(fragment, triggerRoute);
746 8d08f18a Kostas Papadimitriou
    },
747 8d08f18a Kostas Papadimitriou
748 8d08f18a Kostas Papadimitriou
    // Bind all defined routes to `Backbone.history`. We have to reverse the
749 8d08f18a Kostas Papadimitriou
    // order of the routes here to support behavior where the most general
750 8d08f18a Kostas Papadimitriou
    // routes can be defined at the bottom of the route map.
751 8d08f18a Kostas Papadimitriou
    _bindRoutes : function() {
752 8d08f18a Kostas Papadimitriou
      if (!this.routes) return;
753 8d08f18a Kostas Papadimitriou
      var routes = [];
754 8d08f18a Kostas Papadimitriou
      for (var route in this.routes) {
755 8d08f18a Kostas Papadimitriou
        routes.unshift([route, this.routes[route]]);
756 8d08f18a Kostas Papadimitriou
      }
757 8d08f18a Kostas Papadimitriou
      for (var i = 0, l = routes.length; i < l; i++) {
758 8d08f18a Kostas Papadimitriou
        this.route(routes[i][0], routes[i][1], this[routes[i][1]]);
759 8d08f18a Kostas Papadimitriou
      }
760 8d08f18a Kostas Papadimitriou
    },
761 8d08f18a Kostas Papadimitriou
762 8d08f18a Kostas Papadimitriou
    // Convert a route string into a regular expression, suitable for matching
763 8d08f18a Kostas Papadimitriou
    // against the current location hash.
764 8d08f18a Kostas Papadimitriou
    _routeToRegExp : function(route) {
765 8d08f18a Kostas Papadimitriou
      route = route.replace(escapeRegExp, "\\$&")
766 8d08f18a Kostas Papadimitriou
                   .replace(namedParam, "([^\/]*)")
767 8d08f18a Kostas Papadimitriou
                   .replace(splatParam, "(.*?)");
768 8d08f18a Kostas Papadimitriou
      return new RegExp('^' + route + '$');
769 8d08f18a Kostas Papadimitriou
    },
770 8d08f18a Kostas Papadimitriou
771 8d08f18a Kostas Papadimitriou
    // Given a route, and a URL fragment that it matches, return the array of
772 8d08f18a Kostas Papadimitriou
    // extracted parameters.
773 8d08f18a Kostas Papadimitriou
    _extractParameters : function(route, fragment) {
774 8d08f18a Kostas Papadimitriou
      return route.exec(fragment).slice(1);
775 8d08f18a Kostas Papadimitriou
    }
776 8d08f18a Kostas Papadimitriou
777 8d08f18a Kostas Papadimitriou
  });
778 8d08f18a Kostas Papadimitriou
779 8d08f18a Kostas Papadimitriou
  // Backbone.History
780 8d08f18a Kostas Papadimitriou
  // ----------------
781 8d08f18a Kostas Papadimitriou
782 8d08f18a Kostas Papadimitriou
  // Handles cross-browser history management, based on URL fragments. If the
783 8d08f18a Kostas Papadimitriou
  // browser does not support `onhashchange`, falls back to polling.
784 8d08f18a Kostas Papadimitriou
  Backbone.History = function() {
785 8d08f18a Kostas Papadimitriou
    this.handlers = [];
786 8d08f18a Kostas Papadimitriou
    _.bindAll(this, 'checkUrl');
787 8d08f18a Kostas Papadimitriou
  };
788 8d08f18a Kostas Papadimitriou
789 8d08f18a Kostas Papadimitriou
  // Cached regex for cleaning hashes.
790 8d08f18a Kostas Papadimitriou
  var hashStrip = /^#*/;
791 8d08f18a Kostas Papadimitriou
792 8d08f18a Kostas Papadimitriou
  // Cached regex for detecting MSIE.
793 8d08f18a Kostas Papadimitriou
  var isExplorer = /msie [\w.]+/;
794 8d08f18a Kostas Papadimitriou
795 8d08f18a Kostas Papadimitriou
  // Has the history handling already been started?
796 8d08f18a Kostas Papadimitriou
  var historyStarted = false;
797 8d08f18a Kostas Papadimitriou
798 8d08f18a Kostas Papadimitriou
  // Set up all inheritable **Backbone.History** properties and methods.
799 8d08f18a Kostas Papadimitriou
  _.extend(Backbone.History.prototype, {
800 8d08f18a Kostas Papadimitriou
801 8d08f18a Kostas Papadimitriou
    // The default interval to poll for hash changes, if necessary, is
802 8d08f18a Kostas Papadimitriou
    // twenty times a second.
803 8d08f18a Kostas Papadimitriou
    interval: 50,
804 8d08f18a Kostas Papadimitriou
805 8d08f18a Kostas Papadimitriou
    // Get the cross-browser normalized URL fragment, either from the URL,
806 8d08f18a Kostas Papadimitriou
    // the hash, or the override.
807 8d08f18a Kostas Papadimitriou
    getFragment : function(fragment, forcePushState) {
808 8d08f18a Kostas Papadimitriou
      if (fragment == null) {
809 8d08f18a Kostas Papadimitriou
        if (this._hasPushState || forcePushState) {
810 8d08f18a Kostas Papadimitriou
          fragment = window.location.pathname;
811 8d08f18a Kostas Papadimitriou
          var search = window.location.search;
812 8d08f18a Kostas Papadimitriou
          if (search) fragment += search;
813 8d08f18a Kostas Papadimitriou
        } else {
814 8d08f18a Kostas Papadimitriou
          fragment = window.location.hash;
815 8d08f18a Kostas Papadimitriou
        }
816 8d08f18a Kostas Papadimitriou
      }
817 2f834e7d Kostas Papadimitriou
      fragment = decodeURIComponent(fragment.replace(hashStrip, ''));
818 2f834e7d Kostas Papadimitriou
      if (!fragment.indexOf(this.options.root)) fragment = fragment.substr(this.options.root.length);
819 2f834e7d Kostas Papadimitriou
      return fragment;
820 8d08f18a Kostas Papadimitriou
    },
821 8d08f18a Kostas Papadimitriou
822 8d08f18a Kostas Papadimitriou
    // Start the hash change handling, returning `true` if the current URL matches
823 8d08f18a Kostas Papadimitriou
    // an existing route, and `false` otherwise.
824 8d08f18a Kostas Papadimitriou
    start : function(options) {
825 8d08f18a Kostas Papadimitriou
826 8d08f18a Kostas Papadimitriou
      // Figure out the initial configuration. Do we need an iframe?
827 8d08f18a Kostas Papadimitriou
      // Is pushState desired ... is it available?
828 8d08f18a Kostas Papadimitriou
      if (historyStarted) throw new Error("Backbone.history has already been started");
829 8d08f18a Kostas Papadimitriou
      this.options          = _.extend({}, {root: '/'}, this.options, options);
830 8d08f18a Kostas Papadimitriou
      this._wantsPushState  = !!this.options.pushState;
831 8d08f18a Kostas Papadimitriou
      this._hasPushState    = !!(this.options.pushState && window.history && window.history.pushState);
832 8d08f18a Kostas Papadimitriou
      var fragment          = this.getFragment();
833 8d08f18a Kostas Papadimitriou
      var docMode           = document.documentMode;
834 8d08f18a Kostas Papadimitriou
      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));
835 8d08f18a Kostas Papadimitriou
      if (oldIE) {
836 8d08f18a Kostas Papadimitriou
        this.iframe = $('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;
837 8d08f18a Kostas Papadimitriou
        this.navigate(fragment);
838 8d08f18a Kostas Papadimitriou
      }
839 8d08f18a Kostas Papadimitriou
840 8d08f18a Kostas Papadimitriou
      // Depending on whether we're using pushState or hashes, and whether
841 8d08f18a Kostas Papadimitriou
      // 'onhashchange' is supported, determine how we check the URL state.
842 8d08f18a Kostas Papadimitriou
      if (this._hasPushState) {
843 8d08f18a Kostas Papadimitriou
        $(window).bind('popstate', this.checkUrl);
844 8d08f18a Kostas Papadimitriou
      } else if ('onhashchange' in window && !oldIE) {
845 8d08f18a Kostas Papadimitriou
        $(window).bind('hashchange', this.checkUrl);
846 8d08f18a Kostas Papadimitriou
      } else {
847 8d08f18a Kostas Papadimitriou
        setInterval(this.checkUrl, this.interval);
848 8d08f18a Kostas Papadimitriou
      }
849 8d08f18a Kostas Papadimitriou
850 8d08f18a Kostas Papadimitriou
      // Determine if we need to change the base url, for a pushState link
851 8d08f18a Kostas Papadimitriou
      // opened by a non-pushState browser.
852 8d08f18a Kostas Papadimitriou
      this.fragment = fragment;
853 8d08f18a Kostas Papadimitriou
      historyStarted = true;
854 8d08f18a Kostas Papadimitriou
      var loc = window.location;
855 8d08f18a Kostas Papadimitriou
      var atRoot  = loc.pathname == this.options.root;
856 8d08f18a Kostas Papadimitriou
      if (this._wantsPushState && !this._hasPushState && !atRoot) {
857 8d08f18a Kostas Papadimitriou
        this.fragment = this.getFragment(null, true);
858 8d08f18a Kostas Papadimitriou
        window.location.replace(this.options.root + '#' + this.fragment);
859 8d08f18a Kostas Papadimitriou
        // Return immediately as browser will do redirect to new url
860 8d08f18a Kostas Papadimitriou
        return true;
861 8d08f18a Kostas Papadimitriou
      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {
862 8d08f18a Kostas Papadimitriou
        this.fragment = loc.hash.replace(hashStrip, '');
863 8d08f18a Kostas Papadimitriou
        window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);
864 8d08f18a Kostas Papadimitriou
      }
865 8d08f18a Kostas Papadimitriou
866 8d08f18a Kostas Papadimitriou
      if (!this.options.silent) {
867 8d08f18a Kostas Papadimitriou
        return this.loadUrl();
868 8d08f18a Kostas Papadimitriou
      }
869 8d08f18a Kostas Papadimitriou
    },
870 8d08f18a Kostas Papadimitriou
871 8d08f18a Kostas Papadimitriou
    // Add a route to be tested when the fragment changes. Routes added later may
872 8d08f18a Kostas Papadimitriou
    // override previous routes.
873 8d08f18a Kostas Papadimitriou
    route : function(route, callback) {
874 8d08f18a Kostas Papadimitriou
      this.handlers.unshift({route : route, callback : callback});
875 8d08f18a Kostas Papadimitriou
    },
876 8d08f18a Kostas Papadimitriou
877 8d08f18a Kostas Papadimitriou
    // Checks the current URL to see if it has changed, and if it has,
878 8d08f18a Kostas Papadimitriou
    // calls `loadUrl`, normalizing across the hidden iframe.
879 8d08f18a Kostas Papadimitriou
    checkUrl : function(e) {
880 8d08f18a Kostas Papadimitriou
      var current = this.getFragment();
881 8d08f18a Kostas Papadimitriou
      if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);
882 8d08f18a Kostas Papadimitriou
      if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;
883 8d08f18a Kostas Papadimitriou
      if (this.iframe) this.navigate(current);
884 8d08f18a Kostas Papadimitriou
      this.loadUrl() || this.loadUrl(window.location.hash);
885 8d08f18a Kostas Papadimitriou
    },
886 8d08f18a Kostas Papadimitriou
887 8d08f18a Kostas Papadimitriou
    // Attempt to load the current URL fragment. If a route succeeds with a
888 8d08f18a Kostas Papadimitriou
    // match, returns `true`. If no defined routes matches the fragment,
889 8d08f18a Kostas Papadimitriou
    // returns `false`.
890 8d08f18a Kostas Papadimitriou
    loadUrl : function(fragmentOverride) {
891 8d08f18a Kostas Papadimitriou
      var fragment = this.fragment = this.getFragment(fragmentOverride);
892 8d08f18a Kostas Papadimitriou
      var matched = _.any(this.handlers, function(handler) {
893 8d08f18a Kostas Papadimitriou
        if (handler.route.test(fragment)) {
894 8d08f18a Kostas Papadimitriou
          handler.callback(fragment);
895 8d08f18a Kostas Papadimitriou
          return true;
896 8d08f18a Kostas Papadimitriou
        }
897 8d08f18a Kostas Papadimitriou
      });
898 8d08f18a Kostas Papadimitriou
      return matched;
899 8d08f18a Kostas Papadimitriou
    },
900 8d08f18a Kostas Papadimitriou
901 8d08f18a Kostas Papadimitriou
    // Save a fragment into the hash history. You are responsible for properly
902 8d08f18a Kostas Papadimitriou
    // URL-encoding the fragment in advance. This does not trigger
903 8d08f18a Kostas Papadimitriou
    // a `hashchange` event.
904 8d08f18a Kostas Papadimitriou
    navigate : function(fragment, triggerRoute) {
905 8d08f18a Kostas Papadimitriou
      var frag = (fragment || '').replace(hashStrip, '');
906 8d08f18a Kostas Papadimitriou
      if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;
907 8d08f18a Kostas Papadimitriou
      if (this._hasPushState) {
908 8d08f18a Kostas Papadimitriou
        var loc = window.location;
909 8d08f18a Kostas Papadimitriou
        if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;
910 8d08f18a Kostas Papadimitriou
        this.fragment = frag;
911 8d08f18a Kostas Papadimitriou
        window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);
912 8d08f18a Kostas Papadimitriou
      } else {
913 8d08f18a Kostas Papadimitriou
        window.location.hash = this.fragment = frag;
914 8d08f18a Kostas Papadimitriou
        if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {
915 8d08f18a Kostas Papadimitriou
          this.iframe.document.open().close();
916 8d08f18a Kostas Papadimitriou
          this.iframe.location.hash = frag;
917 8d08f18a Kostas Papadimitriou
        }
918 8d08f18a Kostas Papadimitriou
      }
919 8d08f18a Kostas Papadimitriou
      if (triggerRoute) this.loadUrl(fragment);
920 8d08f18a Kostas Papadimitriou
    }
921 8d08f18a Kostas Papadimitriou
922 8d08f18a Kostas Papadimitriou
  });
923 8d08f18a Kostas Papadimitriou
924 8d08f18a Kostas Papadimitriou
  // Backbone.View
925 8d08f18a Kostas Papadimitriou
  // -------------
926 8d08f18a Kostas Papadimitriou
927 8d08f18a Kostas Papadimitriou
  // Creating a Backbone.View creates its initial element outside of the DOM,
928 8d08f18a Kostas Papadimitriou
  // if an existing element is not provided...
929 8d08f18a Kostas Papadimitriou
  Backbone.View = function(options) {
930 8d08f18a Kostas Papadimitriou
    this.cid = _.uniqueId('view');
931 8d08f18a Kostas Papadimitriou
    this._configure(options || {});
932 8d08f18a Kostas Papadimitriou
    this._ensureElement();
933 8d08f18a Kostas Papadimitriou
    this.delegateEvents();
934 8d08f18a Kostas Papadimitriou
    this.initialize.apply(this, arguments);
935 8d08f18a Kostas Papadimitriou
  };
936 8d08f18a Kostas Papadimitriou
937 8d08f18a Kostas Papadimitriou
  // Element lookup, scoped to DOM elements within the current view.
938 8d08f18a Kostas Papadimitriou
  // This should be prefered to global lookups, if you're dealing with
939 8d08f18a Kostas Papadimitriou
  // a specific view.
940 8d08f18a Kostas Papadimitriou
  var selectorDelegate = function(selector) {
941 2f834e7d Kostas Papadimitriou
    return $(selector, this.el);
942 8d08f18a Kostas Papadimitriou
  };
943 8d08f18a Kostas Papadimitriou
944 8d08f18a Kostas Papadimitriou
  // Cached regex to split keys for `delegate`.
945 8d08f18a Kostas Papadimitriou
  var eventSplitter = /^(\S+)\s*(.*)$/;
946 8d08f18a Kostas Papadimitriou
947 8d08f18a Kostas Papadimitriou
  // List of view options to be merged as properties.
948 8d08f18a Kostas Papadimitriou
  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];
949 8d08f18a Kostas Papadimitriou
950 8d08f18a Kostas Papadimitriou
  // Set up all inheritable **Backbone.View** properties and methods.
951 8d08f18a Kostas Papadimitriou
  _.extend(Backbone.View.prototype, Backbone.Events, {
952 8d08f18a Kostas Papadimitriou
953 8d08f18a Kostas Papadimitriou
    // The default `tagName` of a View's element is `"div"`.
954 8d08f18a Kostas Papadimitriou
    tagName : 'div',
955 8d08f18a Kostas Papadimitriou
956 8d08f18a Kostas Papadimitriou
    // Attach the `selectorDelegate` function as the `$` property.
957 8d08f18a Kostas Papadimitriou
    $       : selectorDelegate,
958 8d08f18a Kostas Papadimitriou
959 8d08f18a Kostas Papadimitriou
    // Initialize is an empty function by default. Override it with your own
960 8d08f18a Kostas Papadimitriou
    // initialization logic.
961 8d08f18a Kostas Papadimitriou
    initialize : function(){},
962 8d08f18a Kostas Papadimitriou
963 8d08f18a Kostas Papadimitriou
    // **render** is the core function that your view should override, in order
964 8d08f18a Kostas Papadimitriou
    // to populate its element (`this.el`), with the appropriate HTML. The
965 8d08f18a Kostas Papadimitriou
    // convention is for **render** to always return `this`.
966 8d08f18a Kostas Papadimitriou
    render : function() {
967 8d08f18a Kostas Papadimitriou
      return this;
968 8d08f18a Kostas Papadimitriou
    },
969 8d08f18a Kostas Papadimitriou
970 8d08f18a Kostas Papadimitriou
    // Remove this view from the DOM. Note that the view isn't present in the
971 8d08f18a Kostas Papadimitriou
    // DOM by default, so calling this method may be a no-op.
972 8d08f18a Kostas Papadimitriou
    remove : function() {
973 8d08f18a Kostas Papadimitriou
      $(this.el).remove();
974 8d08f18a Kostas Papadimitriou
      return this;
975 8d08f18a Kostas Papadimitriou
    },
976 8d08f18a Kostas Papadimitriou
977 8d08f18a Kostas Papadimitriou
    // For small amounts of DOM Elements, where a full-blown template isn't
978 8d08f18a Kostas Papadimitriou
    // needed, use **make** to manufacture elements, one at a time.
979 8d08f18a Kostas Papadimitriou
    //
980 8d08f18a Kostas Papadimitriou
    //     var el = this.make('li', {'class': 'row'}, this.model.escape('title'));
981 8d08f18a Kostas Papadimitriou
    //
982 8d08f18a Kostas Papadimitriou
    make : function(tagName, attributes, content) {
983 8d08f18a Kostas Papadimitriou
      var el = document.createElement(tagName);
984 8d08f18a Kostas Papadimitriou
      if (attributes) $(el).attr(attributes);
985 8d08f18a Kostas Papadimitriou
      if (content) $(el).html(content);
986 8d08f18a Kostas Papadimitriou
      return el;
987 8d08f18a Kostas Papadimitriou
    },
988 8d08f18a Kostas Papadimitriou
989 2f834e7d Kostas Papadimitriou
    // Set callbacks, where `this.events` is a hash of
990 8d08f18a Kostas Papadimitriou
    //
991 8d08f18a Kostas Papadimitriou
    // *{"event selector": "callback"}*
992 8d08f18a Kostas Papadimitriou
    //
993 8d08f18a Kostas Papadimitriou
    //     {
994 8d08f18a Kostas Papadimitriou
    //       'mousedown .title':  'edit',
995 8d08f18a Kostas Papadimitriou
    //       'click .button':     'save'
996 8d08f18a Kostas Papadimitriou
    //     }
997 8d08f18a Kostas Papadimitriou
    //
998 8d08f18a Kostas Papadimitriou
    // pairs. Callbacks will be bound to the view, with `this` set properly.
999 8d08f18a Kostas Papadimitriou
    // Uses event delegation for efficiency.
1000 8d08f18a Kostas Papadimitriou
    // Omitting the selector binds the event to `this.el`.
1001 8d08f18a Kostas Papadimitriou
    // This only works for delegate-able events: not `focus`, `blur`, and
1002 8d08f18a Kostas Papadimitriou
    // not `change`, `submit`, and `reset` in Internet Explorer.
1003 8d08f18a Kostas Papadimitriou
    delegateEvents : function(events) {
1004 8d08f18a Kostas Papadimitriou
      if (!(events || (events = this.events))) return;
1005 8d08f18a Kostas Papadimitriou
      if (_.isFunction(events)) events = events.call(this);
1006 2f834e7d Kostas Papadimitriou
      this.undelegateEvents();
1007 8d08f18a Kostas Papadimitriou
      for (var key in events) {
1008 8d08f18a Kostas Papadimitriou
        var method = this[events[key]];
1009 8d08f18a Kostas Papadimitriou
        if (!method) throw new Error('Event "' + events[key] + '" does not exist');
1010 8d08f18a Kostas Papadimitriou
        var match = key.match(eventSplitter);
1011 8d08f18a Kostas Papadimitriou
        var eventName = match[1], selector = match[2];
1012 8d08f18a Kostas Papadimitriou
        method = _.bind(method, this);
1013 8d08f18a Kostas Papadimitriou
        eventName += '.delegateEvents' + this.cid;
1014 8d08f18a Kostas Papadimitriou
        if (selector === '') {
1015 8d08f18a Kostas Papadimitriou
          $(this.el).bind(eventName, method);
1016 8d08f18a Kostas Papadimitriou
        } else {
1017 8d08f18a Kostas Papadimitriou
          $(this.el).delegate(selector, eventName, method);
1018 8d08f18a Kostas Papadimitriou
        }
1019 8d08f18a Kostas Papadimitriou
      }
1020 8d08f18a Kostas Papadimitriou
    },
1021 8d08f18a Kostas Papadimitriou
1022 2f834e7d Kostas Papadimitriou
    // Clears all callbacks previously bound to the view with `delegateEvents`.
1023 2f834e7d Kostas Papadimitriou
    undelegateEvents : function() {
1024 2f834e7d Kostas Papadimitriou
      $(this.el).unbind('.delegateEvents' + this.cid);
1025 2f834e7d Kostas Papadimitriou
    },
1026 2f834e7d Kostas Papadimitriou
1027 8d08f18a Kostas Papadimitriou
    // Performs the initial configuration of a View with a set of options.
1028 8d08f18a Kostas Papadimitriou
    // Keys with special meaning *(model, collection, id, className)*, are
1029 8d08f18a Kostas Papadimitriou
    // attached directly to the view.
1030 8d08f18a Kostas Papadimitriou
    _configure : function(options) {
1031 8d08f18a Kostas Papadimitriou
      if (this.options) options = _.extend({}, this.options, options);
1032 8d08f18a Kostas Papadimitriou
      for (var i = 0, l = viewOptions.length; i < l; i++) {
1033 8d08f18a Kostas Papadimitriou
        var attr = viewOptions[i];
1034 8d08f18a Kostas Papadimitriou
        if (options[attr]) this[attr] = options[attr];
1035 8d08f18a Kostas Papadimitriou
      }
1036 8d08f18a Kostas Papadimitriou
      this.options = options;
1037 8d08f18a Kostas Papadimitriou
    },
1038 8d08f18a Kostas Papadimitriou
1039 8d08f18a Kostas Papadimitriou
    // Ensure that the View has a DOM element to render into.
1040 8d08f18a Kostas Papadimitriou
    // If `this.el` is a string, pass it through `$()`, take the first
1041 8d08f18a Kostas Papadimitriou
    // matching element, and re-assign it to `el`. Otherwise, create
1042 8d08f18a Kostas Papadimitriou
    // an element from the `id`, `className` and `tagName` properties.
1043 8d08f18a Kostas Papadimitriou
    _ensureElement : function() {
1044 8d08f18a Kostas Papadimitriou
      if (!this.el) {
1045 8d08f18a Kostas Papadimitriou
        var attrs = this.attributes || {};
1046 8d08f18a Kostas Papadimitriou
        if (this.id) attrs.id = this.id;
1047 8d08f18a Kostas Papadimitriou
        if (this.className) attrs['class'] = this.className;
1048 8d08f18a Kostas Papadimitriou
        this.el = this.make(this.tagName, attrs);
1049 8d08f18a Kostas Papadimitriou
      } else if (_.isString(this.el)) {
1050 8d08f18a Kostas Papadimitriou
        this.el = $(this.el).get(0);
1051 8d08f18a Kostas Papadimitriou
      }
1052 8d08f18a Kostas Papadimitriou
    }
1053 8d08f18a Kostas Papadimitriou
1054 8d08f18a Kostas Papadimitriou
  });
1055 8d08f18a Kostas Papadimitriou
1056 8d08f18a Kostas Papadimitriou
  // The self-propagating extend function that Backbone classes use.
1057 8d08f18a Kostas Papadimitriou
  var extend = function (protoProps, classProps) {
1058 8d08f18a Kostas Papadimitriou
    var child = inherits(this, protoProps, classProps);
1059 8d08f18a Kostas Papadimitriou
    child.extend = this.extend;
1060 8d08f18a Kostas Papadimitriou
    return child;
1061 8d08f18a Kostas Papadimitriou
  };
1062 8d08f18a Kostas Papadimitriou
1063 8d08f18a Kostas Papadimitriou
  // Set up inheritance for the model, collection, and view.
1064 8d08f18a Kostas Papadimitriou
  Backbone.Model.extend = Backbone.Collection.extend =
1065 8d08f18a Kostas Papadimitriou
    Backbone.Router.extend = Backbone.View.extend = extend;
1066 8d08f18a Kostas Papadimitriou
1067 8d08f18a Kostas Papadimitriou
  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
1068 8d08f18a Kostas Papadimitriou
  var methodMap = {
1069 8d08f18a Kostas Papadimitriou
    'create': 'POST',
1070 8d08f18a Kostas Papadimitriou
    'update': 'PUT',
1071 8d08f18a Kostas Papadimitriou
    'delete': 'DELETE',
1072 258235f4 Kostas Papadimitriou
    'read'  : 'GET',
1073 258235f4 Kostas Papadimitriou
    'head'  : 'HEAD'
1074 8d08f18a Kostas Papadimitriou
  };
1075 8d08f18a Kostas Papadimitriou
1076 8d08f18a Kostas Papadimitriou
  // Backbone.sync
1077 8d08f18a Kostas Papadimitriou
  // -------------
1078 8d08f18a Kostas Papadimitriou
1079 8d08f18a Kostas Papadimitriou
  // Override this function to change the manner in which Backbone persists
1080 8d08f18a Kostas Papadimitriou
  // models to the server. You will be passed the type of request, and the
1081 2f834e7d Kostas Papadimitriou
  // model in question. By default, makes a RESTful Ajax request
1082 8d08f18a Kostas Papadimitriou
  // to the model's `url()`. Some possible customizations could be:
1083 8d08f18a Kostas Papadimitriou
  //
1084 8d08f18a Kostas Papadimitriou
  // * Use `setTimeout` to batch rapid-fire updates into a single request.
1085 8d08f18a Kostas Papadimitriou
  // * Send up the models as XML instead of JSON.
1086 8d08f18a Kostas Papadimitriou
  // * Persist models via WebSockets instead of Ajax.
1087 8d08f18a Kostas Papadimitriou
  //
1088 8d08f18a Kostas Papadimitriou
  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
1089 8d08f18a Kostas Papadimitriou
  // as `POST`, with a `_method` parameter containing the true HTTP method,
1090 8d08f18a Kostas Papadimitriou
  // as well as all requests with the body as `application/x-www-form-urlencoded` instead of
1091 8d08f18a Kostas Papadimitriou
  // `application/json` with the model in a param named `model`.
1092 8d08f18a Kostas Papadimitriou
  // Useful when interfacing with server-side languages like **PHP** that make
1093 8d08f18a Kostas Papadimitriou
  // it difficult to read the body of `PUT` requests.
1094 8d08f18a Kostas Papadimitriou
  Backbone.sync = function(method, model, options) {
1095 8d08f18a Kostas Papadimitriou
    var type = methodMap[method];
1096 8d08f18a Kostas Papadimitriou
1097 8d08f18a Kostas Papadimitriou
    // Default JSON-request options.
1098 2f834e7d Kostas Papadimitriou
    var params = {type : type, dataType : 'json'};
1099 8d08f18a Kostas Papadimitriou
1100 8d08f18a Kostas Papadimitriou
    // Ensure that we have a URL.
1101 2f834e7d Kostas Papadimitriou
    if (!options.url) {
1102 8d08f18a Kostas Papadimitriou
      params.url = getUrl(model, options) || urlError();
1103 8d08f18a Kostas Papadimitriou
    }
1104 8d08f18a Kostas Papadimitriou
1105 8d08f18a Kostas Papadimitriou
    // Ensure that we have the appropriate request data.
1106 2f834e7d Kostas Papadimitriou
    if (!options.data && model && (method == 'create' || method == 'update')) {
1107 8d08f18a Kostas Papadimitriou
      params.contentType = 'application/json';
1108 8d08f18a Kostas Papadimitriou
      params.data = JSON.stringify(model.toJSON());
1109 8d08f18a Kostas Papadimitriou
    }
1110 8d08f18a Kostas Papadimitriou
1111 8d08f18a Kostas Papadimitriou
    // For older servers, emulate JSON by encoding the request into an HTML-form.
1112 8d08f18a Kostas Papadimitriou
    if (Backbone.emulateJSON) {
1113 8d08f18a Kostas Papadimitriou
      params.contentType = 'application/x-www-form-urlencoded';
1114 2f834e7d Kostas Papadimitriou
      params.data = params.data ? {model : params.data} : {};
1115 8d08f18a Kostas Papadimitriou
    }
1116 8d08f18a Kostas Papadimitriou
1117 8d08f18a Kostas Papadimitriou
    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
1118 8d08f18a Kostas Papadimitriou
    // And an `X-HTTP-Method-Override` header.
1119 8d08f18a Kostas Papadimitriou
    if (Backbone.emulateHTTP) {
1120 8d08f18a Kostas Papadimitriou
      if (type === 'PUT' || type === 'DELETE') {
1121 8d08f18a Kostas Papadimitriou
        if (Backbone.emulateJSON) params.data._method = type;
1122 8d08f18a Kostas Papadimitriou
        params.type = 'POST';
1123 8d08f18a Kostas Papadimitriou
        params.beforeSend = function(xhr) {
1124 8d08f18a Kostas Papadimitriou
          xhr.setRequestHeader('X-HTTP-Method-Override', type);
1125 8d08f18a Kostas Papadimitriou
        };
1126 8d08f18a Kostas Papadimitriou
      }
1127 8d08f18a Kostas Papadimitriou
    }
1128 8d08f18a Kostas Papadimitriou
1129 8d08f18a Kostas Papadimitriou
    // Don't process data on a non-GET request.
1130 8d08f18a Kostas Papadimitriou
    if (params.type !== 'GET' && !Backbone.emulateJSON) {
1131 8d08f18a Kostas Papadimitriou
      params.processData = false;
1132 8d08f18a Kostas Papadimitriou
    }
1133 2f834e7d Kostas Papadimitriou
1134 2f834e7d Kostas Papadimitriou
    // Make the request, allowing the user to override any Ajax options.
1135 2f834e7d Kostas Papadimitriou
    return $.ajax(_.extend(params, options));
1136 8d08f18a Kostas Papadimitriou
  };
1137 8d08f18a Kostas Papadimitriou
1138 8d08f18a Kostas Papadimitriou
  // Helpers
1139 8d08f18a Kostas Papadimitriou
  // -------
1140 8d08f18a Kostas Papadimitriou
1141 8d08f18a Kostas Papadimitriou
  // Shared empty constructor function to aid in prototype-chain creation.
1142 8d08f18a Kostas Papadimitriou
  var ctor = function(){};
1143 8d08f18a Kostas Papadimitriou
1144 8d08f18a Kostas Papadimitriou
  // Helper function to correctly set up the prototype chain, for subclasses.
1145 8d08f18a Kostas Papadimitriou
  // Similar to `goog.inherits`, but uses a hash of prototype properties and
1146 8d08f18a Kostas Papadimitriou
  // class properties to be extended.
1147 8d08f18a Kostas Papadimitriou
  var inherits = function(parent, protoProps, staticProps) {
1148 8d08f18a Kostas Papadimitriou
    var child;
1149 8d08f18a Kostas Papadimitriou
1150 8d08f18a Kostas Papadimitriou
    // The constructor function for the new subclass is either defined by you
1151 8d08f18a Kostas Papadimitriou
    // (the "constructor" property in your `extend` definition), or defaulted
1152 8d08f18a Kostas Papadimitriou
    // by us to simply call `super()`.
1153 8d08f18a Kostas Papadimitriou
    if (protoProps && protoProps.hasOwnProperty('constructor')) {
1154 8d08f18a Kostas Papadimitriou
      child = protoProps.constructor;
1155 8d08f18a Kostas Papadimitriou
    } else {
1156 8d08f18a Kostas Papadimitriou
      child = function(){ return parent.apply(this, arguments); };
1157 8d08f18a Kostas Papadimitriou
    }
1158 8d08f18a Kostas Papadimitriou
1159 8d08f18a Kostas Papadimitriou
    // Inherit class (static) properties from parent.
1160 8d08f18a Kostas Papadimitriou
    _.extend(child, parent);
1161 8d08f18a Kostas Papadimitriou
1162 8d08f18a Kostas Papadimitriou
    // Set the prototype chain to inherit from `parent`, without calling
1163 8d08f18a Kostas Papadimitriou
    // `parent`'s constructor function.
1164 8d08f18a Kostas Papadimitriou
    ctor.prototype = parent.prototype;
1165 8d08f18a Kostas Papadimitriou
    child.prototype = new ctor();
1166 8d08f18a Kostas Papadimitriou
1167 8d08f18a Kostas Papadimitriou
    // Add prototype properties (instance properties) to the subclass,
1168 8d08f18a Kostas Papadimitriou
    // if supplied.
1169 8d08f18a Kostas Papadimitriou
    if (protoProps) _.extend(child.prototype, protoProps);
1170 8d08f18a Kostas Papadimitriou
1171 8d08f18a Kostas Papadimitriou
    // Add static properties to the constructor function, if supplied.
1172 8d08f18a Kostas Papadimitriou
    if (staticProps) _.extend(child, staticProps);
1173 8d08f18a Kostas Papadimitriou
1174 8d08f18a Kostas Papadimitriou
    // Correctly set child's `prototype.constructor`.
1175 8d08f18a Kostas Papadimitriou
    child.prototype.constructor = child;
1176 8d08f18a Kostas Papadimitriou
1177 8d08f18a Kostas Papadimitriou
    // Set a convenience property in case the parent's prototype is needed later.
1178 8d08f18a Kostas Papadimitriou
    child.__super__ = parent.prototype;
1179 8d08f18a Kostas Papadimitriou
1180 8d08f18a Kostas Papadimitriou
    return child;
1181 8d08f18a Kostas Papadimitriou
  };
1182 8d08f18a Kostas Papadimitriou
1183 8d08f18a Kostas Papadimitriou
  // Helper function to get a URL from a Model or Collection as a property
1184 8d08f18a Kostas Papadimitriou
  // or as a function.
1185 8d08f18a Kostas Papadimitriou
  var getUrl = function(object, options) {
1186 8d08f18a Kostas Papadimitriou
    if (!(object && object.url)) return null;
1187 8d08f18a Kostas Papadimitriou
    return _.isFunction(object.url) ? object.url(options) : object.url;
1188 8d08f18a Kostas Papadimitriou
  };
1189 8d08f18a Kostas Papadimitriou
1190 8d08f18a Kostas Papadimitriou
  // Throw an error when a URL is needed, and none is supplied.
1191 8d08f18a Kostas Papadimitriou
  var urlError = function() {
1192 8d08f18a Kostas Papadimitriou
    throw new Error('A "url" property or function must be specified');
1193 8d08f18a Kostas Papadimitriou
  };
1194 8d08f18a Kostas Papadimitriou
1195 8d08f18a Kostas Papadimitriou
  // Wrap an optional error callback with a fallback error event.
1196 2f834e7d Kostas Papadimitriou
  var wrapError = function(onError, originalModel, options) {
1197 2f834e7d Kostas Papadimitriou
    return function(model, resp) {
1198 36f921a4 Kostas Papadimitriou
      var model = originalModel;
1199 8d08f18a Kostas Papadimitriou
      if (onError) {
1200 8d08f18a Kostas Papadimitriou
        onError(model, resp, options);
1201 8d08f18a Kostas Papadimitriou
      } else {
1202 8d08f18a Kostas Papadimitriou
        model.trigger('error', model, resp, options);
1203 8d08f18a Kostas Papadimitriou
      }
1204 8d08f18a Kostas Papadimitriou
    };
1205 8d08f18a Kostas Papadimitriou
  };
1206 8d08f18a Kostas Papadimitriou
1207 8d08f18a Kostas Papadimitriou
}).call(this);