| changeset 1512 | 487ca37bb0c7 |
| parent 1304 | 10974bff4dae |
| 1511:25731d245eda | 1512:487ca37bb0c7 |
|---|---|
1 // Backbone.js 1.2.3 |
1 // Backbone.js 1.4.0 |
2 |
2 |
3 // (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors |
3 // (c) 2010-2019 Jeremy Ashkenas and DocumentCloud |
4 // Backbone may be freely distributed under the MIT license. |
4 // Backbone may be freely distributed under the MIT license. |
5 // For all details and documentation: |
5 // For all details and documentation: |
6 // http://backbonejs.org |
6 // http://backbonejs.org |
7 |
7 |
8 (function(factory) { |
8 (function(factory) { |
9 |
9 |
10 // Establish the root object, `window` (`self`) in the browser, or `global` on the server. |
10 // Establish the root object, `window` (`self`) in the browser, or `global` on the server. |
11 // We use `self` instead of `window` for `WebWorker` support. |
11 // We use `self` instead of `window` for `WebWorker` support. |
12 var root = (typeof self == 'object' && self.self == self && self) || |
12 var root = typeof self == 'object' && self.self === self && self || |
13 (typeof global == 'object' && global.global == global && global); |
13 typeof global == 'object' && global.global === global && global; |
14 |
14 |
15 // Set up Backbone appropriately for the environment. Start with AMD. |
15 // Set up Backbone appropriately for the environment. Start with AMD. |
16 if (typeof define === 'function' && define.amd) { |
16 if (typeof define === 'function' && define.amd) { |
17 define(['underscore', 'jquery', 'exports'], function(_, $, exports) { |
17 define(['underscore', 'jquery', 'exports'], function(_, $, exports) { |
18 // Export global even in AMD case in case this script is loaded with |
18 // Export global even in AMD case in case this script is loaded with |
21 }); |
21 }); |
22 |
22 |
23 // Next for Node.js or CommonJS. jQuery may not be needed as a module. |
23 // Next for Node.js or CommonJS. jQuery may not be needed as a module. |
24 } else if (typeof exports !== 'undefined') { |
24 } else if (typeof exports !== 'undefined') { |
25 var _ = require('underscore'), $; |
25 var _ = require('underscore'), $; |
26 try { $ = require('jquery'); } catch(e) {} |
26 try { $ = require('jquery'); } catch (e) {} |
27 factory(root, exports, _, $); |
27 factory(root, exports, _, $); |
28 |
28 |
29 // Finally, as a browser global. |
29 // Finally, as a browser global. |
30 } else { |
30 } else { |
31 root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); |
31 root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$); |
32 } |
32 } |
33 |
33 |
34 }(function(root, Backbone, _, $) { |
34 })(function(root, Backbone, _, $) { |
35 |
35 |
36 // Initial Setup |
36 // Initial Setup |
37 // ------------- |
37 // ------------- |
38 |
38 |
39 // Save the previous value of the `Backbone` variable, so that it can be |
39 // Save the previous value of the `Backbone` variable, so that it can be |
42 |
42 |
43 // Create a local reference to a common array method we'll want to use later. |
43 // Create a local reference to a common array method we'll want to use later. |
44 var slice = Array.prototype.slice; |
44 var slice = Array.prototype.slice; |
45 |
45 |
46 // Current version of the library. Keep in sync with `package.json`. |
46 // Current version of the library. Keep in sync with `package.json`. |
47 Backbone.VERSION = '1.2.3'; |
47 Backbone.VERSION = '1.4.0'; |
48 |
48 |
49 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns |
49 // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns |
50 // the `$` variable. |
50 // the `$` variable. |
51 Backbone.$ = $; |
51 Backbone.$ = $; |
52 |
52 |
65 // Turn on `emulateJSON` to support legacy servers that can't deal with direct |
65 // Turn on `emulateJSON` to support legacy servers that can't deal with direct |
66 // `application/json` requests ... this will encode the body as |
66 // `application/json` requests ... this will encode the body as |
67 // `application/x-www-form-urlencoded` instead and will send the model in a |
67 // `application/x-www-form-urlencoded` instead and will send the model in a |
68 // form param named `model`. |
68 // form param named `model`. |
69 Backbone.emulateJSON = false; |
69 Backbone.emulateJSON = false; |
70 |
|
71 // Proxy Backbone class methods to Underscore functions, wrapping the model's |
|
72 // `attributes` object or collection's `models` array behind the scenes. |
|
73 // |
|
74 // collection.filter(function(model) { return model.get('age') > 10 }); |
|
75 // collection.each(this.addView); |
|
76 // |
|
77 // `Function#apply` can be slow so we use the method's arg count, if we know it. |
|
78 var addMethod = function(length, method, attribute) { |
|
79 switch (length) { |
|
80 case 1: return function() { |
|
81 return _[method](this[attribute]); |
|
82 }; |
|
83 case 2: return function(value) { |
|
84 return _[method](this[attribute], value); |
|
85 }; |
|
86 case 3: return function(iteratee, context) { |
|
87 return _[method](this[attribute], cb(iteratee, this), context); |
|
88 }; |
|
89 case 4: return function(iteratee, defaultVal, context) { |
|
90 return _[method](this[attribute], cb(iteratee, this), defaultVal, context); |
|
91 }; |
|
92 default: return function() { |
|
93 var args = slice.call(arguments); |
|
94 args.unshift(this[attribute]); |
|
95 return _[method].apply(_, args); |
|
96 }; |
|
97 } |
|
98 }; |
|
99 var addUnderscoreMethods = function(Class, methods, attribute) { |
|
100 _.each(methods, function(length, method) { |
|
101 if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); |
|
102 }); |
|
103 }; |
|
104 |
|
105 // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. |
|
106 var cb = function(iteratee, instance) { |
|
107 if (_.isFunction(iteratee)) return iteratee; |
|
108 if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); |
|
109 if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; |
|
110 return iteratee; |
|
111 }; |
|
112 var modelMatcher = function(attrs) { |
|
113 var matcher = _.matches(attrs); |
|
114 return function(model) { |
|
115 return matcher(model.attributes); |
|
116 }; |
|
117 }; |
|
118 |
70 |
119 // Backbone.Events |
71 // Backbone.Events |
120 // --------------- |
72 // --------------- |
121 |
73 |
122 // A module that can be mixed in to *any object* in order to provide it with |
74 // A module that can be mixed in to *any object* in order to provide it with |
132 var Events = Backbone.Events = {}; |
84 var Events = Backbone.Events = {}; |
133 |
85 |
134 // Regular expression used to split event strings. |
86 // Regular expression used to split event strings. |
135 var eventSplitter = /\s+/; |
87 var eventSplitter = /\s+/; |
136 |
88 |
89 // A private global variable to share between listeners and listenees. |
|
90 var _listening; |
|
91 |
|
137 // Iterates over the standard `event, callback` (as well as the fancy multiple |
92 // Iterates over the standard `event, callback` (as well as the fancy multiple |
138 // space-separated events `"change blur", callback` and jQuery-style event |
93 // space-separated events `"change blur", callback` and jQuery-style event |
139 // maps `{event: callback}`). |
94 // maps `{event: callback}`). |
140 var eventsApi = function(iteratee, events, name, callback, opts) { |
95 var eventsApi = function(iteratee, events, name, callback, opts) { |
141 var i = 0, names; |
96 var i = 0, names; |
144 if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; |
99 if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; |
145 for (names = _.keys(name); i < names.length ; i++) { |
100 for (names = _.keys(name); i < names.length ; i++) { |
146 events = eventsApi(iteratee, events, names[i], name[names[i]], opts); |
101 events = eventsApi(iteratee, events, names[i], name[names[i]], opts); |
147 } |
102 } |
148 } else if (name && eventSplitter.test(name)) { |
103 } else if (name && eventSplitter.test(name)) { |
149 // Handle space separated event names by delegating them individually. |
104 // Handle space-separated event names by delegating them individually. |
150 for (names = name.split(eventSplitter); i < names.length; i++) { |
105 for (names = name.split(eventSplitter); i < names.length; i++) { |
151 events = iteratee(events, names[i], callback, opts); |
106 events = iteratee(events, names[i], callback, opts); |
152 } |
107 } |
153 } else { |
108 } else { |
154 // Finally, standard events. |
109 // Finally, standard events. |
158 }; |
113 }; |
159 |
114 |
160 // Bind an event to a `callback` function. Passing `"all"` will bind |
115 // Bind an event to a `callback` function. Passing `"all"` will bind |
161 // the callback to all events fired. |
116 // the callback to all events fired. |
162 Events.on = function(name, callback, context) { |
117 Events.on = function(name, callback, context) { |
163 return internalOn(this, name, callback, context); |
118 this._events = eventsApi(onApi, this._events || {}, name, callback, { |
164 }; |
119 context: context, |
165 |
120 ctx: this, |
166 // Guard the `listening` argument from the public API. |
121 listening: _listening |
167 var internalOn = function(obj, name, callback, context, listening) { |
|
168 obj._events = eventsApi(onApi, obj._events || {}, name, callback, { |
|
169 context: context, |
|
170 ctx: obj, |
|
171 listening: listening |
|
172 }); |
122 }); |
173 |
123 |
174 if (listening) { |
124 if (_listening) { |
175 var listeners = obj._listeners || (obj._listeners = {}); |
125 var listeners = this._listeners || (this._listeners = {}); |
176 listeners[listening.id] = listening; |
126 listeners[_listening.id] = _listening; |
177 } |
127 // Allow the listening to use a counter, instead of tracking |
178 |
128 // callbacks for library interop |
179 return obj; |
129 _listening.interop = false; |
130 } |
|
131 |
|
132 return this; |
|
180 }; |
133 }; |
181 |
134 |
182 // Inversion-of-control versions of `on`. Tell *this* object to listen to |
135 // Inversion-of-control versions of `on`. Tell *this* object to listen to |
183 // an event in another object... keeping track of what it's listening to |
136 // an event in another object... keeping track of what it's listening to |
184 // for easier unbinding later. |
137 // for easier unbinding later. |
185 Events.listenTo = function(obj, name, callback) { |
138 Events.listenTo = function(obj, name, callback) { |
186 if (!obj) return this; |
139 if (!obj) return this; |
187 var id = obj._listenId || (obj._listenId = _.uniqueId('l')); |
140 var id = obj._listenId || (obj._listenId = _.uniqueId('l')); |
188 var listeningTo = this._listeningTo || (this._listeningTo = {}); |
141 var listeningTo = this._listeningTo || (this._listeningTo = {}); |
189 var listening = listeningTo[id]; |
142 var listening = _listening = listeningTo[id]; |
190 |
143 |
191 // This object is not listening to any other events on `obj` yet. |
144 // This object is not listening to any other events on `obj` yet. |
192 // Setup the necessary references to track the listening callbacks. |
145 // Setup the necessary references to track the listening callbacks. |
193 if (!listening) { |
146 if (!listening) { |
194 var thisId = this._listenId || (this._listenId = _.uniqueId('l')); |
147 this._listenId || (this._listenId = _.uniqueId('l')); |
195 listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; |
148 listening = _listening = listeningTo[id] = new Listening(this, obj); |
196 } |
149 } |
197 |
150 |
198 // Bind callbacks on obj, and keep track of them on listening. |
151 // Bind callbacks on obj. |
199 internalOn(obj, name, callback, this, listening); |
152 var error = tryCatchOn(obj, name, callback, this); |
153 _listening = void 0; |
|
154 |
|
155 if (error) throw error; |
|
156 // If the target obj is not Backbone.Events, track events manually. |
|
157 if (listening.interop) listening.on(name, callback); |
|
158 |
|
200 return this; |
159 return this; |
201 }; |
160 }; |
202 |
161 |
203 // The reducing API that adds a callback to the `events` object. |
162 // The reducing API that adds a callback to the `events` object. |
204 var onApi = function(events, name, callback, options) { |
163 var onApi = function(events, name, callback, options) { |
205 if (callback) { |
164 if (callback) { |
206 var handlers = events[name] || (events[name] = []); |
165 var handlers = events[name] || (events[name] = []); |
207 var context = options.context, ctx = options.ctx, listening = options.listening; |
166 var context = options.context, ctx = options.ctx, listening = options.listening; |
208 if (listening) listening.count++; |
167 if (listening) listening.count++; |
209 |
168 |
210 handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening }); |
169 handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); |
211 } |
170 } |
212 return events; |
171 return events; |
172 }; |
|
173 |
|
174 // An try-catch guarded #on function, to prevent poisoning the global |
|
175 // `_listening` variable. |
|
176 var tryCatchOn = function(obj, name, callback, context) { |
|
177 try { |
|
178 obj.on(name, callback, context); |
|
179 } catch (e) { |
|
180 return e; |
|
181 } |
|
213 }; |
182 }; |
214 |
183 |
215 // Remove one or many callbacks. If `context` is null, removes all |
184 // Remove one or many callbacks. If `context` is null, removes all |
216 // callbacks with that function. If `callback` is null, removes all |
185 // callbacks with that function. If `callback` is null, removes all |
217 // callbacks for the event. If `name` is null, removes all bound |
186 // callbacks for the event. If `name` is null, removes all bound |
218 // callbacks for all events. |
187 // callbacks for all events. |
219 Events.off = function(name, callback, context) { |
188 Events.off = function(name, callback, context) { |
220 if (!this._events) return this; |
189 if (!this._events) return this; |
221 this._events = eventsApi(offApi, this._events, name, callback, { |
190 this._events = eventsApi(offApi, this._events, name, callback, { |
222 context: context, |
191 context: context, |
223 listeners: this._listeners |
192 listeners: this._listeners |
224 }); |
193 }); |
194 |
|
225 return this; |
195 return this; |
226 }; |
196 }; |
227 |
197 |
228 // Tell this object to stop listening to either specific events ... or |
198 // Tell this object to stop listening to either specific events ... or |
229 // to every object it's currently listening to. |
199 // to every object it's currently listening to. |
230 Events.stopListening = function(obj, name, callback) { |
200 Events.stopListening = function(obj, name, callback) { |
231 var listeningTo = this._listeningTo; |
201 var listeningTo = this._listeningTo; |
232 if (!listeningTo) return this; |
202 if (!listeningTo) return this; |
233 |
203 |
234 var ids = obj ? [obj._listenId] : _.keys(listeningTo); |
204 var ids = obj ? [obj._listenId] : _.keys(listeningTo); |
235 |
|
236 for (var i = 0; i < ids.length; i++) { |
205 for (var i = 0; i < ids.length; i++) { |
237 var listening = listeningTo[ids[i]]; |
206 var listening = listeningTo[ids[i]]; |
238 |
207 |
239 // If listening doesn't exist, this object is not currently |
208 // If listening doesn't exist, this object is not currently |
240 // listening to obj. Break out early. |
209 // listening to obj. Break out early. |
241 if (!listening) break; |
210 if (!listening) break; |
242 |
211 |
243 listening.obj.off(name, callback, this); |
212 listening.obj.off(name, callback, this); |
213 if (listening.interop) listening.off(name, callback); |
|
244 } |
214 } |
245 if (_.isEmpty(listeningTo)) this._listeningTo = void 0; |
215 if (_.isEmpty(listeningTo)) this._listeningTo = void 0; |
246 |
216 |
247 return this; |
217 return this; |
248 }; |
218 }; |
249 |
219 |
250 // The reducing API that removes a callback from the `events` object. |
220 // The reducing API that removes a callback from the `events` object. |
251 var offApi = function(events, name, callback, options) { |
221 var offApi = function(events, name, callback, options) { |
252 if (!events) return; |
222 if (!events) return; |
253 |
223 |
254 var i = 0, listening; |
|
255 var context = options.context, listeners = options.listeners; |
224 var context = options.context, listeners = options.listeners; |
256 |
225 var i = 0, names; |
257 // Delete all events listeners and "drop" events. |
226 |
258 if (!name && !callback && !context) { |
227 // Delete all event listeners and "drop" events. |
259 var ids = _.keys(listeners); |
228 if (!name && !context && !callback) { |
260 for (; i < ids.length; i++) { |
229 for (names = _.keys(listeners); i < names.length; i++) { |
261 listening = listeners[ids[i]]; |
230 listeners[names[i]].cleanup(); |
262 delete listeners[listening.id]; |
|
263 delete listening.listeningTo[listening.objId]; |
|
264 } |
231 } |
265 return; |
232 return; |
266 } |
233 } |
267 |
234 |
268 var names = name ? [name] : _.keys(events); |
235 names = name ? [name] : _.keys(events); |
269 for (; i < names.length; i++) { |
236 for (; i < names.length; i++) { |
270 name = names[i]; |
237 name = names[i]; |
271 var handlers = events[name]; |
238 var handlers = events[name]; |
272 |
239 |
273 // Bail out if there are no events stored. |
240 // Bail out if there are no events stored. |
274 if (!handlers) break; |
241 if (!handlers) break; |
275 |
242 |
276 // Replace events if there are any remaining. Otherwise, clean up. |
243 // Find any remaining events. |
277 var remaining = []; |
244 var remaining = []; |
278 for (var j = 0; j < handlers.length; j++) { |
245 for (var j = 0; j < handlers.length; j++) { |
279 var handler = handlers[j]; |
246 var handler = handlers[j]; |
280 if ( |
247 if ( |
281 callback && callback !== handler.callback && |
248 callback && callback !== handler.callback && |
282 callback !== handler.callback._callback || |
249 callback !== handler.callback._callback || |
283 context && context !== handler.context |
250 context && context !== handler.context |
284 ) { |
251 ) { |
285 remaining.push(handler); |
252 remaining.push(handler); |
286 } else { |
253 } else { |
287 listening = handler.listening; |
254 var listening = handler.listening; |
288 if (listening && --listening.count === 0) { |
255 if (listening) listening.off(name, callback); |
289 delete listeners[listening.id]; |
|
290 delete listening.listeningTo[listening.objId]; |
|
291 } |
|
292 } |
256 } |
293 } |
257 } |
294 |
258 |
295 // Update tail event if the list has any events. Otherwise, clean up. |
259 // Replace events if there are any remaining. Otherwise, clean up. |
296 if (remaining.length) { |
260 if (remaining.length) { |
297 events[name] = remaining; |
261 events[name] = remaining; |
298 } else { |
262 } else { |
299 delete events[name]; |
263 delete events[name]; |
300 } |
264 } |
301 } |
265 } |
302 if (_.size(events)) return events; |
266 |
267 return events; |
|
303 }; |
268 }; |
304 |
269 |
305 // Bind an event to only be triggered a single time. After the first time |
270 // Bind an event to only be triggered a single time. After the first time |
306 // the callback is invoked, its listener will be removed. If multiple events |
271 // the callback is invoked, its listener will be removed. If multiple events |
307 // are passed in using the space-separated syntax, the handler will fire |
272 // are passed in using the space-separated syntax, the handler will fire |
308 // once for each event, not once for a combination of all events. |
273 // once for each event, not once for a combination of all events. |
309 Events.once = function(name, callback, context) { |
274 Events.once = function(name, callback, context) { |
310 // Map the event into a `{event: once}` object. |
275 // Map the event into a `{event: once}` object. |
311 var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); |
276 var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); |
312 return this.on(events, void 0, context); |
277 if (typeof name === 'string' && context == null) callback = void 0; |
278 return this.on(events, callback, context); |
|
313 }; |
279 }; |
314 |
280 |
315 // Inversion-of-control versions of `once`. |
281 // Inversion-of-control versions of `once`. |
316 Events.listenToOnce = function(obj, name, callback) { |
282 Events.listenToOnce = function(obj, name, callback) { |
317 // Map the event into a `{event: once}` object. |
283 // Map the event into a `{event: once}` object. |
318 var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); |
284 var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); |
319 return this.listenTo(obj, events); |
285 return this.listenTo(obj, events); |
320 }; |
286 }; |
321 |
287 |
322 // Reduces the event callbacks into a map of `{event: onceWrapper}`. |
288 // Reduces the event callbacks into a map of `{event: onceWrapper}`. |
323 // `offer` unbinds the `onceWrapper` after it has been called. |
289 // `offer` unbinds the `onceWrapper` after it has been called. |
334 |
300 |
335 // Trigger one or many events, firing all bound callbacks. Callbacks are |
301 // Trigger one or many events, firing all bound callbacks. Callbacks are |
336 // passed the same arguments as `trigger` is, apart from the event name |
302 // passed the same arguments as `trigger` is, apart from the event name |
337 // (unless you're listening on `"all"`, which will cause your callback to |
303 // (unless you're listening on `"all"`, which will cause your callback to |
338 // receive the true name of the event as the first argument). |
304 // receive the true name of the event as the first argument). |
339 Events.trigger = function(name) { |
305 Events.trigger = function(name) { |
340 if (!this._events) return this; |
306 if (!this._events) return this; |
341 |
307 |
342 var length = Math.max(0, arguments.length - 1); |
308 var length = Math.max(0, arguments.length - 1); |
343 var args = Array(length); |
309 var args = Array(length); |
344 for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; |
310 for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; |
346 eventsApi(triggerApi, this._events, name, void 0, args); |
312 eventsApi(triggerApi, this._events, name, void 0, args); |
347 return this; |
313 return this; |
348 }; |
314 }; |
349 |
315 |
350 // Handles triggering the appropriate event callbacks. |
316 // Handles triggering the appropriate event callbacks. |
351 var triggerApi = function(objEvents, name, cb, args) { |
317 var triggerApi = function(objEvents, name, callback, args) { |
352 if (objEvents) { |
318 if (objEvents) { |
353 var events = objEvents[name]; |
319 var events = objEvents[name]; |
354 var allEvents = objEvents.all; |
320 var allEvents = objEvents.all; |
355 if (events && allEvents) allEvents = allEvents.slice(); |
321 if (events && allEvents) allEvents = allEvents.slice(); |
356 if (events) triggerEvents(events, args); |
322 if (events) triggerEvents(events, args); |
371 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; |
337 case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; |
372 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; |
338 default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; |
373 } |
339 } |
374 }; |
340 }; |
375 |
341 |
342 // A listening class that tracks and cleans up memory bindings |
|
343 // when all callbacks have been offed. |
|
344 var Listening = function(listener, obj) { |
|
345 this.id = listener._listenId; |
|
346 this.listener = listener; |
|
347 this.obj = obj; |
|
348 this.interop = true; |
|
349 this.count = 0; |
|
350 this._events = void 0; |
|
351 }; |
|
352 |
|
353 Listening.prototype.on = Events.on; |
|
354 |
|
355 // Offs a callback (or several). |
|
356 // Uses an optimized counter if the listenee uses Backbone.Events. |
|
357 // Otherwise, falls back to manual tracking to support events |
|
358 // library interop. |
|
359 Listening.prototype.off = function(name, callback) { |
|
360 var cleanup; |
|
361 if (this.interop) { |
|
362 this._events = eventsApi(offApi, this._events, name, callback, { |
|
363 context: void 0, |
|
364 listeners: void 0 |
|
365 }); |
|
366 cleanup = !this._events; |
|
367 } else { |
|
368 this.count--; |
|
369 cleanup = this.count === 0; |
|
370 } |
|
371 if (cleanup) this.cleanup(); |
|
372 }; |
|
373 |
|
374 // Cleans up memory bindings between the listener and the listenee. |
|
375 Listening.prototype.cleanup = function() { |
|
376 delete this.listener._listeningTo[this.obj._listenId]; |
|
377 if (!this.interop) delete this.obj._listeners[this.id]; |
|
378 }; |
|
379 |
|
376 // Aliases for backwards compatibility. |
380 // Aliases for backwards compatibility. |
377 Events.bind = Events.on; |
381 Events.bind = Events.on; |
378 Events.unbind = Events.off; |
382 Events.unbind = Events.off; |
379 |
383 |
380 // Allow the `Backbone` object to serve as a global event bus, for folks who |
384 // Allow the `Backbone` object to serve as a global event bus, for folks who |
392 // Create a new model with the specified attributes. A client id (`cid`) |
396 // Create a new model with the specified attributes. A client id (`cid`) |
393 // is automatically generated and assigned for you. |
397 // is automatically generated and assigned for you. |
394 var Model = Backbone.Model = function(attributes, options) { |
398 var Model = Backbone.Model = function(attributes, options) { |
395 var attrs = attributes || {}; |
399 var attrs = attributes || {}; |
396 options || (options = {}); |
400 options || (options = {}); |
401 this.preinitialize.apply(this, arguments); |
|
397 this.cid = _.uniqueId(this.cidPrefix); |
402 this.cid = _.uniqueId(this.cidPrefix); |
398 this.attributes = {}; |
403 this.attributes = {}; |
399 if (options.collection) this.collection = options.collection; |
404 if (options.collection) this.collection = options.collection; |
400 if (options.parse) attrs = this.parse(attrs, options) || {}; |
405 if (options.parse) attrs = this.parse(attrs, options) || {}; |
401 attrs = _.defaults({}, attrs, _.result(this, 'defaults')); |
406 var defaults = _.result(this, 'defaults'); |
407 attrs = _.defaults(_.extend({}, defaults, attrs), defaults); |
|
402 this.set(attrs, options); |
408 this.set(attrs, options); |
403 this.changed = {}; |
409 this.changed = {}; |
404 this.initialize.apply(this, arguments); |
410 this.initialize.apply(this, arguments); |
405 }; |
411 }; |
406 |
412 |
418 idAttribute: 'id', |
424 idAttribute: 'id', |
419 |
425 |
420 // The prefix is used to create the client id which is used to identify models locally. |
426 // The prefix is used to create the client id which is used to identify models locally. |
421 // You may want to override this if you're experiencing name clashes with model ids. |
427 // You may want to override this if you're experiencing name clashes with model ids. |
422 cidPrefix: 'c', |
428 cidPrefix: 'c', |
429 |
|
430 // preinitialize is an empty function by default. You can override it with a function |
|
431 // or object. preinitialize will run before any instantiation logic is run in the Model. |
|
432 preinitialize: function(){}, |
|
423 |
433 |
424 // Initialize is an empty function by default. Override it with your own |
434 // Initialize is an empty function by default. Override it with your own |
425 // initialization logic. |
435 // initialization logic. |
426 initialize: function(){}, |
436 initialize: function(){}, |
427 |
437 |
504 } |
514 } |
505 unset ? delete current[attr] : current[attr] = val; |
515 unset ? delete current[attr] : current[attr] = val; |
506 } |
516 } |
507 |
517 |
508 // Update the `id`. |
518 // Update the `id`. |
509 this.id = this.get(this.idAttribute); |
519 if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); |
510 |
520 |
511 // Trigger all relevant attribute changes. |
521 // Trigger all relevant attribute changes. |
512 if (!silent) { |
522 if (!silent) { |
513 if (changes.length) this._pending = options; |
523 if (changes.length) this._pending = options; |
514 for (var i = 0; i < changes.length; i++) { |
524 for (var i = 0; i < changes.length; i++) { |
559 // determining if there *would be* a change. |
569 // determining if there *would be* a change. |
560 changedAttributes: function(diff) { |
570 changedAttributes: function(diff) { |
561 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; |
571 if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; |
562 var old = this._changing ? this._previousAttributes : this.attributes; |
572 var old = this._changing ? this._previousAttributes : this.attributes; |
563 var changed = {}; |
573 var changed = {}; |
574 var hasChanged; |
|
564 for (var attr in diff) { |
575 for (var attr in diff) { |
565 var val = diff[attr]; |
576 var val = diff[attr]; |
566 if (_.isEqual(old[attr], val)) continue; |
577 if (_.isEqual(old[attr], val)) continue; |
567 changed[attr] = val; |
578 changed[attr] = val; |
568 } |
579 hasChanged = true; |
569 return _.size(changed) ? changed : false; |
580 } |
581 return hasChanged ? changed : false; |
|
570 }, |
582 }, |
571 |
583 |
572 // Get the previous value of an attribute, recorded at the time the last |
584 // Get the previous value of an attribute, recorded at the time the last |
573 // `"change"` event was fired. |
585 // `"change"` event was fired. |
574 previous: function(attr) { |
586 previous: function(attr) { |
617 // If we're not waiting and attributes exist, save acts as |
629 // If we're not waiting and attributes exist, save acts as |
618 // `set(attr).save(null, opts)` with validation. Otherwise, check if |
630 // `set(attr).save(null, opts)` with validation. Otherwise, check if |
619 // the model will be valid when the attributes, if any, are set. |
631 // the model will be valid when the attributes, if any, are set. |
620 if (attrs && !wait) { |
632 if (attrs && !wait) { |
621 if (!this.set(attrs, options)) return false; |
633 if (!this.set(attrs, options)) return false; |
622 } else { |
634 } else if (!this._validate(attrs, options)) { |
623 if (!this._validate(attrs, options)) return false; |
635 return false; |
624 } |
636 } |
625 |
637 |
626 // After a successful server-side save, the client is (optionally) |
638 // After a successful server-side save, the client is (optionally) |
627 // updated with the server-side state. |
639 // updated with the server-side state. |
628 var model = this; |
640 var model = this; |
640 wrapError(this, options); |
652 wrapError(this, options); |
641 |
653 |
642 // Set temporary attributes if `{wait: true}` to properly find new ids. |
654 // Set temporary attributes if `{wait: true}` to properly find new ids. |
643 if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); |
655 if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); |
644 |
656 |
645 var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); |
657 var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update'; |
646 if (method === 'patch' && !options.attrs) options.attrs = attrs; |
658 if (method === 'patch' && !options.attrs) options.attrs = attrs; |
647 var xhr = this.sync(method, this, options); |
659 var xhr = this.sync(method, this, options); |
648 |
660 |
649 // Restore attributes. |
661 // Restore attributes. |
650 this.attributes = attributes; |
662 this.attributes = attributes; |
712 return !this.has(this.idAttribute); |
724 return !this.has(this.idAttribute); |
713 }, |
725 }, |
714 |
726 |
715 // Check if the model is currently in a valid state. |
727 // Check if the model is currently in a valid state. |
716 isValid: function(options) { |
728 isValid: function(options) { |
717 return this._validate({}, _.defaults({validate: true}, options)); |
729 return this._validate({}, _.extend({}, options, {validate: true})); |
718 }, |
730 }, |
719 |
731 |
720 // Run validation against the next complete set of model attributes, |
732 // Run validation against the next complete set of model attributes, |
721 // returning `true` if all is well. Otherwise, fire an `"invalid"` event. |
733 // returning `true` if all is well. Otherwise, fire an `"invalid"` event. |
722 _validate: function(attrs, options) { |
734 _validate: function(attrs, options) { |
728 return false; |
740 return false; |
729 } |
741 } |
730 |
742 |
731 }); |
743 }); |
732 |
744 |
733 // Underscore methods that we want to implement on the Model, mapped to the |
|
734 // number of arguments they take. |
|
735 var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, |
|
736 omit: 0, chain: 1, isEmpty: 1 }; |
|
737 |
|
738 // Mix in each Underscore method as a proxy to `Model#attributes`. |
|
739 addUnderscoreMethods(Model, modelMethods, 'attributes'); |
|
740 |
|
741 // Backbone.Collection |
745 // Backbone.Collection |
742 // ------------------- |
746 // ------------------- |
743 |
747 |
744 // If models tend to represent a single row of data, a Backbone Collection is |
748 // If models tend to represent a single row of data, a Backbone Collection is |
745 // more analogous to a table full of data ... or a small slice or page of that |
749 // more analogous to a table full of data ... or a small slice or page of that |
751 // Create a new **Collection**, perhaps to contain a specific type of `model`. |
755 // Create a new **Collection**, perhaps to contain a specific type of `model`. |
752 // If a `comparator` is specified, the Collection will maintain |
756 // If a `comparator` is specified, the Collection will maintain |
753 // its models in sort order, as they're added and removed. |
757 // its models in sort order, as they're added and removed. |
754 var Collection = Backbone.Collection = function(models, options) { |
758 var Collection = Backbone.Collection = function(models, options) { |
755 options || (options = {}); |
759 options || (options = {}); |
760 this.preinitialize.apply(this, arguments); |
|
756 if (options.model) this.model = options.model; |
761 if (options.model) this.model = options.model; |
757 if (options.comparator !== void 0) this.comparator = options.comparator; |
762 if (options.comparator !== void 0) this.comparator = options.comparator; |
758 this._reset(); |
763 this._reset(); |
759 this.initialize.apply(this, arguments); |
764 this.initialize.apply(this, arguments); |
760 if (models) this.reset(models, _.extend({silent: true}, options)); |
765 if (models) this.reset(models, _.extend({silent: true}, options)); |
767 // Splices `insert` into `array` at index `at`. |
772 // Splices `insert` into `array` at index `at`. |
768 var splice = function(array, insert, at) { |
773 var splice = function(array, insert, at) { |
769 at = Math.min(Math.max(at, 0), array.length); |
774 at = Math.min(Math.max(at, 0), array.length); |
770 var tail = Array(array.length - at); |
775 var tail = Array(array.length - at); |
771 var length = insert.length; |
776 var length = insert.length; |
772 for (var i = 0; i < tail.length; i++) tail[i] = array[i + at]; |
777 var i; |
778 for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; |
|
773 for (i = 0; i < length; i++) array[i + at] = insert[i]; |
779 for (i = 0; i < length; i++) array[i + at] = insert[i]; |
774 for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; |
780 for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; |
775 }; |
781 }; |
776 |
782 |
777 // Define the Collection's inheritable methods. |
783 // Define the Collection's inheritable methods. |
778 _.extend(Collection.prototype, Events, { |
784 _.extend(Collection.prototype, Events, { |
779 |
785 |
780 // The default model for a collection is just a **Backbone.Model**. |
786 // The default model for a collection is just a **Backbone.Model**. |
781 // This should be overridden in most cases. |
787 // This should be overridden in most cases. |
782 model: Model, |
788 model: Model, |
789 |
|
790 |
|
791 // preinitialize is an empty function by default. You can override it with a function |
|
792 // or object. preinitialize will run before any instantiation logic is run in the Collection. |
|
793 preinitialize: function(){}, |
|
783 |
794 |
784 // Initialize is an empty function by default. Override it with your own |
795 // Initialize is an empty function by default. Override it with your own |
785 // initialization logic. |
796 // initialization logic. |
786 initialize: function(){}, |
797 initialize: function(){}, |
787 |
798 |
805 |
816 |
806 // Remove a model, or a list of models from the set. |
817 // Remove a model, or a list of models from the set. |
807 remove: function(models, options) { |
818 remove: function(models, options) { |
808 options = _.extend({}, options); |
819 options = _.extend({}, options); |
809 var singular = !_.isArray(models); |
820 var singular = !_.isArray(models); |
810 models = singular ? [models] : _.clone(models); |
821 models = singular ? [models] : models.slice(); |
811 var removed = this._removeModels(models, options); |
822 var removed = this._removeModels(models, options); |
812 if (!options.silent && removed) this.trigger('update', this, options); |
823 if (!options.silent && removed.length) { |
824 options.changes = {added: [], merged: [], removed: removed}; |
|
825 this.trigger('update', this, options); |
|
826 } |
|
813 return singular ? removed[0] : removed; |
827 return singular ? removed[0] : removed; |
814 }, |
828 }, |
815 |
829 |
816 // Update a collection by `set`-ing a new list of models, adding new ones, |
830 // Update a collection by `set`-ing a new list of models, adding new ones, |
817 // removing models that are no longer present, and merging models that |
831 // removing models that are no longer present, and merging models that |
818 // already exist in the collection, as necessary. Similar to **Model#set**, |
832 // already exist in the collection, as necessary. Similar to **Model#set**, |
819 // the core operation for updating the data contained by the collection. |
833 // the core operation for updating the data contained by the collection. |
820 set: function(models, options) { |
834 set: function(models, options) { |
821 if (models == null) return; |
835 if (models == null) return; |
822 |
836 |
823 options = _.defaults({}, options, setOptions); |
837 options = _.extend({}, setOptions, options); |
824 if (options.parse && !this._isModel(models)) models = this.parse(models, options); |
838 if (options.parse && !this._isModel(models)) { |
839 models = this.parse(models, options) || []; |
|
840 } |
|
825 |
841 |
826 var singular = !_.isArray(models); |
842 var singular = !_.isArray(models); |
827 models = singular ? [models] : models.slice(); |
843 models = singular ? [models] : models.slice(); |
828 |
844 |
829 var at = options.at; |
845 var at = options.at; |
830 if (at != null) at = +at; |
846 if (at != null) at = +at; |
847 if (at > this.length) at = this.length; |
|
831 if (at < 0) at += this.length + 1; |
848 if (at < 0) at += this.length + 1; |
832 |
849 |
833 var set = []; |
850 var set = []; |
834 var toAdd = []; |
851 var toAdd = []; |
852 var toMerge = []; |
|
835 var toRemove = []; |
853 var toRemove = []; |
836 var modelMap = {}; |
854 var modelMap = {}; |
837 |
855 |
838 var add = options.add; |
856 var add = options.add; |
839 var merge = options.merge; |
857 var merge = options.merge; |
840 var remove = options.remove; |
858 var remove = options.remove; |
841 |
859 |
842 var sort = false; |
860 var sort = false; |
843 var sortable = this.comparator && (at == null) && options.sort !== false; |
861 var sortable = this.comparator && at == null && options.sort !== false; |
844 var sortAttr = _.isString(this.comparator) ? this.comparator : null; |
862 var sortAttr = _.isString(this.comparator) ? this.comparator : null; |
845 |
863 |
846 // Turn bare objects into model references, and prevent invalid models |
864 // Turn bare objects into model references, and prevent invalid models |
847 // from being added. |
865 // from being added. |
848 var model; |
866 var model, i; |
849 for (var i = 0; i < models.length; i++) { |
867 for (i = 0; i < models.length; i++) { |
850 model = models[i]; |
868 model = models[i]; |
851 |
869 |
852 // If a duplicate is found, prevent it from being added and |
870 // If a duplicate is found, prevent it from being added and |
853 // optionally merge it into the existing model. |
871 // optionally merge it into the existing model. |
854 var existing = this.get(model); |
872 var existing = this.get(model); |
855 if (existing) { |
873 if (existing) { |
856 if (merge && model !== existing) { |
874 if (merge && model !== existing) { |
857 var attrs = this._isModel(model) ? model.attributes : model; |
875 var attrs = this._isModel(model) ? model.attributes : model; |
858 if (options.parse) attrs = existing.parse(attrs, options); |
876 if (options.parse) attrs = existing.parse(attrs, options); |
859 existing.set(attrs, options); |
877 existing.set(attrs, options); |
878 toMerge.push(existing); |
|
860 if (sortable && !sort) sort = existing.hasChanged(sortAttr); |
879 if (sortable && !sort) sort = existing.hasChanged(sortAttr); |
861 } |
880 } |
862 if (!modelMap[existing.cid]) { |
881 if (!modelMap[existing.cid]) { |
863 modelMap[existing.cid] = true; |
882 modelMap[existing.cid] = true; |
864 set.push(existing); |
883 set.push(existing); |
888 |
907 |
889 // See if sorting is needed, update `length` and splice in new models. |
908 // See if sorting is needed, update `length` and splice in new models. |
890 var orderChanged = false; |
909 var orderChanged = false; |
891 var replace = !sortable && add && remove; |
910 var replace = !sortable && add && remove; |
892 if (set.length && replace) { |
911 if (set.length && replace) { |
893 orderChanged = this.length != set.length || _.some(this.models, function(model, index) { |
912 orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { |
894 return model !== set[index]; |
913 return m !== set[index]; |
895 }); |
914 }); |
896 this.models.length = 0; |
915 this.models.length = 0; |
897 splice(this.models, set, 0); |
916 splice(this.models, set, 0); |
898 this.length = this.models.length; |
917 this.length = this.models.length; |
899 } else if (toAdd.length) { |
918 } else if (toAdd.length) { |
903 } |
922 } |
904 |
923 |
905 // Silently sort the collection if appropriate. |
924 // Silently sort the collection if appropriate. |
906 if (sort) this.sort({silent: true}); |
925 if (sort) this.sort({silent: true}); |
907 |
926 |
908 // Unless silenced, it's time to fire all appropriate add/sort events. |
927 // Unless silenced, it's time to fire all appropriate add/sort/update events. |
909 if (!options.silent) { |
928 if (!options.silent) { |
910 for (i = 0; i < toAdd.length; i++) { |
929 for (i = 0; i < toAdd.length; i++) { |
911 if (at != null) options.index = at + i; |
930 if (at != null) options.index = at + i; |
912 model = toAdd[i]; |
931 model = toAdd[i]; |
913 model.trigger('add', model, this, options); |
932 model.trigger('add', model, this, options); |
914 } |
933 } |
915 if (sort || orderChanged) this.trigger('sort', this, options); |
934 if (sort || orderChanged) this.trigger('sort', this, options); |
916 if (toAdd.length || toRemove.length) this.trigger('update', this, options); |
935 if (toAdd.length || toRemove.length || toMerge.length) { |
936 options.changes = { |
|
937 added: toAdd, |
|
938 removed: toRemove, |
|
939 merged: toMerge |
|
940 }; |
|
941 this.trigger('update', this, options); |
|
942 } |
|
917 } |
943 } |
918 |
944 |
919 // Return the added (or merged) model (or models). |
945 // Return the added (or merged) model (or models). |
920 return singular ? models[0] : models; |
946 return singular ? models[0] : models; |
921 }, |
947 }, |
961 // Slice out a sub-array of models from the collection. |
987 // Slice out a sub-array of models from the collection. |
962 slice: function() { |
988 slice: function() { |
963 return slice.apply(this.models, arguments); |
989 return slice.apply(this.models, arguments); |
964 }, |
990 }, |
965 |
991 |
966 // Get a model from the set by id. |
992 // Get a model from the set by id, cid, model object with id or cid |
993 // properties, or an attributes object that is transformed through modelId. |
|
967 get: function(obj) { |
994 get: function(obj) { |
968 if (obj == null) return void 0; |
995 if (obj == null) return void 0; |
969 var id = this.modelId(this._isModel(obj) ? obj.attributes : obj); |
996 return this._byId[obj] || |
970 return this._byId[obj] || this._byId[id] || this._byId[obj.cid]; |
997 this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] || |
998 obj.cid && this._byId[obj.cid]; |
|
999 }, |
|
1000 |
|
1001 // Returns `true` if the model is in the collection. |
|
1002 has: function(obj) { |
|
1003 return this.get(obj) != null; |
|
971 }, |
1004 }, |
972 |
1005 |
973 // Get the model at the given index. |
1006 // Get the model at the given index. |
974 at: function(index) { |
1007 at: function(index) { |
975 if (index < 0) index += this.length; |
1008 if (index < 0) index += this.length; |
995 var comparator = this.comparator; |
1028 var comparator = this.comparator; |
996 if (!comparator) throw new Error('Cannot sort a set without a comparator'); |
1029 if (!comparator) throw new Error('Cannot sort a set without a comparator'); |
997 options || (options = {}); |
1030 options || (options = {}); |
998 |
1031 |
999 var length = comparator.length; |
1032 var length = comparator.length; |
1000 if (_.isFunction(comparator)) comparator = _.bind(comparator, this); |
1033 if (_.isFunction(comparator)) comparator = comparator.bind(this); |
1001 |
1034 |
1002 // Run sort based on type of `comparator`. |
1035 // Run sort based on type of `comparator`. |
1003 if (length === 1 || _.isString(comparator)) { |
1036 if (length === 1 || _.isString(comparator)) { |
1004 this.models = this.sortBy(comparator); |
1037 this.models = this.sortBy(comparator); |
1005 } else { |
1038 } else { |
1009 return this; |
1042 return this; |
1010 }, |
1043 }, |
1011 |
1044 |
1012 // Pluck an attribute from each model in the collection. |
1045 // Pluck an attribute from each model in the collection. |
1013 pluck: function(attr) { |
1046 pluck: function(attr) { |
1014 return _.invoke(this.models, 'get', attr); |
1047 return this.map(attr + ''); |
1015 }, |
1048 }, |
1016 |
1049 |
1017 // Fetch the default set of models for this collection, resetting the |
1050 // Fetch the default set of models for this collection, resetting the |
1018 // collection when they arrive. If `reset: true` is passed, the response |
1051 // collection when they arrive. If `reset: true` is passed, the response |
1019 // data will be passed through the `reset` method instead of `set`. |
1052 // data will be passed through the `reset` method instead of `set`. |
1040 model = this._prepareModel(model, options); |
1073 model = this._prepareModel(model, options); |
1041 if (!model) return false; |
1074 if (!model) return false; |
1042 if (!wait) this.add(model, options); |
1075 if (!wait) this.add(model, options); |
1043 var collection = this; |
1076 var collection = this; |
1044 var success = options.success; |
1077 var success = options.success; |
1045 options.success = function(model, resp, callbackOpts) { |
1078 options.success = function(m, resp, callbackOpts) { |
1046 if (wait) collection.add(model, callbackOpts); |
1079 if (wait) collection.add(m, callbackOpts); |
1047 if (success) success.call(callbackOpts.context, model, resp, callbackOpts); |
1080 if (success) success.call(callbackOpts.context, m, resp, callbackOpts); |
1048 }; |
1081 }; |
1049 model.save(null, options); |
1082 model.save(null, options); |
1050 return model; |
1083 return model; |
1051 }, |
1084 }, |
1052 |
1085 |
1063 comparator: this.comparator |
1096 comparator: this.comparator |
1064 }); |
1097 }); |
1065 }, |
1098 }, |
1066 |
1099 |
1067 // Define how to uniquely identify models in the collection. |
1100 // Define how to uniquely identify models in the collection. |
1068 modelId: function (attrs) { |
1101 modelId: function(attrs) { |
1069 return attrs[this.model.prototype.idAttribute || 'id']; |
1102 return attrs[this.model.prototype.idAttribute || 'id']; |
1103 }, |
|
1104 |
|
1105 // Get an iterator of all models in this collection. |
|
1106 values: function() { |
|
1107 return new CollectionIterator(this, ITERATOR_VALUES); |
|
1108 }, |
|
1109 |
|
1110 // Get an iterator of all model IDs in this collection. |
|
1111 keys: function() { |
|
1112 return new CollectionIterator(this, ITERATOR_KEYS); |
|
1113 }, |
|
1114 |
|
1115 // Get an iterator of all [ID, model] tuples in this collection. |
|
1116 entries: function() { |
|
1117 return new CollectionIterator(this, ITERATOR_KEYSVALUES); |
|
1070 }, |
1118 }, |
1071 |
1119 |
1072 // Private method to reset all internal state. Called when the collection |
1120 // Private method to reset all internal state. Called when the collection |
1073 // is first initialized or reset. |
1121 // is first initialized or reset. |
1074 _reset: function() { |
1122 _reset: function() { |
1101 |
1149 |
1102 var index = this.indexOf(model); |
1150 var index = this.indexOf(model); |
1103 this.models.splice(index, 1); |
1151 this.models.splice(index, 1); |
1104 this.length--; |
1152 this.length--; |
1105 |
1153 |
1154 // Remove references before triggering 'remove' event to prevent an |
|
1155 // infinite loop. #3693 |
|
1156 delete this._byId[model.cid]; |
|
1157 var id = this.modelId(model.attributes); |
|
1158 if (id != null) delete this._byId[id]; |
|
1159 |
|
1106 if (!options.silent) { |
1160 if (!options.silent) { |
1107 options.index = index; |
1161 options.index = index; |
1108 model.trigger('remove', model, this, options); |
1162 model.trigger('remove', model, this, options); |
1109 } |
1163 } |
1110 |
1164 |
1111 removed.push(model); |
1165 removed.push(model); |
1112 this._removeReference(model, options); |
1166 this._removeReference(model, options); |
1113 } |
1167 } |
1114 return removed.length ? removed : false; |
1168 return removed; |
1115 }, |
1169 }, |
1116 |
1170 |
1117 // Method for checking whether an object should be considered a model for |
1171 // Method for checking whether an object should be considered a model for |
1118 // the purposes of adding to the collection. |
1172 // the purposes of adding to the collection. |
1119 _isModel: function (model) { |
1173 _isModel: function(model) { |
1120 return model instanceof Model; |
1174 return model instanceof Model; |
1121 }, |
1175 }, |
1122 |
1176 |
1123 // Internal method to create a model's ties to a collection. |
1177 // Internal method to create a model's ties to a collection. |
1124 _addReference: function(model, options) { |
1178 _addReference: function(model, options) { |
1140 // Internal method called every time a model in the set fires an event. |
1194 // Internal method called every time a model in the set fires an event. |
1141 // Sets need to update their indexes when models change ids. All other |
1195 // Sets need to update their indexes when models change ids. All other |
1142 // events simply proxy through. "add" and "remove" events that originate |
1196 // events simply proxy through. "add" and "remove" events that originate |
1143 // in other collections are ignored. |
1197 // in other collections are ignored. |
1144 _onModelEvent: function(event, model, collection, options) { |
1198 _onModelEvent: function(event, model, collection, options) { |
1145 if ((event === 'add' || event === 'remove') && collection !== this) return; |
1199 if (model) { |
1146 if (event === 'destroy') this.remove(model, options); |
1200 if ((event === 'add' || event === 'remove') && collection !== this) return; |
1147 if (event === 'change') { |
1201 if (event === 'destroy') this.remove(model, options); |
1148 var prevId = this.modelId(model.previousAttributes()); |
1202 if (event === 'change') { |
1149 var id = this.modelId(model.attributes); |
1203 var prevId = this.modelId(model.previousAttributes()); |
1150 if (prevId !== id) { |
1204 var id = this.modelId(model.attributes); |
1151 if (prevId != null) delete this._byId[prevId]; |
1205 if (prevId !== id) { |
1152 if (id != null) this._byId[id] = model; |
1206 if (prevId != null) delete this._byId[prevId]; |
1207 if (id != null) this._byId[id] = model; |
|
1208 } |
|
1153 } |
1209 } |
1154 } |
1210 } |
1155 this.trigger.apply(this, arguments); |
1211 this.trigger.apply(this, arguments); |
1156 } |
1212 } |
1157 |
1213 |
1158 }); |
1214 }); |
1159 |
1215 |
1160 // Underscore methods that we want to implement on the Collection. |
1216 // Defining an @@iterator method implements JavaScript's Iterable protocol. |
1161 // 90% of the core usefulness of Backbone Collections is actually implemented |
1217 // In modern ES2015 browsers, this value is found at Symbol.iterator. |
1162 // right here: |
1218 /* global Symbol */ |
1163 var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4, |
1219 var $$iterator = typeof Symbol === 'function' && Symbol.iterator; |
1164 foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3, |
1220 if ($$iterator) { |
1165 select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, |
1221 Collection.prototype[$$iterator] = Collection.prototype.values; |
1166 contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, |
1222 } |
1167 head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, |
1223 |
1168 without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, |
1224 // CollectionIterator |
1169 isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, |
1225 // ------------------ |
1170 sortBy: 3, indexBy: 3}; |
1226 |
1171 |
1227 // A CollectionIterator implements JavaScript's Iterator protocol, allowing the |
1172 // Mix in each Underscore method as a proxy to `Collection#models`. |
1228 // use of `for of` loops in modern browsers and interoperation between |
1173 addUnderscoreMethods(Collection, collectionMethods, 'models'); |
1229 // Backbone.Collection and other JavaScript functions and third-party libraries |
1230 // which can operate on Iterables. |
|
1231 var CollectionIterator = function(collection, kind) { |
|
1232 this._collection = collection; |
|
1233 this._kind = kind; |
|
1234 this._index = 0; |
|
1235 }; |
|
1236 |
|
1237 // This "enum" defines the three possible kinds of values which can be emitted |
|
1238 // by a CollectionIterator that correspond to the values(), keys() and entries() |
|
1239 // methods on Collection, respectively. |
|
1240 var ITERATOR_VALUES = 1; |
|
1241 var ITERATOR_KEYS = 2; |
|
1242 var ITERATOR_KEYSVALUES = 3; |
|
1243 |
|
1244 // All Iterators should themselves be Iterable. |
|
1245 if ($$iterator) { |
|
1246 CollectionIterator.prototype[$$iterator] = function() { |
|
1247 return this; |
|
1248 }; |
|
1249 } |
|
1250 |
|
1251 CollectionIterator.prototype.next = function() { |
|
1252 if (this._collection) { |
|
1253 |
|
1254 // Only continue iterating if the iterated collection is long enough. |
|
1255 if (this._index < this._collection.length) { |
|
1256 var model = this._collection.at(this._index); |
|
1257 this._index++; |
|
1258 |
|
1259 // Construct a value depending on what kind of values should be iterated. |
|
1260 var value; |
|
1261 if (this._kind === ITERATOR_VALUES) { |
|
1262 value = model; |
|
1263 } else { |
|
1264 var id = this._collection.modelId(model.attributes); |
|
1265 if (this._kind === ITERATOR_KEYS) { |
|
1266 value = id; |
|
1267 } else { // ITERATOR_KEYSVALUES |
|
1268 value = [id, model]; |
|
1269 } |
|
1270 } |
|
1271 return {value: value, done: false}; |
|
1272 } |
|
1273 |
|
1274 // Once exhausted, remove the reference to the collection so future |
|
1275 // calls to the next method always return done. |
|
1276 this._collection = void 0; |
|
1277 } |
|
1278 |
|
1279 return {value: void 0, done: true}; |
|
1280 }; |
|
1174 |
1281 |
1175 // Backbone.View |
1282 // Backbone.View |
1176 // ------------- |
1283 // ------------- |
1177 |
1284 |
1178 // Backbone Views are almost more convention than they are actual code. A View |
1285 // Backbone Views are almost more convention than they are actual code. A View |
1185 |
1292 |
1186 // Creating a Backbone.View creates its initial element outside of the DOM, |
1293 // Creating a Backbone.View creates its initial element outside of the DOM, |
1187 // if an existing element is not provided... |
1294 // if an existing element is not provided... |
1188 var View = Backbone.View = function(options) { |
1295 var View = Backbone.View = function(options) { |
1189 this.cid = _.uniqueId('view'); |
1296 this.cid = _.uniqueId('view'); |
1297 this.preinitialize.apply(this, arguments); |
|
1190 _.extend(this, _.pick(options, viewOptions)); |
1298 _.extend(this, _.pick(options, viewOptions)); |
1191 this._ensureElement(); |
1299 this._ensureElement(); |
1192 this.initialize.apply(this, arguments); |
1300 this.initialize.apply(this, arguments); |
1193 }; |
1301 }; |
1194 |
1302 |
1207 // jQuery delegate for element lookup, scoped to DOM elements within the |
1315 // jQuery delegate for element lookup, scoped to DOM elements within the |
1208 // current view. This should be preferred to global lookups where possible. |
1316 // current view. This should be preferred to global lookups where possible. |
1209 $: function(selector) { |
1317 $: function(selector) { |
1210 return this.$el.find(selector); |
1318 return this.$el.find(selector); |
1211 }, |
1319 }, |
1320 |
|
1321 // preinitialize is an empty function by default. You can override it with a function |
|
1322 // or object. preinitialize will run before any instantiation logic is run in the View |
|
1323 preinitialize: function(){}, |
|
1212 |
1324 |
1213 // Initialize is an empty function by default. Override it with your own |
1325 // Initialize is an empty function by default. Override it with your own |
1214 // initialization logic. |
1326 // initialization logic. |
1215 initialize: function(){}, |
1327 initialize: function(){}, |
1216 |
1328 |
1275 for (var key in events) { |
1387 for (var key in events) { |
1276 var method = events[key]; |
1388 var method = events[key]; |
1277 if (!_.isFunction(method)) method = this[method]; |
1389 if (!_.isFunction(method)) method = this[method]; |
1278 if (!method) continue; |
1390 if (!method) continue; |
1279 var match = key.match(delegateEventSplitter); |
1391 var match = key.match(delegateEventSplitter); |
1280 this.delegate(match[1], match[2], _.bind(method, this)); |
1392 this.delegate(match[1], match[2], method.bind(this)); |
1281 } |
1393 } |
1282 return this; |
1394 return this; |
1283 }, |
1395 }, |
1284 |
1396 |
1285 // Add a single event listener to the view's element (or a child element |
1397 // Add a single event listener to the view's element (or a child element |
1331 // subclasses using an alternative DOM manipulation API. |
1443 // subclasses using an alternative DOM manipulation API. |
1332 _setAttributes: function(attributes) { |
1444 _setAttributes: function(attributes) { |
1333 this.$el.attr(attributes); |
1445 this.$el.attr(attributes); |
1334 } |
1446 } |
1335 |
1447 |
1448 }); |
|
1449 |
|
1450 // Proxy Backbone class methods to Underscore functions, wrapping the model's |
|
1451 // `attributes` object or collection's `models` array behind the scenes. |
|
1452 // |
|
1453 // collection.filter(function(model) { return model.get('age') > 10 }); |
|
1454 // collection.each(this.addView); |
|
1455 // |
|
1456 // `Function#apply` can be slow so we use the method's arg count, if we know it. |
|
1457 var addMethod = function(base, length, method, attribute) { |
|
1458 switch (length) { |
|
1459 case 1: return function() { |
|
1460 return base[method](this[attribute]); |
|
1461 }; |
|
1462 case 2: return function(value) { |
|
1463 return base[method](this[attribute], value); |
|
1464 }; |
|
1465 case 3: return function(iteratee, context) { |
|
1466 return base[method](this[attribute], cb(iteratee, this), context); |
|
1467 }; |
|
1468 case 4: return function(iteratee, defaultVal, context) { |
|
1469 return base[method](this[attribute], cb(iteratee, this), defaultVal, context); |
|
1470 }; |
|
1471 default: return function() { |
|
1472 var args = slice.call(arguments); |
|
1473 args.unshift(this[attribute]); |
|
1474 return base[method].apply(base, args); |
|
1475 }; |
|
1476 } |
|
1477 }; |
|
1478 |
|
1479 var addUnderscoreMethods = function(Class, base, methods, attribute) { |
|
1480 _.each(methods, function(length, method) { |
|
1481 if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute); |
|
1482 }); |
|
1483 }; |
|
1484 |
|
1485 // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. |
|
1486 var cb = function(iteratee, instance) { |
|
1487 if (_.isFunction(iteratee)) return iteratee; |
|
1488 if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); |
|
1489 if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; |
|
1490 return iteratee; |
|
1491 }; |
|
1492 var modelMatcher = function(attrs) { |
|
1493 var matcher = _.matches(attrs); |
|
1494 return function(model) { |
|
1495 return matcher(model.attributes); |
|
1496 }; |
|
1497 }; |
|
1498 |
|
1499 // Underscore methods that we want to implement on the Collection. |
|
1500 // 90% of the core usefulness of Backbone Collections is actually implemented |
|
1501 // right here: |
|
1502 var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, |
|
1503 foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, |
|
1504 select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, |
|
1505 contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, |
|
1506 head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, |
|
1507 without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, |
|
1508 isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, |
|
1509 sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; |
|
1510 |
|
1511 |
|
1512 // Underscore methods that we want to implement on the Model, mapped to the |
|
1513 // number of arguments they take. |
|
1514 var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, |
|
1515 omit: 0, chain: 1, isEmpty: 1}; |
|
1516 |
|
1517 // Mix in each Underscore method as a proxy to `Collection#models`. |
|
1518 |
|
1519 _.each([ |
|
1520 [Collection, collectionMethods, 'models'], |
|
1521 [Model, modelMethods, 'attributes'] |
|
1522 ], function(config) { |
|
1523 var Base = config[0], |
|
1524 methods = config[1], |
|
1525 attribute = config[2]; |
|
1526 |
|
1527 Base.mixin = function(obj) { |
|
1528 var mappings = _.reduce(_.functions(obj), function(memo, name) { |
|
1529 memo[name] = 0; |
|
1530 return memo; |
|
1531 }, {}); |
|
1532 addUnderscoreMethods(Base, obj, mappings, attribute); |
|
1533 }; |
|
1534 |
|
1535 addUnderscoreMethods(Base, _, methods, attribute); |
|
1336 }); |
1536 }); |
1337 |
1537 |
1338 // Backbone.sync |
1538 // Backbone.sync |
1339 // ------------- |
1539 // ------------- |
1340 |
1540 |
1413 return xhr; |
1613 return xhr; |
1414 }; |
1614 }; |
1415 |
1615 |
1416 // Map from CRUD to HTTP for our default `Backbone.sync` implementation. |
1616 // Map from CRUD to HTTP for our default `Backbone.sync` implementation. |
1417 var methodMap = { |
1617 var methodMap = { |
1418 'create': 'POST', |
1618 create: 'POST', |
1419 'update': 'PUT', |
1619 update: 'PUT', |
1420 'patch': 'PATCH', |
1620 patch: 'PATCH', |
1421 'delete': 'DELETE', |
1621 delete: 'DELETE', |
1422 'read': 'GET' |
1622 read: 'GET' |
1423 }; |
1623 }; |
1424 |
1624 |
1425 // Set the default implementation of `Backbone.ajax` to proxy through to `$`. |
1625 // Set the default implementation of `Backbone.ajax` to proxy through to `$`. |
1426 // Override this if you'd like to use a different library. |
1626 // Override this if you'd like to use a different library. |
1427 Backbone.ajax = function() { |
1627 Backbone.ajax = function() { |
1433 |
1633 |
1434 // Routers map faux-URLs to actions, and fire events when routes are |
1634 // Routers map faux-URLs to actions, and fire events when routes are |
1435 // matched. Creating a new one sets its `routes` hash, if not set statically. |
1635 // matched. Creating a new one sets its `routes` hash, if not set statically. |
1436 var Router = Backbone.Router = function(options) { |
1636 var Router = Backbone.Router = function(options) { |
1437 options || (options = {}); |
1637 options || (options = {}); |
1638 this.preinitialize.apply(this, arguments); |
|
1438 if (options.routes) this.routes = options.routes; |
1639 if (options.routes) this.routes = options.routes; |
1439 this._bindRoutes(); |
1640 this._bindRoutes(); |
1440 this.initialize.apply(this, arguments); |
1641 this.initialize.apply(this, arguments); |
1441 }; |
1642 }; |
1442 |
1643 |
1447 var splatParam = /\*\w+/g; |
1648 var splatParam = /\*\w+/g; |
1448 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; |
1649 var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; |
1449 |
1650 |
1450 // Set up all inheritable **Backbone.Router** properties and methods. |
1651 // Set up all inheritable **Backbone.Router** properties and methods. |
1451 _.extend(Router.prototype, Events, { |
1652 _.extend(Router.prototype, Events, { |
1653 |
|
1654 // preinitialize is an empty function by default. You can override it with a function |
|
1655 // or object. preinitialize will run before any instantiation logic is run in the Router. |
|
1656 preinitialize: function(){}, |
|
1452 |
1657 |
1453 // Initialize is an empty function by default. Override it with your own |
1658 // Initialize is an empty function by default. Override it with your own |
1454 // initialization logic. |
1659 // initialization logic. |
1455 initialize: function(){}, |
1660 initialize: function(){}, |
1456 |
1661 |
1505 |
1710 |
1506 // Convert a route string into a regular expression, suitable for matching |
1711 // Convert a route string into a regular expression, suitable for matching |
1507 // against the current location hash. |
1712 // against the current location hash. |
1508 _routeToRegExp: function(route) { |
1713 _routeToRegExp: function(route) { |
1509 route = route.replace(escapeRegExp, '\\$&') |
1714 route = route.replace(escapeRegExp, '\\$&') |
1510 .replace(optionalParam, '(?:$1)?') |
1715 .replace(optionalParam, '(?:$1)?') |
1511 .replace(namedParam, function(match, optional) { |
1716 .replace(namedParam, function(match, optional) { |
1512 return optional ? match : '([^/?]+)'; |
1717 return optional ? match : '([^/?]+)'; |
1513 }) |
1718 }) |
1514 .replace(splatParam, '([^?]*?)'); |
1719 .replace(splatParam, '([^?]*?)'); |
1515 return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); |
1720 return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); |
1516 }, |
1721 }, |
1517 |
1722 |
1518 // Given a route, and a URL fragment that it matches, return the array of |
1723 // Given a route, and a URL fragment that it matches, return the array of |
1519 // extracted decoded parameters. Empty or unmatched parameters will be |
1724 // extracted decoded parameters. Empty or unmatched parameters will be |
1537 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) |
1742 // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) |
1538 // and URL fragments. If the browser supports neither (old IE, natch), |
1743 // and URL fragments. If the browser supports neither (old IE, natch), |
1539 // falls back to polling. |
1744 // falls back to polling. |
1540 var History = Backbone.History = function() { |
1745 var History = Backbone.History = function() { |
1541 this.handlers = []; |
1746 this.handlers = []; |
1542 this.checkUrl = _.bind(this.checkUrl, this); |
1747 this.checkUrl = this.checkUrl.bind(this); |
1543 |
1748 |
1544 // Ensure that `History` can be used outside of the browser. |
1749 // Ensure that `History` can be used outside of the browser. |
1545 if (typeof window !== 'undefined') { |
1750 if (typeof window !== 'undefined') { |
1546 this.location = window.location; |
1751 this.location = window.location; |
1547 this.history = window.history; |
1752 this.history = window.history; |
1574 }, |
1779 }, |
1575 |
1780 |
1576 // Does the pathname match the root? |
1781 // Does the pathname match the root? |
1577 matchRoot: function() { |
1782 matchRoot: function() { |
1578 var path = this.decodeFragment(this.location.pathname); |
1783 var path = this.decodeFragment(this.location.pathname); |
1579 var root = path.slice(0, this.root.length - 1) + '/'; |
1784 var rootPath = path.slice(0, this.root.length - 1) + '/'; |
1580 return root === this.root; |
1785 return rootPath === this.root; |
1581 }, |
1786 }, |
1582 |
1787 |
1583 // Unicode characters in `location.pathname` are percent encoded so they're |
1788 // Unicode characters in `location.pathname` are percent encoded so they're |
1584 // decoded for comparison. `%25` should not be decoded since it may be part |
1789 // decoded for comparison. `%25` should not be decoded since it may be part |
1585 // of an encoded parameter. |
1790 // of an encoded parameter. |
1647 if (this._wantsHashChange && this._wantsPushState) { |
1852 if (this._wantsHashChange && this._wantsPushState) { |
1648 |
1853 |
1649 // If we've started off with a route from a `pushState`-enabled |
1854 // If we've started off with a route from a `pushState`-enabled |
1650 // browser, but we're currently in a browser that doesn't support it... |
1855 // browser, but we're currently in a browser that doesn't support it... |
1651 if (!this._hasPushState && !this.atRoot()) { |
1856 if (!this._hasPushState && !this.atRoot()) { |
1652 var root = this.root.slice(0, -1) || '/'; |
1857 var rootPath = this.root.slice(0, -1) || '/'; |
1653 this.location.replace(root + '#' + this.getPath()); |
1858 this.location.replace(rootPath + '#' + this.getPath()); |
1654 // Return immediately as browser will do redirect to new url |
1859 // Return immediately as browser will do redirect to new url |
1655 return true; |
1860 return true; |
1656 |
1861 |
1657 // Or if we've started out with a hash-based route, but we're currently |
1862 // Or if we've started out with a hash-based route, but we're currently |
1658 // in a browser where it could be `pushState`-based instead... |
1863 // in a browser where it could be `pushState`-based instead... |
1677 iWindow.document.close(); |
1882 iWindow.document.close(); |
1678 iWindow.location.hash = '#' + this.fragment; |
1883 iWindow.location.hash = '#' + this.fragment; |
1679 } |
1884 } |
1680 |
1885 |
1681 // Add a cross-platform `addEventListener` shim for older browsers. |
1886 // Add a cross-platform `addEventListener` shim for older browsers. |
1682 var addEventListener = window.addEventListener || function (eventName, listener) { |
1887 var addEventListener = window.addEventListener || function(eventName, listener) { |
1683 return attachEvent('on' + eventName, listener); |
1888 return attachEvent('on' + eventName, listener); |
1684 }; |
1889 }; |
1685 |
1890 |
1686 // Depending on whether we're using pushState or hashes, and whether |
1891 // Depending on whether we're using pushState or hashes, and whether |
1687 // 'onhashchange' is supported, determine how we check the URL state. |
1892 // 'onhashchange' is supported, determine how we check the URL state. |
1698 |
1903 |
1699 // Disable Backbone.history, perhaps temporarily. Not useful in a real app, |
1904 // Disable Backbone.history, perhaps temporarily. Not useful in a real app, |
1700 // but possibly useful for unit testing Routers. |
1905 // but possibly useful for unit testing Routers. |
1701 stop: function() { |
1906 stop: function() { |
1702 // Add a cross-platform `removeEventListener` shim for older browsers. |
1907 // Add a cross-platform `removeEventListener` shim for older browsers. |
1703 var removeEventListener = window.removeEventListener || function (eventName, listener) { |
1908 var removeEventListener = window.removeEventListener || function(eventName, listener) { |
1704 return detachEvent('on' + eventName, listener); |
1909 return detachEvent('on' + eventName, listener); |
1705 }; |
1910 }; |
1706 |
1911 |
1707 // Remove window listeners. |
1912 // Remove window listeners. |
1708 if (this._usePushState) { |
1913 if (this._usePushState) { |
1772 |
1977 |
1773 // Normalize the fragment. |
1978 // Normalize the fragment. |
1774 fragment = this.getFragment(fragment || ''); |
1979 fragment = this.getFragment(fragment || ''); |
1775 |
1980 |
1776 // Don't include a trailing slash on the root. |
1981 // Don't include a trailing slash on the root. |
1777 var root = this.root; |
1982 var rootPath = this.root; |
1778 if (fragment === '' || fragment.charAt(0) === '?') { |
1983 if (fragment === '' || fragment.charAt(0) === '?') { |
1779 root = root.slice(0, -1) || '/'; |
1984 rootPath = rootPath.slice(0, -1) || '/'; |
1780 } |
1985 } |
1781 var url = root + fragment; |
1986 var url = rootPath + fragment; |
1782 |
1987 |
1783 // Strip the hash and decode for matching. |
1988 // Strip the fragment of the query and hash for matching. |
1784 fragment = this.decodeFragment(fragment.replace(pathStripper, '')); |
1989 fragment = fragment.replace(pathStripper, ''); |
1785 |
1990 |
1786 if (this.fragment === fragment) return; |
1991 // Decode for matching. |
1787 this.fragment = fragment; |
1992 var decodedFragment = this.decodeFragment(fragment); |
1993 |
|
1994 if (this.fragment === decodedFragment) return; |
|
1995 this.fragment = decodedFragment; |
|
1788 |
1996 |
1789 // If pushState is available, we use it to set the fragment as a real URL. |
1997 // If pushState is available, we use it to set the fragment as a real URL. |
1790 if (this._usePushState) { |
1998 if (this._usePushState) { |
1791 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); |
1999 this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); |
1792 |
2000 |
1793 // If hash changes haven't been explicitly disabled, update the hash |
2001 // If hash changes haven't been explicitly disabled, update the hash |
1794 // fragment to store history. |
2002 // fragment to store history. |
1795 } else if (this._wantsHashChange) { |
2003 } else if (this._wantsHashChange) { |
1796 this._updateHash(this.location, fragment, options.replace); |
2004 this._updateHash(this.location, fragment, options.replace); |
1797 if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) { |
2005 if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { |
1798 var iWindow = this.iframe.contentWindow; |
2006 var iWindow = this.iframe.contentWindow; |
1799 |
2007 |
1800 // Opening and closing the iframe tricks IE7 and earlier to push a |
2008 // Opening and closing the iframe tricks IE7 and earlier to push a |
1801 // history entry on hash-tag change. When replace is true, we don't |
2009 // history entry on hash-tag change. When replace is true, we don't |
1802 // want this. |
2010 // want this. |
1854 |
2062 |
1855 // Add static properties to the constructor function, if supplied. |
2063 // Add static properties to the constructor function, if supplied. |
1856 _.extend(child, parent, staticProps); |
2064 _.extend(child, parent, staticProps); |
1857 |
2065 |
1858 // Set the prototype chain to inherit from `parent`, without calling |
2066 // Set the prototype chain to inherit from `parent`, without calling |
1859 // `parent` constructor function. |
2067 // `parent`'s constructor function and add the prototype properties. |
1860 var Surrogate = function(){ this.constructor = child; }; |
2068 child.prototype = _.create(parent.prototype, protoProps); |
1861 Surrogate.prototype = parent.prototype; |
2069 child.prototype.constructor = child; |
1862 child.prototype = new Surrogate; |
|
1863 |
|
1864 // Add prototype properties (instance properties) to the subclass, |
|
1865 // if supplied. |
|
1866 if (protoProps) _.extend(child.prototype, protoProps); |
|
1867 |
2070 |
1868 // Set a convenience property in case the parent's prototype is needed |
2071 // Set a convenience property in case the parent's prototype is needed |
1869 // later. |
2072 // later. |
1870 child.__super__ = parent.prototype; |
2073 child.__super__ = parent.prototype; |
1871 |
2074 |
1888 model.trigger('error', model, resp, options); |
2091 model.trigger('error', model, resp, options); |
1889 }; |
2092 }; |
1890 }; |
2093 }; |
1891 |
2094 |
1892 return Backbone; |
2095 return Backbone; |
1893 |
2096 }); |
1894 })); |