js_stack 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,533 @@
1
+ //
2
+ // Backbone-associations.js 0.5.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
+ // Initial Setup
11
+ // --------------
12
+ (function () {
13
+ "use strict";
14
+
15
+ // Save a reference to the global object (`window` in the browser, `exports`
16
+ // on the server).
17
+ var root = this;
18
+
19
+ // The top-level namespace. All public Backbone classes and modules will be attached to this.
20
+ // Exported for the browser and CommonJS.
21
+ var _, Backbone, BackboneModel, BackboneCollection, ModelProto,
22
+ CollectionProto, defaultEvents, AssociatedModel, pathChecker,
23
+ collectionEvents;
24
+
25
+ if (typeof window === 'undefined') {
26
+ _ = require('underscore');
27
+ Backbone = require('backbone');
28
+ if (typeof exports !== 'undefined') {
29
+ exports = module.exports = Backbone;
30
+ }
31
+ } else {
32
+ _ = root._;
33
+ Backbone = root.Backbone;
34
+ }
35
+ // Create local reference `Model` prototype.
36
+ BackboneModel = Backbone.Model;
37
+ BackboneCollection = Backbone.Collection;
38
+ ModelProto = BackboneModel.prototype;
39
+ CollectionProto = BackboneCollection.prototype;
40
+ pathChecker = /[\.\[\]]+/g;
41
+
42
+ // Built-in Backbone `events`.
43
+ defaultEvents = ["change", "add", "remove", "reset", "sort", "destroy"];
44
+ collectionEvents = ["reset", "sort"];
45
+
46
+ Backbone.Associations = {
47
+ VERSION:"0.5.1"
48
+ };
49
+
50
+ // Backbone.AssociatedModel
51
+ // --------------
52
+
53
+ //Add `Many` and `One` relations to Backbone Object.
54
+ Backbone.Associations.Many = Backbone.Many = "Many";
55
+ Backbone.Associations.One = Backbone.One = "One";
56
+ Backbone.Associations.Self = Backbone.Self = "Self";
57
+ // Define `AssociatedModel` (Extends Backbone.Model).
58
+ AssociatedModel = Backbone.AssociatedModel = Backbone.Associations.AssociatedModel = BackboneModel.extend({
59
+ // Define relations with Associated Model.
60
+ relations:undefined,
61
+ // Define `Model` property which can keep track of already fired `events`,
62
+ // and prevent redundant event to be triggered in case of cyclic model graphs.
63
+ _proxyCalls:undefined,
64
+
65
+ // Get the value of an attribute.
66
+ get:function (attr) {
67
+ var obj = ModelProto.get.call(this, attr);
68
+ return obj ? obj : this._getAttr.apply(this, arguments);
69
+ },
70
+
71
+ // Set a hash of model attributes on the Backbone Model.
72
+ set:function (key, value, options) {
73
+ var attributes, result;
74
+ // Duplicate backbone's behavior to allow separate key/value parameters,
75
+ // instead of a single 'attributes' object.
76
+ if (_.isObject(key) || key == null) {
77
+ attributes = key;
78
+ options = value;
79
+ } else {
80
+ attributes = {};
81
+ attributes[key] = value;
82
+ }
83
+ result = this._set(attributes, options);
84
+ // Trigger events which have been blocked until the entire object graph is updated.
85
+ this._processPendingEvents();
86
+ return result;
87
+
88
+ },
89
+
90
+ // Works with an attribute hash and options + fully qualified paths
91
+ _set:function (attributes, options) {
92
+ var attr, modelMap, modelId, obj, result = this;
93
+ if (!attributes) return this;
94
+ for (attr in attributes) {
95
+ //Create a map for each unique object whose attributes we want to set
96
+ modelMap || (modelMap = {});
97
+ if (attr.match(pathChecker)) {
98
+ var pathTokens = getPathArray(attr), initials = _.initial(pathTokens),
99
+ last = pathTokens[pathTokens.length - 1],
100
+ parentModel = this.get(initials);
101
+ if (parentModel instanceof AssociatedModel) {
102
+ obj = modelMap[parentModel.cid] || (modelMap[parentModel.cid] = {'model':parentModel, 'data':{}});
103
+ obj.data[last] = attributes[attr];
104
+ }
105
+ } else {
106
+ obj = modelMap[this.cid] || (modelMap[this.cid] = {'model':this, 'data':{}});
107
+ obj.data[attr] = attributes[attr];
108
+ }
109
+ }
110
+
111
+ if (modelMap) {
112
+ for (modelId in modelMap) {
113
+ obj = modelMap[modelId];
114
+ this._setAttr.call(obj.model, obj.data, options) || (result = false);
115
+
116
+ }
117
+ } else {
118
+ result = this._setAttr.call(this, attributes, options);
119
+ }
120
+ return result;
121
+
122
+ },
123
+
124
+ // Set a hash of model attributes on the object,
125
+ // fire Backbone `event` with options.
126
+ // It maintains relations between models during the set operation.
127
+ // It also bubbles up child events to the parent.
128
+ _setAttr:function (attributes, options) {
129
+ var attr;
130
+ // Extract attributes and options.
131
+ options || (options = {});
132
+ if (options.unset) for (attr in attributes) attributes[attr] = void 0;
133
+ this.parents = this.parents || [];
134
+
135
+ if (this.relations) {
136
+ // Iterate over `this.relations` and `set` model and collection values
137
+ // if `relations` are available.
138
+ _.each(this.relations, function (relation) {
139
+ var relationKey = relation.key,
140
+ relatedModel = relation.relatedModel,
141
+ collectionType = relation.collectionType,
142
+ map = relation.map,
143
+ currVal = this.attributes[relationKey],
144
+ idKey = currVal && currVal.idAttribute,
145
+ val, relationOptions, data, relationValue, newCtx = false;
146
+
147
+ // Call function if relatedModel is implemented as a function
148
+ if (relatedModel && !(relatedModel.prototype instanceof BackboneModel))
149
+ relatedModel = _.isFunction(relatedModel) ?
150
+ relatedModel.call(this, relation, attributes) :
151
+ relatedModel;
152
+
153
+ // Get class if relation and map is stored as a string.
154
+ if (relatedModel && _.isString(relatedModel)) {
155
+ relatedModel = (relatedModel === Backbone.Self) ? this.constructor : map2Scope(relatedModel);
156
+ }
157
+ collectionType && _.isString(collectionType) && (collectionType = map2Scope(collectionType));
158
+ map && _.isString(map) && (map = map2Scope(map));
159
+ // Merge in `options` specific to this relation.
160
+ relationOptions = relation.options ? _.extend({}, relation.options, options) : options;
161
+
162
+ if (attributes[relationKey]) {
163
+ // Get value of attribute with relation key in `val`.
164
+ val = _.result(attributes, relationKey);
165
+ // Map `val` if a transformation function is provided.
166
+ val = map ? map(val) : val;
167
+
168
+ // If `relation.type` is `Backbone.Many`,
169
+ // Create `Backbone.Collection` with passed data and perform Backbone `set`.
170
+ if (relation.type === Backbone.Many) {
171
+ // `collectionType` of defined `relation` should be instance of `Backbone.Collection`.
172
+ if (collectionType && !collectionType.prototype instanceof BackboneCollection) {
173
+ throw new Error('collectionType must inherit from Backbone.Collection');
174
+ }
175
+
176
+ if (val instanceof BackboneCollection) {
177
+ data = val;
178
+ // Compute whether the context is a new one after this assignment.
179
+ newCtx = (currVal !== val);
180
+ } else {
181
+ // Create a new collection
182
+ if (!currVal) {
183
+ data = collectionType ? new collectionType() : this._createCollection(relatedModel);
184
+ } else {
185
+ data = currVal;
186
+ // Setting this flag will prevent events from firing immediately. That way clients
187
+ // will not get events until the entire object graph is updated.
188
+ data._deferEvents = true;
189
+ }
190
+ // Use Backbone.Collection's `reset` or smart `set` method
191
+ data[relationOptions.reset ? 'reset' : 'set'](val, relationOptions);
192
+ }
193
+
194
+ } else if (relation.type === Backbone.One && relatedModel) {
195
+ if (val instanceof AssociatedModel) {
196
+ data = val;
197
+ // Compute whether the context is a new one after this assignment.
198
+ newCtx = (currVal !== val);
199
+ } else {
200
+ //Create a new model
201
+ if (!currVal) {
202
+ data = new relatedModel(val, relationOptions);
203
+ } else {
204
+ //Is the passed in data for the same key?
205
+ if (currVal && val[idKey] && currVal.get(idKey) === val[idKey]) {
206
+ // Setting this flag will prevent events from firing immediately. That way clients
207
+ // will not get events until the entire object graph is updated.
208
+ currVal._deferEvents = true;
209
+ // Perform the traditional `set` operation
210
+ currVal._set(val, relationOptions);
211
+ data = currVal;
212
+ } else {
213
+ data = new relatedModel(val, relationOptions);
214
+ }
215
+ }
216
+ }
217
+
218
+ }
219
+
220
+ attributes[relationKey] = data;
221
+ relationValue = data;
222
+
223
+ // Add proxy events to respective parents.
224
+ // Only add callback if not defined or new Ctx has been identified.
225
+ if (newCtx || (relationValue && !relationValue._proxyCallback)) {
226
+ relationValue._proxyCallback = function () {
227
+ return this._bubbleEvent.call(this, relationKey, relationValue, arguments);
228
+ };
229
+ relationValue.on("all", relationValue._proxyCallback, this);
230
+ }
231
+
232
+ }
233
+ //Distinguish between the value of undefined versus a set no-op
234
+ if (attributes.hasOwnProperty(relationKey)) {
235
+ //Maintain reverse pointers - a.k.a parents
236
+ var updated = attributes[relationKey];
237
+ var original = this.attributes[relationKey];
238
+ if (updated) {
239
+ updated.parents = updated.parents || [];
240
+ (_.indexOf(updated.parents, this) == -1) && updated.parents.push(this);
241
+ } else if (original && original.parents.length > 0) { // New value is undefined
242
+ original.parents = _.difference(original.parents, [this]);
243
+ // Don't bubble to this parent anymore
244
+ original._proxyCallback && original.off("all", original._proxyCallback, this);
245
+ }
246
+ }
247
+ }, this);
248
+ }
249
+ // Return results for `BackboneModel.set`.
250
+ return ModelProto.set.call(this, attributes, options);
251
+ },
252
+ // Bubble-up event to `parent` Model
253
+ _bubbleEvent:function (relationKey, relationValue, eventArguments) {
254
+ var args = eventArguments,
255
+ opt = args[0].split(":"),
256
+ eventType = opt[0],
257
+ catch_all = args[0] == "nested-change",
258
+ eventObject = args[1],
259
+ colObject = args[2],
260
+ indexEventObject = -1,
261
+ _proxyCalls = relationValue._proxyCalls,
262
+ cargs,
263
+ eventPath,
264
+ basecolEventPath,
265
+ isDefaultEvent = _.indexOf(defaultEvents, eventType) !== -1;
266
+
267
+ //Short circuit the listen in to the nested-graph event
268
+ if (catch_all) return;
269
+
270
+ // Change the event name to a fully qualified path.
271
+ _.size(opt) > 1 && (eventPath = opt[1]);
272
+
273
+ if (_.indexOf(collectionEvents, eventType) !== -1) {
274
+ colObject = eventObject;
275
+ }
276
+
277
+ // Find the specific object in the collection which has changed.
278
+ if (relationValue instanceof BackboneCollection && isDefaultEvent && eventObject) {
279
+ var pathTokens = getPathArray(eventPath),
280
+ initialTokens = _.initial(pathTokens), colModel;
281
+
282
+ colModel = relationValue.find(function (model) {
283
+ if (eventObject === model) return true;
284
+ if (!model) return false;
285
+ var changedModel = model.get(initialTokens);
286
+
287
+ if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
288
+ && eventObject === changedModel)
289
+ return true;
290
+
291
+ changedModel = model.get(pathTokens);
292
+
293
+ if ((changedModel instanceof AssociatedModel || changedModel instanceof BackboneCollection)
294
+ && eventObject === changedModel)
295
+ return true;
296
+
297
+ if (changedModel instanceof BackboneCollection && colObject
298
+ && colObject === changedModel)
299
+ return true;
300
+ });
301
+ colModel && (indexEventObject = relationValue.indexOf(colModel));
302
+ }
303
+
304
+ // Manipulate `eventPath`.
305
+ eventPath = relationKey + ((indexEventObject !== -1 && (eventType === "change" || eventPath)) ?
306
+ "[" + indexEventObject + "]" : "") + (eventPath ? "." + eventPath : "");
307
+
308
+ // Short circuit collection * events
309
+ if (/\[\*\]/g.test(eventPath)) return this;
310
+ basecolEventPath = eventPath.replace(/\[\d+\]/g, '[*]');
311
+
312
+ cargs = [];
313
+ cargs.push.apply(cargs, args);
314
+ cargs[0] = eventType + ":" + eventPath;
315
+
316
+ // If event has been already triggered as result of same source `eventPath`,
317
+ // no need to re-trigger event to prevent cycle.
318
+ _proxyCalls = relationValue._proxyCalls = (_proxyCalls || {});
319
+ if (this._isEventAvailable.call(this, _proxyCalls, eventPath)) return this;
320
+
321
+ // Add `eventPath` in `_proxyCalls` to keep track of already triggered `event`.
322
+ _proxyCalls[eventPath] = true;
323
+
324
+ // Set up previous attributes correctly.
325
+ if ("change" === eventType) {
326
+ this._previousAttributes[relationKey] = relationValue._previousAttributes;
327
+ this.changed[relationKey] = relationValue;
328
+ }
329
+
330
+ // Bubble up event to parent `model` with new changed arguments.
331
+ this.trigger.apply(this, cargs);
332
+
333
+ //Only fire for change. Not change:attribute
334
+ if ("change" === eventType && this.get(eventPath) != args[2]) {
335
+ this.trigger.apply(this, ["nested-change", eventPath, args[1]]);
336
+ }
337
+
338
+ // Remove `eventPath` from `_proxyCalls`,
339
+ // if `eventPath` and `_proxyCalls` are available,
340
+ // which allow event to be triggered on for next operation of `set`.
341
+ if (_proxyCalls && eventPath) delete _proxyCalls[eventPath];
342
+
343
+ // Create a collection modified event with wild-card
344
+ if (eventPath !== basecolEventPath) {
345
+ cargs[0] = eventType + ":" + basecolEventPath;
346
+ this.trigger.apply(this, cargs);
347
+ }
348
+
349
+ return this;
350
+ },
351
+
352
+ // Has event been fired from this source. Used to prevent event recursion in cyclic graphs
353
+ _isEventAvailable:function (_proxyCalls, path) {
354
+ return _.find(_proxyCalls, function (value, eventKey) {
355
+ return path.indexOf(eventKey, path.length - eventKey.length) !== -1;
356
+ });
357
+ },
358
+
359
+ // Returns New `collection` of type `relation.relatedModel`.
360
+ _createCollection:function (type) {
361
+ var collection, relatedModel = type;
362
+ _.isString(relatedModel) && (relatedModel = map2Scope(relatedModel));
363
+ // Creates new `Backbone.Collection` and defines model class.
364
+ if (relatedModel && relatedModel.prototype instanceof AssociatedModel) {
365
+ collection = new BackboneCollection();
366
+ collection.model = relatedModel;
367
+ } else {
368
+ throw new Error('type must inherit from Backbone.AssociatedModel');
369
+ }
370
+ return collection;
371
+ },
372
+
373
+ // Process all pending events after the entire object graph has been updated
374
+ _processPendingEvents:function () {
375
+ if (!this.visited) {
376
+ this.visited = true;
377
+
378
+ this._deferEvents = false;
379
+
380
+ // Trigger all pending events
381
+ _.each(this._pendingEvents, function (e) {
382
+ e.c.trigger.apply(e.c, e.a);
383
+ });
384
+
385
+ this._pendingEvents = [];
386
+
387
+ // Traverse down the object graph and call process pending events on sub-trees
388
+ _.each(this.relations, function (relation) {
389
+ var val = this.attributes[relation.key];
390
+ val && val._processPendingEvents();
391
+ }, this);
392
+
393
+ delete this.visited;
394
+ }
395
+ },
396
+
397
+ // Override trigger to defer events in the object graph.
398
+ trigger:function (name) {
399
+ // Defer event processing
400
+ if (this._deferEvents) {
401
+ this._pendingEvents = this._pendingEvents || [];
402
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
403
+ this._pendingEvents.push({c:this, a:arguments});
404
+ } else {
405
+ ModelProto.trigger.apply(this, arguments);
406
+ }
407
+ },
408
+
409
+ // The JSON representation of the model.
410
+ toJSON:function (options) {
411
+ var json, aJson;
412
+ if (!this.visited) {
413
+ this.visited = true;
414
+ // Get json representation from `BackboneModel.toJSON`.
415
+ json = ModelProto.toJSON.apply(this, arguments);
416
+ // If `this.relations` is defined, iterate through each `relation`
417
+ // and added it's json representation to parents' json representation.
418
+ if (this.relations) {
419
+ _.each(this.relations, function (relation) {
420
+ var attr = this.attributes[relation.key];
421
+ if (attr) {
422
+ aJson = attr.toJSON(options);
423
+ json[relation.key] = _.isArray(aJson) ? _.compact(aJson) : aJson;
424
+ }
425
+ }, this);
426
+ }
427
+ delete this.visited;
428
+ }
429
+ return json;
430
+ },
431
+
432
+ // Create a new model with identical attributes to this one.
433
+ clone:function () {
434
+ return new this.constructor(this.toJSON());
435
+ },
436
+
437
+ // Call this if you want to set an `AssociatedModel` to a falsy value like undefined/null directly.
438
+ // Not calling this will leak memory and have wrong parents.
439
+ // See test case "parent relations"
440
+ cleanup:function () {
441
+ _.each(this.relations, function (relation) {
442
+ var val = this.attributes[relation.key];
443
+ val && (val.parents = _.difference(val.parents, [this]));
444
+ }, this);
445
+ this.off();
446
+ },
447
+
448
+ // Navigate the path to the leaf object in the path to query for the attribute value
449
+ _getAttr:function (path) {
450
+
451
+ var result = this,
452
+ //Tokenize the path
453
+ attrs = getPathArray(path),
454
+ key,
455
+ i;
456
+ if (_.size(attrs) < 1) return;
457
+ for (i = 0; i < attrs.length; i++) {
458
+ key = attrs[i];
459
+ if (!result) break;
460
+ //Navigate the path to get to the result
461
+ result = result instanceof BackboneCollection
462
+ ? (isNaN(key) ? undefined : result.at(key))
463
+ : result.attributes[key];
464
+ }
465
+ return result;
466
+ }
467
+ });
468
+
469
+ var delimiters = /[^\.\[\]]+/g;
470
+
471
+ // Tokenize the fully qualified event path
472
+ var getPathArray = function (path) {
473
+ if (path === '') return [''];
474
+ return _.isString(path) ? (path.match(delimiters)) : path || [];
475
+ };
476
+
477
+ var map2Scope = function (path) {
478
+ return _.reduce(path.split('.'), function (memo, elem) {
479
+ return memo[elem]
480
+ }, root);
481
+ };
482
+
483
+ //Infer the relation from the collection's parents and find the appropriate map for the passed in `models`
484
+ var map2models = function (parents, target, models) {
485
+ var relation;
486
+ //Iterate over collection's parents
487
+ _.find(parents, function (parent) {
488
+ //Iterate over relations
489
+ relation = _.find(parent.relations, function (rel) {
490
+ return parent.get(rel.key) === target;
491
+ }, this);
492
+ if (relation) return true;//break;
493
+ }, this);
494
+
495
+ //If we found a relation and it has a mapping function
496
+ if (relation && relation.map) {
497
+ return relation.map(models)
498
+ }
499
+ return models;
500
+ };
501
+
502
+ var proxies = {};
503
+ // Proxy Backbone collection methods
504
+ _.each(['set', 'remove', 'reset'], function (method) {
505
+ proxies[method] = BackboneCollection.prototype[method];
506
+
507
+ CollectionProto[method] = function (models, options) {
508
+ //Short-circuit if this collection doesn't hold `AssociatedModels`
509
+ if (this.model.prototype instanceof AssociatedModel && this.parents) {
510
+ //Find a map function if available and perform a transformation
511
+ arguments[0] = map2models(this.parents, this, models);
512
+ }
513
+ return proxies[method].apply(this, arguments);
514
+ }
515
+ });
516
+
517
+ // Override trigger to defer events in the object graph.
518
+ proxies['trigger'] = CollectionProto['trigger'];
519
+ CollectionProto['trigger'] = function (name) {
520
+ if (this._deferEvents) {
521
+ this._pendingEvents = this._pendingEvents || [];
522
+ // Maintain a queue of pending events to trigger after the entire object graph is updated.
523
+ this._pendingEvents.push({c:this, a:arguments});
524
+ } else {
525
+ proxies['trigger'].apply(this, arguments);
526
+ }
527
+ };
528
+
529
+ // Attach process pending event functionality on collections as well. Re-use from `AssociatedModel`
530
+ CollectionProto._processPendingEvents = AssociatedModel.prototype._processPendingEvents;
531
+
532
+
533
+ }).call(this);
@@ -0,0 +1 @@
1
+ //= require js_stack/backbone.associations/backbone.associations-0.5.1