sudojs-rails 0.4.5 → 0.4.6

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.
@@ -1,119 +1,119 @@
1
1
  (function(window) {
2
2
  // #Sudo Namespace
3
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
- }
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
117
  };
118
118
  // ##Base Class Object
119
119
  //
@@ -123,10 +123,10 @@ var sudo = {
123
123
  //
124
124
  // `constructor`
125
125
  sudo.Base = function() {
126
- // can delegate
127
- this.delegates = [];
128
- // a beautiful and unique snowflake
129
- this.uid = sudo.unique();
126
+ // can delegate
127
+ this.delegates = [];
128
+ // a beautiful and unique snowflake
129
+ this.uid = sudo.unique();
130
130
  };
131
131
  // ###addDelegate
132
132
  // Push an instance of a Class Object into this object's `_delegates_` list.
@@ -134,10 +134,10 @@ sudo.Base = function() {
134
134
  // `param` {Object} an instance of a sudo.delegates Class Object
135
135
  // `returns` {Object} `this`
136
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;
137
+ del.delegator = this;
138
+ this.delegates.push(del);
139
+ if('addedAsDelegate' in del) del.addedAsDelegate(this);
140
+ return this;
141
141
  };
142
142
  // ###base
143
143
  // Lookup the function matching the name passed in and call it with
@@ -148,26 +148,26 @@ sudo.Base.prototype.addDelegate = function addDelegate(del) {
148
148
  // `params` {*} any other number of arguments to be passed to the looked up method
149
149
  // along with the initial method name
150
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);
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
164
  };
165
165
  // ###construct
166
166
  // A convenience method that alleviates the need to place:
167
167
  // `Object.getPrototypeOf(this).consturctor.apply(this, arguments)`
168
168
  // in every constructor
169
169
  sudo.Base.prototype.construct = function construct() {
170
- Object.getPrototypeOf(this).constructor.apply(this, arguments || []);
170
+ Object.getPrototypeOf(this).constructor.apply(this, arguments || []);
171
171
  };
172
172
  // ###delegate
173
173
  // From this object's list of delegates find the object whose `_role_` matches
@@ -179,13 +179,13 @@ sudo.Base.prototype.construct = function construct() {
179
179
  // `param` {String} `meth` Optional method to bind to the action this delegate is being used for
180
180
  // `returns`
181
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
- }
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
189
  };
190
190
  // ###getDelegate
191
191
  // Fetch a delegate whose role property matches the passed in argument.
@@ -195,7 +195,7 @@ sudo.Base.prototype.delegate = function delegate(role, meth) {
195
195
  // `param` {String} `role`
196
196
  // 'returns' {Object|undefined}
197
197
  sudo.Base.prototype.getDelegate = function getDelegate(role) {
198
- return this.delegate(role);
198
+ return this.delegate(role);
199
199
  };
200
200
  // ###removeDelegate
201
201
  // From this objects `delegates` list remove the object (there should only ever be 1)
@@ -204,16 +204,16 @@ sudo.Base.prototype.getDelegate = function getDelegate(role) {
204
204
  // `param` {String} `role`
205
205
  // `returns` {Object} `this`
206
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;
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
217
  };
218
218
  // `private`
219
219
  sudo.Base.prototype.role = 'base';
@@ -226,11 +226,11 @@ sudo.Base.prototype.role = 'base';
226
226
  //
227
227
  // `constructor`
228
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 = [];
229
+ sudo.Base.call(this);
230
+ this.data = data || {};
231
+ // only models are `observable`
232
+ this.callbacks = [];
233
+ this.changeRecords = [];
234
234
  };
235
235
  // Model inherits from sudo.Base
236
236
  // `private`
@@ -241,7 +241,7 @@ sudo.inherit(sudo.Base, sudo.Model);
241
241
  // `param` {String} `key`. The name of the key
242
242
  // `returns` {*}. The value associated with the key or false if not found.
243
243
  sudo.Model.prototype.get = function get(key) {
244
- return this.data[key];
244
+ return this.data[key];
245
245
  };
246
246
  // ###getPath
247
247
  //
@@ -250,7 +250,7 @@ sudo.Model.prototype.get = function get(key) {
250
250
  //
251
251
  // `returns` {*|undefined}. The value at keypath or undefined if not found.
252
252
  sudo.Model.prototype.getPath = function getPath(path) {
253
- return sudo.getPath(path, this.data);
253
+ return sudo.getPath(path, this.data);
254
254
  };
255
255
  // ###gets
256
256
  // Assembles and returns an object of key:value pairs for each key
@@ -259,12 +259,12 @@ sudo.Model.prototype.getPath = function getPath(path) {
259
259
  // `param` {array} `ary`. An array of keys.
260
260
  // `returns` {object}
261
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;
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
268
  };
269
269
  // `private`
270
270
  sudo.Model.prototype.role = 'model';
@@ -275,9 +275,9 @@ sudo.Model.prototype.role = 'model';
275
275
  // `param` {*} `value`. The value associated with the key.
276
276
  // `returns` {Object} `this`
277
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;
278
+ // _NOTE: intentional possibilty of setting a falsy value_
279
+ this.data[key] = value;
280
+ return this;
281
281
  };
282
282
  // ###setPath
283
283
  //
@@ -288,8 +288,8 @@ sudo.Model.prototype.set = function set(key, value) {
288
288
  // `param` {*} `value`
289
289
  // `returns` {Object} this.
290
290
  sudo.Model.prototype.setPath = function setPath(path, value) {
291
- sudo.setPath(path, value, this.data);
292
- return this;
291
+ sudo.setPath(path, value, this.data);
292
+ return this;
293
293
  };
294
294
  // ###sets
295
295
  // Invokes `set()` or `setPath()` for each key value pair in `obj`.
@@ -298,12 +298,12 @@ sudo.Model.prototype.setPath = function setPath(path, value) {
298
298
  // `param` {Object} `obj`. The keys and values to set.
299
299
  // `returns` {Object} `this`
300
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;
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
307
  };
308
308
  // ###unset
309
309
  // Remove a key:value pair from this object's data store
@@ -311,8 +311,8 @@ sudo.Model.prototype.sets = function sets(obj) {
311
311
  // `param` {String} key
312
312
  // `returns` {Object} `this`
313
313
  sudo.Model.prototype.unset = function unset(key) {
314
- delete this.data[key];
315
- return this;
314
+ delete this.data[key];
315
+ return this;
316
316
  };
317
317
  // ###unsetPath
318
318
  // Uses `sudo.unsetPath` operating on this models data hash
@@ -320,8 +320,8 @@ sudo.Model.prototype.unset = function unset(key) {
320
320
  // `param` {String} path
321
321
  // `returns` {Object} `this`
322
322
  sudo.Model.prototype.unsetPath = function unsetPath(path) {
323
- sudo.unsetPath(path, this.data);
324
- return this;
323
+ sudo.unsetPath(path, this.data);
324
+ return this;
325
325
  };
326
326
  // ###unsets
327
327
  // Deletes a number of keys or paths from this object's data store
@@ -329,12 +329,12 @@ sudo.Model.prototype.unsetPath = function unsetPath(path) {
329
329
  // `param` {array} `ary`. An array of keys or paths.
330
330
  // `returns` {Objaect} `this`
331
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;
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
338
  };
339
339
  // ##Container Class Object
340
340
  //
@@ -343,9 +343,9 @@ sudo.Model.prototype.unsets = function unsets(ary) {
343
343
  //
344
344
  // `constructor`
345
345
  sudo.Container = function() {
346
- sudo.Base.call(this);
347
- this.children = [];
348
- this.childNames = {};
346
+ sudo.Base.call(this);
347
+ this.children = [];
348
+ this.childNames = {};
349
349
  };
350
350
  // Container is a subclass of sudo.Base
351
351
  sudo.inherit(sudo.Base, sudo.Container);
@@ -358,16 +358,16 @@ sudo.inherit(sudo.Base, sudo.Container);
358
358
  // `param` {String} `name`. An optional name for the child that will go in the childNames hash.
359
359
  // `returns` {Object} `this`
360
360
  sudo.Container.prototype.addChild = function addChild(child, name) {
361
- var c = this.children;
362
- child.parent = this;
363
- child.index = c.length;
364
- if(name) {
365
- child.name = name;
366
- this.childNames[name] = child.index;
367
- }
368
- c.push(child);
369
- if('addedToParent' in child) child.addedToParent(this);
370
- return this;
361
+ var c = this.children;
362
+ child.parent = this;
363
+ child.index = c.length;
364
+ if(name) {
365
+ child.name = name;
366
+ this.childNames[name] = child.index;
367
+ }
368
+ c.push(child);
369
+ if('addedToParent' in child) child.addedToParent(this);
370
+ return this;
371
371
  };
372
372
  // ###bubble
373
373
  // By default, `bubble` returns the current view's parent (if it has one)
@@ -382,8 +382,8 @@ sudo.Container.prototype.bubble = function bubble() {return this.parent;};
382
382
  // `param` {String|Number} `id`. The string `name` or numeric `index` of the child to fetch.
383
383
  // `returns` {Object|undefined} The found child
384
384
  sudo.Container.prototype.getChild = function getChild(id) {
385
- return typeof id === 'string' ? this.children[this.childNames[id]] :
386
- this.children[id];
385
+ return typeof id === 'string' ? this.children[this.childNames[id]] :
386
+ this.children[id];
387
387
  };
388
388
  // ###_indexChildren_
389
389
  // Method is called with the `index` property of a subview that is being removed.
@@ -391,12 +391,12 @@ sudo.Container.prototype.getChild = function getChild(id) {
391
391
  // `param` {Number} `i`
392
392
  // `private`
393
393
  sudo.Container.prototype._indexChildren_ = function _indexChildren_(i) {
394
- var c = this.children, obj = this.childNames, len;
395
- for (len = c.length; i < len; i++) {
396
- c[i].index--;
397
- // adjust any entries in childNames
398
- if(c[i].name in obj) obj[c[i].name] = c[i].index;
399
- }
394
+ var c = this.children, obj = this.childNames, len;
395
+ for (len = c.length; i < len; i++) {
396
+ c[i].index--;
397
+ // adjust any entries in childNames
398
+ if(c[i].name in obj) obj[c[i].name] = c[i].index;
399
+ }
400
400
  };
401
401
  // ###removeChild
402
402
  // Find the intended child from my list of children and remove it, removing the name reference and re-indexing
@@ -408,21 +408,21 @@ sudo.Container.prototype._indexChildren_ = function _indexChildren_(i) {
408
408
  // An object will be assumed to be an actual sudo Class Object.
409
409
  // `returns` {Object} `this`
410
410
  sudo.Container.prototype.removeChild = function removeChild(arg) {
411
- var i, t = typeof arg, c;
412
- // normalize the input
413
- if(t === 'object') c = arg;
414
- else c = t === 'string' ? this.children[this.childNames[arg]] : this.children[arg];
415
- i = c.index;
416
- // remove from the children Array
417
- this.children.splice(i, 1);
418
- // remove from the named child hash if present
419
- delete this.childNames[c.name];
420
- // child is now an `orphan`
421
- delete c.parent;
411
+ var i, t = typeof arg, c;
412
+ // normalize the input
413
+ if(t === 'object') c = arg;
414
+ else c = t === 'string' ? this.children[this.childNames[arg]] : this.children[arg];
415
+ i = c.index;
416
+ // remove from the children Array
417
+ this.children.splice(i, 1);
418
+ // remove from the named child hash if present
419
+ delete this.childNames[c.name];
420
+ // child is now an `orphan`
421
+ delete c.parent;
422
422
  delete c.index;
423
423
  delete c.name;
424
- this._indexChildren_(i);
425
- return this;
424
+ this._indexChildren_(i);
425
+ return this;
426
426
  };
427
427
  // ###removeChildren
428
428
  // Remove all children, name references and adjust indexes accordingly.
@@ -430,19 +430,19 @@ sudo.Container.prototype.removeChild = function removeChild(arg) {
430
430
  //
431
431
  // `returns` {object} `this`
432
432
  sudo.Container.prototype.removeChildren = function removeChildren() {
433
- while(this.children.length) {
434
- this.children.shift().removeFromParent();
435
- }
436
- return this;
433
+ while(this.children.length) {
434
+ this.children.shift().removeFromParent();
435
+ }
436
+ return this;
437
437
  };
438
438
  // ###removeFromParent
439
439
  // Remove this object from its parents list of children.
440
440
  // Does not alter the dom - do that yourself by overriding this method
441
441
  // or chaining method calls
442
442
  sudo.Container.prototype.removeFromParent = function removeFromParent() {
443
- // will error without a parent, but that would be your fault...
444
- this.parent.removeChild(this);
445
- return this;
443
+ // will error without a parent, but that would be your fault...
444
+ this.parent.removeChild(this);
445
+ return this;
446
446
  };
447
447
  sudo.Container.prototype.role = 'container';
448
448
  // ###send
@@ -463,31 +463,31 @@ sudo.Container.prototype.role = 'container';
463
463
  // Any other args will be passed to the sendMethod after `this`
464
464
  // `returns` {Object} `this`
465
465
  sudo.Container.prototype.send = function send(/*args*/) {
466
- var args = Array.prototype.slice.call(arguments),
467
- d = this.model && this.model.data, meth, targ, fn;
468
- // normalize the input, common use cases first
469
- if(d && 'sendMethod' in d) meth = d.sendMethod;
470
- else if(typeof args[0] === 'string') meth = args.shift();
471
- // less common but viable options
472
- if(!meth) {
473
- // passed as a jquery custom data attr bound in events
474
- meth = 'data' in args[0] ? args[0].data.sendMethod :
475
- // passed in a hash from something or not passed at all
476
- args[0].sendMethod || void 0;
477
- }
478
- // target is either specified or my parent
479
- targ = d && d.sendTarget || this.bubble();
480
- // obvious chance for errors here, don't be dumb
481
- fn = targ[meth];
482
- while(!fn && (targ = targ.bubble())) {
483
- fn = targ[meth];
484
- }
485
- // sendMethods expect a signature (sender, ...)
486
- if(fn) {
487
- args.unshift(this);
488
- fn.apply(targ, args);
489
- }
490
- return this;
466
+ var args = Array.prototype.slice.call(arguments),
467
+ d = this.model && this.model.data, meth, targ, fn;
468
+ // normalize the input, common use cases first
469
+ if(d && 'sendMethod' in d) meth = d.sendMethod;
470
+ else if(typeof args[0] === 'string') meth = args.shift();
471
+ // less common but viable options
472
+ if(!meth) {
473
+ // passed as a jquery custom data attr bound in events
474
+ meth = 'data' in args[0] ? args[0].data.sendMethod :
475
+ // passed in a hash from something or not passed at all
476
+ args[0].sendMethod || void 0;
477
+ }
478
+ // target is either specified or my parent
479
+ targ = d && d.sendTarget || this.bubble();
480
+ // obvious chance for errors here, don't be dumb
481
+ fn = targ[meth];
482
+ while(!fn && (targ = targ.bubble())) {
483
+ fn = targ[meth];
484
+ }
485
+ // sendMethods expect a signature (sender, ...)
486
+ if(fn) {
487
+ args.unshift(this);
488
+ fn.apply(targ, args);
489
+ }
490
+ return this;
491
491
  };
492
492
  // ##View Class Object
493
493
 
@@ -509,14 +509,14 @@ sudo.Container.prototype.send = function send(/*args*/) {
509
509
  //
510
510
  // `constructor`
511
511
  sudo.View = function(el, data) {
512
- sudo.Container.call(this);
513
- // allow model instance to be passed in as well
514
- if(data) {
515
- this.model = data.role === 'model' ? data :
516
- this.model = new sudo.Model(data);
517
- }
518
- this.setEl(el);
519
- if(this.role === 'view') this.init();
512
+ sudo.Container.call(this);
513
+ // allow model instance to be passed in as well
514
+ if(data) {
515
+ this.model = data.role === 'model' ? data :
516
+ this.model = new sudo.Model(data);
517
+ }
518
+ this.setEl(el);
519
+ if(this.role === 'view') this.init();
520
520
  };
521
521
  // View inherits from Container
522
522
  // `private`
@@ -527,16 +527,16 @@ sudo.inherit(sudo.Container, sudo.View);
527
527
  //
528
528
  // `returns` {Object} `this`
529
529
  sudo.View.prototype.becomePremier = function becomePremier() {
530
- var p, f = function() {
531
- this.isPremier = true;
532
- sudo.premier = this;
533
- }.bind(this);
534
- // is there an existing premier that isn't me?
535
- if((p = sudo.premier) && p.uid !== this.uid) {
536
- // ask it to resign and call the cb
537
- p.resignPremier(f);
538
- } else f(); // no existing premier
539
- return this;
530
+ var p, f = function() {
531
+ this.isPremier = true;
532
+ sudo.premier = this;
533
+ }.bind(this);
534
+ // is there an existing premier that isn't me?
535
+ if((p = sudo.premier) && p.uid !== this.uid) {
536
+ // ask it to resign and call the cb
537
+ p.resignPremier(f);
538
+ } else f(); // no existing premier
539
+ return this;
540
540
  };
541
541
  // ###init
542
542
  // A 'contruction-time' hook to call for further initialization needs in
@@ -545,13 +545,13 @@ sudo.View.prototype.init = $.noop;
545
545
  // the el needs to be normalized before use
546
546
  // `private`
547
547
  sudo.View.prototype._normalizedEl_ = function _normalizedEl_(el) {
548
- if(typeof el === 'string') {
549
- return $(el);
550
- } else {
551
- // Passed an already `jquerified` Element?
552
- // It will have a length of 1 if so.
553
- return el.length ? el : $(el);
554
- }
548
+ if(typeof el === 'string') {
549
+ return $(el);
550
+ } else {
551
+ // Passed an already `jquerified` Element?
552
+ // It will have a length of 1 if so.
553
+ return el.length ? el : $(el);
554
+ }
555
555
  };
556
556
  // ### resignPremier
557
557
  // Resign premier status
@@ -560,15 +560,15 @@ sudo.View.prototype._normalizedEl_ = function _normalizedEl_(el) {
560
560
  // after resigning premier status.
561
561
  // `returns` {Object} `this`
562
562
  sudo.View.prototype.resignPremier = function resignPremier(cb) {
563
- var p;
564
- this.isPremier = false;
565
- // only remove the global premier if it is me
566
- if((p = sudo.premier) && p.uid === this.uid) {
567
- sudo.premier = null;
568
- }
569
- // fire the cb if passed
570
- if(cb) cb();
571
- return this;
563
+ var p;
564
+ this.isPremier = false;
565
+ // only remove the global premier if it is me
566
+ if((p = sudo.premier) && p.uid === this.uid) {
567
+ sudo.premier = null;
568
+ }
569
+ // fire the cb if passed
570
+ if(cb) cb();
571
+ return this;
572
572
  };
573
573
  // `private`
574
574
  sudo.View.prototype.role = 'view';
@@ -580,16 +580,16 @@ sudo.View.prototype.role = 'view';
580
580
  // `param` {string=|element} `el`
581
581
  // `returns` {Object} `this`
582
582
  sudo.View.prototype.setEl = function setEl(el) {
583
- var d = this.model && this.model.data, a, t;
584
- if(!el) {
585
- // normalize any relevant data
586
- t = d ? d.tagName || 'div': 'div';
587
- this.$el = $(document.createElement(t));
588
- if(d && (a = d.attributes)) this.$el.attr(a);
589
- } else {
590
- this.$el = this._normalizedEl_(el);
591
- }
592
- return this;
583
+ var d = this.model && this.model.data, a, t;
584
+ if(!el) {
585
+ // normalize any relevant data
586
+ t = d ? d.tagName || 'div': 'div';
587
+ this.$el = $(document.createElement(t));
588
+ if(d && (a = d.attributes)) this.$el.attr(a);
589
+ } else {
590
+ this.$el = this._normalizedEl_(el);
591
+ }
592
+ return this;
593
593
  };
594
594
  // ###this.$
595
595
  // Return a single Element matching `sel` scoped to this View's el.
@@ -598,7 +598,511 @@ sudo.View.prototype.setEl = function setEl(el) {
598
598
  // `param` {string} `sel`. A jQuery compatible selector
599
599
  // `returns` {jQuery} A 'jquerified' result matching the selector
600
600
  sudo.View.prototype.$ = function(sel) {
601
- return this.$el.find(sel);
601
+ return this.$el.find(sel);
602
+ };
603
+ // ##ViewController Class Object
604
+
605
+ // ViewControllers were designed for Rails projects for 2 specific use-cases:
606
+ //
607
+ // 1. ViewControllers can instantiate any `descriptors` found in their model
608
+ // when constructing, adding them as `child` objects. Why? Sometimes a 'partial' will
609
+ // need to define a javascript object that should, by design, be the child of a parent View
610
+ // that is itself defined on the Rails view that owns the 'partial'. Since any JS introduced
611
+ // by a partial will be parsed before the JS on its parent Rails View this usually isn't possible.
612
+ // Our solution? Pushing `Descriptor objects` (see docs) into an array (somewhere in your namespace) from a
613
+ // 'partial' and then passing a reference to that array into the ViewController as 'descriptors'
614
+ // in its optional data argument when instantiated. The ViewController will then iterate over those
615
+ // and instantiate them, adding them as children as it goes (also setting up any stated observers)
616
+ //
617
+ // 2. ViewControllers also abstract away connecting UJS style events by allowing the developer to
618
+ // pass in the name(s) of any desired UJS events to observe: `ujsEvent: ajax:success` for example,
619
+ // and expect that a method named onAjaxSuccess, if present on the ViewController, will be called
620
+ // with the arguments returned by the UJS plugin*
621
+ //
622
+ // `param` {string|element} `el`. Otional el for the View instance.
623
+ // `param` {object} `data`. Optional data object.
624
+ //
625
+ // `see` sudo.View.
626
+ //
627
+ // `constructor`
628
+ sudo.ViewController = function(el, data) {
629
+ sudo.View.call(this, el, data);
630
+ // map the names of events to methods we expect to proxy to
631
+ this.eventMap = {
632
+ 'ajax:before': 'onAjaxBefore',
633
+ 'ajax:beforeSend': 'onAjaxBeforeSend',
634
+ 'ajax:success': 'onAjaxSuccess',
635
+ 'ajax:error': 'onAjaxError',
636
+ 'ajax:complete': 'onAjaxComplete',
637
+ 'ajax:aborted:required': 'onAjaxAbortedRequired',
638
+ 'ajax:aborted:file': 'onAjaxAbortedFile'
639
+ };
640
+ // can be called again if mapping changes...
641
+ if(data) {
642
+ this.doMapping();
643
+ if('descriptor' in data) this.instantiateChildren([data.descriptor]);
644
+ else if('descriptors' in data) this.instantiateChildren();
645
+ }
646
+ if(this.role === 'viewController') this.init();
647
+ };
648
+ // ViewController inherits from View.
649
+ // `private`
650
+ sudo.inherit(sudo.View, sudo.ViewController);
651
+ // ###doMapping
652
+ //
653
+ // assign the proxy mapping for events. This can be called at any time
654
+ // if the listened for events change
655
+ //
656
+ // `returns` {Object} `this`
657
+ sudo.ViewController.prototype.doMapping = function() {
658
+ // either a single event or an array of them
659
+ var i,
660
+ toMap = this.model.data.ujsEvent || this.model.data.ujsEvents;
661
+ if(toMap) {
662
+ if(typeof toMap === 'string') this._mapEvent_(toMap);
663
+ else {
664
+ for(i = 0; i < toMap.length; i++) {
665
+ this._mapEvent_(toMap[i]);
666
+ }
667
+ }
668
+ }
669
+ return this;
670
+ };
671
+ // ###_handleObserve_
672
+ // Helper for instantiateChildren
673
+ // `private`
674
+ sudo.ViewController.prototype._handleObserve_ = function _handleObserve_(obs, c) {
675
+ var obj = obs.object ? this._objectForPath_(obs.object) : this.model;
676
+ obj.observe(c[obs.cb].bind(c));
677
+ };
678
+ // ###instantiateChildren
679
+ // instantiate the children described in the passed in array or the `descriptors` array
680
+ // set in this object's data store
681
+ //
682
+ // `returns` {object} `this`
683
+ sudo.ViewController.prototype.instantiateChildren = function instantiateChildren(ary) {
684
+ var i, j, curr, c, d = ary || this.model.data.descriptors;
685
+ for(i = 0; i < d.length; i++) {
686
+ curr = d[i];
687
+ c = new curr.is_a(curr.el, curr.data);
688
+ this.addChild(c, curr.name);
689
+ // handle any observe(s)
690
+ if('observe' in curr) {
691
+ this._handleObserve_(curr.observe, c);
692
+ }
693
+ else if('observes' in curr) {
694
+ for(j = 0; j < curr.observes.length; j++) {
695
+ this._handleObserve_(curr.observes[j], c);
696
+ }
697
+ }
698
+ }
699
+ return this;
700
+ };
701
+ // ###_mapEvent_
702
+ // Maps the ajax:event names to methods
703
+ // `private`
704
+ sudo.ViewController.prototype._mapEvent_ = function _mapEvent_(name) {
705
+ // because the signatures vary we need specific methods
706
+ this.$el.on(name, this[this.eventMap[name]].bind(this));
707
+ };
708
+ // ###_objectForPath_
709
+ // The objects used for callbacks and connections need to be
710
+ // looked-up via a key-path like address as they likely will not exist
711
+ // when viewController's are instantiated.
712
+ // `private`
713
+ sudo.ViewController.prototype._objectForPath_ = function _objectForPath_(path) {
714
+ return sudo.getPath(path, window);
715
+ };
716
+ // Virtual methods to override in your child classes for
717
+ // any events you chose to listen for
718
+ sudo.ViewController.prototype.onAjaxAbortedFile = $.noop;
719
+ sudo.ViewController.prototype.onAjaxAbortedRequired = $.noop;
720
+ sudo.ViewController.prototype.onAjaxBefore = $.noop;
721
+ sudo.ViewController.prototype.onAjaxBeforeSend = $.noop;
722
+ sudo.ViewController.prototype.onAjaxComplete = $.noop;
723
+ sudo.ViewController.prototype.onAjaxSuccess = $.noop;
724
+ sudo.ViewController.prototype.onAjaxError = $.noop;
725
+ // `private`
726
+ sudo.ViewController.prototype.role = 'viewController';
727
+ // ###Templating
728
+
729
+ // Allow the default {{ js code }}, {{= key }}, and {{- escape stuff }}
730
+ // micro templating delimiters to be overridden if desired
731
+ //
732
+ // `type` {Object}
733
+ sudo.templateSettings = {
734
+ evaluate: /\{\{([\s\S]+?)\}\}/g,
735
+ interpolate: /\{\{=([\s\S]+?)\}\}/g,
736
+ escape: /\{\{-([\s\S]+?)\}\}/g
737
+ };
738
+ // Certain characters need to be escaped so that they can be put
739
+ // into a string literal when templating.
740
+ //
741
+ // `type` {Object}
742
+ sudo.escapes = {};
743
+ (function(s) {
744
+ var e = {
745
+ '\\': '\\',
746
+ "'": "'",
747
+ r: '\r',
748
+ n: '\n',
749
+ t: '\t',
750
+ u2028: '\u2028',
751
+ u2029: '\u2029'
752
+ };
753
+ for (var key in e) s.escapes[e[key]] = key;
754
+ }(sudo));
755
+ // lookup hash for `escape`
756
+ //
757
+ // `type` {Object}
758
+ sudo.htmlEscapes = {
759
+ '&': '&amp;',
760
+ '<': '&lt;',
761
+ '>': '&gt;',
762
+ '"': '&quot;',
763
+ "'": '&#x27;',
764
+ '/': '&#x2F;'
765
+ };
766
+ // Escapes certain characters for templating
767
+ //
768
+ // `type` {regexp}
769
+ sudo.escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
770
+ // Escape unsafe HTML
771
+ //
772
+ // `type` {regexp}
773
+ sudo.htmlEscaper = /[&<>"'\/]/g;
774
+ // Unescapes certain characters for templating
775
+ //
776
+ // `type` {regexp}
777
+ sudo.unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g;
778
+ // ###escape
779
+ // Remove unsafe characters from a string
780
+ //
781
+ // `param` {String} str
782
+ sudo.escape = function(str) {
783
+ return str.replace(sudo.htmlEscaper, function(match) {
784
+ return sudo.htmlEscapes[match];
785
+ });
786
+ };
787
+ // ###unescape
788
+ // Within an interpolation, evaluation, or escaping,
789
+ // remove HTML escaping that had been previously added.
790
+ //
791
+ // `param` {string} str
792
+ sudo.unescape = function unescape(str) {
793
+ return str.replace(sudo.unescaper, function(match, escape) {
794
+ return sudo.escapes[escape];
795
+ });
796
+ };
797
+ // ###template
798
+ // JavaScript micro-templating, similar to John Resig's (and it's offspring) implementation.
799
+ // sudo templating preserves whitespace, and correctly escapes quotes within interpolated code.
800
+ // Unlike others sudo.template requires a scope name (to avoid the use of `with`) and will spit at you
801
+ // if it is not present.
802
+ //
803
+ // `param` {string} `str`. The 'templated' string.
804
+ // `param` {Object} `data`. Optional hash of key:value pairs.
805
+ // `param` {string} `scope`. Optional context name of your `data object`, set to 'data' if falsy.
806
+ sudo.template = function template(str, data, scope) {
807
+ scope || (scope = 'data');
808
+ var settings = sudo.templateSettings, render, template,
809
+ // Compile the template source, taking care to escape characters that
810
+ // cannot be included in a string literal and then unescape them in code blocks.
811
+ source = "_p+='" + str.replace(sudo.escaper, function(match) {
812
+ return '\\' + sudo.escapes[match];
813
+ }).replace(settings.escape, function(match, code) {
814
+ return "'+\n((_t=(" + sudo.unescape(code) + "))==null?'':sudo.escape(_t))+\n'";
815
+ }).replace(settings.interpolate, function(match, code) {
816
+ return "'+\n((_t=(" + sudo.unescape(code) + "))==null?'':_t)+\n'";
817
+ }).replace(settings.evaluate, function(match, code) {
818
+ return "';\n" + sudo.unescape(code) + "\n_p+='";
819
+ }) + "';\n";
820
+ source = "var _t,_p='';" + source + "return _p;\n";
821
+ render = new Function(scope, source);
822
+ if (data) return render(data);
823
+ template = function(data) {
824
+ return render.call(this, data);
825
+ };
826
+ // Provide the compiled function source as a convenience for reflection/compilation
827
+ template.source = 'function(' + scope + '){\n' + source + '}';
828
+ return template;
829
+ };
830
+ // ##DataView Class Object
831
+
832
+ // Create an instance of an Object, inheriting from sudo.View that:
833
+ // 1. Expects to have a template located in its internal data Store accessible via `this.get('template')`.
834
+ // 2. Can have a `renderTarget` property in its data store. If so this will be the location
835
+ // the child injects itself into (if not already in) the DOM
836
+ // 3. Can have a 'renderMethod' property in its data store. If so this is the jQuery method
837
+ // that the child will use to place itself in it's `renderTarget`.
838
+ // 4. Has a `render` method that when called re-hydrates it's $el by passing its
839
+ // internal data store to its template
840
+ // 5. Handles event binding/unbinding by implementing the sudo.extensions.listener
841
+ // extension object
842
+ //
843
+ //`constructor`
844
+ sudo.DataView = function(el, data) {
845
+ sudo.View.call(this, el, data);
846
+ // implements the listener extension
847
+ $.extend(this, sudo.extensions.listener);
848
+ // dataview's models are observable, make it so if not already
849
+ if(!this.model.observe) $.extend(this.model, sudo.extensions.observable);
850
+ // dont autoRender on the setting of events,
851
+ // add to this to prevent others if needed
852
+ this.autoRenderBlacklist = {event: true, events: true};
853
+ // if autorendering, observe your own model
854
+ // use this ref to unobserve if desired
855
+ if(this.model.data.autoRender) this.observer = this.model.observe(this.render.bind(this));
856
+ this.build();
857
+ this.bindEvents();
858
+ if(this.role === 'dataview') this.init();
859
+ };
860
+ // `private`
861
+ sudo.inherit(sudo.View, sudo.DataView);
862
+ // ###addedToParent
863
+ // Container's will check for the presence of this method and call it if it is present
864
+ // after adding a child - essentially, this will auto render the dataview when added to a parent
865
+ sudo.DataView.prototype.addedToParent = function(parent) {
866
+ return this.render();
867
+ };
868
+ // ###build
869
+ // Construct the innerHTML of the $el here so that the behavior of the
870
+ // DataView, that the markup is ready after a subclass calls `this.construct`,
871
+ // is the same as other View classes -- IF there is a template available
872
+ // there may not be yet as some get added later by a ViewController
873
+ sudo.DataView.prototype.build = function build() {
874
+ var t;
875
+ if(!(t = this.model.data.template)) return;
876
+ if(typeof t === 'string') t = sudo.template(t);
877
+ this.$el.html(t(this.model.data));
878
+ this.built = true;
879
+ return this;
880
+ };
881
+ // ###removeFromParent
882
+ // Remove this object from the DOM and its parent's list of children.
883
+ // Overrides `sudo.View.removeFromParent` to actually remove the DOM as well
884
+ //
885
+ // `returns` {Object} `this`
886
+ sudo.DataView.prototype.removeFromParent = function removeFromParent() {
887
+ this.parent.removeChild(this);
888
+ this.$el.remove();
889
+ return this;
890
+ };
891
+ // ###render
892
+ // (Re)hydrate the innerHTML of this object via its template and internal data store.
893
+ // If a `renderTarget` is present this Object will inject itself into the target via
894
+ // `this.get('renderMethod')` or defualt to `$.append`. After injection, the `renderTarget`
895
+ // is deleted from this Objects data store.
896
+ // Event unbinding/rebinding is generally not necessary for the Objects innerHTML as all events from the
897
+ // Object's list of events (`this.get('event(s)'))` are delegated to the $el on instantiation.
898
+ //
899
+ // `param` {object} `change` dataviews may be observing their model if `autoRender: true`
900
+ //
901
+ // `returns` {Object} `this`
902
+ sudo.DataView.prototype.render = function render(change) {
903
+ var d;
904
+ // return early if a `blacklisted` key is set to my model
905
+ if(change && this.autoRenderBlacklist[change.name]) return this;
906
+ d = this.model.data;
907
+ // has `build` been executed already? If not call it again
908
+ if(!this.built) this.build();
909
+ // if there is no template by this point *you are doing it wrong*
910
+ // erase the flag
911
+ else this.built = false;
912
+ if(d.renderTarget) {
913
+ this._normalizedEl_(d.renderTarget)[d.renderMethod || 'append'](this.$el);
914
+ delete d.renderTarget;
915
+ }
916
+ return this;
917
+ };
918
+ // `private`
919
+ sudo.DataView.prototype.role = 'dataview';
920
+ // ##Navigator Class Object
921
+
922
+ // Abstracts location and history events, parsing their information into a
923
+ // normalized object that is then set to an Observable class instance
924
+ //
925
+ // `constructor`
926
+ sudo.Navigator = function(data) {
927
+ this.started = false;
928
+ this.slashStripper = /^\/+|\/+$/g;
929
+ this.leadingStripper = /^[#\/]|\s+$/g;
930
+ this.trailingStripper = /\/$/;
931
+ this.construct(data);
932
+ };
933
+ // Navigator inherits from `sudo.Model`
934
+ sudo.Navigator.prototype = Object.create(sudo.Model.prototype);
935
+ // ###getFragment
936
+ // 'Fragment' is defined as any URL information after the 'root' path
937
+ // including the `search` or `hash`
938
+ //
939
+ // `returns` {String} `fragment`
940
+ // `returns` {String} the normalized current fragment
941
+ sudo.Navigator.prototype.getFragment = function getFragment(fragment) {
942
+ var root = this.data.root;
943
+ if(!fragment) {
944
+ // intentional use of coersion
945
+ if (this.isPushState) {
946
+ fragment = window.location.pathname;
947
+ root = root.replace(this.trailingStripper, '');
948
+ if(!fragment.indexOf(root)) fragment = fragment.substr(root.length);
949
+ } else {
950
+ fragment = this.getHash();
951
+ }
952
+ }
953
+ return decodeURIComponent(fragment.replace(this.leadingStripper, ''));
954
+ };
955
+ // ###getHash
956
+ // Check either the passed in fragment, or the full location.href
957
+ // for a `hash` value
958
+ //
959
+ // `param` {string} `fragment` Optional fragment to check
960
+ // `returns` {String} the normalized current `hash`
961
+ sudo.Navigator.prototype.getHash = function getHash(fragment) {
962
+ fragment || (fragment = window.location.href);
963
+ var match = fragment.match(/#(.*)$/);
964
+ return match ? match[1] : '';
965
+ };
966
+ // ###getSearch
967
+ // Check either the passed in fragment, or the full location.href
968
+ // for a `search` value
969
+ //
970
+ // `param` {string} `fragment` Optional fragment to check
971
+ // `returns` {String} the normalized current `search`
972
+ sudo.Navigator.prototype.getSearch = function getSearch(fragment) {
973
+ fragment || (fragment = window.location.href);
974
+ var match = fragment.match(/\?(.*)$/);
975
+ return match ? match[1] : '';
976
+ };
977
+ // ###getUrl
978
+ // fetch the URL in the form <root + fragment>
979
+ //
980
+ // `returns` {String}
981
+ sudo.Navigator.prototype.getUrl = function getUrl() {
982
+ // note that delegate(_role_) returns the deleagte
983
+ return this.data.root + this.data.fragment;
984
+ };
985
+ // ###go
986
+ // If the passed in 'fragment' is different than the currently stored one,
987
+ // push a new state entry / hash event and set the data where specified
988
+ //
989
+ // `param` {string} `fragment`
990
+ // `returns` {*} call to `setData`
991
+ sudo.Navigator.prototype.go = function go(fragment) {
992
+ if(!this.started) return false;
993
+ if(!this.urlChanged(fragment)) return;
994
+ // TODO ever use replaceState?
995
+ if(this.isPushState) {
996
+ window.history.pushState({}, document.title, this.getUrl());
997
+ } else if(this.isHashChange) {
998
+ window.location.hash = '#' + this.data.fragment;
999
+ }
1000
+ return this.setData();
1001
+ };
1002
+ // ###handleChange
1003
+ // Bound to either the `popstate` or `hashchange` events, if the
1004
+ // URL has indeed changed then parse the relevant data and set it -
1005
+ // triggering change observers
1006
+ //
1007
+ // `returns` {*} call to `setData` or undefined
1008
+ sudo.Navigator.prototype.handleChange = function handleChange(e) {
1009
+ if(this.urlChanged()) {
1010
+ return this.setData();
1011
+ }
1012
+ };
1013
+ // ###parseQuery
1014
+ // Parse and return a hash of the key value pairs contained in
1015
+ // the current `query`
1016
+ //
1017
+ // `returns` {object}
1018
+ sudo.Navigator.prototype.parseQuery = function parseQuery() {
1019
+ var obj = {}, seg = this.data.query,
1020
+ i, s;
1021
+ if(seg) {
1022
+ seg = seg.split('&');
1023
+ for(i = 0; i < seg.length; i++) {
1024
+ if(!seg[i]) continue;
1025
+ s = seg[i].split('=');
1026
+ obj[s[0]] = s[1];
1027
+ }
1028
+ return obj;
1029
+ }
1030
+ };
1031
+ // ###setData
1032
+ // Using the current `fragment` (minus any search or hash data) as a key,
1033
+ // use `parseQuery` as the value for the key, setting it into the specified
1034
+ // model (a stated `Observable` or `this.data`)
1035
+ //
1036
+ // `returns` {object} `this`
1037
+ sudo.Navigator.prototype.setData = function setData() {
1038
+ var frag = this.data.fragment,
1039
+ // data is set in a specified model or in self
1040
+ observable = this.data.observable || this;
1041
+ if(this.data.query) {
1042
+ // we want to set the key minus any search/hash
1043
+ frag = frag.indexOf('?') !== -1 ? frag.split('?')[0] : frag.split('#')[0];
1044
+ }
1045
+ observable.set(frag, this.parseQuery());
1046
+ return this;
1047
+ };
1048
+ // ###start
1049
+ // Gather the necessary information about the current environment and
1050
+ // bind to either (push|pop)state or hashchange.
1051
+ // Also, if given an imcorrect URL for the current environment (hashchange
1052
+ // vs pushState) normalize it and set accordingly (or don't).
1053
+ //
1054
+ // `returns` {object} `this`
1055
+ sudo.Navigator.prototype.start = function start() {
1056
+ var hasPushState, atRoot, loc, tmp;
1057
+ if(this.started) return;
1058
+ hasPushState = window.history && window.history.pushState;
1059
+ this.started = true;
1060
+ // setup the initial configuration
1061
+ this.isHashChange = this.data.useHashChange && 'onhashchange' in window ||
1062
+ (!hasPushState && 'onhashchange' in window);
1063
+ this.isPushState = !this.isHashChange && !!hasPushState;
1064
+ // normalize the root to always contain a leading and trailing slash
1065
+ this.data['root'] = ('/' + this.data['root'] + '/').replace(this.slashStripper, '/');
1066
+ // Get a snapshot of the current fragment
1067
+ this.urlChanged();
1068
+ // monitor URL changes via popState or hashchange
1069
+ if (this.isPushState) {
1070
+ $(window).on('popstate', this.handleChange.bind(this));
1071
+ } else if (this.isHashChange) {
1072
+ $(window).on('hashchange', this.handleChange.bind(this));
1073
+ } else return;
1074
+ atRoot = window.location.pathname.replace(/[^\/]$/, '$&/') === this.data['root'];
1075
+ // somehow a URL got here not in my 'format', unless explicitly told not too, correct this
1076
+ if(!this.data.stay) {
1077
+ if(this.isHashChange && !atRoot) {
1078
+ window.location.replace(this.data['root'] + window.location.search + '#' +
1079
+ this.data.fragment);
1080
+ // return early as browser will redirect
1081
+ return true;
1082
+ // the converse of the above
1083
+ } else if(this.isPushState && atRoot && window.location.hash) {
1084
+ tmp = this.getHash().replace(this.leadingStripper, '');
1085
+ window.history.replaceState({}, document.title, this.data['root'] +
1086
+ tmp + window.location.search);
1087
+ }
1088
+ }
1089
+ // TODO provide option to `go` from inital `start` state?
1090
+ return this;
1091
+ };
1092
+ // ###urlChanged
1093
+ // Is a passed in fragment different from the one currently set at `this.get('fragment')`?
1094
+ // If so set the fragment to the passed fragment passed in (as well as any 'query' data), else
1095
+ // simply return false
1096
+ //
1097
+ // `param` {String} `fragment`
1098
+ // `returns` {bool}
1099
+ sudo.Navigator.prototype.urlChanged = function urlChanged(fragment) {
1100
+ var current = this.getFragment(fragment);
1101
+ // nothing has changed
1102
+ if (current === this.data.fragment) return false;
1103
+ this.data.fragment = current;
1104
+ this.data.query = this.getSearch(current) || this.getHash(current);
1105
+ return true;
602
1106
  };
603
1107
  // ## Observable Extension Object
604
1108
  //
@@ -606,226 +1110,710 @@ sudo.View.prototype.$ = function(sel) {
606
1110
  // Extend a `sudo.Model` class with this object if
607
1111
  // data-mutation-observation is required
608
1112
  sudo.extensions.observable = {
609
- // ###_deliver_
610
- // Called from deliverChangeRecords when ready to send
611
- // changeRecords to observers.
612
- //
613
- // `private`
614
- _deliver_: function _deliver_(obj) {
615
- var i, cb = this.callbacks;
616
- for(i = 0; i < cb.length; i++) {
617
- cb[i](obj);
618
- }
619
- },
620
- // ###deliverChangeRecords
621
- // Iterate through the changeRecords array(emptying it as you go), delivering them to the
622
- // observers. You can override this method to change the standard delivery behavior.
623
- //
624
- // `returns` {Object} `this`
625
- deliverChangeRecords: function deliverChangeRecords() {
626
- var rec, cr = this.changeRecords;
627
- // FIFO
628
- for(rec; cr.length && (rec = cr.shift());) {
629
- this._deliver_(rec);
630
- }
631
- return this;
632
- },
633
- // ###observe
634
- // In a quasi-ES6 Object.observe pattern, calling observe on an `observable` and
635
- // passing a callback will cause that callback to be called whenever any
636
- // property on the observable's data store is set, changed or deleted
637
- // via set, unset, setPath or unsetPath with an object containing:
638
- // {
639
- // type: <new, updated, deleted>,
640
- // object: <the object being observed>,
641
- // name: <the key that was modified>,
642
- // oldValue: <if a previous value existed for this key>
643
- // }
644
- // For ease of 'unobserving' the same Function passed in is returned.
645
- //
646
- // `param` {Function} `fn` The callback to be called with changeRecord(s)
647
- // `returns` {Function} the Function passed in as an argument
648
- observe: function observe(fn) {
649
- // this will fail if mixed-in and no `callbacks` created so don't do that.
650
- // Per the spec, do not allow the same callback to be added
651
- var d = this.callbacks;
652
- if(d.indexOf(fn) === -1) d.push(fn);
653
- return fn;
654
- },
655
- // ###observes
656
- // Allow an array of callbacks to be registered as changeRecord recipients
657
- //
658
- // `param` {Array} ary
659
- // `returns` {Array} the Array passed in to observe
660
- observes: function observes(ary) {
661
- var i;
662
- for(i = 0; i < ary.length; i++) {
663
- this.observe(ary[i]);
664
- }
665
- return ary;
666
- },
667
- // ###set
668
- // Overrides sudo.Base.set to check for observers
669
- //
670
- // `param` {String} `key`. The name of the key
671
- // `param` {*} `value`
672
- // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
673
- // to be delivered upon a call to deliverChangeRecords (truthy)
674
- //
675
- // `returns` {Object|*} `this` or calls deliverChangeRecords
676
- set: function set(key, value, hold) {
677
- var obj = {name: key, object: this.data};
678
- // did this key exist already
679
- if(key in this.data) {
680
- obj.type = 'updated';
681
- // then there is an oldValue
682
- obj.oldValue = this.data[key];
683
- } else obj.type = 'new';
684
- // now actually set the value
685
- this.data[key] = value;
686
- this.changeRecords.push(obj);
687
- // call the observers or not
688
- if(hold) return this;
689
- return this.deliverChangeRecords();
690
- },
691
- // ###setPath
692
- // Overrides sudo.Base.setPath to check for observers.
693
- // Change records originating from a `setPath` operation
694
- // send back the passed in `path` as `name` as well as the
695
- // top level object being observed (this observable's data).
696
- // this allows for easy filtering either manually or via a
697
- // `change delegate`
698
- //
699
- // `param` {String} `path`
700
- // `param` {*} `value`
701
- // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
702
- // to be delivered upon a call to deliverChangeRecords (truthy)
703
- // `returns` {Object|*} `this` or calls deliverChangeRecords
704
- setPath: function setPath(path, value, hold) {
705
- var curr = this.data, obj = {name: path, object: this.data},
706
- p = path.split('.'), key;
707
- for (key; p.length && (key = p.shift());) {
708
- if(!p.length) {
709
- // reached the last refinement, pre-existing?
710
- if (key in curr) {
711
- obj.type = 'updated';
712
- obj.oldValue = curr[key];
713
- } else obj.type = 'new';
714
- curr[key] = value;
715
- } else if (curr[key]) {
716
- curr = curr[key];
717
- } else {
718
- curr = curr[key] = {};
719
- }
720
- }
721
- this.changeRecords.push(obj);
722
- // call all observers or not
723
- if(hold) return this;
724
- return this.deliverChangeRecords();
725
- },
726
- // ###sets
727
- // Overrides Base.sets to hold the call to _deliver_ until
728
- // all operations are done
729
- //
730
- // `returns` {Object|*} `this` or calls deliverChangeRecords
731
- sets: function sets(obj, hold) {
732
- var i, k = Object.keys(obj);
733
- for(i = 0; i < k.length; i++) {
734
- k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]], true) :
735
- this.setPath(k[i], obj[k[i]], true);
736
- }
737
- if(hold) return this;
738
- return this.deliverChangeRecords();
739
- },
740
- // ###unobserve
741
- // Remove a particular callback from this observable
742
- //
743
- // `param` {Function} the function passed in to `observe`
744
- // `returns` {Object} `this`
745
- unobserve: function unobserve(fn) {
746
- var cb = this.callbacks, i = cb.indexOf(fn);
747
- if(i !== -1) cb.splice(i, 1);
748
- return this;
749
- },
750
- // ###unobserves
751
- // Allow an array of callbacks to be unregistered as changeRecord recipients
752
- //
753
- // `param` {Array} ary
754
- // `returns` {Object} `this`
755
- unobserves: function unobserves(ary) {
756
- var i;
757
- for(i = 0; i < ary.length; i++) {
758
- this.unobserve(ary[i]);
759
- }
760
- return this;
761
- },
762
- // ###unset
763
- // Overrides sudo.Base.unset to check for observers
764
- //
765
- // `param` {String} `key`. The name of the key
766
- // `param` {Bool} `hold`
767
- //
768
- // `returns` {Object|*} `this` or calls deliverChangeRecords
769
- unset: function unset(key, hold) {
770
- var obj = {name: key, object: this.data, type: 'deleted'},
771
- val = !!this.data[key];
772
- delete this.data[key];
773
- // call the observers if there was a val to delete
774
- return this._unset_(obj, val, hold);
775
- },
776
- // ###_unset_
777
- // Helper for the unset functions
778
- //
779
- // `private`
780
- _unset_: function _unset_(o, v, h) {
781
- if(v) {
782
- this.changeRecords.push(o);
783
- if(h) return this;
784
- return this.deliverChangeRecords();
785
- }
786
- return this;
787
- },
788
- // ###setPath
789
- // Overrides sudo.Base.unsetPath to check for observers
790
- //
791
- // `param` {String} `path`
792
- // `param` {*} `value`
793
- // `param` {bool} `hold`
794
- //
795
- // `returns` {Object|*} `this` or calls deliverChangeRecords
796
- unsetPath: function unsetPath(path, hold) {
797
- var obj = {name: path, object: this.data, type: 'deleted'},
798
- curr = this.data, p = path.split('.'),
799
- key, val;
800
- for (key; p.length && (key = p.shift());) {
801
- if(!p.length) {
802
- // reached the last refinement
803
- val = !!curr[key];
804
- delete curr[key];
805
- } else {
806
- // this can obviously fail, but can be prevented by checking
807
- // with `getPath` first.
808
- curr = curr[key];
809
- }
810
- }
811
- return this._unset_(obj, val, hold);
812
- },
813
- // ###unsets
814
- // Override of Base.unsets to hold the call to _deliver_ until done
815
- //
816
- // `param` ary
817
- // `param` hold
818
- // `returns` {Object|*} `this` or calls deliverChangeRecords
819
- unsets: function unsets(ary, hold) {
820
- var i;
821
- for(i = 0; i < ary.length; i++) {
822
- ary[i].indexOf('.') === -1 ? this.unset(k[i], true) :
823
- this.unsetPath(k[i], true);
824
- }
825
- if(hold) return this;
826
- return this.deliverChangeRecords();
827
- }
1113
+ // ###_deliver_
1114
+ // Called from deliverChangeRecords when ready to send
1115
+ // changeRecords to observers.
1116
+ //
1117
+ // `private`
1118
+ _deliver_: function _deliver_(obj) {
1119
+ var i, cb = this.callbacks;
1120
+ for(i = 0; i < cb.length; i++) {
1121
+ cb[i](obj);
1122
+ }
1123
+ },
1124
+ // ###deliverChangeRecords
1125
+ // Iterate through the changeRecords array(emptying it as you go), delivering them to the
1126
+ // observers. You can override this method to change the standard delivery behavior.
1127
+ //
1128
+ // `returns` {Object} `this`
1129
+ deliverChangeRecords: function deliverChangeRecords() {
1130
+ var rec, cr = this.changeRecords;
1131
+ // FIFO
1132
+ for(rec; cr.length && (rec = cr.shift());) {
1133
+ this._deliver_(rec);
1134
+ }
1135
+ return this;
1136
+ },
1137
+ // ###observe
1138
+ // In a quasi-ES6 Object.observe pattern, calling observe on an `observable` and
1139
+ // passing a callback will cause that callback to be called whenever any
1140
+ // property on the observable's data store is set, changed or deleted
1141
+ // via set, unset, setPath or unsetPath with an object containing:
1142
+ // {
1143
+ // type: <new, updated, deleted>,
1144
+ // object: <the object being observed>,
1145
+ // name: <the key that was modified>,
1146
+ // oldValue: <if a previous value existed for this key>
1147
+ // }
1148
+ // For ease of 'unobserving' the same Function passed in is returned.
1149
+ //
1150
+ // `param` {Function} `fn` The callback to be called with changeRecord(s)
1151
+ // `returns` {Function} the Function passed in as an argument
1152
+ observe: function observe(fn) {
1153
+ // this will fail if mixed-in and no `callbacks` created so don't do that.
1154
+ // Per the spec, do not allow the same callback to be added
1155
+ var d = this.callbacks;
1156
+ if(d.indexOf(fn) === -1) d.push(fn);
1157
+ return fn;
1158
+ },
1159
+ // ###observes
1160
+ // Allow an array of callbacks to be registered as changeRecord recipients
1161
+ //
1162
+ // `param` {Array} ary
1163
+ // `returns` {Array} the Array passed in to observe
1164
+ observes: function observes(ary) {
1165
+ var i;
1166
+ for(i = 0; i < ary.length; i++) {
1167
+ this.observe(ary[i]);
1168
+ }
1169
+ return ary;
1170
+ },
1171
+ // ###set
1172
+ // Overrides sudo.Base.set to check for observers
1173
+ //
1174
+ // `param` {String} `key`. The name of the key
1175
+ // `param` {*} `value`
1176
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
1177
+ // to be delivered upon a call to deliverChangeRecords (truthy)
1178
+ //
1179
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1180
+ set: function set(key, value, hold) {
1181
+ var obj = {name: key, object: this.data};
1182
+ // did this key exist already
1183
+ if(key in this.data) {
1184
+ obj.type = 'updated';
1185
+ // then there is an oldValue
1186
+ obj.oldValue = this.data[key];
1187
+ } else obj.type = 'new';
1188
+ // now actually set the value
1189
+ this.data[key] = value;
1190
+ this.changeRecords.push(obj);
1191
+ // call the observers or not
1192
+ if(hold) return this;
1193
+ return this.deliverChangeRecords();
1194
+ },
1195
+ // ###setPath
1196
+ // Overrides sudo.Base.setPath to check for observers.
1197
+ // Change records originating from a `setPath` operation
1198
+ // send back the passed in `path` as `name` as well as the
1199
+ // top level object being observed (this observable's data).
1200
+ // this allows for easy filtering either manually or via a
1201
+ // `change delegate`
1202
+ //
1203
+ // `param` {String} `path`
1204
+ // `param` {*} `value`
1205
+ // `param` {Bool} `hold` Call _deliver_ (falsy) or store the change notification
1206
+ // to be delivered upon a call to deliverChangeRecords (truthy)
1207
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1208
+ setPath: function setPath(path, value, hold) {
1209
+ var curr = this.data, obj = {name: path, object: this.data},
1210
+ p = path.split('.'), key;
1211
+ for (key; p.length && (key = p.shift());) {
1212
+ if(!p.length) {
1213
+ // reached the last refinement, pre-existing?
1214
+ if (key in curr) {
1215
+ obj.type = 'updated';
1216
+ obj.oldValue = curr[key];
1217
+ } else obj.type = 'new';
1218
+ curr[key] = value;
1219
+ } else if (curr[key]) {
1220
+ curr = curr[key];
1221
+ } else {
1222
+ curr = curr[key] = {};
1223
+ }
1224
+ }
1225
+ this.changeRecords.push(obj);
1226
+ // call all observers or not
1227
+ if(hold) return this;
1228
+ return this.deliverChangeRecords();
1229
+ },
1230
+ // ###sets
1231
+ // Overrides Base.sets to hold the call to _deliver_ until
1232
+ // all operations are done
1233
+ //
1234
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1235
+ sets: function sets(obj, hold) {
1236
+ var i, k = Object.keys(obj);
1237
+ for(i = 0; i < k.length; i++) {
1238
+ k[i].indexOf('.') === -1 ? this.set(k[i], obj[k[i]], true) :
1239
+ this.setPath(k[i], obj[k[i]], true);
1240
+ }
1241
+ if(hold) return this;
1242
+ return this.deliverChangeRecords();
1243
+ },
1244
+ // ###unobserve
1245
+ // Remove a particular callback from this observable
1246
+ //
1247
+ // `param` {Function} the function passed in to `observe`
1248
+ // `returns` {Object} `this`
1249
+ unobserve: function unobserve(fn) {
1250
+ var cb = this.callbacks, i = cb.indexOf(fn);
1251
+ if(i !== -1) cb.splice(i, 1);
1252
+ return this;
1253
+ },
1254
+ // ###unobserves
1255
+ // Allow an array of callbacks to be unregistered as changeRecord recipients
1256
+ //
1257
+ // `param` {Array} ary
1258
+ // `returns` {Object} `this`
1259
+ unobserves: function unobserves(ary) {
1260
+ var i;
1261
+ for(i = 0; i < ary.length; i++) {
1262
+ this.unobserve(ary[i]);
1263
+ }
1264
+ return this;
1265
+ },
1266
+ // ###unset
1267
+ // Overrides sudo.Base.unset to check for observers
1268
+ //
1269
+ // `param` {String} `key`. The name of the key
1270
+ // `param` {Bool} `hold`
1271
+ //
1272
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1273
+ unset: function unset(key, hold) {
1274
+ var obj = {name: key, object: this.data, type: 'deleted'},
1275
+ val = !!this.data[key];
1276
+ delete this.data[key];
1277
+ // call the observers if there was a val to delete
1278
+ return this._unset_(obj, val, hold);
1279
+ },
1280
+ // ###_unset_
1281
+ // Helper for the unset functions
1282
+ //
1283
+ // `private`
1284
+ _unset_: function _unset_(o, v, h) {
1285
+ if(v) {
1286
+ this.changeRecords.push(o);
1287
+ if(h) return this;
1288
+ return this.deliverChangeRecords();
1289
+ }
1290
+ return this;
1291
+ },
1292
+ // ###setPath
1293
+ // Overrides sudo.Base.unsetPath to check for observers
1294
+ //
1295
+ // `param` {String} `path`
1296
+ // `param` {*} `value`
1297
+ // `param` {bool} `hold`
1298
+ //
1299
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1300
+ unsetPath: function unsetPath(path, hold) {
1301
+ var obj = {name: path, object: this.data, type: 'deleted'},
1302
+ curr = this.data, p = path.split('.'),
1303
+ key, val;
1304
+ for (key; p.length && (key = p.shift());) {
1305
+ if(!p.length) {
1306
+ // reached the last refinement
1307
+ val = !!curr[key];
1308
+ delete curr[key];
1309
+ } else {
1310
+ // this can obviously fail, but can be prevented by checking
1311
+ // with `getPath` first.
1312
+ curr = curr[key];
1313
+ }
1314
+ }
1315
+ return this._unset_(obj, val, hold);
1316
+ },
1317
+ // ###unsets
1318
+ // Override of Base.unsets to hold the call to _deliver_ until done
1319
+ //
1320
+ // `param` ary
1321
+ // `param` hold
1322
+ // `returns` {Object|*} `this` or calls deliverChangeRecords
1323
+ unsets: function unsets(ary, hold) {
1324
+ var i;
1325
+ for(i = 0; i < ary.length; i++) {
1326
+ ary[i].indexOf('.') === -1 ? this.unset(k[i], true) :
1327
+ this.unsetPath(k[i], true);
1328
+ }
1329
+ if(hold) return this;
1330
+ return this.deliverChangeRecords();
1331
+ }
828
1332
  };
1333
+ // ##Bindable Extension Object
1334
+
1335
+ // Bindable methods allow various properties and attributes of
1336
+ // a sudo Class Object to be synchronized with the data contained
1337
+ // in a changeRecord recieved via observe().
1338
+ //
1339
+ // `namespace`
1340
+ sudo.extensions.bindable = {
1341
+ // List of attributes - $.attr() to be used.
1342
+ //
1343
+ // `private`
1344
+ _attr_: {
1345
+ accesskey: true,
1346
+ align: true,
1347
+ alt: true,
1348
+ contenteditable: true,
1349
+ draggable: true,
1350
+ href: true,
1351
+ label: true,
1352
+ name: true,
1353
+ rel: true,
1354
+ src: true,
1355
+ tabindex: true,
1356
+ title: true
1357
+ },
1358
+ // Some bindings defer to jQuery.css() to be bound.
1359
+ //
1360
+ // `private`
1361
+ _css_: {
1362
+ display: true,
1363
+ visibility: true
1364
+ },
1365
+ // ###_handleAttr_
1366
+ // bind the jQuery prop() method to this object, now exposed
1367
+ // by this name, matching passed `bindings` arguments.
1368
+ //
1369
+ // `param` {string} `meth` The name of the method to be bound
1370
+ // `returns` {Object} `this`
1371
+ // `private`
1372
+ _handleAttr_: function _handleAttr_(meth) {
1373
+ this[meth] = function(obj) {
1374
+ if(obj.name === meth) this.$el.attr(meth, obj.object[obj.name]);
1375
+ return this;
1376
+ };
1377
+ return this;
1378
+ },
1379
+ // ###_handleCss_
1380
+ // bind the jQuery css() method to this object, now exposed
1381
+ // by this name, matching passed `bindings` arguments.
1382
+ //
1383
+ // `param` {string} `meth` The name of the method to be bound
1384
+ // `returns` {Object} `this`
1385
+ // `private`
1386
+ _handleCss_: function _handleCss_(meth) {
1387
+ this[meth] = function(obj) {
1388
+ if(obj.name === meth) this.$el.css(meth, obj.object[obj.name]);
1389
+ return this;
1390
+ };
1391
+ return this;
1392
+ },
1393
+ // ###_handleData_
1394
+ // bind the jQuery data() method to this object, now exposed
1395
+ // by this name, matching passed `bindings` arguments.
1396
+ //
1397
+ // `param` {string} `meth` The name of the method to be bound
1398
+ // `returns` {Object} `this`
1399
+ // `private`
1400
+ _handleData_: function _handleData_(meth) {
1401
+ this[meth] = function(obj) {
1402
+ if(obj.name === meth) {
1403
+ this.$el.data(obj.object[obj.name].key, obj.object[obj.name].value);
1404
+ return this;
1405
+ }
1406
+ };
1407
+ return this;
1408
+ },
1409
+ // ###_handleProp_
1410
+ // bind the jQuery attr() method to this object, now exposed
1411
+ // by this name, matching passed `bindings` arguments.
1412
+ //
1413
+ // NOTE: If more than 1 data-* attribute is desired you must
1414
+ // set those up manually as <obj>.data({..}) is what will be
1415
+ // constructed via this method.
1416
+ //
1417
+ // `param` {string} `meth` The name of the method to be bound.
1418
+ // `returns` {Object} `this`
1419
+ // `private`
1420
+ _handleProp_: function _handleProp_(meth) {
1421
+ this[meth] = function(obj) {
1422
+ if(obj.name === meth) this.$el.prop(meth, obj.object[obj.name]);
1423
+ return this;
1424
+ };
1425
+ return this;
1426
+ },
1427
+ // ###_handleSpec_
1428
+ // bind the jQuery shorthand methods to this object matching
1429
+ // passed `bindings` arguments.
1430
+ //
1431
+ // `param` {string} `meth` The name of the method to be bound.
1432
+ // `returns` {Object} `this`
1433
+ // `private`
1434
+ _handleSpec_: function _handleSpec_(meth) {
1435
+ this[meth] = function(obj) {
1436
+ if(obj.name === meth) this.$el[meth](obj.object[obj.name]);
1437
+ return this;
1438
+ };
1439
+ return this;
1440
+ },
1441
+ // List of properties - $.prop() to be used.
1442
+ //
1443
+ // `private`
1444
+ _prop_: {
1445
+ checked: true,
1446
+ defaultValue: true,
1447
+ disabled: true,
1448
+ location: true,
1449
+ multiple: true,
1450
+ readOnly: true,
1451
+ selected: true
1452
+ },
1453
+ // ###_setBinding_
1454
+ // Given a single explicit binding, create it. Called from
1455
+ // _setbindings_ as a convenience for normalizing the
1456
+ // single vs. multiple bindings scenario
1457
+ //
1458
+ // `param` {string} `b` The binding.
1459
+ // `private`
1460
+ _setBinding_: function _setBinding_(b) {
1461
+ if(b in this._spec_) return this[this._spec_[b]](b);
1462
+ if(b in this._css_) return this._handleCss_(b);
1463
+ if(b in this._attr_) return this._handleAttr_(b);
1464
+ if(b in this._prop_) return this._handleProp_(b);
1465
+ },
1466
+ // ###setBindings
1467
+ // Inspect the binding (in the single-bound use case), or the
1468
+ // bindings Array in this Object's data store and
1469
+ // create the bound functions expected.
1470
+ //
1471
+ // `returns` {Object} `this`
1472
+ setBindings: function setBindings() {
1473
+ var d = this.model.data, b, i;
1474
+ // handle the single binding use case
1475
+ if((b = d.binding)) return this._setBinding_(b);
1476
+ if(!(b = d.bindings)) return this;
1477
+ for(i = 0; i < b.length; i++) {
1478
+ this._setBinding_(b[i]);
1479
+ }
1480
+ return this;
1481
+ },
1482
+ // `Special` binding cases. jQuery shorthand methods to be used.
1483
+ //
1484
+ // `private`
1485
+ _spec_: {
1486
+ data: '_handleData_',
1487
+ html: '_handleSpec_',
1488
+ text: '_handleSpec_',
1489
+ val: '_handleSpec_'
1490
+ }
1491
+ };
1492
+ // ##Listener Extension Object
1493
+
1494
+ // Handles event binding/unbinding via an events array in the form:
1495
+ // events: [{
1496
+ // name: `eventName`,
1497
+ // sel: `an_optional_delegator`,
1498
+ // data: an_optional_hash_of_data
1499
+ // fn: `function name`
1500
+ // }, {...
1501
+ // This array will be searched for via `this.get('events')`. There is a
1502
+ // single-event use case as well, pass a single object literal in the above form.
1503
+ // with the key `event`:
1504
+ // event: {...same as above}
1505
+ // Details about the hashes in the array:
1506
+ // A. name -> jQuery compatible event name
1507
+ // B. sel -> Optional jQuery compatible selector used to delegate events
1508
+ // C. data: A hash that will be passed as the custom jQuery Event.data object
1509
+ // D. fn -> If a {String} bound to the named function on this object, if a
1510
+ // function assumed to be anonymous and called with no scope manipulation
1511
+ sudo.extensions.listener = {
1512
+ // ###bindEvents
1513
+ // Bind the events in the data store to this object's $el
1514
+ //
1515
+ // `returns` {Object} `this`
1516
+ bindEvents: function bindEvents() {
1517
+ var e;
1518
+ if((e = this.model.data.event || this.model.data.events)) this._handleEvents_(e, 1);
1519
+ return this;
1520
+ },
1521
+ // Use the jQuery `on` or 'off' method, optionally delegating to a selector if present
1522
+ // `private`
1523
+ _handleEvents_: function _handleEvents_(e, which) {
1524
+ var i;
1525
+ if(Array.isArray(e)) {
1526
+ for(i = 0; i < e.length; i++) {
1527
+ this._handleEvent_(e[i], which);
1528
+ }
1529
+ } else {
1530
+ this._handleEvent_(e, which);
1531
+ }
1532
+ },
1533
+ // helper for binding and unbinding an individual event
1534
+ // `param` {Object} e. An event descriptor
1535
+ // `param` {String} which. `on` or `off`
1536
+ // `private`
1537
+ _handleEvent_: function _handleEvent_(e, which) {
1538
+ if(which) {
1539
+ this.$el.on(e.name, e.sel, e.data, typeof e.fn === 'string' ? this[e.fn].bind(this) : e.fn);
1540
+ } else {
1541
+ // do not re-bind the fn going to off otherwise the unbind will fail
1542
+ this.$el.off(e.name, e.sel);
1543
+ }
1544
+ },
1545
+ // ###rebindEvents
1546
+ // Convenience method for `this.unbindEvents().bindEvents()`
1547
+ //
1548
+ // 'returns' {object} 'this'
1549
+ rebindEvents: function rebindEvents() {
1550
+ return this.unbindEvents().bindEvents();
1551
+ },
1552
+ // ###unbindEvents
1553
+ // Unbind the events in the data store from this object's $el
1554
+ //
1555
+ // `returns` {Object} `this`
1556
+ unbindEvents: function unbindEvents() {
1557
+ var e;
1558
+ if((e = this.model.data.event || this.model.data.events)) this._handleEvents_(e);
1559
+ return this;
1560
+ }
1561
+ };
1562
+ // ##sudo persistable extension
1563
+ //
1564
+ // A mixin providing restful CRUD operations for a sudo.Model instance.
1565
+ //
1566
+ // create : POST
1567
+ // read : GET
1568
+ // update : PUT or PATCH (configurable)
1569
+ // destroy : DELETE
1570
+ //
1571
+ // Before use be sure to set an `ajax` property {object} with at least
1572
+ // a `baseUrl: ...` key. The model's id (if present -- indicating a persisted model)
1573
+ // is appended to the baseUrl (baseUrl/id) by default. You can override this behavior
1574
+ // by simply setting a `url: ...` in the `ajax` options hash or pass in the same when
1575
+ // calling any of the methods (or override the model.url() method).
1576
+ //
1577
+ // Place any other default options in the `ajax` hash
1578
+ // that you would want sent to a $.ajax({...}) call. Again, you can also override those
1579
+ // defaults by passing in a hash of options to any method:
1580
+ // `this.model.update({patch: true})` etc...
1581
+ sudo.extensions.persistable = {
1582
+ // ###create
1583
+ //
1584
+ // Save this model on the server. If a subset of this model's attributes
1585
+ // have not been stated (ajax:{data:{...}}) send all of the model's data.
1586
+ // Anticipate that the server response will send back the
1587
+ // state of the model on the server and set it here (via a success callback).
1588
+ //
1589
+ // `param` {object} `params` Hash of options for the XHR call
1590
+ // `returns` {object} The jQuery XHR object
1591
+ create: function create(params) {
1592
+ return this._sendData_('POST', params);
1593
+ },
1594
+ // ###destroy
1595
+ //
1596
+ // Delete this model on the server
1597
+ //
1598
+ // `param` {object} `params` Optional hash of options for the XHR
1599
+ // `returns` {object} jqXhr
1600
+ destroy: function destroy(params) {
1601
+ return this._sendData_('DELETE', params);
1602
+ },
1603
+ // ###_normalizeParams_
1604
+ // Abstracted logic for preparing the options object. This looks at
1605
+ // the set `ajax` property, allowing any passed in params to override.
1606
+ //
1607
+ // Sets defaults: JSON dataType and a success callback that simply `sets()` the
1608
+ // data returned from the server
1609
+ //
1610
+ // `returns` {object} A normalized params object for the XHR call
1611
+ _normalizeParams_: function _normalizeParams_(meth, opts, params) {
1612
+ opts || (opts = $.extend({}, this.data.ajax));
1613
+ opts.url || (opts.url = this.url(opts.baseUrl));
1614
+ opts.type || (opts.type = meth);
1615
+ opts.dataType || (opts.dataType = 'json');
1616
+ // the default success callback is to set the data returned from the server
1617
+ // or just the status as `ajaxStatus` if no data was returned
1618
+ opts.success || (opts.success = function(data, status, jqXhr) {
1619
+ data ? this.sets(data) : this.set('ajaxStatus', status);
1620
+ }.bind(this));
1621
+ // allow the passed in params to override any set in this model's `ajax` options
1622
+ return params ? $.extend(opts, params) : opts;
1623
+ },
1624
+ // ###read
1625
+ //
1626
+ // Fetch this models state from the server and set it here. The
1627
+ // `Model.sets()` method is used with the returned data (we are
1628
+ // asssuming the default json dataType). Pass in (via the params arg)
1629
+ // a success function to override this default.
1630
+ //
1631
+ // Maps to the http GET method.
1632
+ //
1633
+ // `param` {object} `params`. Optional info for the XHR call. If
1634
+ // present will override any set in this model's `ajax` options object.
1635
+ // `returns` {object} The jQuery XHR object
1636
+ read: function read(params) {
1637
+ return $.ajax(this._normalizeParams_('GET', null, params));
1638
+ },
1639
+ // ###save
1640
+ //
1641
+ // Convenience method removing the need to know if a model is new (not yet persisted)
1642
+ // or has been loaded/refreshed from the server.
1643
+ //
1644
+ // `param` {object} `params` Hash of options for the XHR call
1645
+ // `returns` {object} The jQuery XHR object
1646
+ save: function save(params) {
1647
+ return ('id' in this.data) ? this.update(params) : this.create(params);
1648
+ },
1649
+ // ###_sendData_
1650
+ // The Create, Update and Patch methods all send data to the server,
1651
+ // varying only in their HTTP method. Abstracted logic is here.
1652
+ //
1653
+ // `returns` {object} jqXhr
1654
+ _sendData_: function _sendData_(meth, params) {
1655
+ opts = $.extend({}, this.data.ajax);
1656
+ opts.contentType || (opts.contentType = 'application/json');
1657
+ opts.data || (opts.data = this.data);
1658
+ // non GET requests do not 'processData'
1659
+ if(!('processData' in opts)) opts.processData = false;
1660
+ return $.ajax(this._normalizeParams_(meth, opts, params));
1661
+ },
1662
+ // ###update
1663
+ //
1664
+ // If this model has been persisted to/from the server (it has an `id` attribute)
1665
+ // send the specified data (or all the model's data) to the server at `url` via
1666
+ // the `PUT` http verb or `PATCH` if {patch: true} is in the ajax options (or the
1667
+ // passed in params)
1668
+ //
1669
+ // NOTE: update does not check is this is a new model or not, do that yourself
1670
+ // or use the `save()` method (that does check).
1671
+ //
1672
+ // `param` {object} `params` Optional hash of options for the XHR
1673
+ // `returns` {object|bool} the jqXhr if called false if not
1674
+ update: function update(params) {
1675
+ return this._sendData_((this.data.ajax.patch || params && params.patch) ?
1676
+ 'PATCH' : 'PUT', params);
1677
+ },
1678
+ // ###url
1679
+ //
1680
+ // Takes the base url and appends this models id if present
1681
+ // (narmalizing the trailong slash if needed).
1682
+ // Override if you need to change the format of the calculated url.
1683
+ //
1684
+ // `param` {string} `base` the baseUrl set in this models ajax options
1685
+ url: function url(base) {
1686
+ // could possibly be 0...
1687
+ if('id' in this.data) {
1688
+ return base + (base.charAt(base.length - 1) === '/' ?
1689
+ '' : '/') + encodeURIComponent(this.data.id);
1690
+ } else return base;
1691
+ }
1692
+ };
1693
+ //##Change Delegate
1694
+
1695
+ // Delegates, if present, can override or extend the behavior
1696
+ // of objects. The change delegate is specifically designed to
1697
+ // filter change records from an Observable instance and only forward
1698
+ // the ones matching a given `filters` criteria (key or path).
1699
+ // The forwarded messages will be sent to the specified method
1700
+ // on the delegates `delegator` (bound to the _delegator_ scope)
1701
+ //
1702
+ // `param` {Object} data
1703
+ sudo.delegates.Change = function(data) {
1704
+ this.construct(data);
1705
+ };
1706
+ // Delegates inherit from Model
1707
+ sudo.delegates.Change.prototype = Object.create(sudo.Model.prototype);
1708
+ // ###addFilter
1709
+ // Place an entry into this object's hash of filters
1710
+ //
1711
+ // `param` {string} `key`
1712
+ // `param` {string} `val`
1713
+ // `returns` {object} this
1714
+ sudo.delegates.Change.prototype.addFilter = function addFilter(key, val) {
1715
+ this.data.filters[key] = val;
1716
+ return this;
1717
+ };
1718
+ // ###filter
1719
+ // Change records are delivered here and filtered, calling any matching
1720
+ // methods specified in `this.get('filters').
1721
+ //
1722
+ // `returns` {Object} a call to the specified _delegator_ method, passing
1723
+ // a hash containing:
1724
+ // 1. the `type` of Change
1725
+ // 2. the `name` of the changed property
1726
+ // 3. the value located at the key/path
1727
+ // 4. the `oldValue` of the key if present
1728
+ sudo.delegates.Change.prototype.filter = function filter(change) {
1729
+ var filters = this.data.filters, name = change.name,
1730
+ type = change.type, obj = {};
1731
+ // does my delegator care about this?
1732
+ if(name in filters && filters.hasOwnProperty(name)) {
1733
+ // assemble the object to return to the method
1734
+ obj.type = type;
1735
+ obj.name = name;
1736
+ obj.oldValue = change.oldValue;
1737
+ // delete operations will not have any value so no need to look
1738
+ if(type !== 'deleted') {
1739
+ obj.value = name.indexOf('.') === -1 ? change.object[change.name] :
1740
+ sudo.getPath(name, change.object);
1741
+ }
1742
+ return this.delegator[filters[name]].call(this.delegator, obj);
1743
+ }
1744
+ };
1745
+ // ###removeFilter
1746
+ // Remove an entry from this object's hash of filters
1747
+ //
1748
+ // `param` {string} `key`
1749
+ // `returns` {object} this
1750
+ sudo.delegates.Change.prototype.removeFilter = function removeFilter(key) {
1751
+ delete this.data.filters[key];
1752
+ return this;
1753
+ };
1754
+ // `private`
1755
+ sudo.delegates.Change.prototype.role = 'change';
1756
+ //##Data Delegate
1757
+
1758
+ // Delegates, if present, can extend the behavior
1759
+ // of objects, lessening the need for subclassing.
1760
+ // The data delegate is specifically designed to
1761
+ // filter through an object, looking for specified keys or paths
1762
+ // and returning values for those if found
1763
+ //
1764
+ // `param` {Object} data
1765
+ // `returns` {*} the value found at the specified key/path if found
1766
+ sudo.delegates.Data = function(data) {
1767
+ this.construct(data);
1768
+ };
1769
+ // inherits from Model
1770
+ sudo.delegates.Data.prototype = Object.create(sudo.Model.prototype);
1771
+ // ###addFilter
1772
+ // Place an entry into this object's hash of filters
1773
+ //
1774
+ // `param` {string} `key`
1775
+ // `param` {string} `val`
1776
+ // `returns` {object} this
1777
+ sudo.delegates.Data.prototype.addFilter = function addFilter(key, val) {
1778
+ this.data.filters[key] = val;
1779
+ return this;
1780
+ };
1781
+ // ###filter
1782
+ // iterates over a given object literal and returns a value (if present)
1783
+ // located at a given key or path
1784
+ //
1785
+ // `param` {Object} `obj`
1786
+ sudo.delegates.Data.prototype.filter = function(obj) {
1787
+ var filters = this.data.filters,
1788
+ ary = Object.keys(filters), key, i, o, k;
1789
+ for(i = 0; i < ary.length; i++) {
1790
+ key = ary[i];
1791
+ // keys and paths need different handling
1792
+ if(key.indexOf('.') === -1) {
1793
+ if(key in obj) this.delegator[filters[key]].call(
1794
+ this.delegator, obj[key]);
1795
+ } else {
1796
+ // the chars after the last refinement are the key we need to check for
1797
+ k = key.slice(key.lastIndexOf('.') + 1);
1798
+ // and the ones prior are the object
1799
+ o = sudo.getPath(key.slice(0, key.lastIndexOf('.')), obj);
1800
+ if(o && k in o) this.delegator[filters[key]].call(
1801
+ this.delegator, o[k]);
1802
+ }
1803
+ }
1804
+ };
1805
+ // ###removeFilter
1806
+ // Remove an entry from this object's hash of filters
1807
+ //
1808
+ // `param` {string} `key`
1809
+ // `returns` {object} this
1810
+ sudo.delegates.Data.prototype.removeFilter = function removeFilter(key) {
1811
+ delete this.data.filters[key];
1812
+ return this;
1813
+ };
1814
+ // `private`
1815
+ sudo.delegates.Data.prototype.role = 'data';
1816
+
829
1817
  sudo.version = "0.9.5";
830
1818
  window.sudo = sudo;
831
1819
  if(typeof window._ === "undefined") window._ = sudo;