1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | (function(factory) {
|
9 |
|
10 |
|
11 |
|
12 | var root = typeof self == 'object' && self.self === self && self ||
|
13 | typeof global == 'object' && global.global === global && global;
|
14 |
|
15 |
|
16 | if (typeof define === 'function' && define.amd) {
|
17 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
|
18 |
|
19 |
|
20 | root.Backbone = factory(root, exports, _, $);
|
21 | });
|
22 |
|
23 |
|
24 | } else if (typeof exports !== 'undefined') {
|
25 | var _ = require('underscore'), $;
|
26 | try { $ = require('jquery'); } catch (e) {}
|
27 | factory(root, exports, _, $);
|
28 |
|
29 |
|
30 | } else {
|
31 | root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
|
32 | }
|
33 |
|
34 | })(function(root, Backbone, _, $) {
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | var previousBackbone = root.Backbone;
|
42 |
|
43 |
|
44 | var slice = Array.prototype.slice;
|
45 |
|
46 |
|
47 | Backbone.VERSION = '1.4.0';
|
48 |
|
49 |
|
50 |
|
51 | Backbone.$ = $;
|
52 |
|
53 |
|
54 |
|
55 | Backbone.noConflict = function() {
|
56 | root.Backbone = previousBackbone;
|
57 | return this;
|
58 | };
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | Backbone.emulateHTTP = false;
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | Backbone.emulateJSON = false;
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 | var Events = Backbone.Events = {};
|
85 |
|
86 |
|
87 | var eventSplitter = /\s+/;
|
88 |
|
89 |
|
90 | var _listening;
|
91 |
|
92 |
|
93 |
|
94 |
|
95 | var eventsApi = function(iteratee, events, name, callback, opts) {
|
96 | var i = 0, names;
|
97 | if (name && typeof name === 'object') {
|
98 |
|
99 | if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
|
100 | for (names = _.keys(name); i < names.length ; i++) {
|
101 | events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
|
102 | }
|
103 | } else if (name && eventSplitter.test(name)) {
|
104 |
|
105 | for (names = name.split(eventSplitter); i < names.length; i++) {
|
106 | events = iteratee(events, names[i], callback, opts);
|
107 | }
|
108 | } else {
|
109 |
|
110 | events = iteratee(events, name, callback, opts);
|
111 | }
|
112 | return events;
|
113 | };
|
114 |
|
115 |
|
116 |
|
117 | Events.on = function(name, callback, context) {
|
118 | this._events = eventsApi(onApi, this._events || {}, name, callback, {
|
119 | context: context,
|
120 | ctx: this,
|
121 | listening: _listening
|
122 | });
|
123 |
|
124 | if (_listening) {
|
125 | var listeners = this._listeners || (this._listeners = {});
|
126 | listeners[_listening.id] = _listening;
|
127 |
|
128 |
|
129 | _listening.interop = false;
|
130 | }
|
131 |
|
132 | return this;
|
133 | };
|
134 |
|
135 |
|
136 |
|
137 |
|
138 | Events.listenTo = function(obj, name, callback) {
|
139 | if (!obj) return this;
|
140 | var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
|
141 | var listeningTo = this._listeningTo || (this._listeningTo = {});
|
142 | var listening = _listening = listeningTo[id];
|
143 |
|
144 |
|
145 |
|
146 | if (!listening) {
|
147 | this._listenId || (this._listenId = _.uniqueId('l'));
|
148 | listening = _listening = listeningTo[id] = new Listening(this, obj);
|
149 | }
|
150 |
|
151 |
|
152 | var error = tryCatchOn(obj, name, callback, this);
|
153 | _listening = void 0;
|
154 |
|
155 | if (error) throw error;
|
156 |
|
157 | if (listening.interop) listening.on(name, callback);
|
158 |
|
159 | return this;
|
160 | };
|
161 |
|
162 |
|
163 | var onApi = function(events, name, callback, options) {
|
164 | if (callback) {
|
165 | var handlers = events[name] || (events[name] = []);
|
166 | var context = options.context, ctx = options.ctx, listening = options.listening;
|
167 | if (listening) listening.count++;
|
168 |
|
169 | handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
|
170 | }
|
171 | return events;
|
172 | };
|
173 |
|
174 |
|
175 |
|
176 | var tryCatchOn = function(obj, name, callback, context) {
|
177 | try {
|
178 | obj.on(name, callback, context);
|
179 | } catch (e) {
|
180 | return e;
|
181 | }
|
182 | };
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | Events.off = function(name, callback, context) {
|
189 | if (!this._events) return this;
|
190 | this._events = eventsApi(offApi, this._events, name, callback, {
|
191 | context: context,
|
192 | listeners: this._listeners
|
193 | });
|
194 |
|
195 | return this;
|
196 | };
|
197 |
|
198 |
|
199 |
|
200 | Events.stopListening = function(obj, name, callback) {
|
201 | var listeningTo = this._listeningTo;
|
202 | if (!listeningTo) return this;
|
203 |
|
204 | var ids = obj ? [obj._listenId] : _.keys(listeningTo);
|
205 | for (var i = 0; i < ids.length; i++) {
|
206 | var listening = listeningTo[ids[i]];
|
207 |
|
208 |
|
209 |
|
210 | if (!listening) break;
|
211 |
|
212 | listening.obj.off(name, callback, this);
|
213 | if (listening.interop) listening.off(name, callback);
|
214 | }
|
215 | if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
|
216 |
|
217 | return this;
|
218 | };
|
219 |
|
220 |
|
221 | var offApi = function(events, name, callback, options) {
|
222 | if (!events) return;
|
223 |
|
224 | var context = options.context, listeners = options.listeners;
|
225 | var i = 0, names;
|
226 |
|
227 |
|
228 | if (!name && !context && !callback) {
|
229 | for (names = _.keys(listeners); i < names.length; i++) {
|
230 | listeners[names[i]].cleanup();
|
231 | }
|
232 | return;
|
233 | }
|
234 |
|
235 | names = name ? [name] : _.keys(events);
|
236 | for (; i < names.length; i++) {
|
237 | name = names[i];
|
238 | var handlers = events[name];
|
239 |
|
240 |
|
241 | if (!handlers) break;
|
242 |
|
243 |
|
244 | var remaining = [];
|
245 | for (var j = 0; j < handlers.length; j++) {
|
246 | var handler = handlers[j];
|
247 | if (
|
248 | callback && callback !== handler.callback &&
|
249 | callback !== handler.callback._callback ||
|
250 | context && context !== handler.context
|
251 | ) {
|
252 | remaining.push(handler);
|
253 | } else {
|
254 | var listening = handler.listening;
|
255 | if (listening) listening.off(name, callback);
|
256 | }
|
257 | }
|
258 |
|
259 |
|
260 | if (remaining.length) {
|
261 | events[name] = remaining;
|
262 | } else {
|
263 | delete events[name];
|
264 | }
|
265 | }
|
266 |
|
267 | return events;
|
268 | };
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | Events.once = function(name, callback, context) {
|
275 |
|
276 | var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
|
277 | if (typeof name === 'string' && context == null) callback = void 0;
|
278 | return this.on(events, callback, context);
|
279 | };
|
280 |
|
281 |
|
282 | Events.listenToOnce = function(obj, name, callback) {
|
283 |
|
284 | var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
|
285 | return this.listenTo(obj, events);
|
286 | };
|
287 |
|
288 |
|
289 |
|
290 | var onceMap = function(map, name, callback, offer) {
|
291 | if (callback) {
|
292 | var once = map[name] = _.once(function() {
|
293 | offer(name, once);
|
294 | callback.apply(this, arguments);
|
295 | });
|
296 | once._callback = callback;
|
297 | }
|
298 | return map;
|
299 | };
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | Events.trigger = function(name) {
|
306 | if (!this._events) return this;
|
307 |
|
308 | var length = Math.max(0, arguments.length - 1);
|
309 | var args = Array(length);
|
310 | for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
|
311 |
|
312 | eventsApi(triggerApi, this._events, name, void 0, args);
|
313 | return this;
|
314 | };
|
315 |
|
316 |
|
317 | var triggerApi = function(objEvents, name, callback, args) {
|
318 | if (objEvents) {
|
319 | var events = objEvents[name];
|
320 | var allEvents = objEvents.all;
|
321 | if (events && allEvents) allEvents = allEvents.slice();
|
322 | if (events) triggerEvents(events, args);
|
323 | if (allEvents) triggerEvents(allEvents, [name].concat(args));
|
324 | }
|
325 | return objEvents;
|
326 | };
|
327 |
|
328 |
|
329 |
|
330 |
|
331 | var triggerEvents = function(events, args) {
|
332 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
|
333 | switch (args.length) {
|
334 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
|
335 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
|
336 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
|
337 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
|
338 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
|
339 | }
|
340 | };
|
341 |
|
342 |
|
343 |
|
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 |
|
356 |
|
357 |
|
358 |
|
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 |
|
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 |
|
380 |
|
381 | Events.bind = Events.on;
|
382 | Events.unbind = Events.off;
|
383 |
|
384 |
|
385 |
|
386 | _.extend(Backbone, Events);
|
387 |
|
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 | var Model = Backbone.Model = function(attributes, options) {
|
399 | var attrs = attributes || {};
|
400 | options || (options = {});
|
401 | this.preinitialize.apply(this, arguments);
|
402 | this.cid = _.uniqueId(this.cidPrefix);
|
403 | this.attributes = {};
|
404 | if (options.collection) this.collection = options.collection;
|
405 | if (options.parse) attrs = this.parse(attrs, options) || {};
|
406 | var defaults = _.result(this, 'defaults');
|
407 | attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
|
408 | this.set(attrs, options);
|
409 | this.changed = {};
|
410 | this.initialize.apply(this, arguments);
|
411 | };
|
412 |
|
413 |
|
414 | _.extend(Model.prototype, Events, {
|
415 |
|
416 |
|
417 | changed: null,
|
418 |
|
419 |
|
420 | validationError: null,
|
421 |
|
422 |
|
423 |
|
424 | idAttribute: 'id',
|
425 |
|
426 |
|
427 |
|
428 | cidPrefix: 'c',
|
429 |
|
430 |
|
431 |
|
432 | preinitialize: function(){},
|
433 |
|
434 |
|
435 |
|
436 | initialize: function(){},
|
437 |
|
438 |
|
439 | toJSON: function(options) {
|
440 | return _.clone(this.attributes);
|
441 | },
|
442 |
|
443 |
|
444 |
|
445 | sync: function() {
|
446 | return Backbone.sync.apply(this, arguments);
|
447 | },
|
448 |
|
449 |
|
450 | get: function(attr) {
|
451 | return this.attributes[attr];
|
452 | },
|
453 |
|
454 |
|
455 | escape: function(attr) {
|
456 | return _.escape(this.get(attr));
|
457 | },
|
458 |
|
459 |
|
460 |
|
461 | has: function(attr) {
|
462 | return this.get(attr) != null;
|
463 | },
|
464 |
|
465 |
|
466 | matches: function(attrs) {
|
467 | return !!_.iteratee(attrs, this)(this.attributes);
|
468 | },
|
469 |
|
470 |
|
471 |
|
472 |
|
473 | set: function(key, val, options) {
|
474 | if (key == null) return this;
|
475 |
|
476 |
|
477 | var attrs;
|
478 | if (typeof key === 'object') {
|
479 | attrs = key;
|
480 | options = val;
|
481 | } else {
|
482 | (attrs = {})[key] = val;
|
483 | }
|
484 |
|
485 | options || (options = {});
|
486 |
|
487 |
|
488 | if (!this._validate(attrs, options)) return false;
|
489 |
|
490 |
|
491 | var unset = options.unset;
|
492 | var silent = options.silent;
|
493 | var changes = [];
|
494 | var changing = this._changing;
|
495 | this._changing = true;
|
496 |
|
497 | if (!changing) {
|
498 | this._previousAttributes = _.clone(this.attributes);
|
499 | this.changed = {};
|
500 | }
|
501 |
|
502 | var current = this.attributes;
|
503 | var changed = this.changed;
|
504 | var prev = this._previousAttributes;
|
505 |
|
506 |
|
507 | for (var attr in attrs) {
|
508 | val = attrs[attr];
|
509 | if (!_.isEqual(current[attr], val)) changes.push(attr);
|
510 | if (!_.isEqual(prev[attr], val)) {
|
511 | changed[attr] = val;
|
512 | } else {
|
513 | delete changed[attr];
|
514 | }
|
515 | unset ? delete current[attr] : current[attr] = val;
|
516 | }
|
517 |
|
518 |
|
519 | if (this.idAttribute in attrs) this.id = this.get(this.idAttribute);
|
520 |
|
521 |
|
522 | if (!silent) {
|
523 | if (changes.length) this._pending = options;
|
524 | for (var i = 0; i < changes.length; i++) {
|
525 | this.trigger('change:' + changes[i], this, current[changes[i]], options);
|
526 | }
|
527 | }
|
528 |
|
529 |
|
530 |
|
531 | if (changing) return this;
|
532 | if (!silent) {
|
533 | while (this._pending) {
|
534 | options = this._pending;
|
535 | this._pending = false;
|
536 | this.trigger('change', this, options);
|
537 | }
|
538 | }
|
539 | this._pending = false;
|
540 | this._changing = false;
|
541 | return this;
|
542 | },
|
543 |
|
544 |
|
545 |
|
546 | unset: function(attr, options) {
|
547 | return this.set(attr, void 0, _.extend({}, options, {unset: true}));
|
548 | },
|
549 |
|
550 |
|
551 | clear: function(options) {
|
552 | var attrs = {};
|
553 | for (var key in this.attributes) attrs[key] = void 0;
|
554 | return this.set(attrs, _.extend({}, options, {unset: true}));
|
555 | },
|
556 |
|
557 |
|
558 |
|
559 | hasChanged: function(attr) {
|
560 | if (attr == null) return !_.isEmpty(this.changed);
|
561 | return _.has(this.changed, attr);
|
562 | },
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 | changedAttributes: function(diff) {
|
571 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
|
572 | var old = this._changing ? this._previousAttributes : this.attributes;
|
573 | var changed = {};
|
574 | var hasChanged;
|
575 | for (var attr in diff) {
|
576 | var val = diff[attr];
|
577 | if (_.isEqual(old[attr], val)) continue;
|
578 | changed[attr] = val;
|
579 | hasChanged = true;
|
580 | }
|
581 | return hasChanged ? changed : false;
|
582 | },
|
583 |
|
584 |
|
585 |
|
586 | previous: function(attr) {
|
587 | if (attr == null || !this._previousAttributes) return null;
|
588 | return this._previousAttributes[attr];
|
589 | },
|
590 |
|
591 |
|
592 |
|
593 | previousAttributes: function() {
|
594 | return _.clone(this._previousAttributes);
|
595 | },
|
596 |
|
597 |
|
598 |
|
599 | fetch: function(options) {
|
600 | options = _.extend({parse: true}, options);
|
601 | var model = this;
|
602 | var success = options.success;
|
603 | options.success = function(resp) {
|
604 | var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
605 | if (!model.set(serverAttrs, options)) return false;
|
606 | if (success) success.call(options.context, model, resp, options);
|
607 | model.trigger('sync', model, resp, options);
|
608 | };
|
609 | wrapError(this, options);
|
610 | return this.sync('read', this, options);
|
611 | },
|
612 |
|
613 |
|
614 |
|
615 |
|
616 | save: function(key, val, options) {
|
617 |
|
618 | var attrs;
|
619 | if (key == null || typeof key === 'object') {
|
620 | attrs = key;
|
621 | options = val;
|
622 | } else {
|
623 | (attrs = {})[key] = val;
|
624 | }
|
625 |
|
626 | options = _.extend({validate: true, parse: true}, options);
|
627 | var wait = options.wait;
|
628 |
|
629 |
|
630 |
|
631 |
|
632 | if (attrs && !wait) {
|
633 | if (!this.set(attrs, options)) return false;
|
634 | } else if (!this._validate(attrs, options)) {
|
635 | return false;
|
636 | }
|
637 |
|
638 |
|
639 |
|
640 | var model = this;
|
641 | var success = options.success;
|
642 | var attributes = this.attributes;
|
643 | options.success = function(resp) {
|
644 |
|
645 | model.attributes = attributes;
|
646 | var serverAttrs = options.parse ? model.parse(resp, options) : resp;
|
647 | if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
|
648 | if (serverAttrs && !model.set(serverAttrs, options)) return false;
|
649 | if (success) success.call(options.context, model, resp, options);
|
650 | model.trigger('sync', model, resp, options);
|
651 | };
|
652 | wrapError(this, options);
|
653 |
|
654 |
|
655 | if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
|
656 |
|
657 | var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
|
658 | if (method === 'patch' && !options.attrs) options.attrs = attrs;
|
659 | var xhr = this.sync(method, this, options);
|
660 |
|
661 |
|
662 | this.attributes = attributes;
|
663 |
|
664 | return xhr;
|
665 | },
|
666 |
|
667 |
|
668 |
|
669 |
|
670 | destroy: function(options) {
|
671 | options = options ? _.clone(options) : {};
|
672 | var model = this;
|
673 | var success = options.success;
|
674 | var wait = options.wait;
|
675 |
|
676 | var destroy = function() {
|
677 | model.stopListening();
|
678 | model.trigger('destroy', model, model.collection, options);
|
679 | };
|
680 |
|
681 | options.success = function(resp) {
|
682 | if (wait) destroy();
|
683 | if (success) success.call(options.context, model, resp, options);
|
684 | if (!model.isNew()) model.trigger('sync', model, resp, options);
|
685 | };
|
686 |
|
687 | var xhr = false;
|
688 | if (this.isNew()) {
|
689 | _.defer(options.success);
|
690 | } else {
|
691 | wrapError(this, options);
|
692 | xhr = this.sync('delete', this, options);
|
693 | }
|
694 | if (!wait) destroy();
|
695 | return xhr;
|
696 | },
|
697 |
|
698 |
|
699 |
|
700 |
|
701 | url: function() {
|
702 | var base =
|
703 | _.result(this, 'urlRoot') ||
|
704 | _.result(this.collection, 'url') ||
|
705 | urlError();
|
706 | if (this.isNew()) return base;
|
707 | var id = this.get(this.idAttribute);
|
708 | return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
|
709 | },
|
710 |
|
711 |
|
712 |
|
713 | parse: function(resp, options) {
|
714 | return resp;
|
715 | },
|
716 |
|
717 |
|
718 | clone: function() {
|
719 | return new this.constructor(this.attributes);
|
720 | },
|
721 |
|
722 |
|
723 | isNew: function() {
|
724 | return !this.has(this.idAttribute);
|
725 | },
|
726 |
|
727 |
|
728 | isValid: function(options) {
|
729 | return this._validate({}, _.extend({}, options, {validate: true}));
|
730 | },
|
731 |
|
732 |
|
733 |
|
734 | _validate: function(attrs, options) {
|
735 | if (!options.validate || !this.validate) return true;
|
736 | attrs = _.extend({}, this.attributes, attrs);
|
737 | var error = this.validationError = this.validate(attrs, options) || null;
|
738 | if (!error) return true;
|
739 | this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
|
740 | return false;
|
741 | }
|
742 |
|
743 | });
|
744 |
|
745 |
|
746 |
|
747 |
|
748 |
|
749 |
|
750 |
|
751 |
|
752 |
|
753 |
|
754 |
|
755 |
|
756 |
|
757 |
|
758 | var Collection = Backbone.Collection = function(models, options) {
|
759 | options || (options = {});
|
760 | this.preinitialize.apply(this, arguments);
|
761 | if (options.model) this.model = options.model;
|
762 | if (options.comparator !== void 0) this.comparator = options.comparator;
|
763 | this._reset();
|
764 | this.initialize.apply(this, arguments);
|
765 | if (models) this.reset(models, _.extend({silent: true}, options));
|
766 | };
|
767 |
|
768 |
|
769 | var setOptions = {add: true, remove: true, merge: true};
|
770 | var addOptions = {add: true, remove: false};
|
771 |
|
772 |
|
773 | var splice = function(array, insert, at) {
|
774 | at = Math.min(Math.max(at, 0), array.length);
|
775 | var tail = Array(array.length - at);
|
776 | var length = insert.length;
|
777 | var i;
|
778 | for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
|
779 | for (i = 0; i < length; i++) array[i + at] = insert[i];
|
780 | for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
|
781 | };
|
782 |
|
783 |
|
784 | _.extend(Collection.prototype, Events, {
|
785 |
|
786 |
|
787 |
|
788 | model: Model,
|
789 |
|
790 |
|
791 |
|
792 |
|
793 | preinitialize: function(){},
|
794 |
|
795 |
|
796 |
|
797 | initialize: function(){},
|
798 |
|
799 |
|
800 |
|
801 | toJSON: function(options) {
|
802 | return this.map(function(model) { return model.toJSON(options); });
|
803 | },
|
804 |
|
805 |
|
806 | sync: function() {
|
807 | return Backbone.sync.apply(this, arguments);
|
808 | },
|
809 |
|
810 |
|
811 |
|
812 |
|
813 | add: function(models, options) {
|
814 | return this.set(models, _.extend({merge: false}, options, addOptions));
|
815 | },
|
816 |
|
817 |
|
818 | remove: function(models, options) {
|
819 | options = _.extend({}, options);
|
820 | var singular = !_.isArray(models);
|
821 | models = singular ? [models] : models.slice();
|
822 | var removed = this._removeModels(models, options);
|
823 | if (!options.silent && removed.length) {
|
824 | options.changes = {added: [], merged: [], removed: removed};
|
825 | this.trigger('update', this, options);
|
826 | }
|
827 | return singular ? removed[0] : removed;
|
828 | },
|
829 |
|
830 |
|
831 |
|
832 |
|
833 |
|
834 | set: function(models, options) {
|
835 | if (models == null) return;
|
836 |
|
837 | options = _.extend({}, setOptions, options);
|
838 | if (options.parse && !this._isModel(models)) {
|
839 | models = this.parse(models, options) || [];
|
840 | }
|
841 |
|
842 | var singular = !_.isArray(models);
|
843 | models = singular ? [models] : models.slice();
|
844 |
|
845 | var at = options.at;
|
846 | if (at != null) at = +at;
|
847 | if (at > this.length) at = this.length;
|
848 | if (at < 0) at += this.length + 1;
|
849 |
|
850 | var set = [];
|
851 | var toAdd = [];
|
852 | var toMerge = [];
|
853 | var toRemove = [];
|
854 | var modelMap = {};
|
855 |
|
856 | var add = options.add;
|
857 | var merge = options.merge;
|
858 | var remove = options.remove;
|
859 |
|
860 | var sort = false;
|
861 | var sortable = this.comparator && at == null && options.sort !== false;
|
862 | var sortAttr = _.isString(this.comparator) ? this.comparator : null;
|
863 |
|
864 |
|
865 |
|
866 | var model, i;
|
867 | for (i = 0; i < models.length; i++) {
|
868 | model = models[i];
|
869 |
|
870 |
|
871 |
|
872 | var existing = this.get(model);
|
873 | if (existing) {
|
874 | if (merge && model !== existing) {
|
875 | var attrs = this._isModel(model) ? model.attributes : model;
|
876 | if (options.parse) attrs = existing.parse(attrs, options);
|
877 | existing.set(attrs, options);
|
878 | toMerge.push(existing);
|
879 | if (sortable && !sort) sort = existing.hasChanged(sortAttr);
|
880 | }
|
881 | if (!modelMap[existing.cid]) {
|
882 | modelMap[existing.cid] = true;
|
883 | set.push(existing);
|
884 | }
|
885 | models[i] = existing;
|
886 |
|
887 | // If this is a new, valid model, push it to the `toAdd` list.
|
888 | } else if (add) {
|
889 | model = models[i] = this._prepareModel(model, options);
|
890 | if (model) {
|
891 | toAdd.push(model);
|
892 | this._addReference(model, options);
|
893 | modelMap[model.cid] = true;
|
894 | set.push(model);
|
895 | }
|
896 | }
|
897 | }
|
898 |
|
899 | // Remove stale models.
|
900 | if (remove) {
|
901 | for (i = 0; i < this.length; i++) {
|
902 | model = this.models[i];
|
903 | if (!modelMap[model.cid]) toRemove.push(model);
|
904 | }
|
905 | if (toRemove.length) this._removeModels(toRemove, options);
|
906 | }
|
907 |
|
908 |
|
909 | var orderChanged = false;
|
910 | var replace = !sortable && add && remove;
|
911 | if (set.length && replace) {
|
912 | orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
|
913 | return m !== set[index];
|
914 | });
|
915 | this.models.length = 0;
|
916 | splice(this.models, set, 0);
|
917 | this.length = this.models.length;
|
918 | } else if (toAdd.length) {
|
919 | if (sortable) sort = true;
|
920 | splice(this.models, toAdd, at == null ? this.length : at);
|
921 | this.length = this.models.length;
|
922 | }
|
923 |
|
924 |
|
925 | if (sort) this.sort({silent: true});
|
926 |
|
927 |
|
928 | if (!options.silent) {
|
929 | for (i = 0; i < toAdd.length; i++) {
|
930 | if (at != null) options.index = at + i;
|
931 | model = toAdd[i];
|
932 | model.trigger('add', model, this, options);
|
933 | }
|
934 | if (sort || orderChanged) this.trigger('sort', 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 | }
|
943 | }
|
944 |
|
945 |
|
946 | return singular ? models[0] : models;
|
947 | },
|
948 |
|
949 |
|
950 |
|
951 |
|
952 |
|
953 | reset: function(models, options) {
|
954 | options = options ? _.clone(options) : {};
|
955 | for (var i = 0; i < this.models.length; i++) {
|
956 | this._removeReference(this.models[i], options);
|
957 | }
|
958 | options.previousModels = this.models;
|
959 | this._reset();
|
960 | models = this.add(models, _.extend({silent: true}, options));
|
961 | if (!options.silent) this.trigger('reset', this, options);
|
962 | return models;
|
963 | },
|
964 |
|
965 |
|
966 | push: function(model, options) {
|
967 | return this.add(model, _.extend({at: this.length}, options));
|
968 | },
|
969 |
|
970 |
|
971 | pop: function(options) {
|
972 | var model = this.at(this.length - 1);
|
973 | return this.remove(model, options);
|
974 | },
|
975 |
|
976 |
|
977 | unshift: function(model, options) {
|
978 | return this.add(model, _.extend({at: 0}, options));
|
979 | },
|
980 |
|
981 |
|
982 | shift: function(options) {
|
983 | var model = this.at(0);
|
984 | return this.remove(model, options);
|
985 | },
|
986 |
|
987 |
|
988 | slice: function() {
|
989 | return slice.apply(this.models, arguments);
|
990 | },
|
991 |
|
992 |
|
993 |
|
994 | get: function(obj) {
|
995 | if (obj == null) return void 0;
|
996 | return this._byId[obj] ||
|
997 | this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] ||
|
998 | obj.cid && this._byId[obj.cid];
|
999 | },
|
1000 |
|
1001 |
|
1002 | has: function(obj) {
|
1003 | return this.get(obj) != null;
|
1004 | },
|
1005 |
|
1006 |
|
1007 | at: function(index) {
|
1008 | if (index < 0) index += this.length;
|
1009 | return this.models[index];
|
1010 | },
|
1011 |
|
1012 |
|
1013 |
|
1014 | where: function(attrs, first) {
|
1015 | return this[first ? 'find' : 'filter'](attrs);
|
1016 | },
|
1017 |
|
1018 |
|
1019 |
|
1020 | findWhere: function(attrs) {
|
1021 | return this.where(attrs, true);
|
1022 | },
|
1023 |
|
1024 |
|
1025 |
|
1026 |
|
1027 | sort: function(options) {
|
1028 | var comparator = this.comparator;
|
1029 | if (!comparator) throw new Error('Cannot sort a set without a comparator');
|
1030 | options || (options = {});
|
1031 |
|
1032 | var length = comparator.length;
|
1033 | if (_.isFunction(comparator)) comparator = comparator.bind(this);
|
1034 |
|
1035 |
|
1036 | if (length === 1 || _.isString(comparator)) {
|
1037 | this.models = this.sortBy(comparator);
|
1038 | } else {
|
1039 | this.models.sort(comparator);
|
1040 | }
|
1041 | if (!options.silent) this.trigger('sort', this, options);
|
1042 | return this;
|
1043 | },
|
1044 |
|
1045 |
|
1046 | pluck: function(attr) {
|
1047 | return this.map(attr + '');
|
1048 | },
|
1049 |
|
1050 |
|
1051 |
|
1052 |
|
1053 | fetch: function(options) {
|
1054 | options = _.extend({parse: true}, options);
|
1055 | var success = options.success;
|
1056 | var collection = this;
|
1057 | options.success = function(resp) {
|
1058 | var method = options.reset ? 'reset' : 'set';
|
1059 | collection[method](resp, options);
|
1060 | if (success) success.call(options.context, collection, resp, options);
|
1061 | collection.trigger('sync', collection, resp, options);
|
1062 | };
|
1063 | wrapError(this, options);
|
1064 | return this.sync('read', this, options);
|
1065 | },
|
1066 |
|
1067 |
|
1068 |
|
1069 |
|
1070 | create: function(model, options) {
|
1071 | options = options ? _.clone(options) : {};
|
1072 | var wait = options.wait;
|
1073 | model = this._prepareModel(model, options);
|
1074 | if (!model) return false;
|
1075 | if (!wait) this.add(model, options);
|
1076 | var collection = this;
|
1077 | var success = options.success;
|
1078 | options.success = function(m, resp, callbackOpts) {
|
1079 | if (wait) collection.add(m, callbackOpts);
|
1080 | if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
|
1081 | };
|
1082 | model.save(null, options);
|
1083 | return model;
|
1084 | },
|
1085 |
|
1086 |
|
1087 |
|
1088 | parse: function(resp, options) {
|
1089 | return resp;
|
1090 | },
|
1091 |
|
1092 |
|
1093 | clone: function() {
|
1094 | return new this.constructor(this.models, {
|
1095 | model: this.model,
|
1096 | comparator: this.comparator
|
1097 | });
|
1098 | },
|
1099 |
|
1100 |
|
1101 | modelId: function(attrs) {
|
1102 | return attrs[this.model.prototype.idAttribute || 'id'];
|
1103 | },
|
1104 |
|
1105 |
|
1106 | values: function() {
|
1107 | return new CollectionIterator(this, ITERATOR_VALUES);
|
1108 | },
|
1109 |
|
1110 |
|
1111 | keys: function() {
|
1112 | return new CollectionIterator(this, ITERATOR_KEYS);
|
1113 | },
|
1114 |
|
1115 |
|
1116 | entries: function() {
|
1117 | return new CollectionIterator(this, ITERATOR_KEYSVALUES);
|
1118 | },
|
1119 |
|
1120 |
|
1121 |
|
1122 | _reset: function() {
|
1123 | this.length = 0;
|
1124 | this.models = [];
|
1125 | this._byId = {};
|
1126 | },
|
1127 |
|
1128 |
|
1129 |
|
1130 | _prepareModel: function(attrs, options) {
|
1131 | if (this._isModel(attrs)) {
|
1132 | if (!attrs.collection) attrs.collection = this;
|
1133 | return attrs;
|
1134 | }
|
1135 | options = options ? _.clone(options) : {};
|
1136 | options.collection = this;
|
1137 | var model = new this.model(attrs, options);
|
1138 | if (!model.validationError) return model;
|
1139 | this.trigger('invalid', this, model.validationError, options);
|
1140 | return false;
|
1141 | },
|
1142 |
|
1143 |
|
1144 | _removeModels: function(models, options) {
|
1145 | var removed = [];
|
1146 | for (var i = 0; i < models.length; i++) {
|
1147 | var model = this.get(models[i]);
|
1148 | if (!model) continue;
|
1149 |
|
1150 | var index = this.indexOf(model);
|
1151 | this.models.splice(index, 1);
|
1152 | this.length--;
|
1153 |
|
1154 |
|
1155 |
|
1156 | delete this._byId[model.cid];
|
1157 | var id = this.modelId(model.attributes);
|
1158 | if (id != null) delete this._byId[id];
|
1159 |
|
1160 | if (!options.silent) {
|
1161 | options.index = index;
|
1162 | model.trigger('remove', model, this, options);
|
1163 | }
|
1164 |
|
1165 | removed.push(model);
|
1166 | this._removeReference(model, options);
|
1167 | }
|
1168 | return removed;
|
1169 | },
|
1170 |
|
1171 |
|
1172 |
|
1173 | _isModel: function(model) {
|
1174 | return model instanceof Model;
|
1175 | },
|
1176 |
|
1177 |
|
1178 | _addReference: function(model, options) {
|
1179 | this._byId[model.cid] = model;
|
1180 | var id = this.modelId(model.attributes);
|
1181 | if (id != null) this._byId[id] = model;
|
1182 | model.on('all', this._onModelEvent, this);
|
1183 | },
|
1184 |
|
1185 |
|
1186 | _removeReference: function(model, options) {
|
1187 | delete this._byId[model.cid];
|
1188 | var id = this.modelId(model.attributes);
|
1189 | if (id != null) delete this._byId[id];
|
1190 | if (this === model.collection) delete model.collection;
|
1191 | model.off('all', this._onModelEvent, this);
|
1192 | },
|
1193 |
|
1194 |
|
1195 |
|
1196 |
|
1197 |
|
1198 | _onModelEvent: function(event, model, collection, options) {
|
1199 | if (model) {
|
1200 | if ((event === 'add' || event === 'remove') && collection !== this) return;
|
1201 | if (event === 'destroy') this.remove(model, options);
|
1202 | if (event === 'change') {
|
1203 | var prevId = this.modelId(model.previousAttributes());
|
1204 | var id = this.modelId(model.attributes);
|
1205 | if (prevId !== id) {
|
1206 | if (prevId != null) delete this._byId[prevId];
|
1207 | if (id != null) this._byId[id] = model;
|
1208 | }
|
1209 | }
|
1210 | }
|
1211 | this.trigger.apply(this, arguments);
|
1212 | }
|
1213 |
|
1214 | });
|
1215 |
|
1216 |
|
1217 |
|
1218 |
|
1219 | var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
|
1220 | if ($$iterator) {
|
1221 | Collection.prototype[$$iterator] = Collection.prototype.values;
|
1222 | }
|
1223 |
|
1224 |
|
1225 |
|
1226 |
|
1227 |
|
1228 |
|
1229 |
|
1230 |
|
1231 | var CollectionIterator = function(collection, kind) {
|
1232 | this._collection = collection;
|
1233 | this._kind = kind;
|
1234 | this._index = 0;
|
1235 | };
|
1236 |
|
1237 |
|
1238 |
|
1239 |
|
1240 | var ITERATOR_VALUES = 1;
|
1241 | var ITERATOR_KEYS = 2;
|
1242 | var ITERATOR_KEYSVALUES = 3;
|
1243 |
|
1244 |
|
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 |
|
1255 | if (this._index < this._collection.length) {
|
1256 | var model = this._collection.at(this._index);
|
1257 | this._index++;
|
1258 |
|
1259 |
|
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 {
|
1268 | value = [id, model];
|
1269 | }
|
1270 | }
|
1271 | return {value: value, done: false};
|
1272 | }
|
1273 |
|
1274 |
|
1275 |
|
1276 | this._collection = void 0;
|
1277 | }
|
1278 |
|
1279 | return {value: void 0, done: true};
|
1280 | };
|
1281 |
|
1282 |
|
1283 |
|
1284 |
|
1285 |
|
1286 |
|
1287 |
|
1288 |
|
1289 |
|
1290 |
|
1291 |
|
1292 |
|
1293 |
|
1294 |
|
1295 | var View = Backbone.View = function(options) {
|
1296 | this.cid = _.uniqueId('view');
|
1297 | this.preinitialize.apply(this, arguments);
|
1298 | _.extend(this, _.pick(options, viewOptions));
|
1299 | this._ensureElement();
|
1300 | this.initialize.apply(this, arguments);
|
1301 | };
|
1302 |
|
1303 |
|
1304 | var delegateEventSplitter = /^(\S+)\s*(.*)$/;
|
1305 |
|
1306 |
|
1307 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
|
1308 |
|
1309 |
|
1310 | _.extend(View.prototype, Events, {
|
1311 |
|
1312 |
|
1313 | tagName: 'div',
|
1314 |
|
1315 |
|
1316 |
|
1317 | $: function(selector) {
|
1318 | return this.$el.find(selector);
|
1319 | },
|
1320 |
|
1321 |
|
1322 |
|
1323 | preinitialize: function(){},
|
1324 |
|
1325 |
|
1326 |
|
1327 | initialize: function(){},
|
1328 |
|
1329 |
|
1330 |
|
1331 |
|
1332 | render: function() {
|
1333 | return this;
|
1334 | },
|
1335 |
|
1336 |
|
1337 |
|
1338 | remove: function() {
|
1339 | this._removeElement();
|
1340 | this.stopListening();
|
1341 | return this;
|
1342 | },
|
1343 |
|
1344 |
|
1345 |
|
1346 |
|
1347 | _removeElement: function() {
|
1348 | this.$el.remove();
|
1349 | },
|
1350 |
|
1351 |
|
1352 |
|
1353 | setElement: function(element) {
|
1354 | this.undelegateEvents();
|
1355 | this._setElement(element);
|
1356 | this.delegateEvents();
|
1357 | return this;
|
1358 | },
|
1359 |
|
1360 |
|
1361 |
|
1362 |
|
1363 |
|
1364 |
|
1365 | _setElement: function(el) {
|
1366 | this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
|
1367 | this.el = this.$el[0];
|
1368 | },
|
1369 |
|
1370 |
|
1371 |
|
1372 |
|
1373 |
|
1374 |
|
1375 |
|
1376 |
|
1377 |
|
1378 |
|
1379 |
|
1380 |
|
1381 |
|
1382 |
|
1383 | delegateEvents: function(events) {
|
1384 | events || (events = _.result(this, 'events'));
|
1385 | if (!events) return this;
|
1386 | this.undelegateEvents();
|
1387 | for (var key in events) {
|
1388 | var method = events[key];
|
1389 | if (!_.isFunction(method)) method = this[method];
|
1390 | if (!method) continue;
|
1391 | var match = key.match(delegateEventSplitter);
|
1392 | this.delegate(match[1], match[2], method.bind(this));
|
1393 | }
|
1394 | return this;
|
1395 | },
|
1396 |
|
1397 |
|
1398 |
|
1399 |
|
1400 | delegate: function(eventName, selector, listener) {
|
1401 | this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1402 | return this;
|
1403 | },
|
1404 |
|
1405 |
|
1406 |
|
1407 |
|
1408 | undelegateEvents: function() {
|
1409 | if (this.$el) this.$el.off('.delegateEvents' + this.cid);
|
1410 | return this;
|
1411 | },
|
1412 |
|
1413 |
|
1414 |
|
1415 | undelegate: function(eventName, selector, listener) {
|
1416 | this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
|
1417 | return this;
|
1418 | },
|
1419 |
|
1420 |
|
1421 |
|
1422 | _createElement: function(tagName) {
|
1423 | return document.createElement(tagName);
|
1424 | },
|
1425 |
|
1426 |
|
1427 |
|
1428 |
|
1429 |
|
1430 | _ensureElement: function() {
|
1431 | if (!this.el) {
|
1432 | var attrs = _.extend({}, _.result(this, 'attributes'));
|
1433 | if (this.id) attrs.id = _.result(this, 'id');
|
1434 | if (this.className) attrs['class'] = _.result(this, 'className');
|
1435 | this.setElement(this._createElement(_.result(this, 'tagName')));
|
1436 | this._setAttributes(attrs);
|
1437 | } else {
|
1438 | this.setElement(_.result(this, 'el'));
|
1439 | }
|
1440 | },
|
1441 |
|
1442 |
|
1443 |
|
1444 | _setAttributes: function(attributes) {
|
1445 | this.$el.attr(attributes);
|
1446 | }
|
1447 |
|
1448 | });
|
1449 |
|
1450 |
|
1451 |
|
1452 |
|
1453 |
|
1454 |
|
1455 |
|
1456 |
|
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 |
|
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 |
|
1500 |
|
1501 |
|
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 |
|
1513 |
|
1514 | var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
|
1515 | omit: 0, chain: 1, isEmpty: 1};
|
1516 |
|
1517 |
|
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);
|
1536 | });
|
1537 |
|
1538 |
|
1539 |
|
1540 |
|
1541 |
|
1542 |
|
1543 |
|
1544 |
|
1545 |
|
1546 |
|
1547 |
|
1548 |
|
1549 |
|
1550 |
|
1551 |
|
1552 |
|
1553 |
|
1554 |
|
1555 |
|
1556 | Backbone.sync = function(method, model, options) {
|
1557 | var type = methodMap[method];
|
1558 |
|
1559 |
|
1560 | _.defaults(options || (options = {}), {
|
1561 | emulateHTTP: Backbone.emulateHTTP,
|
1562 | emulateJSON: Backbone.emulateJSON
|
1563 | });
|
1564 |
|
1565 |
|
1566 | var params = {type: type, dataType: 'json'};
|
1567 |
|
1568 |
|
1569 | if (!options.url) {
|
1570 | params.url = _.result(model, 'url') || urlError();
|
1571 | }
|
1572 |
|
1573 |
|
1574 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
|
1575 | params.contentType = 'application/json';
|
1576 | params.data = JSON.stringify(options.attrs || model.toJSON(options));
|
1577 | }
|
1578 |
|
1579 |
|
1580 | if (options.emulateJSON) {
|
1581 | params.contentType = 'application/x-www-form-urlencoded';
|
1582 | params.data = params.data ? {model: params.data} : {};
|
1583 | }
|
1584 |
|
1585 |
|
1586 |
|
1587 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
|
1588 | params.type = 'POST';
|
1589 | if (options.emulateJSON) params.data._method = type;
|
1590 | var beforeSend = options.beforeSend;
|
1591 | options.beforeSend = function(xhr) {
|
1592 | xhr.setRequestHeader('X-HTTP-Method-Override', type);
|
1593 | if (beforeSend) return beforeSend.apply(this, arguments);
|
1594 | };
|
1595 | }
|
1596 |
|
1597 |
|
1598 | if (params.type !== 'GET' && !options.emulateJSON) {
|
1599 | params.processData = false;
|
1600 | }
|
1601 |
|
1602 |
|
1603 | var error = options.error;
|
1604 | options.error = function(xhr, textStatus, errorThrown) {
|
1605 | options.textStatus = textStatus;
|
1606 | options.errorThrown = errorThrown;
|
1607 | if (error) error.call(options.context, xhr, textStatus, errorThrown);
|
1608 | };
|
1609 |
|
1610 |
|
1611 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
|
1612 | model.trigger('request', model, xhr, options);
|
1613 | return xhr;
|
1614 | };
|
1615 |
|
1616 |
|
1617 | var methodMap = {
|
1618 | create: 'POST',
|
1619 | update: 'PUT',
|
1620 | patch: 'PATCH',
|
1621 | delete: 'DELETE',
|
1622 | read: 'GET'
|
1623 | };
|
1624 |
|
1625 |
|
1626 |
|
1627 | Backbone.ajax = function() {
|
1628 | return Backbone.$.ajax.apply(Backbone.$, arguments);
|
1629 | };
|
1630 |
|
1631 |
|
1632 |
|
1633 |
|
1634 |
|
1635 |
|
1636 | var Router = Backbone.Router = function(options) {
|
1637 | options || (options = {});
|
1638 | this.preinitialize.apply(this, arguments);
|
1639 | if (options.routes) this.routes = options.routes;
|
1640 | this._bindRoutes();
|
1641 | this.initialize.apply(this, arguments);
|
1642 | };
|
1643 |
|
1644 |
|
1645 |
|
1646 | var optionalParam = /\((.*?)\)/g;
|
1647 | var namedParam = /(\(\?)?:\w+/g;
|
1648 | var splatParam = /\*\w+/g;
|
1649 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
1650 |
|
1651 |
|
1652 | _.extend(Router.prototype, Events, {
|
1653 |
|
1654 |
|
1655 |
|
1656 | preinitialize: function(){},
|
1657 |
|
1658 |
|
1659 |
|
1660 | initialize: function(){},
|
1661 |
|
1662 |
|
1663 |
|
1664 |
|
1665 |
|
1666 |
|
1667 |
|
1668 | route: function(route, name, callback) {
|
1669 | if (!_.isRegExp(route)) route = this._routeToRegExp(route);
|
1670 | if (_.isFunction(name)) {
|
1671 | callback = name;
|
1672 | name = '';
|
1673 | }
|
1674 | if (!callback) callback = this[name];
|
1675 | var router = this;
|
1676 | Backbone.history.route(route, function(fragment) {
|
1677 | var args = router._extractParameters(route, fragment);
|
1678 | if (router.execute(callback, args, name) !== false) {
|
1679 | router.trigger.apply(router, ['route:' + name].concat(args));
|
1680 | router.trigger('route', name, args);
|
1681 | Backbone.history.trigger('route', router, name, args);
|
1682 | }
|
1683 | });
|
1684 | return this;
|
1685 | },
|
1686 |
|
1687 |
|
1688 |
|
1689 | execute: function(callback, args, name) {
|
1690 | if (callback) callback.apply(this, args);
|
1691 | },
|
1692 |
|
1693 |
|
1694 | navigate: function(fragment, options) {
|
1695 | Backbone.history.navigate(fragment, options);
|
1696 | return this;
|
1697 | },
|
1698 |
|
1699 |
|
1700 |
|
1701 |
|
1702 | _bindRoutes: function() {
|
1703 | if (!this.routes) return;
|
1704 | this.routes = _.result(this, 'routes');
|
1705 | var route, routes = _.keys(this.routes);
|
1706 | while ((route = routes.pop()) != null) {
|
1707 | this.route(route, this.routes[route]);
|
1708 | }
|
1709 | },
|
1710 |
|
1711 |
|
1712 |
|
1713 | _routeToRegExp: function(route) {
|
1714 | route = route.replace(escapeRegExp, '\\$&')
|
1715 | .replace(optionalParam, '(?:$1)?')
|
1716 | .replace(namedParam, function(match, optional) {
|
1717 | return optional ? match : '([^/?]+)';
|
1718 | })
|
1719 | .replace(splatParam, '([^?]*?)');
|
1720 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
|
1721 | },
|
1722 |
|
1723 |
|
1724 |
|
1725 |
|
1726 | _extractParameters: function(route, fragment) {
|
1727 | var params = route.exec(fragment).slice(1);
|
1728 | return _.map(params, function(param, i) {
|
1729 |
|
1730 | if (i === params.length - 1) return param || null;
|
1731 | return param ? decodeURIComponent(param) : null;
|
1732 | });
|
1733 | }
|
1734 |
|
1735 | });
|
1736 |
|
1737 |
|
1738 |
|
1739 |
|
1740 |
|
1741 |
|
1742 |
|
1743 |
|
1744 |
|
1745 | var History = Backbone.History = function() {
|
1746 | this.handlers = [];
|
1747 | this.checkUrl = this.checkUrl.bind(this);
|
1748 |
|
1749 |
|
1750 | if (typeof window !== 'undefined') {
|
1751 | this.location = window.location;
|
1752 | this.history = window.history;
|
1753 | }
|
1754 | };
|
1755 |
|
1756 |
|
1757 | var routeStripper = /^[#\/]|\s+$/g;
|
1758 |
|
1759 |
|
1760 | var rootStripper = /^\/+|\/+$/g;
|
1761 |
|
1762 |
|
1763 | var pathStripper = /#.*$/;
|
1764 |
|
1765 |
|
1766 | History.started = false;
|
1767 |
|
1768 |
|
1769 | _.extend(History.prototype, Events, {
|
1770 |
|
1771 |
|
1772 |
|
1773 | interval: 50,
|
1774 |
|
1775 |
|
1776 | atRoot: function() {
|
1777 | var path = this.location.pathname.replace(/[^\/]$/, '$&/');
|
1778 | return path === this.root && !this.getSearch();
|
1779 | },
|
1780 |
|
1781 |
|
1782 | matchRoot: function() {
|
1783 | var path = this.decodeFragment(this.location.pathname);
|
1784 | var rootPath = path.slice(0, this.root.length - 1) + '/';
|
1785 | return rootPath === this.root;
|
1786 | },
|
1787 |
|
1788 |
|
1789 |
|
1790 |
|
1791 | decodeFragment: function(fragment) {
|
1792 | return decodeURI(fragment.replace(/%25/g, '%2525'));
|
1793 | },
|
1794 |
|
1795 |
|
1796 |
|
1797 | getSearch: function() {
|
1798 | var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
|
1799 | return match ? match[0] : '';
|
1800 | },
|
1801 |
|
1802 |
|
1803 |
|
1804 | getHash: function(window) {
|
1805 | var match = (window || this).location.href.match(/#(.*)$/);
|
1806 | return match ? match[1] : '';
|
1807 | },
|
1808 |
|
1809 |
|
1810 | getPath: function() {
|
1811 | var path = this.decodeFragment(
|
1812 | this.location.pathname + this.getSearch()
|
1813 | ).slice(this.root.length - 1);
|
1814 | return path.charAt(0) === '/' ? path.slice(1) : path;
|
1815 | },
|
1816 |
|
1817 |
|
1818 | getFragment: function(fragment) {
|
1819 | if (fragment == null) {
|
1820 | if (this._usePushState || !this._wantsHashChange) {
|
1821 | fragment = this.getPath();
|
1822 | } else {
|
1823 | fragment = this.getHash();
|
1824 | }
|
1825 | }
|
1826 | return fragment.replace(routeStripper, '');
|
1827 | },
|
1828 |
|
1829 |
|
1830 |
|
1831 | start: function(options) {
|
1832 | if (History.started) throw new Error('Backbone.history has already been started');
|
1833 | History.started = true;
|
1834 |
|
1835 |
|
1836 |
|
1837 | this.options = _.extend({root: '/'}, this.options, options);
|
1838 | this.root = this.options.root;
|
1839 | this._wantsHashChange = this.options.hashChange !== false;
|
1840 | this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
|
1841 | this._useHashChange = this._wantsHashChange && this._hasHashChange;
|
1842 | this._wantsPushState = !!this.options.pushState;
|
1843 | this._hasPushState = !!(this.history && this.history.pushState);
|
1844 | this._usePushState = this._wantsPushState && this._hasPushState;
|
1845 | this.fragment = this.getFragment();
|
1846 |
|
1847 |
|
1848 | this.root = ('/' + this.root + '/').replace(rootStripper, '/');
|
1849 |
|
1850 |
|
1851 |
|
1852 | if (this._wantsHashChange && this._wantsPushState) {
|
1853 |
|
1854 |
|
1855 |
|
1856 | if (!this._hasPushState && !this.atRoot()) {
|
1857 | var rootPath = this.root.slice(0, -1) || '/';
|
1858 | this.location.replace(rootPath + '#' + this.getPath());
|
1859 |
|
1860 | return true;
|
1861 |
|
1862 |
|
1863 |
|
1864 | } else if (this._hasPushState && this.atRoot()) {
|
1865 | this.navigate(this.getHash(), {replace: true});
|
1866 | }
|
1867 |
|
1868 | }
|
1869 |
|
1870 |
|
1871 |
|
1872 |
|
1873 | if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
|
1874 | this.iframe = document.createElement('iframe');
|
1875 | this.iframe.src = 'javascript:0';
|
1876 | this.iframe.style.display = 'none';
|
1877 | this.iframe.tabIndex = -1;
|
1878 | var body = document.body;
|
1879 |
|
1880 | var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
|
1881 | iWindow.document.open();
|
1882 | iWindow.document.close();
|
1883 | iWindow.location.hash = '#' + this.fragment;
|
1884 | }
|
1885 |
|
1886 |
|
1887 | var addEventListener = window.addEventListener || function(eventName, listener) {
|
1888 | return attachEvent('on' + eventName, listener);
|
1889 | };
|
1890 |
|
1891 |
|
1892 |
|
1893 | if (this._usePushState) {
|
1894 | addEventListener('popstate', this.checkUrl, false);
|
1895 | } else if (this._useHashChange && !this.iframe) {
|
1896 | addEventListener('hashchange', this.checkUrl, false);
|
1897 | } else if (this._wantsHashChange) {
|
1898 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
|
1899 | }
|
1900 |
|
1901 | if (!this.options.silent) return this.loadUrl();
|
1902 | },
|
1903 |
|
1904 |
|
1905 |
|
1906 | stop: function() {
|
1907 |
|
1908 | var removeEventListener = window.removeEventListener || function(eventName, listener) {
|
1909 | return detachEvent('on' + eventName, listener);
|
1910 | };
|
1911 |
|
1912 |
|
1913 | if (this._usePushState) {
|
1914 | removeEventListener('popstate', this.checkUrl, false);
|
1915 | } else if (this._useHashChange && !this.iframe) {
|
1916 | removeEventListener('hashchange', this.checkUrl, false);
|
1917 | }
|
1918 |
|
1919 |
|
1920 | if (this.iframe) {
|
1921 | document.body.removeChild(this.iframe);
|
1922 | this.iframe = null;
|
1923 | }
|
1924 |
|
1925 |
|
1926 | if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
|
1927 | History.started = false;
|
1928 | },
|
1929 |
|
1930 |
|
1931 |
|
1932 | route: function(route, callback) {
|
1933 | this.handlers.unshift({route: route, callback: callback});
|
1934 | },
|
1935 |
|
1936 |
|
1937 |
|
1938 | checkUrl: function(e) {
|
1939 | var current = this.getFragment();
|
1940 |
|
1941 |
|
1942 |
|
1943 | if (current === this.fragment && this.iframe) {
|
1944 | current = this.getHash(this.iframe.contentWindow);
|
1945 | }
|
1946 |
|
1947 | if (current === this.fragment) return false;
|
1948 | if (this.iframe) this.navigate(current);
|
1949 | this.loadUrl();
|
1950 | },
|
1951 |
|
1952 |
|
1953 |
|
1954 |
|
1955 | loadUrl: function(fragment) {
|
1956 |
|
1957 | if (!this.matchRoot()) return false;
|
1958 | fragment = this.fragment = this.getFragment(fragment);
|
1959 | return _.some(this.handlers, function(handler) {
|
1960 | if (handler.route.test(fragment)) {
|
1961 | handler.callback(fragment);
|
1962 | return true;
|
1963 | }
|
1964 | });
|
1965 | },
|
1966 |
|
1967 |
|
1968 |
|
1969 |
|
1970 |
|
1971 |
|
1972 |
|
1973 |
|
1974 | navigate: function(fragment, options) {
|
1975 | if (!History.started) return false;
|
1976 | if (!options || options === true) options = {trigger: !!options};
|
1977 |
|
1978 |
|
1979 | fragment = this.getFragment(fragment || '');
|
1980 |
|
1981 |
|
1982 | var rootPath = this.root;
|
1983 | if (fragment === '' || fragment.charAt(0) === '?') {
|
1984 | rootPath = rootPath.slice(0, -1) || '/';
|
1985 | }
|
1986 | var url = rootPath + fragment;
|
1987 |
|
1988 |
|
1989 | fragment = fragment.replace(pathStripper, '');
|
1990 |
|
1991 |
|
1992 | var decodedFragment = this.decodeFragment(fragment);
|
1993 |
|
1994 | if (this.fragment === decodedFragment) return;
|
1995 | this.fragment = decodedFragment;
|
1996 |
|
1997 |
|
1998 | if (this._usePushState) {
|
1999 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
|
2000 |
|
2001 |
|
2002 |
|
2003 | } else if (this._wantsHashChange) {
|
2004 | this._updateHash(this.location, fragment, options.replace);
|
2005 | if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
|
2006 | var iWindow = this.iframe.contentWindow;
|
2007 |
|
2008 |
|
2009 |
|
2010 |
|
2011 | if (!options.replace) {
|
2012 | iWindow.document.open();
|
2013 | iWindow.document.close();
|
2014 | }
|
2015 |
|
2016 | this._updateHash(iWindow.location, fragment, options.replace);
|
2017 | }
|
2018 |
|
2019 |
|
2020 |
|
2021 | } else {
|
2022 | return this.location.assign(url);
|
2023 | }
|
2024 | if (options.trigger) return this.loadUrl(fragment);
|
2025 | },
|
2026 |
|
2027 |
|
2028 |
|
2029 | _updateHash: function(location, fragment, replace) {
|
2030 | if (replace) {
|
2031 | var href = location.href.replace(/(javascript:|#).*$/, '');
|
2032 | location.replace(href + '#' + fragment);
|
2033 | } else {
|
2034 |
|
2035 | location.hash = '#' + fragment;
|
2036 | }
|
2037 | }
|
2038 |
|
2039 | });
|
2040 |
|
2041 |
|
2042 | Backbone.history = new History;
|
2043 |
|
2044 |
|
2045 |
|
2046 |
|
2047 |
|
2048 |
|
2049 |
|
2050 | var extend = function(protoProps, staticProps) {
|
2051 | var parent = this;
|
2052 | var child;
|
2053 |
|
2054 |
|
2055 |
|
2056 |
|
2057 | if (protoProps && _.has(protoProps, 'constructor')) {
|
2058 | child = protoProps.constructor;
|
2059 | } else {
|
2060 | child = function(){ return parent.apply(this, arguments); };
|
2061 | }
|
2062 |
|
2063 |
|
2064 | _.extend(child, parent, staticProps);
|
2065 |
|
2066 |
|
2067 |
|
2068 | child.prototype = _.create(parent.prototype, protoProps);
|
2069 | child.prototype.constructor = child;
|
2070 |
|
2071 |
|
2072 |
|
2073 | child.__super__ = parent.prototype;
|
2074 |
|
2075 | return child;
|
2076 | };
|
2077 |
|
2078 |
|
2079 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
|
2080 |
|
2081 |
|
2082 | var urlError = function() {
|
2083 | throw new Error('A "url" property or function must be specified');
|
2084 | };
|
2085 |
|
2086 |
|
2087 | var wrapError = function(model, options) {
|
2088 | var error = options.error;
|
2089 | options.error = function(resp) {
|
2090 | if (error) error.call(options.context, model, resp, options);
|
2091 | model.trigger('error', model, resp, options);
|
2092 | };
|
2093 | };
|
2094 |
|
2095 | return Backbone;
|
2096 | });
|