js_stack 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,722 @@
1
+ //
2
+ // Backbone-associations.js 0.6.1
3
+ //
4
+ // (c) 2013 Dhruva Ray, Jaynti Kanani, Persistent Systems Ltd.
5
+ // Backbone-associations may be freely distributed under the MIT license.
6
+ // For all details and documentation:
7
+ // https://github.com/dhruvaray/backbone-associations/
8
+ //
9
+
10
+ (function(root, factory) {
11
+ // Set up Backbone-associations appropriately for the environment. Start with AMD.
12
+ if (typeof define === 'function' && define.amd) {
13
+ define(['underscore', 'backbone'], function(_, Backbone) {
14
+ // Export global even in AMD case in case this script is loaded with
15
+ // others that may still expect a global Backbone.
16
+ return factory(root, Backbone, _);
17
+ });
18
+
19
+ // Next for Node.js or CommonJS.
20
+ } else if (typeof exports !== 'undefined') {
21
+ var _ = require('underscore'),
22
+ Backbone = require('backbone');
23
+ factory(root, Backbone, _);
24
+ if (typeof module !== 'undefined' && module.exports) {
25
+ module.exports = Backbone;
26
+ }
27
+ exports = Backbone;
28
+
29
+ // Finally, as a browser global.
30
+ } else {
31
+ factory(root, root.Backbone, root._);
32
+ }
33
+
34
+ }(this, function(root, Backbone, _) {
35
+ "use strict";
36
+
37
+ // Initial Setup
38
+ // --------------
39
+
40
+ // The top-level namespace. All public Backbone classes and modules will be attached to this.
41
+ // Exported for the browser and CommonJS.
42
+ var BackboneModel, BackboneCollection, ModelProto, BackboneEvent,
43
+ CollectionProto, AssociatedModel, pathChecker,
44
+ delimiters, pathSeparator, sourceModel, sourceKey, endPoints = {};
45
+
46
+ // Create local reference `Model` prototype.
47
+ BackboneModel = Backbone.Model;
48
+ BackboneCollection = Backbone.Collection;
49
+ ModelProto = BackboneModel.prototype;
50
+ CollectionProto = BackboneCollection.prototype;
51
+ BackboneEvent = Backbone.Events;
52
+
53
+ Backbone.Associations = {
54
+ VERSION: "0.6.1"
55
+ };
56
+
57
+ // Alternative scopes other than root
58
+ Backbone.Associations.scopes = [];
59
+
60
+ // Define `getter` and `setter` for `separator`
61
+ var getSeparator = function() {
62
+ return pathSeparator;
63
+ };
64
+ // Define `setSeperator`
65
+ var setSeparator = function(value) {
66
+ if (!_.isString(value) || _.size(value) < 1) {
67
+ value = ".";
68
+ }
69
+ // set private properties
70
+ pathSeparator = value;
71
+ pathChecker = new RegExp("[\\" + pathSeparator + "\\[\\]]+", "g");
72
+ delimiters = new RegExp("[^\\" + pathSeparator + "\\[\\]]+", "g");
73
+ };
74
+
75
+ try {
76
+ // Define `SEPERATOR` property to Backbone.Associations
77
+ Object.defineProperty(Backbone.Associations, 'SEPARATOR', {
78
+ enumerable: true,
79
+ get: getSeparator,
80
+ set: setSeparator
81
+ });
82
+ } catch (e) {}
83
+
84
+ // Backbone.AssociatedModel
85
+ // --------------
86
+
87
+ //Add `Many` and `One` relations to Backbone Object.
88
+ Backbone.Associations.Many = Backbone.Many = "Many";
89
+ Backbone.Associations.One = Backbone.One = "One";
90
+ Backbone.Associations.Self = Backbone.Self = "Self";
91
+ // Set default separator
92
+ Backbone.Associations.SEPARATOR = ".";
93
+ Backbone.Associations.getSeparator = getSeparator;
94
+ Backbone.Associations.setSeparator = setSeparator;
95
+
96
+ Backbone.Associations.EVENTS_BUBBLE = true;
97
+ Backbone.Associations.EVENTS_WILDCARD = true;
98
+ Backbone.Associations.EVENTS_NC = false;
99
+
100
+
101
+ setSeparator();
102
+
103
+ // Define `AssociatedModel` (Extends Backbone.Model).
104
+ AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
105
+ // Define relations with Associated Model.
106
+ relations:undefined,
107
+ // Define `Model` property which can keep track of already fired `events`,
108
+ // and prevent redundant event to be triggered in case of cyclic model graphs.
109
+ _proxyCalls:undefined,
110
+
111
+ on: function (name, callback, context) {
112
+
113
+ var result = BackboneEvent.on.apply(this, arguments);
114
+
115
+ // No optimization possible if nested-events is wanted by the application
116
+ if (Backbone.Associations.EVENTS_NC) return result;
117
+
118
+ // Regular expression used to split event strings.
119
+ var eventSplitter = /\s+/;
120
+
121
+ // Handle atomic event names only
122
+ if (_.isString(name) && name && (!eventSplitter.test(name)) && callback) {
123
+ var endPoint = getPathEndPoint(name);
124
+ if (endPoint) {
125
+ //Increment end point counter. Represents # of nodes which listen to this end point
126
+ endPoints[endPoint] = (typeof endPoints[endPoint] === 'undefined') ? 1 : (endPoints[endPoint] + 1);
127
+ }
128
+ }
129
+ return result;
130
+ },
131
+
132
+ off: function (name, callback, context) {
133
+
134
+ // No optimization possible if nested-events is wanted by the application
135
+ if (Backbone.Associations.EVENTS_NC) return BackboneEvent.off.apply(this, arguments);
136
+
137
+ var eventSplitter = /\s+/,
138
+ events = this._events,
139
+ listeners = {},
140
+ names = events ? _.keys(events) : [],
141
+ all = (!name && !callback && !context),
142
+ atomic_event = (_.isString(name) && (!eventSplitter.test(name)));
143
+
144
+ if (all || atomic_event) {
145
+ for (var i = 0, l = names.length; i < l; i++) {
146
+ // Store the # of callbacks listening to the event name prior to the `off` call
147
+ listeners[names[i]] = events[names[i]] ? events[names[i]].length : 0;
148
+ }
149
+ }
150
+ // Call Backbone off implementation
151
+ var result = BackboneEvent.off.apply(this, arguments);
152
+
153
+ if (all || atomic_event) {
154
+ for (i = 0, l = names.length; i < l; i++) {
155
+ var endPoint = getPathEndPoint(names[i]);
156
+ if (endPoint) {
157
+ if (events[names[i]]) {
158
+ // Some listeners wiped out for this name for this object
159
+ endPoints[endPoint] -= (listeners[names[i]] - events[names[i]].length);
160
+ } else {
161
+ // All listeners wiped out for this name for this object
162
+ endPoints[endPoint] -= listeners[names[i]];
163
+ }
164
+ }
165
+ }
166
+ }
167
+ return result;
168
+ },
169
+
170
+ // Get the value of an attribute.
171
+ get:function (attr) {
172
+ var obj = ModelProto.get.call(this, attr);
173
+ return obj ? obj : this._getAttr.apply(this, arguments);
174
+ },
175
+
176
+ // Set a hash of model attributes on the Backbone Model.
177
+ set:function (key, value, options) {
178
+ var attributes, result;
179
+ // Duplicate backbone's behavior to allow separate key/value parameters,
180
+ // instead of a single 'attributes' object.
181
+ if (_.isObject(key) || key == null) {
182
+ attributes = key;
183
+ options = value;
184
+ } else {
185
+ attributes = {};
186
+ attributes[key] = value;
187
+ }
188
+ result = this._set(attributes, options);
189
+ // Trigger events which have been blocked until the entire object graph is updated.
190
+ this._processPendingEvents();
191
+ return result;
192
+
193
+ },
194
+
195
+ // Works with an attribute hash and options + fully qualified paths
196
+ _set:function (attributes, options) {
197
+ var attr, modelMap, modelId, obj, result = this;
198
+ if (!attributes) return this;
199
+ for (attr in attributes) {
200
+ //Create a map for each unique object whose attributes we want to set
201
+ modelMap || (modelMap = {});
202
+ if (attr.match(pathChecker)) {
203
+ var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
204
+ last = pathTokens[pathTokens.length - 1],
205
+ parentModel = this.get(initials);
206
+ if (parentModel instanceof AssociatedModel) {
207
+ obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
208
+ obj.data[last] = attributes[attr];
209
+ }
210
+ } else {
211
+ obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
212
+ obj.data[attr] = attributes[attr];
213
+ }
214
+ }
215
+
216
+ if (modelMap) {
217
+ for (modelId in modelMap) {
218
+ obj = modelMap[modelId];
219
+ this._setAttr.call(obj.model, obj.data, options) || (result = false);
220
+
221
+ }
222
+ } else {
223
+ result = this._setAttr.call(this, attributes, options);
224
+ }
225
+ return result;
226
+
227
+ },
228
+
229
+ // Set a hash of model attributes on the object,
230
+ // fire Backbone `event` with options.
231
+ // It maintains relations between models during the set operation.
232
+ // It also bubbles up child events to the parent.
233
+ _setAttr:function (attributes, options) {
234
+ var attr;
235
+ // Extract attributes and options.
236
+ options || (options = {});
237
+ if (options.unset) for (attr in attributes) attributes[attr] = void 0;
238
+ this.parents = this.parents || [];
239
+
240
+ if (this.relations) {
241
+ // Iterate over `this.relations` and `set` model and collection values
242
+ // if `relations` are available.
243
+ _.each(this.relations, function (relation) {
244
+ var relationKey = relation.key,
245
+ relatedModel = relation.relatedModel,
246
+ collectionType = relation.collectionType,
247
+ activationContext = relation.scope || root,
248
+ map = relation.map,
249
+ currVal = this.attributes[relationKey],
250
+ idKey = currVal && currVal.idAttribute,
251
+ val, relationOptions, data, relationValue, newCtx = false;
252
+
253
+ // Call function if relatedModel is implemented as a function
254
+ if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
255
+ relatedModel = _.isFunction(relatedModel) ?
256
+ relatedModel.call(this, relation, attributes) :
257
+ relatedModel;
258
+
259
+ // Get class if relation and map is stored as a string.
260
+ if (relatedModel && _.isString(relatedModel)) {
261
+ relatedModel = (relatedModel === Backbone.Self) ?
262
+ this.constructor :
263
+ map2Scope(relatedModel, activationContext);
264
+ }
265
+
266
+ map && _.isString(map) && (map = map2Scope(map, activationContext));
267
+ // Merge in `options` specific to this relation.
268
+ relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
269
+
270
+ if (attributes[relationKey]) {
271
+ // Get value of attribute with relation key in `val`.
272
+ val = _.result(attributes, relationKey);
273
+ // Map `val` if a transformation function is provided.
274
+ val = map ? map.call(this, val, collectionType ? collectionType : relatedModel) : val;
275
+
276
+ // If `relation.type` is `Backbone.Many`,
277
+ // Create `Backbone.Collection` with passed data and perform Backbone `set`.
278
+ if (relation.type === Backbone.Many) {
279
+
280
+ if (collectionType && _.isFunction(collectionType) &&
281
+ (collectionType.prototype instanceof BackboneModel))
282
+ throw new Error('type is of Backbone.Model. Specify derivatives of Backbone.Collection');
283
+
284
+ // Call function if collectionType is implemented as a function
285
+ if (collectionType && !(collectionType.prototype instanceof BackboneCollection))
286
+ collectionType = _.isFunction(collectionType) ?
287
+ collectionType.call(this, relation, attributes) : collectionType;
288
+
289
+ collectionType && _.isString(collectionType) &&
290
+ (collectionType = map2Scope(collectionType, activationContext));
291
+
292
+ if ((!relatedModel) && (!collectionType))
293
+ throw new Error('specify either a relatedModel or collectionType');
294
+
295
+ // `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
296
+ if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
297
+ throw new Error('collectionType must inherit from Backbone.Collection');
298
+ }
299
+
300
+ if (currVal) {
301
+ // Setting this flag will prevent events from firing immediately. That way clients
302
+ // will not get events until the entire object graph is updated.
303
+ currVal._deferEvents = true;
304
+
305
+ // Use Backbone.Collection's `reset` or smart `set` method
306
+ currVal[relationOptions.reset ? 'reset' : 'set'](
307
+ val instanceof BackboneCollection ? val.models : val, relationOptions);
308
+
309
+ data = currVal;
310
+
311
+ } else {
312
+ newCtx = true;
313
+
314
+ if (val instanceof BackboneCollection) {
315
+ data = val;
316
+ } else {
317
+ data = collectionType ?
318
+ new collectionType() : this._createCollection(relatedModel, activationContext);
319
+ data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
320
+ }
321
+ }
322
+
323
+ } else if (relation.type === Backbone.One) {
324
+
325
+ if (!relatedModel)
326
+ throw new Error('specify a relatedModel for Backbone.One type');
327
+
328
+ if (!(relatedModel.prototype instanceof Backbone.AssociatedModel))
329
+ throw new Error('specify an AssociatedModel for Backbone.One type');
330
+
331
+ data = val instanceof AssociatedModel ? val : new relatedModel(val, relationOptions);
332
+ //Is the passed in data for the same key?
333
+ if (currVal && data.attributes[idKey] &&
334
+ currVal.attributes[idKey] === data.attributes[idKey]) {
335
+ // Setting this flag will prevent events from firing immediately. That way clients
336
+ // will not get events until the entire object graph is updated.
337
+ currVal._deferEvents = true;
338
+ // Perform the traditional `set` operation
339
+ currVal._set(val instanceof AssociatedModel ? val.attributes : val, relationOptions);
340
+ data = currVal;
341
+ } else {
342
+ newCtx = true;
343
+ }
344
+
345
+ } else {
346
+ throw new Error('type attribute must be specified and have the values Backbone.One or Backbone.Many');
347
+ }
348
+
349
+
350
+ attributes[relationKey] = data;
351
+ relationValue = data;
352
+
353
+ // Add proxy events to respective parents.
354
+ // Only add callback if not defined or new Ctx has been identified.
355
+ if (newCtx || (relationValue && !relationValue._proxyCallback)) {
356
+ relationValue._proxyCallback = function () {
357
+ return Backbone.Associations.EVENTS_BUBBLE &&
358
+ this._bubbleEvent.call(this, relationKey, relationValue, arguments);
359
+ };
360
+ relationValue.on("all", relationValue._proxyCallback, this);
361
+ }
362
+
363
+ }
364
+ //Distinguish between the value of undefined versus a set no-op
365
+ if (attributes.hasOwnProperty(relationKey)) {
366
+ //Maintain reverse pointers - a.k.a parents
367
+ var updated = attributes[relationKey];
368
+ var original = this.attributes[relationKey];
369
+ if (updated) {
370
+ updated.parents = updated.parents || [];
371
+ (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
372
+ } else if (original && original.parents.length > 0) { // New value is undefined
373
+ original.parents = _.difference(original.parents, [this]);
374
+ // Don't bubble to this parent anymore
375
+ original._proxyCallback && original.off("all", original._proxyCallback, this);
376
+ }
377
+ }
378
+ }, this);
379
+ }
380
+ // Return results for `BackboneModel.set`.
381
+ return ModelProto.set.call(this, attributes, options);
382
+ },
383
+
384
+
385
+ // Bubble-up event to `parent` Model
386
+ _bubbleEvent:function (relationKey, relationValue, eventArguments) {
387
+ var args = eventArguments,
388
+ opt = args[0].split(":"),
389
+ eventType = opt[0],
390
+ catch_all = args[0] == "nested-change",
391
+ isChangeEvent = eventType === "change",
392
+ eventObject = args[1],
393
+ indexEventObject = -1,
394
+ _proxyCalls = relationValue._proxyCalls,
395
+ cargs,
396
+ eventPath = opt[1],
397
+ eSrc = !eventPath || (eventPath.indexOf(pathSeparator) == -1),
398
+ basecolEventPath;
399
+
400
+
401
+ // Short circuit the listen in to the nested-graph event
402
+ if (catch_all) return;
403
+
404
+ // Record the source of the event
405
+ if (eSrc) sourceKey = (getPathEndPoint(args[0]) || relationKey);
406
+
407
+ // Short circuit the event bubbling as there are no listeners for this end point
408
+ if (!Backbone.Associations.EVENTS_NC && !endPoints[sourceKey]) return;
409
+
410
+ // Short circuit the listen in to the wild-card event
411
+ if (Backbone.Associations.EVENTS_WILDCARD) {
412
+ if (/\[\*\]/g.test(eventPath)) return this;
413
+ }
414
+
415
+ if (relationValue instanceof BackboneCollection && (isChangeEvent || eventPath)) {
416
+ // O(n) search :(
417
+ indexEventObject = relationValue.indexOf(sourceModel || eventObject);
418
+ }
419
+
420
+ if (this instanceof BackboneModel) {
421
+ // A quicker way to identify the model which caused an update inside the collection (while bubbling up)
422
+ sourceModel = this;
423
+ }
424
+ // Manipulate `eventPath`.
425
+ eventPath = relationKey + ((indexEventObject !== -1 && (isChangeEvent || eventPath)) ?
426
+ "[" + indexEventObject + "]" : "") + (eventPath ? pathSeparator + eventPath : "");
427
+
428
+ // Short circuit collection * events
429
+
430
+ if (Backbone.Associations.EVENTS_WILDCARD) {
431
+ basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
432
+ }
433
+
434
+ cargs = [];
435
+ cargs.push.apply(cargs, args);
436
+ cargs[0] = eventType + ":" + eventPath;
437
+
438
+ // Create a collection modified event with wild-card
439
+ if (Backbone.Associations.EVENTS_WILDCARD && eventPath !== basecolEventPath) {
440
+ cargs[0] = cargs[0] + " " + eventType + ":" + basecolEventPath;
441
+ }
442
+
443
+ // If event has been already triggered as result of same source `eventPath`,
444
+ // no need to re-trigger event to prevent cycle.
445
+ _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
446
+ if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
447
+
448
+ // Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
449
+ _proxyCalls[eventPath] = true;
450
+
451
+
452
+ // Set up previous attributes correctly.
453
+ if (isChangeEvent) {
454
+ this._previousAttributes[relationKey] = relationValue._previousAttributes;
455
+ this.changed[relationKey] = relationValue;
456
+ }
457
+
458
+
459
+ // Bubble up event to parent `model` with new changed arguments.
460
+
461
+ this.trigger.apply(this, cargs);
462
+
463
+ //Only fire for change. Not change:attribute
464
+ if (Backbone.Associations.EVENTS_NC && isChangeEvent && this.get(eventPath) != args[2]) {
465
+ var ncargs = ["nested-change", eventPath, args[1]];
466
+ args[2] && ncargs.push(args[2]); //args[2] will be options if present
467
+ this.trigger.apply(this, ncargs);
468
+ }
469
+
470
+ // Remove `eventPath` from `_proxyCalls`,
471
+ // if `eventPath` and `_proxyCalls` are available,
472
+ // which allow event to be triggered on for next operation of `set`.
473
+ if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
474
+
475
+ sourceModel = undefined;
476
+
477
+ return this;
478
+ },
479
+
480
+ // Has event been fired from this source. Used to prevent event recursion in cyclic graphs
481
+ _isEventAvailable:function (_proxyCalls, path) {
482
+ return _.find(_proxyCalls, function (value, eventKey) {
483
+ return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
484
+ });
485
+ },
486
+
487
+ // Returns New `collection` of type `relation.relatedModel`.
488
+ _createCollection: function (type, context) {
489
+ var collection, relatedModel = type;
490
+ _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel, context));
491
+ // Creates new `Backbone.Collection` and defines model class.
492
+ if (relatedModel && (relatedModel.prototype instanceof AssociatedModel) || _.isFunction(relatedModel)) {
493
+ collection = new BackboneCollection();
494
+ collection.model = relatedModel;
495
+ } else {
496
+ throw new Error('type must inherit from Backbone.AssociatedModel');
497
+ }
498
+ return collection;
499
+ },
500
+
501
+ // Process all pending events after the entire object graph has been updated
502
+ _processPendingEvents:function () {
503
+ if (!this._processedEvents) {
504
+ this._processedEvents = true;
505
+
506
+ this._deferEvents = false;
507
+
508
+ // Trigger all pending events
509
+ _.each(this._pendingEvents, function (e) {
510
+ e.c.trigger.apply(e.c, e.a);
511
+ });
512
+
513
+ this._pendingEvents = [];
514
+
515
+ // Traverse down the object graph and call process pending events on sub-trees
516
+ _.each(this.relations, function (relation) {
517
+ var val = this.attributes[relation.key];
518
+ val && val._processPendingEvents();
519
+ }, this);
520
+
521
+ delete this._processedEvents;
522
+ }
523
+ },
524
+
525
+ // Override trigger to defer events in the object graph.
526
+ trigger:function (name) {
527
+ // Defer event processing
528
+ if (this._deferEvents) {
529
+ this._pendingEvents = this._pendingEvents || [];
530
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
531
+ this._pendingEvents.push({c:this, a:arguments});
532
+ } else {
533
+ ModelProto.trigger.apply(this, arguments);
534
+ }
535
+ },
536
+
537
+ // The JSON representation of the model.
538
+ toJSON:function (options) {
539
+ var json = {}, aJson;
540
+ json[this.idAttribute] = this.id;
541
+ if (!this.visited) {
542
+ this.visited = true;
543
+ // Get json representation from `BackboneModel.toJSON`.
544
+ json = ModelProto.toJSON.apply(this, arguments);
545
+
546
+ // Pick up only the keys you want to serialize
547
+ if (options && options.serialize_keys) {
548
+ json = _.pick(json, options.serialize_keys);
549
+ }
550
+ // If `this.relations` is defined, iterate through each `relation`
551
+ // and added it's json representation to parents' json representation.
552
+ if (this.relations) {
553
+ _.each(this.relations, function (relation) {
554
+ var key = relation.key,
555
+ remoteKey = relation.remoteKey,
556
+ attr = this.attributes[key],
557
+ serialize = !relation.isTransient,
558
+ serialize_keys = relation.serialize || []
559
+
560
+ // Remove default Backbone serialization for associations.
561
+ delete json[key];
562
+
563
+ //Assign to remoteKey if specified. Otherwise use the default key.
564
+ //Only for non-transient relationships
565
+ if (serialize) {
566
+
567
+ // Pass the keys to serialize as options to the toJSON method.
568
+ if (serialize_keys.length) {
569
+ options ?
570
+ (options.serialize_keys = serialize_keys) :
571
+ (options = {serialize_keys: serialize_keys})
572
+ }
573
+
574
+ aJson = attr && attr.toJSON ? attr.toJSON(options) : attr;
575
+ json[remoteKey || key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
576
+ }
577
+
578
+ }, this);
579
+ }
580
+
581
+ delete this.visited;
582
+ }
583
+ return json;
584
+ },
585
+
586
+ // Create a new model with identical attributes to this one.
587
+ clone: function (options) {
588
+ return new this.constructor(this.toJSON(options));
589
+ },
590
+
591
+ // Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly.
592
+ // Not calling this will leak memory and have wrong parents.
593
+ // See test case "parent relations"
594
+ cleanup:function () {
595
+ _.each(this.relations, function (relation) {
596
+ var val = this.attributes[relation.key];
597
+ val && (val.parents = _.difference(val.parents, [this]));
598
+ }, this);
599
+ this.off();
600
+ },
601
+
602
+ // Navigate the path to the leaf object in the path to query for the attribute value
603
+ _getAttr:function (path) {
604
+
605
+ var result = this,
606
+ //Tokenize the path
607
+ attrs = getPathArray(path),
608
+ key,
609
+ i;
610
+ if (_.size(attrs) < 1) return;
611
+ for (i = 0; i < attrs.length; i++) {
612
+ key = attrs[i];
613
+ if (!result) break;
614
+ //Navigate the path to get to the result
615
+ result = result instanceof BackboneCollection
616
+ ? (isNaN(key) ? undefined : result.at(key))
617
+ : result.attributes[key];
618
+ }
619
+ return result;
620
+ }
621
+ });
622
+
623
+ // Tokenize the fully qualified event path
624
+ var getPathArray = function (path) {
625
+ if (path === '') return [''];
626
+ return _.isString(path) ? (path.match(delimiters)) : path || [];
627
+ };
628
+
629
+ // Get the end point of the path.
630
+ var getPathEndPoint = function (path) {
631
+
632
+ if (!path) return path;
633
+
634
+ //event_type:<path>
635
+ var tokens = path.split(":");
636
+
637
+ if (tokens.length > 1) {
638
+ path = tokens[tokens.length - 1];
639
+ tokens = path.split(pathSeparator);
640
+ return tokens.length > 1 ? tokens[tokens.length - 1].split('[')[0] : tokens[0].split('[')[0];
641
+ } else {
642
+ //path of 0 depth
643
+ return "";
644
+ }
645
+
646
+ };
647
+
648
+ var map2Scope = function (path, context) {
649
+ var target,
650
+ scopes = [context];
651
+
652
+ //Check global scopes after passed-in context
653
+ scopes.push.apply(scopes, Backbone.Associations.scopes);
654
+
655
+ for (var ctx, i = 0, l = scopes.length; i < l; ++i) {
656
+ if (ctx = scopes[i]) {
657
+ target = _.reduce(path.split(pathSeparator), function (memo, elem) {
658
+ return memo[elem];
659
+ }, ctx);
660
+ if (target) break;
661
+ }
662
+ }
663
+ return target;
664
+ };
665
+
666
+
667
+ //Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
668
+ var map2models = function (parents, target, models) {
669
+ var relation, surrogate;
670
+ //Iterate over collection's parents
671
+ _.find(parents, function (parent) {
672
+ //Iterate over relations
673
+ relation = _.find(parent.relations, function (rel) {
674
+ return parent.get(rel.key) === target;
675
+ }, this);
676
+ if (relation) {
677
+ surrogate = parent;//surrogate for transformation
678
+ return true;//break;
679
+ }
680
+ }, this);
681
+
682
+ //If we found a relation and it has a mapping function
683
+ if (relation && relation.map) {
684
+ return relation.map.call(surrogate, models, target);
685
+ }
686
+ return models;
687
+ };
688
+
689
+ var proxies = {};
690
+ // Proxy Backbone collection methods
691
+ _.each(['set', 'remove', 'reset'], function (method) {
692
+ proxies[method] = BackboneCollection.prototype[method];
693
+
694
+ CollectionProto[method] = function (models, options) {
695
+ //Short-circuit if this collection doesn't hold `AssociatedModels`
696
+ if (this.model.prototype instanceof AssociatedModel && this.parents) {
697
+ //Find a map function if available and perform a transformation
698
+ arguments[0] = map2models(this.parents, this, models);
699
+ }
700
+ return proxies[method].apply(this, arguments);
701
+ }
702
+ });
703
+
704
+ // Override trigger to defer events in the object graph.
705
+ proxies['trigger'] = CollectionProto['trigger'];
706
+ CollectionProto['trigger'] = function (name) {
707
+ if (this._deferEvents) {
708
+ this._pendingEvents = this._pendingEvents || [];
709
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
710
+ this._pendingEvents.push({c:this, a:arguments});
711
+ } else {
712
+ proxies['trigger'].apply(this, arguments);
713
+ }
714
+ };
715
+
716
+ // Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
717
+ CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
718
+ CollectionProto.on = AssociatedModel.prototype.on;
719
+ CollectionProto.off = AssociatedModel.prototype.off;
720
+
721
+ return Backbone;
722
+ }));