js_stack 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -12
- data/README.md +35 -208
- data/lib/js_stack/version.rb +1 -1
- data/vendor/assets/javascripts/js_stack/base/backbone/1.1.1.js +1609 -0
- data/vendor/assets/javascripts/js_stack/base/backbone.js +1 -1
- data/vendor/assets/javascripts/js_stack/base/underscore/1.6.0.js +1344 -0
- data/vendor/assets/javascripts/js_stack/base/underscore.js +1 -1
- data/vendor/assets/javascripts/js_stack/plugins/backbone/associations/0.6.1.js +722 -0
- data/vendor/assets/javascripts/js_stack/plugins/backbone/mutators/0.4.1.js +207 -0
- data/vendor/assets/javascripts/js_stack/plugins/backbone.associations.js +1 -1
- data/vendor/assets/javascripts/js_stack/plugins/backbone.mutators.js +1 -0
- data/vendor/assets/javascripts/js_stack/plugins/cocktail/0.5.3.js +101 -0
- data/vendor/assets/javascripts/js_stack/plugins/cocktail.js +1 -0
- metadata +9 -2
@@ -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
|
+
}));
|