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.
- checksums.yaml +4 -4
- data/Rakefile +0 -1
- data/lib/generators/sudojs/class/class_generator.rb +3 -1
- data/lib/sudojs/version.rb +1 -1
- data/vendor/assets/javascripts/sudojs/es5-sham.js +21 -21
- data/vendor/assets/javascripts/sudojs/es5-shim.js +944 -944
- data/vendor/assets/javascripts/sudojs/sudo.js +1505 -517
- metadata +2 -3
- data/vendor/assets/javascripts/sudojs/sudo-x.js +0 -1820
@@ -1,119 +1,119 @@
|
|
1
1
|
(function(window) {
|
2
2
|
// #Sudo Namespace
|
3
3
|
var sudo = {
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
292
|
-
|
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
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
315
|
-
|
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
|
-
|
324
|
-
|
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
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
347
|
-
|
348
|
-
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
386
|
-
|
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
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
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
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
-
|
425
|
-
|
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
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
444
|
-
|
445
|
-
|
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
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
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
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
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
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
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
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
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
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
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
|
-
|
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
|
+
'&': '&',
|
760
|
+
'<': '<',
|
761
|
+
'>': '>',
|
762
|
+
'"': '"',
|
763
|
+
"'": ''',
|
764
|
+
'/': '/'
|
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
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
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;
|