sudojs-rails 0.1.7

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.
@@ -0,0 +1,778 @@
1
+ (function(window) {
2
+ // #Sudo Namespace
3
+ var sudo = {
4
+ // Namespace for `Delegate` Class Objects used to delegate functionality
5
+ // from a `delegator`
6
+ //
7
+ // `namespace`
8
+ delegates: {},
9
+ // The sudo.ext namespace holds the `extensions` which are stand alone `modules` that
10
+ // can be `implemented` (mixed-in) to sudo Class Objects
11
+ //
12
+ // `namespace`
13
+ ext: {},
14
+ // ###_inherit_
15
+ // Inherit the prototype from a parent to a child.
16
+ // Set the childs constructor for subclasses of child.
17
+ // A _private_ method as subclasses of the library base classes will not
18
+ // want to use this function in *most* use-cases. Why? User Sudo Class Objects
19
+ // possess their own constructors and any call back to a `superclass` constructor
20
+ // will generally be looking for the library Object's constructor.
21
+ //
22
+ // `param` {function} `parent`
23
+ // `param` {function} `child`
24
+ //
25
+ // `private`
26
+ _inherit_: function _inherit_(parent, child) {
27
+ child.prototype = Object.create(parent.prototype);
28
+ child.prototype.constructor = child;
29
+ },
30
+ // ###makeMeASandwich
31
+ // Notice there is no need to extrinsically instruct *how* to
32
+ // make the sandwich, just the elegant single command.
33
+ //
34
+ // `returns` {string}
35
+ makeMeASandwich: function makeMeASandwich() {return 'Okay.';},
36
+ // ###namespace
37
+ // Convenience method for assuring a Namespace is defined. Uses
38
+ // the optional `obj` arg with the Base objects `setPath` method.
39
+ //
40
+ // `param` {string} `path`. The path that leads to a blank Object.
41
+ namespace: function namespace(path) {
42
+ if (!sudo.Base.prototype.getPath.call(this, path, window)) {
43
+ sudo.Base.prototype.setPath.call(this, path, {}, window);
44
+ }
45
+ },
46
+ // ###premier
47
+ // The premier object takes precedence over all others so define it at the topmost level.
48
+ //
49
+ // `type` {Object}
50
+ premier: null,
51
+ // ####_uid_
52
+ // Some sudo Objects use a unique integer as a `tag` for identification.
53
+ // (Views for example). This ensures they are indeed unique.
54
+ //
55
+ // `private`
56
+ _uid_: 0,
57
+ // ####_unique_
58
+ // An integer used as 'tags' by some sudo Objects as well
59
+ // as a unique string for views when needed
60
+ //
61
+ // `param` {string} prefix. Optional string identifier
62
+ //
63
+ // `private`
64
+ _unique_: function _unique_(prefix) {
65
+ return prefix ? prefix + this._uid_++ : this._uid_++;
66
+ }
67
+ };
68
+ // ##Base Class Object
69
+ //
70
+ // All sudo.js objects inherit base
71
+ //
72
+ // `param` {Object} data. An optional data object for this instance.
73
+ //
74
+ // `constructor`
75
+ sudo.Base = function(data) {
76
+ this._data_ = data || {};
77
+ // can delegate
78
+ this._delegates_ = [];
79
+ // may implement `observable`
80
+ this._callbacks_ = [];
81
+ this._changeRecords_ = [];
82
+ // a beautiful and unique snowflake
83
+ this._uid_ = sudo._unique_();
84
+ };
85
+ // ###addDelegate
86
+ // Push an instance of a Class Object into this object's `_delegates_` list.
87
+ //
88
+ // `param` {Object} an instance of a sudo.delegates Class Object
89
+ // `returns` {Object} `this`
90
+ sudo.Base.prototype.addDelegate = function addDelegate(del) {
91
+ del._delegator_ = this;
92
+ this._delegates_.push(del);
93
+ return this;
94
+ };
95
+ // ###base
96
+ // Lookup the function matching the name passed in and call it with
97
+ // any passed in argumets scoped to the calling object.
98
+ // This method will avoid the recursive-loop problem by making sure
99
+ // that the first match is not the function that called `base`.
100
+ //
101
+ // `params` {*} any other number of arguments to be passed to the looked up method
102
+ sudo.Base.prototype.base = function base() {
103
+ var args = Array.prototype.slice.call(arguments),
104
+ name = args.shift(),
105
+ found = false,
106
+ obj = this,
107
+ curr;
108
+ // find method on the prototype, excluding the caller
109
+ while(!found) {
110
+ curr = Object.getPrototypeOf(obj);
111
+ if(curr[name] && curr[name] !== this[name]) found = true;
112
+ // keep digging
113
+ else obj = curr;
114
+ }
115
+ return curr[name].apply(this, args);
116
+ };
117
+ // ###construct
118
+ // A convenience method that alleviates the need to place:
119
+ // `Object.getPrototypeOf(this).consturctor.apply(this, arguments)`
120
+ // in every constructor
121
+ sudo.Base.prototype.construct = function construct() {
122
+ Object.getPrototypeOf(this).constructor.apply(this, arguments || []);
123
+ };
124
+ // ###delegate
125
+ // From this object's list of delegates find the object whose `_role_` matches
126
+ // the passed `name` and:
127
+ // 1. if `meth` is falsy return the delegate.
128
+ // 2 if `meth` is truthy bind its method (to the delegate) and return the method
129
+ //
130
+ // `param` {String} `role` The _role_ property to match in this object's delegates list
131
+ // `param` {String} `meth` Optional method to bind to the action this delegate is being used for
132
+ // `returns`
133
+ sudo.Base.prototype.delegate = function delegate(role, meth) {
134
+ var del = this._delegates_, i;
135
+ for(i = 0; i < del.length; i++) {
136
+ if(del[i]._role_ === role) {
137
+ if(!meth) return del[i];
138
+ return del[i][meth].bind(del[i]);
139
+ }
140
+ }
141
+ };
142
+ // ###get
143
+ // Returns the value associated with a key in this object's data store.
144
+ //
145
+ // `param` {String} `key`. The name of the key
146
+ // `returns` {*}. The value associated with the key or false if not found.
147
+ sudo.Base.prototype.get = function get(key) {
148
+ return this._data_[key];
149
+ };
150
+ // ###getDelegate
151
+ // Fetch a delegate whose _role_ property matches the passed in argument.
152
+ // Uses the `delegate` method in its 'single argument' form, included for
153
+ // API consistency
154
+ //
155
+ // `param` {String} `role`
156
+ // 'returns' {Object|undefined}
157
+ sudo.Base.prototype.getDelegate = function getDelegate(role) {
158
+ return this.delegate(role);
159
+ };
160
+ // ###getPath
161
+ // Extract a value located at `path` relative to this objects data store
162
+ //
163
+ // `param` {String} `path`. The key in the form of a dot-delimited path.
164
+ // `param` {boolean} `obj`. Optional override, forcing getPath to operate on the passed in `obj`
165
+ // argument rather than the default data store owned by `this` Object.
166
+ // `returns` {*|undefined}. The value at keypath or undefined if not found.
167
+ sudo.Base.prototype.getPath = function getPath(path, obj) {
168
+ var key, curr = obj || this._data_, p;
169
+ p = path.split('.');
170
+ for (key; p.length && (key = p.shift());) {
171
+ if(!p.length) {
172
+ return curr[key];
173
+ } else {
174
+ curr = curr[key] || {};
175
+ }
176
+ }
177
+ return curr;
178
+ };
179
+ // ###gets
180
+ // Assembles and returns an object of key:value pairs for each key
181
+ // contained in the passed in Array.
182
+ //
183
+ // `param` {array} `ary`. An array of keys.
184
+ // `returns` {object}
185
+ sudo.Base.prototype.gets = function gets(ary) {
186
+ var i, obj = {};
187
+ for (i = 0; i < ary.length; i++) {
188
+ obj[ary[i]] = ary[i].indexOf('.') === -1 ? this._data_[ary[i]] :
189
+ this.getPath(ary[i]);
190
+ }
191
+ return obj;
192
+ };
193
+ // ###removeDelegate
194
+ // From this objects `delegates` list remove the object (there should only ever be 1)
195
+ // whose _role_ matches the passed in argument
196
+ //
197
+ // `param` {String} `role`
198
+ // `returns` {Object} `this`
199
+ sudo.Base.prototype.removeDelegate = function removeDelegate(role) {
200
+ var del = this._delegates_, i;
201
+ for(i = 0; i < del.length; i++) {
202
+ if(del[i]._role_ === role) {
203
+ // no _delegator_ for you
204
+ del[i]._delegator_ = void 0;
205
+ del.splice(i, 1);
206
+ return this;
207
+ }
208
+ }
209
+ return this;
210
+ };
211
+ // `private`
212
+ sudo.Base.prototype._role_ = 'base';
213
+ // ###set
214
+ // Set a key:value pair in `this` object's data store.
215
+ //
216
+ // `param` {String} `key`. The name of the key.
217
+ // `param` {*} `value`. The value associated with the key.
218
+ // `returns` {Object} `this`
219
+ sudo.Base.prototype.set = function set(key, value) {
220
+ // _NOTE: intentional possibilty of setting a falsy value_
221
+ this._data_[key] = value;
222
+ return this;
223
+ };
224
+ // ###setPath
225
+ // Traverse the keypath and get each object
226
+ // (or make blank ones) eventually setting the value
227
+ // at the end of the path
228
+ //
229
+ // `param` {string} `path`. The path to traverse when setting a value.
230
+ // `param` {*} `value`. What to set.
231
+ // `param` {Object} `obj`. Optional flag to force setPath to operate on the passed object.
232
+ // `returns` {Object} `this`
233
+ sudo.Base.prototype.setPath = function setPath(path, value, obj) {
234
+ var curr = obj || this._data_, p = path.split('.'), key;
235
+ for (key; p.length && (key = p.shift());) {
236
+ if(!p.length) {
237
+ curr[key] = value;
238
+ } else if (curr[key]) {
239
+ curr = curr[key];
240
+ } else {
241
+ curr = curr[key] = {};
242
+ }
243
+ }
244
+ return this;
245
+ };
246
+ // ###sets
247
+ // Invokes `set()` or `setPath()` for each key value pair in `obj`.
248
+ // Any listeners for those keys or paths will be called.
249
+ //
250
+ // `param` {Object} `obj`. The keys and values to set.
251
+ // `returns` {Object} `this`
252
+ sudo.Base.prototype.sets = function sets(obj) {
253
+ var i, k = Object.keys(obj);
254
+ for(i = 0; i < k.length; i++) {
255
+ k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]]) :
256
+ this.setPath(k[i], obj[k[i]]);
257
+ }
258
+ return this;
259
+ };
260
+ // ###unset
261
+ // Remove a key:value pair from this object's data store
262
+ //
263
+ // `param` {String} key
264
+ // `returns` {Object} `this`
265
+ sudo.Base.prototype.unset = function unset(key) {
266
+ delete this._data_[key];
267
+ return this;
268
+ };
269
+ // ###unsetPath
270
+ // Remove a key:value pair from this object's data store
271
+ // located at <path>
272
+ //
273
+ // `param` {String} path
274
+ // `returns` {Object} `this`
275
+ sudo.Base.prototype.unsetPath = function unsetPath(path) {
276
+ var curr = this._data_, p = path.split('.'), key;
277
+ for (key; p.length && (key = p.shift());) {
278
+ if(!p.length) {
279
+ delete curr[key];
280
+ } else {
281
+ // this can fail if a faulty path is passed.
282
+ // using getPath beforehand can prevent that
283
+ curr = curr[key];
284
+ }
285
+ }
286
+ return this;
287
+ };
288
+ // ###unsets
289
+ // Deletes a number of keys or paths from this object's data store
290
+ //
291
+ // `param` {array} `ary`. An array of keys or paths.
292
+ // `returns` {Objaect} `this`
293
+ sudo.Base.prototype.unsets = function unsets(ary) {
294
+ var i;
295
+ for(i = 0; i < ary.length; i++) {
296
+ ary[i].indexOf('.') === -1 ? this.unset(ary[i]) :
297
+ this.unsetPath(ary[i]);
298
+ }
299
+ return this;
300
+ };
301
+ // ##Container Class Object
302
+ //
303
+ // A container is any object that can both contain other objects and
304
+ // itself be contained
305
+ //
306
+ // `constructor`
307
+ sudo.Container = function(data) {
308
+ sudo.Base.call(this, data);
309
+ this._children_ = [];
310
+ this._childNames_ = {};
311
+ };
312
+ // `private`
313
+ sudo._inherit_(sudo.Base, sudo.Container);
314
+ // ###addChild
315
+ // Adds a View to this container's list of children.
316
+ // Also adds an 'index' property and an entry in the childNames hash.
317
+ // If `addedToParent` if found on the child, call it, sending `this` as an argument.
318
+ //
319
+ // `param` {Object} `child`. View (or View subclass) instance.
320
+ // `param` {String} `name`. An optional name for the child that will go in the childNames hash.
321
+ // `returns` {Object} `this`
322
+ sudo.Container.prototype.addChild = function addChild(child, name) {
323
+ var c = this._children_;
324
+ child._parent_ = this;
325
+ child._index_ = c.length;
326
+ if(name) {
327
+ child._name_ = name;
328
+ this._childNames_[name] = child._index_;
329
+ }
330
+ c.push(child);
331
+ if('addedToParent' in child) child.addedToParent(this);
332
+ return this;
333
+ };
334
+ // ###bubble
335
+ // By default, `bubble` returns the current view's parent (if it has one)
336
+ //
337
+ // `returns` {Object|undefined}
338
+ sudo.Container.prototype.bubble = function bubble() {return this._parent_;};
339
+ // ###getChild
340
+ // If a child was added with a name, via `addChild`,
341
+ // that object can be fetched by name. This prevents us from having to reference a
342
+ // containers children by index. That is possible however, though not preferred.
343
+ //
344
+ // `param` {String|Number} `id`. The string `name` or numeric `index` of the child to fetch.
345
+ // `returns` {Object|undefined} The found child
346
+ sudo.Container.prototype.getChild = function getChild(id) {
347
+ return typeof id === 'string' ? this._children_[this._childNames_[id]] :
348
+ this._children_[id];
349
+ };
350
+ // ###_indexChildren_
351
+ // Method is called with the `index` property of a subview that is being removed.
352
+ // Beginning at <i> decrement subview indices.
353
+ // `param` {Number} `i`
354
+ // `private`
355
+ sudo.Container.prototype._indexChildren_ = function _indexChildren_(i) {
356
+ var c = this._children_, obj = this._childNames_, len;
357
+ for (len = c.length; i < len; i++) {
358
+ c[i]._index_--;
359
+ // adjust any entries in childNames
360
+ if(c[i]._name_ in obj) obj[c[i]._name_] = c[i]._index_;
361
+ }
362
+ };
363
+ // ###removeChild
364
+ // Find the intended child from my list of children and remove it, removing the name reference and re-indexing
365
+ // remaining children. This method does not remove the child's DOM.
366
+ // Override this method, doing whatever you want to the child's DOM, then call `base('removeChild')` to do so.
367
+ //
368
+ // `param` {String|Number|Object} `arg`. Children will always have an `index` number, and optionally a `name`.
369
+ // If passed a string `name` is assumed, so be sure to pass an actual number if expecting to use index.
370
+ // An object will be assumed to be an actual sudo Class Object.
371
+ // `returns` {Object} `this`
372
+ sudo.Container.prototype.removeChild = function removeChild(arg) {
373
+ var t = typeof arg, c;
374
+ // passed an object (or an array - which will fail so don't do that), proceed
375
+ if(t === 'object') this._removeChild_(arg);
376
+ else {
377
+ c = t === 'string' ? this._children_[this._childNames_[arg]] : this._children_[arg];
378
+ this._removeChild_(c);
379
+ }
380
+ return this;
381
+ };
382
+ // ###_removeChild_
383
+ // Helper method for 'public' removeChild
384
+ // `param` {Object} `child`. The view that is going to be removed.
385
+ // `private`
386
+ sudo.Container.prototype._removeChild_ = function _removeChild_(child) {
387
+ var i = child._index_;
388
+ // remove from the children Array
389
+ this._children_.splice(i, 1);
390
+ // remove from the named child hash if present
391
+ delete this._childNames_[child._name_];
392
+ // child is now an `orphan`
393
+ delete child._parent_;
394
+ this._indexChildren_(i);
395
+ };
396
+ // ###removeFromParent
397
+ // Remove this object from its parents list of children.
398
+ // Does not alter the dom - do that yourself by overriding this method
399
+ // or chaining method calls
400
+ sudo.Container.prototype.removeFromParent = function removeFromParent() {
401
+ // will error without a parent, but that would be your fault...
402
+ this._parent_._removeChild_(this);
403
+ return this;
404
+ };
405
+ sudo.Container.prototype._role_ = 'container';
406
+ // ###send
407
+ // The call to the specific method on a (un)specified target happens here.
408
+ // If this Object is part of a `sudo.ext.container` maintained hierarchy
409
+ // the 'target' may be left out, causing the `bubble()` method to be called.
410
+ // What this does is allow children of a `sudo.ext.container` to simply pass
411
+ // events upward, delegating the responsibility of deciding what to do to the parent.
412
+ //
413
+ // `param` {*} Any number of arguments is supported, but the first is the only one searched for info.
414
+ // A sendMethod will be located by:
415
+ // 1. using the first argument if it is a string
416
+ // 2. looking for a `sendMethod` property if it is an object
417
+ // In the case a specified target exists at `this.get('sendTarget')` it will be used
418
+ // Any other args will be passed to the sendMethod after `this`
419
+ // `returns` {Object} `this`
420
+ sudo.Container.prototype.send = function send(/*args*/) {
421
+ var args = Array.prototype.slice.call(arguments),
422
+ meth, targ, fn;
423
+ // normalize the input, common use cases first
424
+ if('sendMethod' in this._data_) meth = this._data_.sendMethod;
425
+ else if(typeof args[0] === 'string') meth = args.shift();
426
+ // less common but viable options
427
+ if(!meth) {
428
+ // passed as a jquery custom data attr bound in events
429
+ meth = 'data' in args[0] ? args[0].data.sendMethod :
430
+ // passed in a hash from something or not passed at all
431
+ args[0].sendMethod || void 0;
432
+ }
433
+ // target is either specified or my parent
434
+ targ = this._data_['sendTarget'] || this._parent_;
435
+ // obvious chance for errors here, don't be dumb
436
+ fn = targ[meth];
437
+ while(!fn && (targ = targ.bubble())) {
438
+ fn = targ[meth];
439
+ }
440
+ // sendMethods expect a signature (sender, ...)
441
+ if(fn) {
442
+ args.unshift(this);
443
+ fn.apply(targ, args);
444
+ }
445
+ return this;
446
+ };
447
+ // ##View Class Object
448
+
449
+ // Create an instance of a sudo.View object. A view is any object
450
+ // that maintains its own `el`, that being some type of DOM element.
451
+ // Pass in a string selector or an actual dom node reference to have the object
452
+ // set that as its `el`. If no `el` is specified one will be created upon instantiation
453
+ // based on the `tagName` (`div` by default). Specify `className`, `id` (or other attributes if desired)
454
+ // as an (optional) `attributes` object literal on the `data` arg.
455
+ //
456
+ // The view object uses jquery for dom manipulation
457
+ // and event delegation etc... A jquerified `this` reference is located
458
+ // at `this.$el` and `this.$` scopes queries to this objects `el`, i.e it's
459
+ // a shortcut for `this.$el.find(selector)`
460
+ //
461
+ // `param` {string|element|jQuery} `el`. Otional el for the View instance.
462
+ // `param` {Object} `data`. Optional data object.
463
+ //
464
+ // `constructor`
465
+ sudo.View = function(el, data) {
466
+ sudo.Container.call(this, data);
467
+ this.setEl(el);
468
+ if(this._role_ === 'view') this.init();
469
+ };
470
+ // View inherits from Container
471
+ // `private`
472
+ sudo._inherit_(sudo.Container, sudo.View);
473
+ // ###becomePremier
474
+ // Premier functionality provides hooks for behavioral differentiation
475
+ // among elements or class objects.
476
+ //
477
+ // `returns` {Object} `this`
478
+ sudo.View.prototype.becomePremier = function becomePremier() {
479
+ var p, f = function() {
480
+ this._isPremier_ = true;
481
+ sudo.premier = this;
482
+ }.bind(this);
483
+ // is there an existing premier that isn't me?
484
+ if((p = sudo.premier) && p._uid_ !== this._uid_) {
485
+ // ask it to resign and call the cb
486
+ p.resignPremier(f);
487
+ } else f(); // no existing premier
488
+ return this;
489
+ };
490
+ // ###init
491
+ // A 'contruction-time' hook to call for further initialization needs in
492
+ // View objects (and their subclasses). A noop by default child classes should override.
493
+ sudo.View.prototype.init = $.noop;
494
+ // the el needs to be normalized before use
495
+ // `private`
496
+ sudo.View.prototype._normalizedEl_ = function _normalizedEl_(el) {
497
+ if(typeof el === 'string') {
498
+ return $(el);
499
+ } else {
500
+ // Passed an already `jquerified` Element?
501
+ // It will have a length of 1 if so.
502
+ return el.length ? el : $(el);
503
+ }
504
+ };
505
+ // ### resignPremier
506
+ // Resign premier status
507
+ //
508
+ // `param` {Function} `cb`. An optional callback to execute
509
+ // after resigning premier status.
510
+ // `returns` {Object} `this`
511
+ sudo.View.prototype.resignPremier = function resignPremier(cb) {
512
+ var p;
513
+ this._isPremier_ = false;
514
+ // only remove the global premier if it is me
515
+ if((p = sudo.premier) && p._uid_ === this._uid_) {
516
+ sudo.premier = null;
517
+ }
518
+ // fire the cb if passed
519
+ if(cb) cb();
520
+ return this;
521
+ };
522
+ // `private`
523
+ sudo.View.prototype._role_ = 'view';
524
+ // ###setEl
525
+ // A view must have an element, set that here.
526
+ // Stores a jquerified object as `this.$el` the raw
527
+ // node is always then available as `this.$el[0]`.
528
+ //
529
+ // `param` {string=|element} `el`
530
+ // `returns` {Object} `this`
531
+ sudo.View.prototype.setEl = function setEl(el) {
532
+ var a, t;
533
+ if(!el) {
534
+ // normalize any relevant data
535
+ t = this._data_['tagName'] || 'div';
536
+ this.$el = $(document.createElement(t));
537
+ if((a = this._data_['attributes'])) this.$el.attr(a);
538
+ } else {
539
+ this.$el = this._normalizedEl_(el);
540
+ }
541
+ return this;
542
+ };
543
+ // ###this.$
544
+ // Return a single Element matching `sel` scoped to this View's el.
545
+ // This is an alias to `this.$el.find(sel)`.
546
+ //
547
+ // `param` {string} `sel`. A jQuery compatible selector
548
+ // `returns` {jQuery} A 'jquerified' result matching the selector
549
+ sudo.View.prototype.$ = function(sel) {
550
+ return this.$el.find(sel);
551
+ };
552
+ // ## Observable Extension Object
553
+ // Implementaion of the ES6 Harmony Observer pattern
554
+ sudo.ext.observable = {
555
+ // ###_deliver_
556
+ // Called from deliverChangeRecords when ready to send
557
+ // changeRecords to observers.
558
+ //
559
+ // `private`
560
+ _deliver_: function _deliver_(obj) {
561
+ var i, cb = this._callbacks_;
562
+ for(i = 0; i < cb.length; i++) {
563
+ cb[i](obj);
564
+ }
565
+ },
566
+ // ###deliverChangeRecords
567
+ // Iterate through the changeRecords array(emptying it as you go), delivering them to the
568
+ // observers. You can override this method to change the standard delivery behavior.
569
+ //
570
+ // `returns` {Object} `this`
571
+ deliverChangeRecords: function deliverChangeRecords() {
572
+ var rec, cr = this._changeRecords_;
573
+ // FIFO
574
+ for(rec; cr.length && (rec = cr.shift());) {
575
+ this._deliver_(rec);
576
+ }
577
+ return this;
578
+ },
579
+ // ###observe
580
+ // In a quasi-ES6 Object.observe pattern, calling observe on an `observable` and
581
+ // passing a callback will cause that callback to be called whenever any
582
+ // property on the observable's data store is set, changed or deleted
583
+ // via set, unset, setPath or unsetPath with an object containing:
584
+ // {
585
+ // type: <new, updated, deleted>,
586
+ // object: <the object being observed>,
587
+ // name: <the key that was modified>,
588
+ // oldValue: <if a previous value existed for this key>
589
+ // }
590
+ // For ease of 'unobserving' the same Function passed in is returned.
591
+ //
592
+ // `param` {Function} `fn` The callback to be called with changeRecord(s)
593
+ // `returns` {Function} the Function passed in as an argument
594
+ observe: function observe(fn) {
595
+ // this will fail if mixed-in and no `callbacks` created so don't do that.
596
+ // Per the spec, do not allow the same callback to be added
597
+ var d = this._callbacks_;
598
+ if(d.indexOf(fn) === -1) d.push(fn);
599
+ return fn;
600
+ },
601
+ // ###observes
602
+ // Allow an array of callbacks to be registered as changeRecord recipients
603
+ //
604
+ // `param` {Array} ary
605
+ // `returns` {Object} `this`
606
+ observes: function observes(ary) {
607
+ var i;
608
+ for(i = 0; i < ary.length; i++) {
609
+ this.observe(ary[i]);
610
+ }
611
+ return this;
612
+ },
613
+ // ###set
614
+ // Overrides sudo.Base.set to check for observers
615
+ //
616
+ // `param` {String} `key`. The name of the key
617
+ // `param` {*} `value`
618
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
619
+ // to be delivered upon a call to deliverChangeRecords (truthy)
620
+ //
621
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
622
+ set: function set(key, value, hold) {
623
+ var obj = {name: key, object: this._data_};
624
+ // did this key exist already
625
+ if(key in this._data_) {
626
+ obj.type = 'updated';
627
+ // then there is an oldValue
628
+ obj.oldValue = this._data_[key];
629
+ } else obj.type = 'new';
630
+ // now actually set the value
631
+ this._data_[key] = value;
632
+ this._changeRecords_.push(obj);
633
+ // call the observers or not
634
+ if(hold) return this;
635
+ return this.deliverChangeRecords();
636
+ },
637
+ // ###setPath
638
+ // Overrides sudo.Base.setPath to check for observers.
639
+ // Change records originating from a `setPath` operation
640
+ // send back the passed in `path` as `name` as well as the
641
+ // top level object being observed (this observable's _data_).
642
+ // this allows for easy filtering either manually or via a
643
+ // `change delegate`
644
+ //
645
+ // `param` {String} `path`
646
+ // `param` {*} `value`
647
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
648
+ // to be delivered upon a call to deliverChangeRecords (truthy)
649
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
650
+ setPath: function setPath(path, value, hold) {
651
+ var curr = this._data_, obj = {name: path, object: this._data_},
652
+ p = path.split('.'), key;
653
+ for (key; p.length && (key = p.shift());) {
654
+ if(!p.length) {
655
+ // reached the last refinement, pre-existing?
656
+ if (key in curr) {
657
+ obj.type = 'updated';
658
+ obj.oldValue = curr[key];
659
+ } else obj.type = 'new';
660
+ curr[key] = value;
661
+ } else if (curr[key]) {
662
+ curr = curr[key];
663
+ } else {
664
+ curr = curr[key] = {};
665
+ }
666
+ }
667
+ this._changeRecords_.push(obj);
668
+ // call all observers or not
669
+ if(hold) return this;
670
+ return this.deliverChangeRecords();
671
+ },
672
+ // ###sets
673
+ // Overrides Base.sets to hold the call to _deliver_ until
674
+ // all operations are done
675
+ //
676
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
677
+ sets: function sets(obj, hold) {
678
+ var i, k = Object.keys(obj);
679
+ for(i = 0; i < k.length; i++) {
680
+ k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]], true) :
681
+ this.setPath(k[i], obj[k[i]], true);
682
+ }
683
+ if(hold) return this;
684
+ return this.deliverChangeRecords();
685
+ },
686
+ // ###unobserve
687
+ // Remove a particular callback from this observable
688
+ //
689
+ // `param` {Function} the function passed in to `observe`
690
+ // `returns` {Object} `this`
691
+ unobserve: function unobserve(fn) {
692
+ var cb = this._callbacks_, i = cb.indexOf(fn);
693
+ if(i !== -1) cb.splice(i, 1);
694
+ return this;
695
+ },
696
+ // ###unobserves
697
+ // Allow an array of callbacks to be unregistered as changeRecord recipients
698
+ //
699
+ // `param` {Array} ary
700
+ // `returns` {Object} `this`
701
+ unobserves: function unobserves(ary) {
702
+ var i;
703
+ for(i = 0; i < ary.length; i++) {
704
+ this.unobserve(ary[i]);
705
+ }
706
+ return this;
707
+ },
708
+ // ###unset
709
+ // Overrides sudo.Base.unset to check for observers
710
+ //
711
+ // `param` {String} `key`. The name of the key
712
+ // `param` {Bool} `hold`
713
+ //
714
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
715
+ unset: function unset(key, hold) {
716
+ var obj = {name: key, object: this._data_, type: 'deleted'},
717
+ val = !!this._data_[key];
718
+ delete this._data_[key];
719
+ // call the observers if there was a val to delete
720
+ return this._unset_(obj, val, hold);
721
+ },
722
+ // ###_unset_
723
+ // Helper for the unset functions
724
+ //
725
+ // `private`
726
+ _unset_: function _unset_(o, v, h) {
727
+ if(v) {
728
+ this._changeRecords_.push(o);
729
+ if(h) return this;
730
+ return this.deliverChangeRecords();
731
+ }
732
+ return this;
733
+ },
734
+ // ###setPath
735
+ // Overrides sudo.Base.unsetPath to check for observers
736
+ //
737
+ // `param` {String} `path`
738
+ // `param` {*} `value`
739
+ // `param` {bool} `hold`
740
+ //
741
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
742
+ unsetPath: function unsetPath(path, hold) {
743
+ var obj = {name: path, object: this._data_, type: 'deleted'},
744
+ curr = this._data_, p = path.split('.'),
745
+ key, val;
746
+ for (key; p.length && (key = p.shift());) {
747
+ if(!p.length) {
748
+ // reached the last refinement
749
+ val = !!curr[key];
750
+ delete curr[key];
751
+ } else {
752
+ // this can obviously fail, but can be prevented by checking
753
+ // with `getPath` first.
754
+ curr = curr[key];
755
+ }
756
+ }
757
+ return this._unset_(obj, val, hold);
758
+ },
759
+ // ###unsets
760
+ // Override of Base.unsets to hold the call to _deliver_ until done
761
+ //
762
+ // `param` ary
763
+ // `param` hold
764
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
765
+ unsets: function unsets(ary, hold) {
766
+ var i;
767
+ for(i = 0; i < ary.length; i++) {
768
+ ary[i].indexOf('.') === -1 ? this.unset(k[i], true) :
769
+ this.unsetPath(k[i], true);
770
+ }
771
+ if(hold) return this;
772
+ return this.deliverChangeRecords();
773
+ }
774
+ };
775
+ sudo.version = "0.8.0";
776
+ window.sudo = sudo;
777
+ if(typeof window._ === "undefined") window._ = sudo;
778
+ }).call(this, this);