sudo-js-rails 0.0.2

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,1580 @@
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.extensions namespace holds the objects that are stand alone `modules` which
10
+ // can be `implemented` (mixed-in) in sudo Class Objects
11
+ //
12
+ // `namespace`
13
+ extensions: {},
14
+ // ###getPath
15
+ // Extract a value located at `path` relative to the passed in object
16
+ //
17
+ // `param` {String} `path`. The key in the form of a dot-delimited path.
18
+ // `param` {object} `obj`. An object literal to operate on.
19
+ //
20
+ // `returns` {*|undefined}. The value at keypath or undefined if not found.
21
+ getPath: function getPath(path, obj) {
22
+ var key, p;
23
+ p = path.split('.');
24
+ for (key; p.length && (key = p.shift());) {
25
+ if(!p.length) {
26
+ return obj[key];
27
+ } else {
28
+ obj = obj[key] || {};
29
+ }
30
+ }
31
+ return obj;
32
+ },
33
+ // ###inherit
34
+ // Inherit the prototype from a parent to a child.
35
+ // Set the childs constructor for subclasses of child.
36
+ // Subclasses of the library base classes will not
37
+ // want to use this function in *most* use-cases. Why? User Sudo Class Objects
38
+ // possess their own constructors and any call back to a `superclass` constructor
39
+ // will generally be looking for the library Object's constructor.
40
+ //
41
+ // `param` {function} `parent`
42
+ // `param` {function} `child`
43
+ inherit: function inherit(parent, child) {
44
+ child.prototype = Object.create(parent.prototype);
45
+ child.prototype.constructor = child;
46
+ },
47
+ // ###makeMeASandwich
48
+ // Notice there is no need to extrinsically instruct *how* to
49
+ // make the sandwich, just the elegant single command.
50
+ //
51
+ // `returns` {string}
52
+ makeMeASandwich: function makeMeASandwich() {return 'Okay.';},
53
+ // ###namespace
54
+ // Method for assuring a Namespace is defined.
55
+ //
56
+ // `param` {string} `path`. The path that leads to a blank Object.
57
+ namespace: function namespace(path) {
58
+ if (!this.getPath(path, window)) {
59
+ this.setPath(path, {}, window);
60
+ }
61
+ },
62
+ // ###premier
63
+ // The premier object takes precedence over all others so define it at the topmost level.
64
+ //
65
+ // `type` {Object}
66
+ premier: null,
67
+ // ###setPath
68
+ // Traverse the keypath and get each object
69
+ // (or make blank ones) eventually setting the value
70
+ // at the end of the path
71
+ //
72
+ // `param` {string} `path`. The path to traverse when setting a value.
73
+ // `param` {*} `value`. What to set.
74
+ // `param` {Object} `obj`. The object literal to operate on.
75
+ setPath: function setPath(path, value, obj) {
76
+ var p = path.split('.'), key;
77
+ for (key; p.length && (key = p.shift());) {
78
+ if(!p.length) {
79
+ obj[key] = value;
80
+ } else if (obj[key]) {
81
+ obj = obj[key];
82
+ } else {
83
+ obj = obj[key] = {};
84
+ }
85
+ }
86
+ },
87
+ // ####uid
88
+ // Some sudo Objects use a unique integer as a `tag` for identification.
89
+ // (Views for example). This ensures they are indeed unique.
90
+ uid: 0,
91
+ // ####unique
92
+ // An integer used as 'tags' by some sudo Objects as well
93
+ // as a unique string for views when needed
94
+ //
95
+ // `param` {string} prefix. Optional string identifier
96
+ unique: function unique(prefix) {
97
+ return prefix ? prefix + this.uid++ : this.uid++;
98
+ },
99
+ // ###unsetPath
100
+ // Remove a key:value pair from this object's data store
101
+ // located at <path>
102
+ //
103
+ // `param` {String} `path`
104
+ // `param` {Object} `obj` The object to operate on.
105
+ unsetPath: function unsetPath(path, obj) {
106
+ var p = path.split('.'), key;
107
+ for (key; p.length && (key = p.shift());) {
108
+ if(!p.length) {
109
+ delete obj[key];
110
+ } else {
111
+ // this can fail if a faulty path is passed.
112
+ // using getPath beforehand can prevent that
113
+ obj = obj[key];
114
+ }
115
+ }
116
+ }
117
+ };
118
+ // ##Base Class Object
119
+ //
120
+ // All sudo.js objects inherit base, giving the ability
121
+ // to utilize delegation, the `base` function and the
122
+ // `construct` convenience method.
123
+ //
124
+ // `constructor`
125
+ sudo.Base = function() {
126
+ // can delegate
127
+ this.delegates = [];
128
+ // a beautiful and unique snowflake
129
+ this.uid = sudo.unique();
130
+ };
131
+ // ###addDelegate
132
+ // Push an instance of a Class Object into this object's `_delegates_` list.
133
+ //
134
+ // `param` {Object} an instance of a sudo.delegates Class Object
135
+ // `returns` {Object} `this`
136
+ sudo.Base.prototype.addDelegate = function addDelegate(del) {
137
+ del.delegator = this;
138
+ this.delegates.push(del);
139
+ if('addedAsDelegate' in del) del.addedAsDelegate(this);
140
+ return this;
141
+ };
142
+ // ###base
143
+ // Lookup the function matching the name passed in and call it with
144
+ // any passed in argumets scoped to the calling object.
145
+ // This method will avoid the recursive-loop problem by making sure
146
+ // that the first match is not the function that called `base`.
147
+ //
148
+ // `params` {*} any other number of arguments to be passed to the looked up method
149
+ // along with the initial method name
150
+ sudo.Base.prototype.base = function base() {
151
+ var args = Array.prototype.slice.call(arguments),
152
+ name = args.shift(),
153
+ found = false,
154
+ obj = this,
155
+ curr;
156
+ // find method on the prototype, excluding the caller
157
+ while(!found) {
158
+ curr = Object.getPrototypeOf(obj);
159
+ if(curr[name] && curr[name] !== this[name]) found = true;
160
+ // keep digging
161
+ else obj = curr;
162
+ }
163
+ return curr[name].apply(this, args);
164
+ };
165
+ // ###construct
166
+ // A convenience method that alleviates the need to place:
167
+ // `Object.getPrototypeOf(this).consturctor.apply(this, arguments)`
168
+ // in every constructor
169
+ sudo.Base.prototype.construct = function construct() {
170
+ Object.getPrototypeOf(this).constructor.apply(this, arguments || []);
171
+ };
172
+ // ###delegate
173
+ // From this object's list of delegates find the object whose `_role_` matches
174
+ // the passed `name` and:
175
+ // 1. if `meth` is falsy return the delegate.
176
+ // 2 if `meth` is truthy bind its method (to the delegate) and return the method
177
+ //
178
+ // `param` {String} `role` The role property to match in this object's delegates list
179
+ // `param` {String} `meth` Optional method to bind to the action this delegate is being used for
180
+ // `returns`
181
+ sudo.Base.prototype.delegate = function delegate(role, meth) {
182
+ var del = this.delegates, i;
183
+ for(i = 0; i < del.length; i++) {
184
+ if(del[i].role === role) {
185
+ if(!meth) return del[i];
186
+ return del[i][meth].bind(del[i]);
187
+ }
188
+ }
189
+ };
190
+ // ###getDelegate
191
+ // Fetch a delegate whose role property matches the passed in argument.
192
+ // Uses the `delegate` method in its 'single argument' form, included for
193
+ // API consistency
194
+ //
195
+ // `param` {String} `role`
196
+ // 'returns' {Object|undefined}
197
+ sudo.Base.prototype.getDelegate = function getDelegate(role) {
198
+ return this.delegate(role);
199
+ };
200
+ // ###removeDelegate
201
+ // From this objects `delegates` list remove the object (there should only ever be 1)
202
+ // whose role matches the passed in argument
203
+ //
204
+ // `param` {String} `role`
205
+ // `returns` {Object} `this`
206
+ sudo.Base.prototype.removeDelegate = function removeDelegate(role) {
207
+ var del = this.delegates, i;
208
+ for(i = 0; i < del.length; i++) {
209
+ if(del[i].role === role) {
210
+ // no _delegator_ for you
211
+ del[i].delegator = void 0;
212
+ del.splice(i, 1);
213
+ return this;
214
+ }
215
+ }
216
+ return this;
217
+ };
218
+ // `private`
219
+ sudo.Base.prototype.role = 'base';
220
+ // ##Model Class Object
221
+ //
222
+ // Model Objects expose methods for setting and getting data, and
223
+ // can be observed if implementing the `Observable Extension`
224
+ //
225
+ // `param` {object} data. An initial state for this model.
226
+ //
227
+ // `constructor`
228
+ sudo.Model = function(data) {
229
+ sudo.Base.call(this);
230
+ this.data = data || {};
231
+ // only models are `observable`
232
+ this.callbacks = [];
233
+ this.changeRecords = [];
234
+ };
235
+ // Model inherits from sudo.Base
236
+ // `private`
237
+ sudo.inherit(sudo.Base, sudo.Model);
238
+ // ###get
239
+ // Returns the value associated with a key.
240
+ //
241
+ // `param` {String} `key`. The name of the key
242
+ // `returns` {*}. The value associated with the key or false if not found.
243
+ sudo.Model.prototype.get = function get(key) {
244
+ return this.data[key];
245
+ };
246
+ // ###getPath
247
+ //
248
+ // Uses the sudo namespace's getpath function operating on the model's
249
+ // data hash.
250
+ //
251
+ // `returns` {*|undefined}. The value at keypath or undefined if not found.
252
+ sudo.Model.prototype.getPath = function getPath(path) {
253
+ return sudo.getPath(path, this.data);
254
+ };
255
+ // ###gets
256
+ // Assembles and returns an object of key:value pairs for each key
257
+ // contained in the passed in Array.
258
+ //
259
+ // `param` {array} `ary`. An array of keys.
260
+ // `returns` {object}
261
+ sudo.Model.prototype.gets = function gets(ary) {
262
+ var i, obj = {};
263
+ for (i = 0; i < ary.length; i++) {
264
+ obj[ary[i]] = ary[i].indexOf('.') === -1 ? this.data[ary[i]] :
265
+ this.getPath(ary[i]);
266
+ }
267
+ return obj;
268
+ };
269
+ // `private`
270
+ sudo.Model.prototype.role = 'model';
271
+ // ###set
272
+ // Set a key:value pair.
273
+ //
274
+ // `param` {String} `key`. The name of the key.
275
+ // `param` {*} `value`. The value associated with the key.
276
+ // `returns` {Object} `this`
277
+ sudo.Model.prototype.set = function set(key, value) {
278
+ // _NOTE: intentional possibilty of setting a falsy value_
279
+ this.data[key] = value;
280
+ return this;
281
+ };
282
+ // ###setPath
283
+ //
284
+ // Uses the sudo namespace's setpath function operating on the model's
285
+ // data hash.
286
+ //
287
+ // `param` {String} `path`
288
+ // `param` {*} `value`
289
+ // `returns` {Object} this.
290
+ sudo.Model.prototype.setPath = function setPath(path, value) {
291
+ sudo.setPath(path, value, this.data);
292
+ return this;
293
+ };
294
+ // ###sets
295
+ // Invokes `set()` or `setPath()` for each key value pair in `obj`.
296
+ // Any listeners for those keys or paths will be called.
297
+ //
298
+ // `param` {Object} `obj`. The keys and values to set.
299
+ // `returns` {Object} `this`
300
+ sudo.Model.prototype.sets = function sets(obj) {
301
+ var i, k = Object.keys(obj);
302
+ for(i = 0; i < k.length; i++) {
303
+ k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]]) :
304
+ this.setPath(k[i], obj[k[i]]);
305
+ }
306
+ return this;
307
+ };
308
+ // ###unset
309
+ // Remove a key:value pair from this object's data store
310
+ //
311
+ // `param` {String} key
312
+ // `returns` {Object} `this`
313
+ sudo.Model.prototype.unset = function unset(key) {
314
+ delete this.data[key];
315
+ return this;
316
+ };
317
+ // ###unsetPath
318
+ // Uses `sudo.unsetPath` operating on this models data hash
319
+ //
320
+ // `param` {String} path
321
+ // `returns` {Object} `this`
322
+ sudo.Model.prototype.unsetPath = function unsetPath(path) {
323
+ sudo.unsetPath(path, this.data);
324
+ return this;
325
+ };
326
+ // ###unsets
327
+ // Deletes a number of keys or paths from this object's data store
328
+ //
329
+ // `param` {array} `ary`. An array of keys or paths.
330
+ // `returns` {Objaect} `this`
331
+ sudo.Model.prototype.unsets = function unsets(ary) {
332
+ var i;
333
+ for(i = 0; i < ary.length; i++) {
334
+ ary[i].indexOf('.') === -1 ? this.unset(ary[i]) :
335
+ this.unsetPath(ary[i]);
336
+ }
337
+ return this;
338
+ };
339
+ // ##Container Class Object
340
+ //
341
+ // A container is any object that can both contain other objects and
342
+ // itself be contained.
343
+ //
344
+ // `param` {Array|Object} 'arg'. Optional array or hash
345
+ // of child objects which the Container will add as child objects
346
+ // via `addChildren`
347
+ //
348
+ // `constructor`
349
+ sudo.Container = function(arg) {
350
+ sudo.Base.call(this);
351
+ this.children = [];
352
+ this.childNames = {};
353
+ if(arg) this.addChildren(arg);
354
+ };
355
+ // Container is a subclass of sudo.Base
356
+ sudo.inherit(sudo.Base, sudo.Container);
357
+ // ###addChild
358
+ // Adds a View to this container's list of children.
359
+ // Also adds an 'index' property and an entry in the childNames hash.
360
+ // If `addedToParent` if found on the child, call it, sending `this` as an argument.
361
+ //
362
+ // `param` {Object} `child`. View (or View subclass) instance.
363
+ // `param` {String} `name`. An optional name for the child that will go in the childNames hash.
364
+ // `returns` {Object} `this`
365
+ sudo.Container.prototype.addChild = function addChild(child, name) {
366
+ var c = this.children;
367
+ child.parent = this;
368
+ child.index = c.length;
369
+ if(name) {
370
+ child.name = name;
371
+ this.childNames[name] = child.index;
372
+ }
373
+ c.push(child);
374
+ if('addedToParent' in child) child.addedToParent(this);
375
+ return this;
376
+ };
377
+ // ###addChildren
378
+ // Allows for multiple children to be added to this Container by passing
379
+ // either an Array or an Object literal.
380
+ //
381
+ // see `addChild`
382
+ //
383
+ // `param` {Array|Object} `arg`. An array of children to add or an
384
+ // Object literal in the form {name: child}
385
+ // `returns` {Object} `this`
386
+ sudo.Container.prototype.addChildren = function addChildren(arg) {
387
+ var i, keys;
388
+ // Array?
389
+ if(Array.isArray(arg)) {
390
+ for (i = 0; i < arg.length; i++) {
391
+ this.addChild(arg[i]);
392
+ }
393
+ } else {
394
+ keys = Object.keys(arg);
395
+ for (i = 0; i < keys.length; i++) {
396
+ this.addChild(arg[keys[i]] , keys[i]);
397
+ }
398
+ }
399
+ return this;
400
+ };
401
+ // ###bubble
402
+ // By default, `bubble` returns the current view's parent (if it has one)
403
+ //
404
+ // `returns` {Object|undefined}
405
+ sudo.Container.prototype.bubble = function bubble() {return this.parent;};
406
+ // ###eachChild
407
+ // Call a named method and pass any args to each child in a container's
408
+ // collection of children
409
+ //
410
+ // `param` {*} Any number of arguments the first of which must be
411
+ // The named method to look for and call. Other args are passed through
412
+ // `returns` {object} `this`
413
+ sudo.Container.prototype.eachChild = function eachChild(/*args*/) {
414
+ var args = Array.prototype.slice.call(arguments),
415
+ which = args.shift(), i, len, curr;
416
+ for (i = 0, len = this.children.length; i < len; i++) {
417
+ curr = this.children[i];
418
+ if(which in curr) curr[which].apply(curr, args);
419
+ }
420
+ return this;
421
+ };
422
+ // ###getChild
423
+ // If a child was added with a name, via `addChild`,
424
+ // that object can be fetched by name. This prevents us from having to reference a
425
+ // containers children by index. That is possible however, though not preferred.
426
+ //
427
+ // `param` {String|Number} `id`. The string `name` or numeric `index` of the child to fetch.
428
+ // `returns` {Object|undefined} The found child
429
+ sudo.Container.prototype.getChild = function getChild(id) {
430
+ return typeof id === 'string' ? this.children[this.childNames[id]] :
431
+ this.children[id];
432
+ };
433
+ // ###_indexChildren_
434
+ // Method is called with the `index` property of a subview that is being removed.
435
+ // Beginning at <i> decrement subview indices.
436
+ // `param` {Number} `i`
437
+ // `private`
438
+ sudo.Container.prototype._indexChildren_ = function _indexChildren_(i) {
439
+ var c = this.children, obj = this.childNames, len;
440
+ for (len = c.length; i < len; i++) {
441
+ c[i].index--;
442
+ // adjust any entries in childNames
443
+ if(c[i].name in obj) obj[c[i].name] = c[i].index;
444
+ }
445
+ };
446
+ // ###removeChild
447
+ // Find the intended child from my list of children and remove it, removing the name reference and re-indexing
448
+ // remaining children. This method does not remove the child's DOM.
449
+ // Override this method, doing whatever you want to the child's DOM, then call `base('removeChild')` to do so.
450
+ //
451
+ // `param` {String|Number|Object} `arg`. Children will always have an `index` number, and optionally a `name`.
452
+ // If passed a string `name` is assumed, so be sure to pass an actual number if expecting to use index.
453
+ // An object will be assumed to be an actual sudo Class Object.
454
+ // `returns` {Object} `this`
455
+ sudo.Container.prototype.removeChild = function removeChild(arg) {
456
+ var i, t = typeof arg, c;
457
+ // normalize the input
458
+ if(t === 'object') c = arg;
459
+ else c = t === 'string' ? this.children[this.childNames[arg]] : this.children[arg];
460
+ i = c.index;
461
+ // remove from the children Array
462
+ this.children.splice(i, 1);
463
+ // remove from the named child hash if present
464
+ delete this.childNames[c.name];
465
+ // child is now an `orphan`
466
+ delete c.parent;
467
+ delete c.index;
468
+ delete c.name;
469
+ this._indexChildren_(i);
470
+ return this;
471
+ };
472
+ // ###removeChildren
473
+ // Remove all children, name references and adjust indexes accordingly.
474
+ // This method calls removeFromParent as each child may have overridden logic there.
475
+ //
476
+ // see `removeChild`
477
+ //
478
+ // `param` {bool} `keep` Optional arg to instruct the parent to `detach` its $el
479
+ // rather than the default `remove` if truthy
480
+ // `returns` {object} `this`
481
+ sudo.Container.prototype.removeChildren = function removeChildren(keep) {
482
+ while(this.children.length) {
483
+ this.children.shift().removeFromParent(keep);
484
+ }
485
+ return this;
486
+ };
487
+ // ###removeFromParent
488
+ // Remove this object from its parents list of children.
489
+ // Does not alter the dom - do that yourself by overriding this method
490
+ // or chaining method calls
491
+ sudo.Container.prototype.removeFromParent = function removeFromParent() {
492
+ // will error without a parent, but that would be your fault...
493
+ this.parent.removeChild(this);
494
+ return this;
495
+ };
496
+ sudo.Container.prototype.role = 'container';
497
+ // ###send
498
+ // The call to the specific method on a (un)specified target happens here.
499
+ // If this Object is part of a `sudo.Container` maintained hierarchy
500
+ // the 'target' may be left out, causing the `bubble()` method to be called.
501
+ // What this does is allow children of a `sudo.Container` to simply pass
502
+ // events upward, delegating the responsibility of deciding what to do to the parent.
503
+ //
504
+ // TODO Currently, only the first target method found is called, then the
505
+ // bubbling is stopped. Should bubbling continue all the way up the 'chain'?
506
+ //
507
+ // `param` {*} Any number of arguments is supported, but the first is the only one searched for info.
508
+ // A sendMethod will be located by:
509
+ // 1. using the first argument if it is a string
510
+ // 2. looking for a `sendMethod` property if it is an object
511
+ // In the case a specified target exists at `this.model.get('sendTarget')` it will be used
512
+ // Any other args will be passed to the sendMethod after `this`
513
+ // `returns` {Object} `this`
514
+ sudo.Container.prototype.send = function send(/*args*/) {
515
+ var args = Array.prototype.slice.call(arguments),
516
+ d = this.model && this.model.data, meth, targ, fn;
517
+ // normalize the input, common use cases first
518
+ if(d && 'sendMethod' in d) meth = d.sendMethod;
519
+ else if(typeof args[0] === 'string') meth = args.shift();
520
+ // less common but viable options
521
+ if(!meth) {
522
+ // passed as a custom data attr bound in events
523
+ meth = 'data' in args[0] ? args[0].data.sendMethod :
524
+ // passed in a hash from something or not passed at all
525
+ args[0].sendMethod || void 0;
526
+ }
527
+ // target is either specified or my parent
528
+ targ = d && d.sendTarget || this.bubble();
529
+ // obvious chance for errors here, don't be dumb
530
+ fn = targ[meth];
531
+ while(!fn && (targ = targ.bubble())) {
532
+ fn = targ[meth];
533
+ }
534
+ // sendMethods expect a signature (sender, ...)
535
+ if(fn) {
536
+ args.unshift(this);
537
+ fn.apply(targ, args);
538
+ }
539
+ return this;
540
+ };
541
+ // ##View Class Object
542
+
543
+ // Create an instance of a sudo.View object. A view is any object
544
+ // that maintains its own `el`, that being some type of DOM element.
545
+ // Pass in a string selector or an actual dom node reference to have the object
546
+ // set that as its `el`. If no `el` is specified one will be created upon instantiation
547
+ // based on the `tagName` (`div` by default). Specify `className`, `id` (or other attributes if desired)
548
+ // as an (optional) `attributes` object literal on the `data` arg.
549
+ //
550
+ // The view object uses zepto for dom manipulation
551
+ // and event delegation etc... A querified `this` reference is located
552
+ // at `this.$el` and `this.$` scopes queries to this objects `el`, i.e it's
553
+ // a shortcut for `this.$el.find(selector)`
554
+ //
555
+ // `param` {string|element|Query} `el`. Otional el for the View instance.
556
+ // `param` {Object} `data`. Optional data object-literal which becomes the initial state
557
+ // of a new model located at `this.model`. Also can be a reference to an existing sudo.Model instance
558
+ //
559
+ // `constructor`
560
+ sudo.View = function(el, data) {
561
+ sudo.Container.call(this);
562
+ // allow model instance to be passed in as well
563
+ if(data) {
564
+ this.model = data.role === 'model' ? data :
565
+ this.model = new sudo.Model(data);
566
+ }
567
+ this.setEl(el);
568
+ if(this.role === 'view') this.init();
569
+ };
570
+ // View inherits from Container
571
+ // `private`
572
+ sudo.inherit(sudo.Container, sudo.View);
573
+ // ###becomePremier
574
+ // Premier functionality provides hooks for behavioral differentiation
575
+ // among elements or class objects.
576
+ //
577
+ // `returns` {Object} `this`
578
+ sudo.View.prototype.becomePremier = function becomePremier() {
579
+ var p, f = function() {
580
+ this.isPremier = true;
581
+ sudo.premier = this;
582
+ }.bind(this);
583
+ // is there an existing premier that isn't me?
584
+ if((p = sudo.premier) && p.uid !== this.uid) {
585
+ // ask it to resign and call the cb
586
+ p.resignPremier(f);
587
+ } else f(); // no existing premier
588
+ return this;
589
+ };
590
+ // ###init
591
+ // A 'contruction-time' hook to call for further initialization needs in
592
+ // View objects (and their subclasses). A noop by default child classes should override.
593
+ sudo.View.prototype.init = $.noop;
594
+ // the el needs to be normalized before use
595
+ // `private`
596
+ sudo.View.prototype._normalizedEl_ = function _normalizedEl_(el) {
597
+ if(typeof el === 'string') {
598
+ return $(el);
599
+ } else {
600
+ // Passed an already `querified` Element?
601
+ // It will have a length of 1 if so.
602
+ return el.length ? el : $(el);
603
+ }
604
+ };
605
+ // ### resignPremier
606
+ // Resign premier status
607
+ //
608
+ // `param` {Function} `cb`. An optional callback to execute
609
+ // after resigning premier status.
610
+ // `returns` {Object} `this`
611
+ sudo.View.prototype.resignPremier = function resignPremier(cb) {
612
+ var p;
613
+ this.isPremier = false;
614
+ // only remove the global premier if it is me
615
+ if((p = sudo.premier) && p.uid === this.uid) {
616
+ sudo.premier = null;
617
+ }
618
+ // fire the cb if passed
619
+ if(cb) cb();
620
+ return this;
621
+ };
622
+ // `private`
623
+ sudo.View.prototype.role = 'view';
624
+ // ###setEl
625
+ // A view must have an element, set that here.
626
+ // Stores a querified object as `this.$el` the raw
627
+ // node is always then available as `this.$el[0]`.
628
+ //
629
+ // `param` {string=|element} `el`
630
+ // `returns` {Object} `this`
631
+ sudo.View.prototype.setEl = function setEl(el) {
632
+ var d = this.model && this.model.data, a, t;
633
+ if(!el) {
634
+ // normalize any relevant data
635
+ t = d ? d.tagName || 'div': 'div';
636
+ this.$el = $(document.createElement(t));
637
+ if(d && (a = d.attributes)) this.$el.attr(a);
638
+ } else {
639
+ this.$el = this._normalizedEl_(el);
640
+ }
641
+ return this;
642
+ };
643
+ // ###this.$
644
+ // Return a single Element matching `sel` scoped to this View's el.
645
+ // This is an alias to `this.$el.find(sel)`.
646
+ //
647
+ // `param` {string} `sel`. A Query compatible selector
648
+ // `returns` {Query} A 'querified' result matching the selector
649
+ sudo.View.prototype.$ = function(sel) {
650
+ return this.$el.find(sel);
651
+ };
652
+ // ###Templating
653
+
654
+ // Allow the default {{ js code }}, {{= key }}, and {{- escape stuff }}
655
+ // micro templating delimiters to be overridden if desired
656
+ //
657
+ // `type` {Object}
658
+ sudo.templateSettings = {
659
+ evaluate: /\{\{([\s\S]+?)\}\}/g,
660
+ interpolate: /\{\{=([\s\S]+?)\}\}/g,
661
+ escape: /\{\{-([\s\S]+?)\}\}/g
662
+ };
663
+ // Certain characters need to be escaped so that they can be put
664
+ // into a string literal when templating.
665
+ //
666
+ // `type` {Object}
667
+ sudo.escapes = {};
668
+ (function(s) {
669
+ var e = {
670
+ '\\': '\\',
671
+ "'": "'",
672
+ r: '\r',
673
+ n: '\n',
674
+ t: '\t',
675
+ u2028: '\u2028',
676
+ u2029: '\u2029'
677
+ };
678
+ for (var key in e) s.escapes[e[key]] = key;
679
+ }(sudo));
680
+ // lookup hash for `escape`
681
+ //
682
+ // `type` {Object}
683
+ sudo.htmlEscapes = {
684
+ '&': '&amp;',
685
+ '<': '&lt;',
686
+ '>': '&gt;',
687
+ '"': '&quot;',
688
+ "'": '&#x27;',
689
+ '/': '&#x2F;'
690
+ };
691
+ // Escapes certain characters for templating
692
+ //
693
+ // `type` {regexp}
694
+ sudo.escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
695
+ // Escape unsafe HTML
696
+ //
697
+ // `type` {regexp}
698
+ sudo.htmlEscaper = /[&<>"'\/]/g;
699
+ // Unescapes certain characters for templating
700
+ //
701
+ // `type` {regexp}
702
+ sudo.unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g;
703
+ // ###escape
704
+ // Remove unsafe characters from a string
705
+ //
706
+ // `param` {String} str
707
+ sudo.escape = function(str) {
708
+ return str.replace(sudo.htmlEscaper, function(match) {
709
+ return sudo.htmlEscapes[match];
710
+ });
711
+ };
712
+ // ###unescape
713
+ // Within an interpolation, evaluation, or escaping,
714
+ // remove HTML escaping that had been previously added.
715
+ //
716
+ // `param` {string} str
717
+ sudo.unescape = function unescape(str) {
718
+ return str.replace(sudo.unescaper, function(match, escape) {
719
+ return sudo.escapes[escape];
720
+ });
721
+ };
722
+ // ###template
723
+ // JavaScript micro-templating, similar to John Resig's (and it's offspring) implementation.
724
+ // sudo templating preserves whitespace, and correctly escapes quotes within interpolated code.
725
+ // Unlike others sudo.template requires a scope name (to avoid the use of `with`) and will spit at you
726
+ // if it is not present.
727
+ //
728
+ // `param` {string} `str`. The 'templated' string.
729
+ // `param` {Object} `data`. Optional hash of key:value pairs.
730
+ // `param` {string} `scope`. Optional context name of your `data object`, set to 'data' if falsy.
731
+ sudo.template = function template(str, data, scope) {
732
+ scope || (scope = 'data');
733
+ var settings = sudo.templateSettings, render, template,
734
+ // Compile the template source, taking care to escape characters that
735
+ // cannot be included in a string literal and then unescape them in code blocks.
736
+ source = "_p+='" + str.replace(sudo.escaper, function(match) {
737
+ return '\\' + sudo.escapes[match];
738
+ }).replace(settings.escape, function(match, code) {
739
+ return "'+\n((_t=(" + sudo.unescape(code) + "))==null?'':sudo.escape(_t))+\n'";
740
+ }).replace(settings.interpolate, function(match, code) {
741
+ return "'+\n((_t=(" + sudo.unescape(code) + "))==null?'':_t)+\n'";
742
+ }).replace(settings.evaluate, function(match, code) {
743
+ return "';\n" + sudo.unescape(code) + "\n_p+='";
744
+ }) + "';\n";
745
+ source = "var _t,_p='';" + source + "return _p;\n";
746
+ render = new Function(scope, source);
747
+ if (data) return render(data);
748
+ template = function(data) {
749
+ return render.call(this, data);
750
+ };
751
+ // Provide the compiled function source as a convenience for reflection/compilation
752
+ template.source = 'function(' + scope + '){\n' + source + '}';
753
+ return template;
754
+ };
755
+ // ##DataView Class Object
756
+
757
+ // Create an instance of an Object, inheriting from sudo.View that:
758
+ // 1. Expects to have a template located in its internal data Store accessible via `this.get('template')`.
759
+ // 2. Can have a `renderTarget` property in its data store. If so this will be the location
760
+ // the child injects itself into (if not already in) the DOM
761
+ // 3. Can have a 'renderMethod' property in its data store. If so this is the jQuery method
762
+ // that the child will use to place itself in it's `renderTarget`.
763
+ // 4. Has a `render` method that when called re-hydrates it's $el by passing its
764
+ // internal data store to its template
765
+ // 5. Handles event binding/unbinding by implementing the sudo.extensions.listener
766
+ // extension object
767
+ //
768
+ //`constructor`
769
+ sudo.DataView = function(el, data) {
770
+ sudo.View.call(this, el, data);
771
+ // implements the listener extension
772
+ $.extend(this, sudo.extensions.listener);
773
+ if(this.model.data.autoRender) {
774
+ // dont autoRender on the setting of events,
775
+ this.autoRenderBlacklist = {event: true, events: true};
776
+ // autoRender types observe their own model
777
+ if(!this.model.observe) $.extend(this.model, sudo.extensions.observable);
778
+ }
779
+ if(this.role === 'dataview') this.init();
780
+ };
781
+ // `private`
782
+ sudo.inherit(sudo.View, sudo.DataView);
783
+ // ###addedToParent
784
+ // Container's will check for the presence of this method and call it if it is present
785
+ // after adding a child - essentially, this will auto render the dataview when added to a parent
786
+ // if not an autoRender (which will render on model change), as well as setup the events (in children too)
787
+ sudo.DataView.prototype.addedToParent = function(parent) {
788
+ this.bindEvents();
789
+ // non-autoRender types should render now
790
+ if(!this.model.data.autoRender) return this.render();
791
+ // autoRender Dataviews should only render on model change
792
+ else this.observer = this.model.observe(this.render.bind(this));
793
+ return this;
794
+ };
795
+ // ###removeFromParent
796
+ // Remove this object from the DOM and its parent's list of children.
797
+ // Overrides `sudo.View.removeFromParent` to unbind events and `remove` its $el
798
+ //
799
+ // `returns` {Object} `this`
800
+ sudo.DataView.prototype.removeFromParent = function removeFromParent() {
801
+ this.parent.removeChild(this);
802
+ this.unbindEvents().$el.remove();
803
+ // in the case that this.model is 'foreign'
804
+ if(this.observer) this.model.unobserve(this.observer);
805
+ return this;
806
+ };
807
+ // ###render
808
+ // (Re)hydrate the innerHTML of this object via its template and data store.
809
+ // If a `renderTarget` is present this Object will inject itself into the target via
810
+ // `this.get('renderMethod')` or defualt to `$.append`. After injection, the `renderTarget`
811
+ // is deleted from this Objects data store (to prevent multiple injection).
812
+ // Event unbinding/rebinding is generally not necessary for the Objects innerHTML as all events from the
813
+ // Object's list of events (`this.get('event(s)'))` are delegated to the $el when added to parent.
814
+ //
815
+ // `param` {object} `change` dataviews may be observing their model if `autoRender: true`
816
+ //
817
+ // `returns` {Object} `this`
818
+ sudo.DataView.prototype.render = function render(change) {
819
+ // return early if a `blacklisted` key is set to my model
820
+ if(change && this.autoRenderBlacklist[change.name]) return this;
821
+ var d = this.model.data;
822
+ // (re)hydrate the innerHTML
823
+ if(typeof d.template === 'string') d.template = sudo.template(d.template);
824
+ this.$el.html(d.template(d));
825
+ // am I in the dom yet?
826
+ if(d.renderTarget) {
827
+ this._normalizedEl_(d.renderTarget)[d.renderMethod || 'append'](this.$el);
828
+ delete d.renderTarget;
829
+ }
830
+ return this;
831
+ };
832
+ // `private`
833
+ sudo.DataView.prototype.role = 'dataview';
834
+ // ##Navigator Class Object
835
+
836
+ // Abstracts location and history events, parsing their information into a
837
+ // normalized object that is then set to an Observable class instance
838
+ //
839
+ // `constructor`
840
+ sudo.Navigator = function(data) {
841
+ this.started = false;
842
+ this.slashStripper = /^\/+|\/+$/g;
843
+ this.leadingStripper = /^[#\/]|\s+$/g;
844
+ this.trailingStripper = /\/$/;
845
+ this.construct(data);
846
+ };
847
+ // Navigator inherits from `sudo.Model`
848
+ sudo.Navigator.prototype = Object.create(sudo.Model.prototype);
849
+ // ###getFragment
850
+ // 'Fragment' is defined as any URL information after the 'root' path
851
+ // including the `search` or `hash`
852
+ //
853
+ // `returns` {String} `fragment`
854
+ // `returns` {String} the normalized current fragment
855
+ sudo.Navigator.prototype.getFragment = function getFragment(fragment) {
856
+ var root = this.data.root;
857
+ if(!fragment) {
858
+ // intentional use of coersion
859
+ if (this.isPushState) {
860
+ fragment = window.location.pathname;
861
+ root = root.replace(this.trailingStripper, '');
862
+ if(!fragment.indexOf(root)) fragment = fragment.substr(root.length);
863
+ } else {
864
+ fragment = this.getHash();
865
+ }
866
+ }
867
+ return decodeURIComponent(fragment.replace(this.leadingStripper, ''));
868
+ };
869
+ // ###getHash
870
+ // Check either the passed in fragment, or the full location.href
871
+ // for a `hash` value
872
+ //
873
+ // `param` {string} `fragment` Optional fragment to check
874
+ // `returns` {String} the normalized current `hash`
875
+ sudo.Navigator.prototype.getHash = function getHash(fragment) {
876
+ fragment || (fragment = window.location.href);
877
+ var match = fragment.match(/#(.*)$/);
878
+ return match ? match[1] : '';
879
+ };
880
+ // ###getSearch
881
+ // Check either the passed in fragment, or the full location.href
882
+ // for a `search` value
883
+ //
884
+ // `param` {string} `fragment` Optional fragment to check
885
+ // `returns` {String} the normalized current `search`
886
+ sudo.Navigator.prototype.getSearch = function getSearch(fragment) {
887
+ fragment || (fragment = window.location.href);
888
+ var match = fragment.match(/\?(.*)$/);
889
+ return match ? match[1] : '';
890
+ };
891
+ // ###getUrl
892
+ // fetch the URL in the form <root + fragment>
893
+ //
894
+ // `returns` {String}
895
+ sudo.Navigator.prototype.getUrl = function getUrl() {
896
+ // note that delegate(_role_) returns the deleagte
897
+ return this.data.root + this.data.fragment;
898
+ };
899
+ // ###go
900
+ // If the passed in 'fragment' is different than the currently stored one,
901
+ // push a new state entry / hash event and set the data where specified
902
+ //
903
+ // `param` {string} `fragment`
904
+ // `returns` {*} call to `setData`
905
+ sudo.Navigator.prototype.go = function go(fragment) {
906
+ if(!this.started) return false;
907
+ if(!this.urlChanged(fragment)) return;
908
+ // TODO ever use replaceState?
909
+ if(this.isPushState) {
910
+ window.history.pushState({}, document.title, this.getUrl());
911
+ } else if(this.isHashChange) {
912
+ window.location.hash = '#' + this.data.fragment;
913
+ }
914
+ return this.setData();
915
+ };
916
+ // ###handleChange
917
+ // Bound to either the `popstate` or `hashchange` events, if the
918
+ // URL has indeed changed then parse the relevant data and set it -
919
+ // triggering change observers
920
+ //
921
+ // `returns` {*} call to `setData` or undefined
922
+ sudo.Navigator.prototype.handleChange = function handleChange(e) {
923
+ if(this.urlChanged()) {
924
+ return this.setData();
925
+ }
926
+ };
927
+ // ###parseQuery
928
+ // Parse and return a hash of the key value pairs contained in
929
+ // the current `query`
930
+ //
931
+ // `returns` {object}
932
+ sudo.Navigator.prototype.parseQuery = function parseQuery() {
933
+ var obj = {}, seg = this.data.query,
934
+ i, s;
935
+ if(seg) {
936
+ seg = seg.split('&');
937
+ for(i = 0; i < seg.length; i++) {
938
+ if(!seg[i]) continue;
939
+ s = seg[i].split('=');
940
+ obj[s[0]] = s[1];
941
+ }
942
+ return obj;
943
+ }
944
+ };
945
+ // ###setData
946
+ // Using the current `fragment` (minus any search or hash data) as a key,
947
+ // use `parseQuery` as the value for the key, setting it into the specified
948
+ // model (a stated `Observable` or `this.data`)
949
+ //
950
+ // `returns` {object} `this`
951
+ sudo.Navigator.prototype.setData = function setData() {
952
+ var frag = this.data.fragment,
953
+ // data is set in a specified model or in self
954
+ observable = this.data.observable || this;
955
+ if(this.data.query) {
956
+ // we want to set the key minus any search/hash
957
+ frag = frag.indexOf('?') !== -1 ? frag.split('?')[0] : frag.split('#')[0];
958
+ }
959
+ observable.set(frag, this.parseQuery());
960
+ return this;
961
+ };
962
+ // ###start
963
+ // Gather the necessary information about the current environment and
964
+ // bind to either (push|pop)state or hashchange.
965
+ // Also, if given an imcorrect URL for the current environment (hashchange
966
+ // vs pushState) normalize it and set accordingly (or don't).
967
+ //
968
+ // `returns` {object} `this`
969
+ sudo.Navigator.prototype.start = function start() {
970
+ var hasPushState, atRoot, loc, tmp;
971
+ if(this.started) return;
972
+ hasPushState = window.history && window.history.pushState;
973
+ this.started = true;
974
+ // setup the initial configuration
975
+ this.isHashChange = this.data.useHashChange && 'onhashchange' in window ||
976
+ (!hasPushState && 'onhashchange' in window);
977
+ this.isPushState = !this.isHashChange && !!hasPushState;
978
+ // normalize the root to always contain a leading and trailing slash
979
+ this.data['root'] = ('/' + this.data['root'] + '/').replace(this.slashStripper, '/');
980
+ // Get a snapshot of the current fragment
981
+ this.urlChanged();
982
+ // monitor URL changes via popState or hashchange
983
+ if (this.isPushState) {
984
+ $(window).on('popstate', this.handleChange.bind(this));
985
+ } else if (this.isHashChange) {
986
+ $(window).on('hashchange', this.handleChange.bind(this));
987
+ } else return;
988
+ atRoot = window.location.pathname.replace(/[^\/]$/, '$&/') === this.data['root'];
989
+ // somehow a URL got here not in my 'format', unless explicitly told not too, correct this
990
+ if(!this.data.stay) {
991
+ if(this.isHashChange && !atRoot) {
992
+ window.location.replace(this.data['root'] + window.location.search + '#' +
993
+ this.data.fragment);
994
+ // return early as browser will redirect
995
+ return true;
996
+ // the converse of the above
997
+ } else if(this.isPushState && atRoot && window.location.hash) {
998
+ tmp = this.getHash().replace(this.leadingStripper, '');
999
+ window.history.replaceState({}, document.title, this.data['root'] +
1000
+ tmp + window.location.search);
1001
+ }
1002
+ }
1003
+ // TODO provide option to `go` from inital `start` state?
1004
+ return this;
1005
+ };
1006
+ // ###urlChanged
1007
+ // Is a passed in fragment different from the one currently set at `this.get('fragment')`?
1008
+ // If so set the fragment to the passed fragment passed in (as well as any 'query' data), else
1009
+ // simply return false
1010
+ //
1011
+ // `param` {String} `fragment`
1012
+ // `returns` {bool}
1013
+ sudo.Navigator.prototype.urlChanged = function urlChanged(fragment) {
1014
+ var current = this.getFragment(fragment);
1015
+ // nothing has changed
1016
+ if (current === this.data.fragment) return false;
1017
+ this.data.fragment = current;
1018
+ // the fragment and the href need to checked here, optimized for the 'go' scenario
1019
+ this.data.query = (this.getSearch(current) || this.getSearch()) ||
1020
+ (this.getHash(current) || this.getHash());
1021
+ return true;
1022
+ };
1023
+ // ## Observable Extension Object
1024
+ //
1025
+ // Implementaion of the ES6 Harmony Observer pattern.
1026
+ // Extend a `sudo.Model` class with this object if
1027
+ // data-mutation-observation is required
1028
+ sudo.extensions.observable = {
1029
+ // ###_deliver_
1030
+ // Called from deliverChangeRecords when ready to send
1031
+ // changeRecords to observers.
1032
+ //
1033
+ // `private`
1034
+ _deliver_: function _deliver_(obj) {
1035
+ var i, cb = this.callbacks;
1036
+ for(i = 0; i < cb.length; i++) {
1037
+ cb[i](obj);
1038
+ }
1039
+ },
1040
+ // ###deliverChangeRecords
1041
+ // Iterate through the changeRecords array(emptying it as you go), delivering them to the
1042
+ // observers. You can override this method to change the standard delivery behavior.
1043
+ //
1044
+ // `returns` {Object} `this`
1045
+ deliverChangeRecords: function deliverChangeRecords() {
1046
+ var rec, cr = this.changeRecords;
1047
+ // FIFO
1048
+ for(rec; cr.length && (rec = cr.shift());) {
1049
+ this._deliver_(rec);
1050
+ }
1051
+ return this;
1052
+ },
1053
+ // ###observe
1054
+ // In a quasi-ES6 Object.observe pattern, calling observe on an `observable` and
1055
+ // passing a callback will cause that callback to be called whenever any
1056
+ // property on the observable's data store is set, changed or deleted
1057
+ // via set, unset, setPath or unsetPath with an object containing:
1058
+ // {
1059
+ // type: <new, updated, deleted>,
1060
+ // object: <the object being observed>,
1061
+ // name: <the key that was modified>,
1062
+ // oldValue: <if a previous value existed for this key>
1063
+ // }
1064
+ // For ease of 'unobserving' the same Function passed in is returned.
1065
+ //
1066
+ // `param` {Function} `fn` The callback to be called with changeRecord(s)
1067
+ // `returns` {Function} the Function passed in as an argument
1068
+ observe: function observe(fn) {
1069
+ // this will fail if mixed-in and no `callbacks` created so don't do that.
1070
+ // Per the spec, do not allow the same callback to be added
1071
+ var d = this.callbacks;
1072
+ if(d.indexOf(fn) === -1) d.push(fn);
1073
+ return fn;
1074
+ },
1075
+ // ###observes
1076
+ // Allow an array of callbacks to be registered as changeRecord recipients
1077
+ //
1078
+ // `param` {Array} ary
1079
+ // `returns` {Array} the Array passed in to observe
1080
+ observes: function observes(ary) {
1081
+ var i;
1082
+ for(i = 0; i < ary.length; i++) {
1083
+ this.observe(ary[i]);
1084
+ }
1085
+ return ary;
1086
+ },
1087
+ // ###set
1088
+ // Overrides sudo.Base.set to check for observers
1089
+ //
1090
+ // `param` {String} `key`. The name of the key
1091
+ // `param` {*} `value`
1092
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
1093
+ // to be delivered upon a call to deliverChangeRecords (truthy)
1094
+ //
1095
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1096
+ set: function set(key, value, hold) {
1097
+ var obj = {name: key, object: this.data};
1098
+ // did this key exist already
1099
+ if(key in this.data) {
1100
+ obj.type = 'updated';
1101
+ // then there is an oldValue
1102
+ obj.oldValue = this.data[key];
1103
+ } else obj.type = 'new';
1104
+ // now actually set the value
1105
+ this.data[key] = value;
1106
+ this.changeRecords.push(obj);
1107
+ // call the observers or not
1108
+ if(hold) return this;
1109
+ return this.deliverChangeRecords();
1110
+ },
1111
+ // ###setPath
1112
+ // Overrides sudo.Base.setPath to check for observers.
1113
+ // Change records originating from a `setPath` operation
1114
+ // send back the passed in `path` as `name` as well as the
1115
+ // top level object being observed (this observable's data).
1116
+ // this allows for easy filtering either manually or via a
1117
+ // `change delegate`
1118
+ //
1119
+ // `param` {String} `path`
1120
+ // `param` {*} `value`
1121
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
1122
+ // to be delivered upon a call to deliverChangeRecords (truthy)
1123
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1124
+ setPath: function setPath(path, value, hold) {
1125
+ var curr = this.data, obj = {name: path, object: this.data},
1126
+ p = path.split('.'), key;
1127
+ for (key; p.length && (key = p.shift());) {
1128
+ if(!p.length) {
1129
+ // reached the last refinement, pre-existing?
1130
+ if (key in curr) {
1131
+ obj.type = 'updated';
1132
+ obj.oldValue = curr[key];
1133
+ } else obj.type = 'new';
1134
+ curr[key] = value;
1135
+ } else if (curr[key]) {
1136
+ curr = curr[key];
1137
+ } else {
1138
+ curr = curr[key] = {};
1139
+ }
1140
+ }
1141
+ this.changeRecords.push(obj);
1142
+ // call all observers or not
1143
+ if(hold) return this;
1144
+ return this.deliverChangeRecords();
1145
+ },
1146
+ // ###sets
1147
+ // Overrides Base.sets to hold the call to _deliver_ until
1148
+ // all operations are done
1149
+ //
1150
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1151
+ sets: function sets(obj, hold) {
1152
+ var i, k = Object.keys(obj);
1153
+ for(i = 0; i < k.length; i++) {
1154
+ k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]], true) :
1155
+ this.setPath(k[i], obj[k[i]], true);
1156
+ }
1157
+ if(hold) return this;
1158
+ return this.deliverChangeRecords();
1159
+ },
1160
+ // ###unobserve
1161
+ // Remove a particular callback from this observable
1162
+ //
1163
+ // `param` {Function} the function passed in to `observe`
1164
+ // `returns` {Object} `this`
1165
+ unobserve: function unobserve(fn) {
1166
+ var cb = this.callbacks, i = cb.indexOf(fn);
1167
+ if(i !== -1) cb.splice(i, 1);
1168
+ return this;
1169
+ },
1170
+ // ###unobserves
1171
+ // Allow an array of callbacks to be unregistered as changeRecord recipients
1172
+ //
1173
+ // `param` {Array} ary
1174
+ // `returns` {Object} `this`
1175
+ unobserves: function unobserves(ary) {
1176
+ var i;
1177
+ for(i = 0; i < ary.length; i++) {
1178
+ this.unobserve(ary[i]);
1179
+ }
1180
+ return this;
1181
+ },
1182
+ // ###unset
1183
+ // Overrides sudo.Base.unset to check for observers
1184
+ //
1185
+ // `param` {String} `key`. The name of the key
1186
+ // `param` {Bool} `hold`
1187
+ //
1188
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1189
+ unset: function unset(key, hold) {
1190
+ var obj = {name: key, object: this.data, type: 'deleted'},
1191
+ val = !!this.data[key];
1192
+ delete this.data[key];
1193
+ // call the observers if there was a val to delete
1194
+ return this._unset_(obj, val, hold);
1195
+ },
1196
+ // ###_unset_
1197
+ // Helper for the unset functions
1198
+ //
1199
+ // `private`
1200
+ _unset_: function _unset_(o, v, h) {
1201
+ if(v) {
1202
+ this.changeRecords.push(o);
1203
+ if(h) return this;
1204
+ return this.deliverChangeRecords();
1205
+ }
1206
+ return this;
1207
+ },
1208
+ // ###setPath
1209
+ // Overrides sudo.Base.unsetPath to check for observers
1210
+ //
1211
+ // `param` {String} `path`
1212
+ // `param` {*} `value`
1213
+ // `param` {bool} `hold`
1214
+ //
1215
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1216
+ unsetPath: function unsetPath(path, hold) {
1217
+ var obj = {name: path, object: this.data, type: 'deleted'},
1218
+ curr = this.data, p = path.split('.'),
1219
+ key, val;
1220
+ for (key; p.length && (key = p.shift());) {
1221
+ if(!p.length) {
1222
+ // reached the last refinement
1223
+ val = !!curr[key];
1224
+ delete curr[key];
1225
+ } else {
1226
+ // this can obviously fail, but can be prevented by checking
1227
+ // with `getPath` first.
1228
+ curr = curr[key];
1229
+ }
1230
+ }
1231
+ return this._unset_(obj, val, hold);
1232
+ },
1233
+ // ###unsets
1234
+ // Override of Base.unsets to hold the call to _deliver_ until done
1235
+ //
1236
+ // `param` ary
1237
+ // `param` hold
1238
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1239
+ unsets: function unsets(ary, hold) {
1240
+ var i;
1241
+ for(i = 0; i < ary.length; i++) {
1242
+ ary[i].indexOf('.') === -1 ? this.unset(k[i], true) :
1243
+ this.unsetPath(k[i], true);
1244
+ }
1245
+ if(hold) return this;
1246
+ return this.deliverChangeRecords();
1247
+ }
1248
+ };
1249
+ // ##Listener Extension Object
1250
+
1251
+ // Handles event binding/unbinding via an events array in the form:
1252
+ // events: [{
1253
+ // name: `eventName`,
1254
+ // sel: `an_optional_delegator`,
1255
+ // data: an_optional_hash_of_data
1256
+ // fn: `function name`
1257
+ // }, {...
1258
+ // This array will be searched for via `this.get('events')`. There is a
1259
+ // single-event use case as well, pass a single object literal in the above form.
1260
+ // with the key `event`:
1261
+ // event: {...same as above}
1262
+ // Details about the hashes in the array:
1263
+ // A. name -> jQuery compatible event name
1264
+ // B. sel -> Optional jQuery compatible selector used to delegate events
1265
+ // C. data: A hash that will be passed as the custom jQuery Event.data object
1266
+ // D. fn -> If a {String} bound to the named function on this object, if a
1267
+ // function assumed to be anonymous and called with no scope manipulation
1268
+ sudo.extensions.listener = {
1269
+ // ###bindEvents
1270
+ // Bind the events in the data store to this object's $el
1271
+ //
1272
+ // `returns` {Object} `this`
1273
+ bindEvents: function bindEvents() {
1274
+ var e;
1275
+ if((e = this.model.data.event || this.model.data.events)) this._handleEvents_(e, 1);
1276
+ return this;
1277
+ },
1278
+ // Use the jQuery `on` or 'off' method, optionally delegating to a selector if present
1279
+ // `private`
1280
+ _handleEvents_: function _handleEvents_(e, which) {
1281
+ var i;
1282
+ if(Array.isArray(e)) {
1283
+ for(i = 0; i < e.length; i++) {
1284
+ this._handleEvent_(e[i], which);
1285
+ }
1286
+ } else {
1287
+ this._handleEvent_(e, which);
1288
+ }
1289
+ },
1290
+ // helper for binding and unbinding an individual event
1291
+ // `param` {Object} e. An event descriptor
1292
+ // `param` {String} which. `on` or `off`
1293
+ // `private`
1294
+ _handleEvent_: function _handleEvent_(e, which) {
1295
+ if(which) {
1296
+ this.$el.on(e.name, e.sel, e.data, typeof e.fn === 'string' ? this[e.fn].bind(this) : e.fn);
1297
+ } else {
1298
+ // do not re-bind the fn going to off otherwise the unbind will fail
1299
+ this.$el.off(e.name, e.sel);
1300
+ }
1301
+ },
1302
+ // ###rebindEvents
1303
+ // Convenience method for `this.unbindEvents().bindEvents()`
1304
+ //
1305
+ // 'returns' {object} 'this'
1306
+ rebindEvents: function rebindEvents() {
1307
+ return this.unbindEvents().bindEvents();
1308
+ },
1309
+ // ###unbindEvents
1310
+ // Unbind the events in the data store from this object's $el
1311
+ //
1312
+ // `returns` {Object} `this`
1313
+ unbindEvents: function unbindEvents() {
1314
+ var e;
1315
+ if((e = this.model.data.event || this.model.data.events)) this._handleEvents_(e);
1316
+ return this;
1317
+ }
1318
+ };
1319
+ // ##sudo persistable extension
1320
+ //
1321
+ // A mixin providing restful CRUD operations for a sudo.Model instance.
1322
+ //
1323
+ // create : POST
1324
+ // read : GET
1325
+ // update : PUT or PATCH (configurable)
1326
+ // destroy : DELETE
1327
+ //
1328
+ // Before use be sure to set an `ajax` property {object} with at least
1329
+ // a `baseUrl: ...` key. The model's id (if present -- indicating a persisted model)
1330
+ // is appended to the baseUrl (baseUrl/id) by default. You can override this behavior
1331
+ // by simply setting a `url: ...` in the `ajax` options hash or pass in the same when
1332
+ // calling any of the methods (or override the model.url() method).
1333
+ //
1334
+ // Place any other default options in the `ajax` hash
1335
+ // that you would want sent to a $.ajax({...}) call. Again, you can also override those
1336
+ // defaults by passing in a hash of options to any method:
1337
+ // `this.model.update({patch: true})` etc...
1338
+ sudo.extensions.persistable = {
1339
+ // ###create
1340
+ //
1341
+ // Save this model on the server. If a subset of this model's attributes
1342
+ // have not been stated (ajax:{data:{...}}) send all of the model's data.
1343
+ // Anticipate that the server response will send back the
1344
+ // state of the model on the server and set it here (via a success callback).
1345
+ //
1346
+ // `param` {object} `params` Hash of options for the XHR call
1347
+ // `returns` {object} The XHR object
1348
+ create: function create(params) {
1349
+ return this._sendData_('POST', params);
1350
+ },
1351
+ // ###destroy
1352
+ //
1353
+ // Delete this model on the server
1354
+ //
1355
+ // `param` {object} `params` Optional hash of options for the XHR
1356
+ // `returns` {object} Xhr
1357
+ destroy: function destroy(params) {
1358
+ return this._sendData_('DELETE', params);
1359
+ },
1360
+ // ###_normalizeParams_
1361
+ // Abstracted logic for preparing the options object. This looks at
1362
+ // the set `ajax` property, allowing any passed in params to override.
1363
+ //
1364
+ // Sets defaults: JSON dataType and a success callback that simply `sets()` the
1365
+ // data returned from the server
1366
+ //
1367
+ // `returns` {object} A normalized params object for the XHR call
1368
+ _normalizeParams_: function _normalizeParams_(meth, opts, params) {
1369
+ opts || (opts = $.extend({}, this.data.ajax));
1370
+ opts.url || (opts.url = this.url(opts.baseUrl));
1371
+ opts.type || (opts.type = meth);
1372
+ opts.dataType || (opts.dataType = 'json');
1373
+ var isJson = opts.dataType === 'json';
1374
+ // by default turn off the global ajax triggers as all data
1375
+ // should flow thru the models to their observers
1376
+ opts.global || (opts.global = false);
1377
+ // the default success callback is to set the data returned from the server
1378
+ // or just the status as `ajaxStatus` if no data was returned
1379
+ opts.success || (opts.success = function(data, status, xhr) {
1380
+ data ? this.sets((isJson && typeof data === 'string') ? JSON.parse(data) : data) :
1381
+ this.set('ajaxStatus', status);
1382
+ }.bind(this));
1383
+ // allow the passed in params to override any set in this model's `ajax` options
1384
+ return params ? $.extend(opts, params) : opts;
1385
+ },
1386
+ // ###read
1387
+ //
1388
+ // Fetch this models state from the server and set it here. The
1389
+ // `Model.sets()` method is used with the returned data (we are
1390
+ // asssuming the default json dataType). Pass in (via the params arg)
1391
+ // a success function to override this default.
1392
+ //
1393
+ // Maps to the http GET method.
1394
+ //
1395
+ // `param` {object} `params`. Optional info for the XHR call. If
1396
+ // present will override any set in this model's `ajax` options object.
1397
+ // `returns` {object} The XHR object
1398
+ read: function read(params) {
1399
+ return $.ajax(this._normalizeParams_('GET', null, params));
1400
+ },
1401
+ // ###save
1402
+ //
1403
+ // Convenience method removing the need to know if a model is new (not yet persisted)
1404
+ // or has been loaded/refreshed from the server.
1405
+ //
1406
+ // `param` {object} `params` Hash of options for the XHR call
1407
+ // `returns` {object} The XHR object
1408
+ save: function save(params) {
1409
+ return ('id' in this.data) ? this.update(params) : this.create(params);
1410
+ },
1411
+ // ###_sendData_
1412
+ // The Create, Update and Patch methods all send data to the server,
1413
+ // varying only in their HTTP method. Abstracted logic is here.
1414
+ //
1415
+ // `returns` {object} Xhr
1416
+ _sendData_: function _sendData_(meth, params) {
1417
+ opts = $.extend({}, this.data.ajax);
1418
+ opts.contentType || (opts.contentType = 'application/json');
1419
+ opts.data || (opts.data = this.data);
1420
+ // assure that, in the default json case, opts.data is json
1421
+ if(opts.contentType === 'application/json' && (typeof opts.data !== 'string')) {
1422
+ opts.data = JSON.stringify(opts.data);
1423
+ }
1424
+ // non GET requests do not 'processData'
1425
+ if(!('processData' in opts)) opts.processData = false;
1426
+ return $.ajax(this._normalizeParams_(meth, opts, params));
1427
+ },
1428
+ // ###update
1429
+ //
1430
+ // If this model has been persisted to/from the server (it has an `id` attribute)
1431
+ // send the specified data (or all the model's data) to the server at `url` via
1432
+ // the `PUT` http verb or `PATCH` if {patch: true} is in the ajax options (or the
1433
+ // passed in params)
1434
+ //
1435
+ // NOTE: update does not check is this is a new model or not, do that yourself
1436
+ // or use the `save()` method (that does check).
1437
+ //
1438
+ // `param` {object} `params` Optional hash of options for the XHR
1439
+ // `returns` {object|bool} the Xhr if called false if not
1440
+ update: function update(params) {
1441
+ return this._sendData_((this.data.ajax.patch || params && params.patch) ?
1442
+ 'PATCH' : 'PUT', params);
1443
+ },
1444
+ // ###url
1445
+ //
1446
+ // Takes the base url and appends this models id if present
1447
+ // (narmalizing the trailong slash if needed).
1448
+ // Override if you need to change the format of the calculated url.
1449
+ //
1450
+ // `param` {string} `base` the baseUrl set in this models ajax options
1451
+ url: function url(base) {
1452
+ if(!base) return void 0;
1453
+ // could possibly be 0...
1454
+ if('id' in this.data) {
1455
+ return base + (base.charAt(base.length - 1) === '/' ?
1456
+ '' : '/') + encodeURIComponent(this.data.id);
1457
+ } else return base;
1458
+ }
1459
+ };
1460
+ //##Filtered Delegate
1461
+
1462
+ // The base type for both the Data and Change delegates.
1463
+ //
1464
+ // `param` {Object} data
1465
+ sudo.delegates.Filtered = function(data) {
1466
+ sudo.Model.call(this, data);
1467
+ };
1468
+ // The filtered delegate
1469
+ sudo.inherit(sudo.Model, sudo.delegates.Filtered);
1470
+ // ###addFilter
1471
+ // Place an entry into this object's hash of filters
1472
+ //
1473
+ // `param` {string} `key`
1474
+ // `param` {string} `val`
1475
+ // `returns` {object} this
1476
+ sudo.delegates.Filtered.prototype.addFilter = function addFilter(key, val) {
1477
+ this.data.filters[key] = val;
1478
+ return this;
1479
+ };
1480
+ // ###removeFilter
1481
+ // Remove an entry from this object's hash of filters
1482
+ //
1483
+ // `param` {string} `key`
1484
+ // `returns` {object} this
1485
+ sudo.delegates.Filtered.prototype.removeFilter = function removeFilter(key) {
1486
+ delete this.data.filters[key];
1487
+ return this;
1488
+ };
1489
+ // `private`
1490
+ sudo.delegates.Filtered.prototype.role = 'filtered';
1491
+ //##Change Delegate
1492
+
1493
+ // Delegates, if present, can override or extend the behavior
1494
+ // of objects. The change delegate is specifically designed to
1495
+ // filter change records from an Observable instance and only forward
1496
+ // the ones matching a given `filters` criteria (key or path).
1497
+ // The forwarded messages will be sent to the specified method
1498
+ // on the delegates `delegator` (bound to the _delegator_ scope)
1499
+ //
1500
+ // `param` {Object} data
1501
+ sudo.delegates.Change = function(data) {
1502
+ this.construct(data);
1503
+ };
1504
+ // Delegates inherit from the Filtered Delegate
1505
+ sudo.delegates.Change.prototype = Object.create(sudo.delegates.Filtered.prototype);
1506
+ // ###filter
1507
+ // Change records are delivered here and filtered, calling any matching
1508
+ // methods specified in `this.get('filters').
1509
+ //
1510
+ // `returns` {Object} a call to the specified _delegator_ method, passing
1511
+ // a hash containing:
1512
+ // 1. the `type` of Change
1513
+ // 2. the `name` of the changed property
1514
+ // 3. the value located at the key/path
1515
+ // 4. the `oldValue` of the key if present
1516
+ sudo.delegates.Change.prototype.filter = function filter(change) {
1517
+ var filters = this.data.filters, name = change.name,
1518
+ type = change.type, obj = {};
1519
+ // does my delegator care about this?
1520
+ if(name in filters && filters.hasOwnProperty(name)) {
1521
+ // assemble the object to return to the method
1522
+ obj.type = type;
1523
+ obj.name = name;
1524
+ obj.oldValue = change.oldValue;
1525
+ // delete operations will not have any value so no need to look
1526
+ if(type !== 'deleted') {
1527
+ obj.value = name.indexOf('.') === -1 ? change.object[change.name] :
1528
+ sudo.getPath(name, change.object);
1529
+ }
1530
+ return this.delegator[filters[name]].call(this.delegator, obj);
1531
+ }
1532
+ };
1533
+ // `private`
1534
+ sudo.delegates.Change.prototype.role = 'change';
1535
+ //##Data Delegate
1536
+
1537
+ // Delegates, if present, can extend the behavior
1538
+ // of objects, lessening the need for subclassing.
1539
+ // The data delegate is specifically designed to
1540
+ // filter through an object, looking for specified keys or paths
1541
+ // and returning values for those if found
1542
+ //
1543
+ // `param` {Object} data
1544
+ // `returns` {*} the value found at the specified key/path if found
1545
+ sudo.delegates.Data = function(data) {
1546
+ this.construct(data);
1547
+ };
1548
+ // inherits from the Filtered Delegate
1549
+ sudo.delegates.Data.prototype = Object.create(sudo.delegates.Filtered.prototype);
1550
+ // ###filter
1551
+ // iterates over a given object literal and returns a value (if present)
1552
+ // located at a given key or path
1553
+ //
1554
+ // `param` {Object} `obj`
1555
+ sudo.delegates.Data.prototype.filter = function(obj) {
1556
+ var filters = this.data.filters,
1557
+ ary = Object.keys(filters), key, i, o, k;
1558
+ for(i = 0; i < ary.length; i++) {
1559
+ key = ary[i];
1560
+ // keys and paths need different handling
1561
+ if(key.indexOf('.') === -1) {
1562
+ if(key in obj) this.delegator[filters[key]].call(
1563
+ this.delegator, obj[key]);
1564
+ } else {
1565
+ // the chars after the last refinement are the key we need to check for
1566
+ k = key.slice(key.lastIndexOf('.') + 1);
1567
+ // and the ones prior are the object
1568
+ o = sudo.getPath(key.slice(0, key.lastIndexOf('.')), obj);
1569
+ if(o && k in o) this.delegator[filters[key]].call(
1570
+ this.delegator, o[k]);
1571
+ }
1572
+ }
1573
+ };
1574
+ // `private`
1575
+ sudo.delegates.Data.prototype.role = 'data';
1576
+
1577
+ sudo.version = "0.9.6";
1578
+ window.sudo = sudo;
1579
+ if(typeof window._ === "undefined") window._ = sudo;
1580
+ }).call(this, this);