agilityjs-rails 0.1.3.1
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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/lib/agilityjs/rails.rb +8 -0
- data/lib/agilityjs/rails/version.rb +5 -0
- data/vendor/assets/javascripts/agility.js +1156 -0
- data/vendor/assets/javascripts/agility.min.js +80 -0
- metadata +94 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0309e780671966279a9bab67c4b92ffa35d442df
|
4
|
+
data.tar.gz: 073dcef93eacbf89b26bdccec85ae3e2064f31a8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 77d57cc75d63413e07cb2eda98b524ad6dc3cfd76cc4e27fa540fe7ea56662d076b808763f6fd1b3697d2cb2c6f2652230a52b21b81d1c57995c087944f1a340
|
7
|
+
data.tar.gz: c001c34d30b790aff98c82118cca654961fa3f4ab576247cba7c24e52e7321e2a8501baadc2b4a4cc4627a16c12fe569684760b08ee0e64cc7b8500197c51928
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Octavian Neamtu
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# agilityjs-rails
|
2
|
+
|
3
|
+
AgilityJS is an awesomely lightweight JS MVC library. The versions of this gem match the release versions of AgilityJS starting with 0.1.3. Check out http://agilityjs.com/ for more information!
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'agilityjs-rails'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install agilityjs-rails
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Add
|
22
|
+
//= require agilityjs.min
|
23
|
+
to your `application.js`.
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create new Pull Request
|
@@ -0,0 +1,1156 @@
|
|
1
|
+
/*
|
2
|
+
|
3
|
+
Agility.js
|
4
|
+
Licensed under the MIT license
|
5
|
+
Copyright (c) Artur B. Adib, 2011
|
6
|
+
http://agilityjs.com
|
7
|
+
|
8
|
+
*/
|
9
|
+
|
10
|
+
// Sandboxed, so kids don't get hurt. Inspired by jQuery's code:
|
11
|
+
// Creates local ref to window for performance reasons (as JS looks up local vars first)
|
12
|
+
// Redefines undefined as it could have been tampered with
|
13
|
+
(function(window, undefined){
|
14
|
+
|
15
|
+
if (!window.jQuery) {
|
16
|
+
throw "agility.js: jQuery not found";
|
17
|
+
}
|
18
|
+
|
19
|
+
// Local references
|
20
|
+
var document = window.document,
|
21
|
+
location = window.location,
|
22
|
+
|
23
|
+
// In case $ is being used by another lib
|
24
|
+
$ = jQuery,
|
25
|
+
|
26
|
+
// Main agility object builder
|
27
|
+
agility,
|
28
|
+
|
29
|
+
// Internal utility functions
|
30
|
+
util = {},
|
31
|
+
|
32
|
+
// Default object prototype
|
33
|
+
defaultPrototype = {},
|
34
|
+
|
35
|
+
// Global object counter
|
36
|
+
idCounter = 0,
|
37
|
+
|
38
|
+
// Constant
|
39
|
+
ROOT_SELECTOR = '&';
|
40
|
+
|
41
|
+
//////////////////////////////////////////////////////////////////////////
|
42
|
+
//
|
43
|
+
// Modernizing old JS
|
44
|
+
//
|
45
|
+
|
46
|
+
// Modified from Douglas Crockford's Object.create()
|
47
|
+
// The condition below ensures we override other manual implementations (most are not adequate)
|
48
|
+
if (!Object.create || Object.create.toString().search(/native code/i)<0) {
|
49
|
+
Object.create = function(obj){
|
50
|
+
var Aux = function(){};
|
51
|
+
$.extend(Aux.prototype, obj); // simply setting Aux.prototype = obj somehow messes with constructor, so getPrototypeOf wouldn't work in IE
|
52
|
+
return new Aux();
|
53
|
+
};
|
54
|
+
}
|
55
|
+
|
56
|
+
// Modified from John Resig's Object.getPrototypeOf()
|
57
|
+
// The condition below ensures we override other manual implementations (most are not adequate)
|
58
|
+
if (!Object.getPrototypeOf || Object.getPrototypeOf.toString().search(/native code/i)<0) {
|
59
|
+
if ( typeof "test".__proto__ === "object" ) {
|
60
|
+
Object.getPrototypeOf = function(object){
|
61
|
+
return object.__proto__;
|
62
|
+
};
|
63
|
+
} else {
|
64
|
+
Object.getPrototypeOf = function(object){
|
65
|
+
// May break if the constructor has been tampered with
|
66
|
+
return object.constructor.prototype;
|
67
|
+
};
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
//////////////////////////////////////////////////////////////////////////
|
73
|
+
//
|
74
|
+
// util.*
|
75
|
+
//
|
76
|
+
|
77
|
+
// Checks if provided obj is an agility object
|
78
|
+
util.isAgility = function(obj){
|
79
|
+
return obj._agility === true;
|
80
|
+
};
|
81
|
+
|
82
|
+
// Scans object for functions (depth=2) and proxies their 'this' to dest.
|
83
|
+
// * To ensure it works with previously proxied objects, we save the original function as
|
84
|
+
// a '._preProxy' method and when available always use that as the proxy source.
|
85
|
+
// * To skip a given method, create a sub-method called '_noProxy'.
|
86
|
+
util.proxyAll = function(obj, dest){
|
87
|
+
if (!obj || !dest) {
|
88
|
+
throw "agility.js: util.proxyAll needs two arguments";
|
89
|
+
}
|
90
|
+
for (var attr1 in obj) {
|
91
|
+
var proxied = obj[attr1];
|
92
|
+
// Proxy root methods
|
93
|
+
if (typeof obj[attr1] === 'function') {
|
94
|
+
proxied = obj[attr1]._noProxy ? obj[attr1] : $.proxy(obj[attr1]._preProxy || obj[attr1], dest);
|
95
|
+
proxied._preProxy = obj[attr1]._noProxy ? undefined : (obj[attr1]._preProxy || obj[attr1]); // save original
|
96
|
+
obj[attr1] = proxied;
|
97
|
+
}
|
98
|
+
// Proxy sub-methods (model.*, view.*, etc)
|
99
|
+
else if (typeof obj[attr1] === 'object') {
|
100
|
+
for (var attr2 in obj[attr1]) {
|
101
|
+
var proxied2 = obj[attr1][attr2];
|
102
|
+
if (typeof obj[attr1][attr2] === 'function') {
|
103
|
+
proxied2 = obj[attr1][attr2]._noProxy ? obj[attr1][attr2] : $.proxy(obj[attr1][attr2]._preProxy || obj[attr1][attr2], dest);
|
104
|
+
proxied2._preProxy = obj[attr1][attr2]._noProxy ? undefined : (obj[attr1][attr2]._preProxy || obj[attr1][attr2]); // save original
|
105
|
+
proxied[attr2] = proxied2;
|
106
|
+
}
|
107
|
+
} // for attr2
|
108
|
+
obj[attr1] = proxied;
|
109
|
+
} // if not func
|
110
|
+
} // for attr1
|
111
|
+
}; // proxyAll
|
112
|
+
|
113
|
+
// Reverses the order of events attached to an object
|
114
|
+
util.reverseEvents = function(obj, eventType){
|
115
|
+
var events = $(obj).data('events');
|
116
|
+
if (events !== undefined && events[eventType] !== undefined){
|
117
|
+
// can't reverse what's not there
|
118
|
+
var reverseEvents = [];
|
119
|
+
for (var e in events[eventType]){
|
120
|
+
if (!events[eventType].hasOwnProperty(e)) continue;
|
121
|
+
reverseEvents.unshift(events[eventType][e]);
|
122
|
+
}
|
123
|
+
events[eventType] = reverseEvents;
|
124
|
+
}
|
125
|
+
}; //reverseEvents
|
126
|
+
|
127
|
+
// Determines # of attributes of given object (prototype inclusive)
|
128
|
+
util.size = function(obj){
|
129
|
+
var size = 0, key;
|
130
|
+
for (key in obj) {
|
131
|
+
size++;
|
132
|
+
}
|
133
|
+
return size;
|
134
|
+
};
|
135
|
+
|
136
|
+
// Find controllers to be extended (with syntax '~'), redefine those to encompass previously defined controllers
|
137
|
+
// Example:
|
138
|
+
// var a = $$({}, '<button>A</button>', {'click &': function(){ alert('A'); }});
|
139
|
+
// var b = $$(a, {}, '<button>B</button>', {'~click &': function(){ alert('B'); }});
|
140
|
+
// Clicking on button B will alert both 'A' and 'B'.
|
141
|
+
util.extendController = function(object) {
|
142
|
+
for (var controllerName in object.controller) {
|
143
|
+
(function(){ // new scope as we need one new function handler per controller
|
144
|
+
var matches, extend, eventName,
|
145
|
+
previousHandler, currentHandler, newHandler;
|
146
|
+
|
147
|
+
if (typeof object.controller[controllerName] === 'function') {
|
148
|
+
matches = controllerName.match(/^(\~)*(.+)/); // 'click button', '~click button', '_create', etc
|
149
|
+
extend = matches[1];
|
150
|
+
eventName = matches[2];
|
151
|
+
|
152
|
+
if (!extend) return; // nothing to do
|
153
|
+
|
154
|
+
// Redefine controller:
|
155
|
+
// '~click button' ---> 'click button' = previousHandler + currentHandler
|
156
|
+
previousHandler = object.controller[eventName] ? (object.controller[eventName]._preProxy || object.controller[eventName]) : undefined;
|
157
|
+
currentHandler = object.controller[controllerName];
|
158
|
+
newHandler = function() {
|
159
|
+
if (previousHandler) previousHandler.apply(this, arguments);
|
160
|
+
if (currentHandler) currentHandler.apply(this, arguments);
|
161
|
+
};
|
162
|
+
|
163
|
+
object.controller[eventName] = newHandler;
|
164
|
+
delete object.controller[controllerName]; // delete '~click button'
|
165
|
+
} // if function
|
166
|
+
})();
|
167
|
+
} // for controllerName
|
168
|
+
};
|
169
|
+
|
170
|
+
//////////////////////////////////////////////////////////////////////////
|
171
|
+
//
|
172
|
+
// Default object prototype
|
173
|
+
//
|
174
|
+
|
175
|
+
defaultPrototype = {
|
176
|
+
|
177
|
+
_agility: true,
|
178
|
+
|
179
|
+
//////////////////////////////////////////////////////////////////////////
|
180
|
+
//
|
181
|
+
// _container
|
182
|
+
//
|
183
|
+
// API and related auxiliary functions for storing child Agility objects.
|
184
|
+
// Not all methods are exposed. See 'shortcuts' below for exposed methods.
|
185
|
+
//
|
186
|
+
|
187
|
+
_container: {
|
188
|
+
|
189
|
+
// Adds child object to container, appends/prepends/etc view, listens for child removal
|
190
|
+
_insertObject: function(obj, selector, method){
|
191
|
+
var self = this;
|
192
|
+
if (!util.isAgility(obj)) {
|
193
|
+
throw "agility.js: append argument is not an agility object";
|
194
|
+
}
|
195
|
+
this._container.children[obj._id] = obj; // children is *not* an array; this is for simpler lookups by global object id
|
196
|
+
this.trigger(method, [obj, selector]);
|
197
|
+
obj._parent = this;
|
198
|
+
// ensures object is removed from container when destroyed:
|
199
|
+
obj.bind('destroy', function(event, id){
|
200
|
+
self._container.remove(id);
|
201
|
+
});
|
202
|
+
return this;
|
203
|
+
},
|
204
|
+
|
205
|
+
append: function(obj, selector) {
|
206
|
+
return this._container._insertObject.call(this, obj, selector, 'append');
|
207
|
+
},
|
208
|
+
|
209
|
+
prepend: function(obj, selector) {
|
210
|
+
return this._container._insertObject.call(this, obj, selector, 'prepend');
|
211
|
+
},
|
212
|
+
|
213
|
+
after: function(obj, selector) {
|
214
|
+
return this._container._insertObject.call(this, obj, selector, 'after');
|
215
|
+
},
|
216
|
+
|
217
|
+
before: function(obj, selector) {
|
218
|
+
return this._container._insertObject.call(this, obj, selector, 'before');
|
219
|
+
},
|
220
|
+
|
221
|
+
// Removes child object from container
|
222
|
+
remove: function(id){
|
223
|
+
delete this._container.children[id];
|
224
|
+
this.trigger('remove', id);
|
225
|
+
return this;
|
226
|
+
},
|
227
|
+
|
228
|
+
// Iterates over all child objects in container
|
229
|
+
each: function(fn){
|
230
|
+
$.each(this._container.children, fn);
|
231
|
+
return this; // for chainable calls
|
232
|
+
},
|
233
|
+
|
234
|
+
// Removes all objects in container
|
235
|
+
empty: function(){
|
236
|
+
this.each(function(){
|
237
|
+
this.destroy();
|
238
|
+
});
|
239
|
+
return this;
|
240
|
+
},
|
241
|
+
|
242
|
+
// Number of children
|
243
|
+
size: function() {
|
244
|
+
return util.size(this._container.children);
|
245
|
+
}
|
246
|
+
|
247
|
+
},
|
248
|
+
|
249
|
+
//////////////////////////////////////////////////////////////////////////
|
250
|
+
//
|
251
|
+
// _events
|
252
|
+
//
|
253
|
+
// API and auxiliary functions for handling events. Not all methods are exposed.
|
254
|
+
// See 'shortcuts' below for exposed methods.
|
255
|
+
//
|
256
|
+
|
257
|
+
_events: {
|
258
|
+
|
259
|
+
// Parses event string like:
|
260
|
+
// 'event' : custom event
|
261
|
+
// 'event selector' : DOM event using 'selector'
|
262
|
+
// Returns { type:'event' [, selector:'selector'] }
|
263
|
+
parseEventStr: function(eventStr){
|
264
|
+
var eventObj = { type:eventStr },
|
265
|
+
spacePos = eventStr.search(/\s/);
|
266
|
+
// DOM event 'event selector', e.g. 'click button'
|
267
|
+
if (spacePos > -1) {
|
268
|
+
eventObj.type = eventStr.substr(0, spacePos);
|
269
|
+
eventObj.selector = eventStr.substr(spacePos+1);
|
270
|
+
}
|
271
|
+
return eventObj;
|
272
|
+
},
|
273
|
+
|
274
|
+
// Binds eventStr to fn. eventStr is parsed as per parseEventStr()
|
275
|
+
bind: function(eventStr, fn){
|
276
|
+
var eventObj = this._events.parseEventStr(eventStr);
|
277
|
+
// DOM event 'event selector', e.g. 'click button'
|
278
|
+
if (eventObj.selector) {
|
279
|
+
// Manually override root selector, as jQuery selectors can't select self object
|
280
|
+
if (eventObj.selector === ROOT_SELECTOR) {
|
281
|
+
this.view.$().bind(eventObj.type, fn);
|
282
|
+
}
|
283
|
+
else {
|
284
|
+
this.view.$().delegate(eventObj.selector, eventObj.type, fn);
|
285
|
+
}
|
286
|
+
}
|
287
|
+
// Custom event
|
288
|
+
else {
|
289
|
+
$(this._events.data).bind(eventObj.type, fn);
|
290
|
+
}
|
291
|
+
return this; // for chainable calls
|
292
|
+
}, // bind
|
293
|
+
|
294
|
+
// Triggers eventStr. Syntax for eventStr is same as that for bind()
|
295
|
+
trigger: function(eventStr, params){
|
296
|
+
var eventObj = this._events.parseEventStr(eventStr);
|
297
|
+
// DOM event 'event selector', e.g. 'click button'
|
298
|
+
if (eventObj.selector) {
|
299
|
+
// Manually override root selector, as jQuery selectors can't select self object
|
300
|
+
if (eventObj.selector === ROOT_SELECTOR) {
|
301
|
+
this.view.$().trigger(eventObj.type, params);
|
302
|
+
}
|
303
|
+
else {
|
304
|
+
this.view.$().find(eventObj.selector).trigger(eventObj.type, params);
|
305
|
+
}
|
306
|
+
}
|
307
|
+
// Custom event
|
308
|
+
else {
|
309
|
+
$(this._events.data).trigger('_'+eventObj.type, params);
|
310
|
+
// fire 'pre' hooks in reverse attachment order ( last first )
|
311
|
+
util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
|
312
|
+
$(this._events.data).trigger('pre:' + eventObj.type, params);
|
313
|
+
// put the order of events back
|
314
|
+
util.reverseEvents(this._events.data, 'pre:' + eventObj.type);
|
315
|
+
$(this._events.data).trigger(eventObj.type, params);
|
316
|
+
if(this.parent())
|
317
|
+
this.parent().trigger((eventObj.type.match(/^child:/) ? '' : 'child:') + eventObj.type, params);
|
318
|
+
$(this._events.data).trigger('post:' + eventObj.type, params);
|
319
|
+
}
|
320
|
+
return this; // for chainable calls
|
321
|
+
} // trigger
|
322
|
+
|
323
|
+
}, // _events
|
324
|
+
|
325
|
+
//////////////////////////////////////////////////////////////////////////
|
326
|
+
//
|
327
|
+
// Model
|
328
|
+
//
|
329
|
+
// Main model API. All methods are exposed, but methods starting with '_'
|
330
|
+
// are meant to be used internally only.
|
331
|
+
//
|
332
|
+
|
333
|
+
model: {
|
334
|
+
|
335
|
+
// Setter
|
336
|
+
set: function(arg, params) {
|
337
|
+
var self = this;
|
338
|
+
var modified = []; // list of modified model attributes
|
339
|
+
if (typeof arg === 'object') {
|
340
|
+
var _clone = false;
|
341
|
+
if (params && params.reset) {
|
342
|
+
_clone = this.model._data; // hold on to data for change events
|
343
|
+
this.model._data = $.extend({}, arg); // erases previous model attributes without pointing to object
|
344
|
+
}
|
345
|
+
else {
|
346
|
+
$.extend(this.model._data, arg); // default is extend
|
347
|
+
}
|
348
|
+
for (var key in arg) {
|
349
|
+
delete _clone[ key ]; // no need to fire change twice
|
350
|
+
modified.push(key);
|
351
|
+
}
|
352
|
+
for (key in _clone) {
|
353
|
+
modified.push(key);
|
354
|
+
}
|
355
|
+
}
|
356
|
+
else {
|
357
|
+
throw "agility.js: unknown argument type in model.set()";
|
358
|
+
}
|
359
|
+
|
360
|
+
// Events
|
361
|
+
if (params && params.silent===true) return this; // do not fire events
|
362
|
+
this.trigger('change');
|
363
|
+
$.each(modified, function(index, val){
|
364
|
+
self.trigger('change:'+val);
|
365
|
+
});
|
366
|
+
return this; // for chainable calls
|
367
|
+
},
|
368
|
+
|
369
|
+
// Getter
|
370
|
+
get: function(arg){
|
371
|
+
// Full model getter
|
372
|
+
if (arg === undefined) {
|
373
|
+
return this.model._data;
|
374
|
+
}
|
375
|
+
// Attribute getter
|
376
|
+
if (typeof arg === 'string') {
|
377
|
+
return this.model._data[arg];
|
378
|
+
}
|
379
|
+
throw 'agility.js: unknown argument for getter';
|
380
|
+
},
|
381
|
+
|
382
|
+
// Resetter (to initial model upon object initialization)
|
383
|
+
reset: function(){
|
384
|
+
this.model.set(this.model._initData, {reset:true});
|
385
|
+
return this; // for chainable calls
|
386
|
+
},
|
387
|
+
|
388
|
+
// Number of model properties
|
389
|
+
size: function(){
|
390
|
+
return util.size(this.model._data);
|
391
|
+
},
|
392
|
+
|
393
|
+
// Convenience function - loops over each model property
|
394
|
+
each: function(fn){
|
395
|
+
$.each(this.model._data, fn);
|
396
|
+
return this; // for chainable calls
|
397
|
+
}
|
398
|
+
|
399
|
+
}, // model prototype
|
400
|
+
|
401
|
+
//////////////////////////////////////////////////////////////////////////
|
402
|
+
//
|
403
|
+
// View
|
404
|
+
//
|
405
|
+
// Main view API. All methods are exposed, but methods starting with '_'
|
406
|
+
// are meant to be used internally only.
|
407
|
+
//
|
408
|
+
|
409
|
+
view: {
|
410
|
+
|
411
|
+
// Defaults
|
412
|
+
format: '<div/>',
|
413
|
+
style: '',
|
414
|
+
|
415
|
+
// Shortcut to view.$root or view.$root.find(), depending on selector presence
|
416
|
+
$: function(selector){
|
417
|
+
return (!selector || selector === ROOT_SELECTOR) ? this.view.$root : this.view.$root.find(selector);
|
418
|
+
},
|
419
|
+
|
420
|
+
// Render $root
|
421
|
+
// Only function to access $root directly other than $()
|
422
|
+
render: function(){
|
423
|
+
// Without format there is no view
|
424
|
+
if (this.view.format.length === 0) {
|
425
|
+
throw "agility.js: empty format in view.render()";
|
426
|
+
}
|
427
|
+
if (this.view.$root.size() === 0) {
|
428
|
+
this.view.$root = $(this.view.format);
|
429
|
+
}
|
430
|
+
else {
|
431
|
+
this.view.$root.html( $(this.view.format).html() ); // can't overwrite $root as this would reset its presence in the DOM and all events already bound, and
|
432
|
+
}
|
433
|
+
// Ensure we have a valid (non-empty) $root
|
434
|
+
if (this.view.$root.size() === 0) {
|
435
|
+
throw 'agility.js: could not generate html from format';
|
436
|
+
}
|
437
|
+
return this;
|
438
|
+
}, // render
|
439
|
+
|
440
|
+
// Parse data-bind string of the type '[attribute][=] variable[, [attribute][=] variable ]...'
|
441
|
+
// If the variable is not an attribute, it must occur by itself
|
442
|
+
// all pairs in the list are assumed to be attributes
|
443
|
+
// Returns { key:'model key', attr: [ {attr : 'attribute', attrVar : 'variable' }... ] }
|
444
|
+
_parseBindStr: function(str){
|
445
|
+
var obj = {key:null, attr:[]},
|
446
|
+
pairs = str.split(','),
|
447
|
+
regex = /([a-zA-Z0-9_\-]+)(?:[\s=]+([a-zA-Z0-9_\-]+))?/,
|
448
|
+
keyAssigned = false,
|
449
|
+
matched;
|
450
|
+
|
451
|
+
if (pairs.length > 0) {
|
452
|
+
for (var i = 0; i < pairs.length; i++) {
|
453
|
+
matched = pairs[i].match(regex);
|
454
|
+
// [ "attribute variable", "attribute", "variable" ]
|
455
|
+
// or [ "attribute=variable", "attribute", "variable" ]
|
456
|
+
// or
|
457
|
+
// [ "variable", "variable", undefined ]
|
458
|
+
// in some IE it will be [ "variable", "variable", "" ]
|
459
|
+
// or
|
460
|
+
// null
|
461
|
+
if (matched) {
|
462
|
+
if (typeof(matched[2]) === "undefined" || matched[2] === "") {
|
463
|
+
if (keyAssigned) {
|
464
|
+
throw new Error("You may specify only one key (" +
|
465
|
+
keyAssigned + " has already been specified in data-bind=" +
|
466
|
+
str + ")");
|
467
|
+
} else {
|
468
|
+
keyAssigned = matched[1];
|
469
|
+
obj.key = matched[1];
|
470
|
+
}
|
471
|
+
} else {
|
472
|
+
obj.attr.push({attr: matched[1], attrVar: matched[2]});
|
473
|
+
}
|
474
|
+
} // if (matched)
|
475
|
+
} // for (pairs.length)
|
476
|
+
} // if (pairs.length > 0)
|
477
|
+
|
478
|
+
return obj;
|
479
|
+
},
|
480
|
+
|
481
|
+
// Apply two-way (DOM <--> Model) bindings to elements with 'data-bind' attributes
|
482
|
+
bindings: function(){
|
483
|
+
var self = this;
|
484
|
+
var $rootNode = this.view.$().filter('[data-bind]');
|
485
|
+
var $childNodes = this.view.$('[data-bind]');
|
486
|
+
var createAttributePairClosure = function(bindData, node, i) {
|
487
|
+
var attrPair = bindData.attr[i]; // capture the attribute pair in closure
|
488
|
+
return function() {
|
489
|
+
node.attr(attrPair.attr, self.model.get(attrPair.attrVar));
|
490
|
+
};
|
491
|
+
};
|
492
|
+
$rootNode.add($childNodes).each(function(){
|
493
|
+
var $node = $(this);
|
494
|
+
var bindData = self.view._parseBindStr( $node.data('bind') );
|
495
|
+
|
496
|
+
var bindAttributesOneWay = function() {
|
497
|
+
// 1-way attribute binding
|
498
|
+
if (bindData.attr) {
|
499
|
+
for (var i = 0; i < bindData.attr.length; i++) {
|
500
|
+
self.bind('_change:'+bindData.attr[i].attrVar,
|
501
|
+
createAttributePairClosure(bindData, $node, i));
|
502
|
+
} // for (bindData.attr)
|
503
|
+
} // if (bindData.attr)
|
504
|
+
}; // bindAttributesOneWay()
|
505
|
+
|
506
|
+
// <input type="checkbox">: 2-way binding
|
507
|
+
if ($node.is('input:checkbox')) {
|
508
|
+
// Model --> DOM
|
509
|
+
self.bind('_change:'+bindData.key, function(){
|
510
|
+
$node.prop("checked", self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
|
511
|
+
});
|
512
|
+
// DOM --> Model
|
513
|
+
$node.change(function(){
|
514
|
+
var obj = {};
|
515
|
+
obj[bindData.key] = $(this).prop("checked");
|
516
|
+
self.model.set(obj); // not silent as user might be listening to change events
|
517
|
+
});
|
518
|
+
// 1-way attribute binding
|
519
|
+
bindAttributesOneWay();
|
520
|
+
}
|
521
|
+
|
522
|
+
// <select>: 2-way binding
|
523
|
+
else if ($node.is('select')) {
|
524
|
+
// Model --> DOM
|
525
|
+
self.bind('_change:'+bindData.key, function(){
|
526
|
+
var nodeName = $node.attr('name');
|
527
|
+
var modelValue = self.model.get(bindData.key);
|
528
|
+
$node.val(modelValue);
|
529
|
+
});
|
530
|
+
// DOM --> Model
|
531
|
+
$node.change(function(){
|
532
|
+
var obj = {};
|
533
|
+
obj[bindData.key] = $node.val();
|
534
|
+
self.model.set(obj); // not silent as user might be listening to change events
|
535
|
+
});
|
536
|
+
// 1-way attribute binding
|
537
|
+
bindAttributesOneWay();
|
538
|
+
}
|
539
|
+
|
540
|
+
// <input type="radio">: 2-way binding
|
541
|
+
else if ($node.is('input:radio')) {
|
542
|
+
// Model --> DOM
|
543
|
+
self.bind('_change:'+bindData.key, function(){
|
544
|
+
var nodeName = $node.attr('name');
|
545
|
+
var modelValue = self.model.get(bindData.key);
|
546
|
+
$node.siblings('input[name="'+nodeName+'"]').filter('[value="'+modelValue+'"]').prop("checked", true); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
|
547
|
+
});
|
548
|
+
// DOM --> Model
|
549
|
+
$node.change(function(){
|
550
|
+
if (!$node.prop("checked")) return; // only handles check=true events
|
551
|
+
var obj = {};
|
552
|
+
obj[bindData.key] = $node.val();
|
553
|
+
self.model.set(obj); // not silent as user might be listening to change events
|
554
|
+
});
|
555
|
+
// 1-way attribute binding
|
556
|
+
bindAttributesOneWay();
|
557
|
+
}
|
558
|
+
|
559
|
+
// <input type="search"> (model is updated after every keypress event)
|
560
|
+
else if ($node.is('input[type="search"]')) {
|
561
|
+
// Model --> DOM
|
562
|
+
self.bind('_change:'+bindData.key, function(){
|
563
|
+
$node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
|
564
|
+
});
|
565
|
+
// Model <-- DOM
|
566
|
+
$node.keypress(function(){
|
567
|
+
// Without timeout $node.val() misses the last entered character
|
568
|
+
setTimeout(function(){
|
569
|
+
var obj = {};
|
570
|
+
obj[bindData.key] = $node.val();
|
571
|
+
self.model.set(obj); // not silent as user might be listening to change events
|
572
|
+
}, 50);
|
573
|
+
});
|
574
|
+
// 1-way attribute binding
|
575
|
+
bindAttributesOneWay();
|
576
|
+
}
|
577
|
+
|
578
|
+
// <input type="text">, <input>, and <textarea>: 2-way binding
|
579
|
+
else if ($node.is('input:text, textarea')) {
|
580
|
+
// Model --> DOM
|
581
|
+
self.bind('_change:'+bindData.key, function(){
|
582
|
+
$node.val(self.model.get(bindData.key)); // this won't fire a DOM 'change' event, saving us from an infinite event loop (Model <--> DOM)
|
583
|
+
});
|
584
|
+
// Model <-- DOM
|
585
|
+
$node.change(function(){
|
586
|
+
var obj = {};
|
587
|
+
obj[bindData.key] = $(this).val();
|
588
|
+
self.model.set(obj); // not silent as user might be listening to change events
|
589
|
+
});
|
590
|
+
// 1-way attribute binding
|
591
|
+
bindAttributesOneWay();
|
592
|
+
}
|
593
|
+
|
594
|
+
// all other <tag>s: 1-way binding
|
595
|
+
else {
|
596
|
+
if (bindData.key) {
|
597
|
+
self.bind('_change:'+bindData.key, function(){
|
598
|
+
if (self.model.get(bindData.key)) {
|
599
|
+
$node.text(self.model.get(bindData.key).toString());
|
600
|
+
} else {
|
601
|
+
$node.text('');
|
602
|
+
}
|
603
|
+
});
|
604
|
+
}
|
605
|
+
bindAttributesOneWay();
|
606
|
+
}
|
607
|
+
}); // nodes.each()
|
608
|
+
return this;
|
609
|
+
}, // bindings()
|
610
|
+
|
611
|
+
// Triggers _change and _change:* events so that view is updated as per view.bindings()
|
612
|
+
sync: function(){
|
613
|
+
var self = this;
|
614
|
+
// Trigger change events so that view is updated according to model
|
615
|
+
this.model.each(function(key, val){
|
616
|
+
self.trigger('_change:'+key);
|
617
|
+
});
|
618
|
+
if (this.model.size() > 0) {
|
619
|
+
this.trigger('_change');
|
620
|
+
}
|
621
|
+
return this;
|
622
|
+
},
|
623
|
+
|
624
|
+
// Applies style dynamically
|
625
|
+
stylize: function(){
|
626
|
+
var objClass,
|
627
|
+
regex = new RegExp(ROOT_SELECTOR, 'g');
|
628
|
+
if (this.view.style.length === 0 || this.view.$().size() === 0) {
|
629
|
+
return;
|
630
|
+
}
|
631
|
+
// Own style
|
632
|
+
// Object gets own class name ".agility_123", and <head> gets a corresponding <style>
|
633
|
+
if (this.view.hasOwnProperty('style')) {
|
634
|
+
objClass = 'agility_' + this._id;
|
635
|
+
var styleStr = this.view.style.replace(regex, '.'+objClass);
|
636
|
+
$('head', window.document).append('<style type="text/css">'+styleStr+'</style>');
|
637
|
+
this.view.$().addClass(objClass);
|
638
|
+
}
|
639
|
+
// Inherited style
|
640
|
+
// Object inherits CSS class name from first ancestor to have own view.style
|
641
|
+
else {
|
642
|
+
// Returns id of first ancestor to have 'own' view.style
|
643
|
+
var ancestorWithStyle = function(object) {
|
644
|
+
while (object !== null) {
|
645
|
+
object = Object.getPrototypeOf(object);
|
646
|
+
if (object.view.hasOwnProperty('style'))
|
647
|
+
return object._id;
|
648
|
+
}
|
649
|
+
return undefined;
|
650
|
+
}; // ancestorWithStyle
|
651
|
+
|
652
|
+
var ancestorId = ancestorWithStyle(this);
|
653
|
+
objClass = 'agility_' + ancestorId;
|
654
|
+
this.view.$().addClass(objClass);
|
655
|
+
}
|
656
|
+
return this;
|
657
|
+
}
|
658
|
+
|
659
|
+
}, // view prototype
|
660
|
+
|
661
|
+
//////////////////////////////////////////////////////////////////////////
|
662
|
+
//
|
663
|
+
// Controller
|
664
|
+
//
|
665
|
+
// Default controllers, i.e. event handlers. Event handlers that start
|
666
|
+
// with '_' are of internal use only, and take precedence over any other
|
667
|
+
// handler without that prefix. (See trigger()).
|
668
|
+
//
|
669
|
+
|
670
|
+
controller: {
|
671
|
+
|
672
|
+
// Triggered after self creation
|
673
|
+
_create: function(event){
|
674
|
+
this.view.stylize();
|
675
|
+
this.view.bindings(); // Model-View bindings
|
676
|
+
this.view.sync(); // syncs View with Model
|
677
|
+
},
|
678
|
+
|
679
|
+
// Triggered upon removing self
|
680
|
+
_destroy: function(event){
|
681
|
+
// destroy any appended agility objects
|
682
|
+
this._container.empty();
|
683
|
+
// destroy self
|
684
|
+
this.view.$().remove();
|
685
|
+
},
|
686
|
+
|
687
|
+
// Triggered after child obj is appended to container
|
688
|
+
_append: function(event, obj, selector){
|
689
|
+
this.view.$(selector).append(obj.view.$());
|
690
|
+
},
|
691
|
+
|
692
|
+
// Triggered after child obj is prepended to container
|
693
|
+
_prepend: function(event, obj, selector){
|
694
|
+
this.view.$(selector).prepend(obj.view.$());
|
695
|
+
},
|
696
|
+
|
697
|
+
// Triggered after child obj is inserted in the container
|
698
|
+
_before: function(event, obj, selector){
|
699
|
+
if (!selector) throw 'agility.js: _before needs a selector';
|
700
|
+
this.view.$(selector).before(obj.view.$());
|
701
|
+
},
|
702
|
+
|
703
|
+
// Triggered after child obj is inserted in the container
|
704
|
+
_after: function(event, obj, selector){
|
705
|
+
if (!selector) throw 'agility.js: _after needs a selector';
|
706
|
+
this.view.$(selector).after(obj.view.$());
|
707
|
+
},
|
708
|
+
|
709
|
+
// Triggered after a child obj is removed from container (or self-removed)
|
710
|
+
_remove: function(event, id){
|
711
|
+
},
|
712
|
+
|
713
|
+
// Triggered after model is changed
|
714
|
+
'_change': function(event){
|
715
|
+
}
|
716
|
+
|
717
|
+
}, // controller prototype
|
718
|
+
|
719
|
+
//////////////////////////////////////////////////////////////////////////
|
720
|
+
//
|
721
|
+
// Shortcuts
|
722
|
+
//
|
723
|
+
|
724
|
+
//
|
725
|
+
// Self
|
726
|
+
//
|
727
|
+
destroy: function() {
|
728
|
+
this.trigger('destroy', this._id); // parent must listen to 'remove' event and handle container removal!
|
729
|
+
// can't return this as it might not exist anymore!
|
730
|
+
},
|
731
|
+
parent: function(){
|
732
|
+
return this._parent;
|
733
|
+
},
|
734
|
+
|
735
|
+
//
|
736
|
+
// _container shortcuts
|
737
|
+
//
|
738
|
+
append: function(){
|
739
|
+
this._container.append.apply(this, arguments);
|
740
|
+
return this; // for chainable calls
|
741
|
+
},
|
742
|
+
prepend: function(){
|
743
|
+
this._container.prepend.apply(this, arguments);
|
744
|
+
return this; // for chainable calls
|
745
|
+
},
|
746
|
+
after: function(){
|
747
|
+
this._container.after.apply(this, arguments);
|
748
|
+
return this; // for chainable calls
|
749
|
+
},
|
750
|
+
before: function(){
|
751
|
+
this._container.before.apply(this, arguments);
|
752
|
+
return this; // for chainable calls
|
753
|
+
},
|
754
|
+
remove: function(){
|
755
|
+
this._container.remove.apply(this, arguments);
|
756
|
+
return this; // for chainable calls
|
757
|
+
},
|
758
|
+
size: function(){
|
759
|
+
return this._container.size.apply(this, arguments);
|
760
|
+
},
|
761
|
+
each: function(){
|
762
|
+
return this._container.each.apply(this, arguments);
|
763
|
+
},
|
764
|
+
empty: function(){
|
765
|
+
return this._container.empty.apply(this, arguments);
|
766
|
+
},
|
767
|
+
|
768
|
+
//
|
769
|
+
// _events shortcuts
|
770
|
+
//
|
771
|
+
bind: function(){
|
772
|
+
this._events.bind.apply(this, arguments);
|
773
|
+
return this; // for chainable calls
|
774
|
+
},
|
775
|
+
trigger: function(){
|
776
|
+
this._events.trigger.apply(this, arguments);
|
777
|
+
return this; // for chainable calls
|
778
|
+
}
|
779
|
+
|
780
|
+
}; // prototype
|
781
|
+
|
782
|
+
//////////////////////////////////////////////////////////////////////////
|
783
|
+
//
|
784
|
+
// Main object builder
|
785
|
+
//
|
786
|
+
|
787
|
+
// Main agility object builder
|
788
|
+
agility = function(){
|
789
|
+
|
790
|
+
// Real array of arguments
|
791
|
+
var args = Array.prototype.slice.call(arguments, 0),
|
792
|
+
|
793
|
+
// Object to be returned by builder
|
794
|
+
object = {},
|
795
|
+
|
796
|
+
prototype = defaultPrototype;
|
797
|
+
|
798
|
+
//////////////////////////////////////////////////////////////////////////
|
799
|
+
//
|
800
|
+
// Define object prototype
|
801
|
+
//
|
802
|
+
|
803
|
+
// Inherit object prototype
|
804
|
+
if (typeof args[0] === "object" && util.isAgility(args[0])) {
|
805
|
+
prototype = args[0];
|
806
|
+
args.shift(); // remaining args now work as though object wasn't specified
|
807
|
+
} // build from agility object
|
808
|
+
|
809
|
+
// Build object from prototype as well as the individual prototype parts
|
810
|
+
// This enables differential inheritance at the sub-object level, e.g. object.view.format
|
811
|
+
object = Object.create(prototype);
|
812
|
+
object.model = Object.create(prototype.model);
|
813
|
+
object.view = Object.create(prototype.view);
|
814
|
+
object.controller = Object.create(prototype.controller);
|
815
|
+
object._container = Object.create(prototype._container);
|
816
|
+
object._events = Object.create(prototype._events);
|
817
|
+
|
818
|
+
// Fresh 'own' properties (i.e. properties that are not inherited at all)
|
819
|
+
object._id = idCounter++;
|
820
|
+
object._parent = null;
|
821
|
+
object._events.data = {}; // event bindings will happen below
|
822
|
+
object._container.children = {};
|
823
|
+
object.view.$root = $(); // empty jQuery object
|
824
|
+
|
825
|
+
// Cloned own properties (i.e. properties that are inherited by direct copy instead of by prototype chain)
|
826
|
+
// This prevents children from altering parents models
|
827
|
+
object.model._data = prototype.model._data ? $.extend(true, {}, prototype.model._data) : {};
|
828
|
+
object._data = prototype._data ? $.extend(true, {}, prototype._data) : {};
|
829
|
+
|
830
|
+
//////////////////////////////////////////////////////////////////////////
|
831
|
+
//
|
832
|
+
// Extend model, view, controller
|
833
|
+
//
|
834
|
+
|
835
|
+
// Just the default prototype
|
836
|
+
if (args.length === 0) {
|
837
|
+
}
|
838
|
+
|
839
|
+
// Prototype differential from single {model,view,controller} object
|
840
|
+
else if (args.length === 1 && typeof args[0] === 'object' && (args[0].model || args[0].view || args[0].controller) ) {
|
841
|
+
for (var prop in args[0]) {
|
842
|
+
if (prop === 'model') {
|
843
|
+
$.extend(object.model._data, args[0].model);
|
844
|
+
}
|
845
|
+
else if (prop === 'view') {
|
846
|
+
$.extend(object.view, args[0].view);
|
847
|
+
}
|
848
|
+
else if (prop === 'controller') {
|
849
|
+
$.extend(object.controller, args[0].controller);
|
850
|
+
util.extendController(object);
|
851
|
+
}
|
852
|
+
// User-defined methods
|
853
|
+
else {
|
854
|
+
object[prop] = args[0][prop];
|
855
|
+
}
|
856
|
+
}
|
857
|
+
} // {model, view, controller} arg
|
858
|
+
|
859
|
+
// Prototype differential from separate {model}, {view}, {controller} arguments
|
860
|
+
else {
|
861
|
+
|
862
|
+
// Model from string
|
863
|
+
if (typeof args[0] === 'object') {
|
864
|
+
$.extend(object.model._data, args[0]);
|
865
|
+
}
|
866
|
+
else if (args[0]) {
|
867
|
+
throw "agility.js: unknown argument type (model)";
|
868
|
+
}
|
869
|
+
|
870
|
+
// View format from shorthand string (..., '<div>whatever</div>', ...)
|
871
|
+
if (typeof args[1] === 'string') {
|
872
|
+
object.view.format = args[1]; // extend view with .format
|
873
|
+
}
|
874
|
+
// View from object (..., {format:'<div>whatever</div>'}, ...)
|
875
|
+
else if (typeof args[1] === 'object') {
|
876
|
+
$.extend(object.view, args[1]);
|
877
|
+
}
|
878
|
+
else if (args[1]) {
|
879
|
+
throw "agility.js: unknown argument type (view)";
|
880
|
+
}
|
881
|
+
|
882
|
+
// View style from shorthand string (..., ..., 'p {color:red}', ...)
|
883
|
+
if (typeof args[2] === 'string') {
|
884
|
+
object.view.style = args[2];
|
885
|
+
args.splice(2, 1); // so that controller code below works
|
886
|
+
}
|
887
|
+
|
888
|
+
// Controller from object (..., ..., {method:function(){}})
|
889
|
+
if (typeof args[2] === 'object') {
|
890
|
+
$.extend(object.controller, args[2]);
|
891
|
+
util.extendController(object);
|
892
|
+
}
|
893
|
+
else if (args[2]) {
|
894
|
+
throw "agility.js: unknown argument type (controller)";
|
895
|
+
}
|
896
|
+
|
897
|
+
} // ({model}, {view}, {controller}) args
|
898
|
+
|
899
|
+
//////////////////////////////////////////////////////////////////////////
|
900
|
+
//
|
901
|
+
// Bootstrap: Bindings, initializations, etc
|
902
|
+
//
|
903
|
+
|
904
|
+
// Save model's initial state (so it can be .reset() later)
|
905
|
+
object.model._initData = $.extend({}, object.model._data);
|
906
|
+
|
907
|
+
// object.* will have their 'this' === object. This should come before call to object.* below.
|
908
|
+
util.proxyAll(object, object);
|
909
|
+
|
910
|
+
// Initialize $root, needed for DOM events binding below
|
911
|
+
object.view.render();
|
912
|
+
|
913
|
+
// Bind all controllers to their events
|
914
|
+
|
915
|
+
var bindEvent = function(ev, handler){
|
916
|
+
if (typeof handler === 'function') {
|
917
|
+
object.bind(ev, handler);
|
918
|
+
}
|
919
|
+
};
|
920
|
+
|
921
|
+
for (var eventStr in object.controller) {
|
922
|
+
var events = eventStr.split(';');
|
923
|
+
var handler = object.controller[eventStr];
|
924
|
+
$.each(events, function(i, ev){
|
925
|
+
ev = ev.trim();
|
926
|
+
bindEvent(ev, handler);
|
927
|
+
});
|
928
|
+
}
|
929
|
+
|
930
|
+
|
931
|
+
// Auto-triggers create event
|
932
|
+
object.trigger('create');
|
933
|
+
|
934
|
+
return object;
|
935
|
+
|
936
|
+
}; // agility
|
937
|
+
|
938
|
+
//////////////////////////////////////////////////////////////////////////
|
939
|
+
//
|
940
|
+
// Global objects
|
941
|
+
//
|
942
|
+
|
943
|
+
// $$.document is a special Agility object, whose view is attached to <body>
|
944
|
+
// This object is the main entry point for all DOM operations
|
945
|
+
agility.document = agility({
|
946
|
+
view: {
|
947
|
+
$: function(selector){ return selector ? $(selector, 'body') : $('body'); }
|
948
|
+
},
|
949
|
+
controller: {
|
950
|
+
// Override default controller
|
951
|
+
// (don't render, don't stylize, etc)
|
952
|
+
_create: function(){}
|
953
|
+
}
|
954
|
+
});
|
955
|
+
|
956
|
+
// Shortcut to prototype for plugins
|
957
|
+
agility.fn = defaultPrototype;
|
958
|
+
|
959
|
+
// isAgility test
|
960
|
+
agility.isAgility = function(obj) {
|
961
|
+
if (typeof obj !== 'object') return false;
|
962
|
+
return util.isAgility(obj);
|
963
|
+
};
|
964
|
+
|
965
|
+
// Globals
|
966
|
+
window.agility = window.$$ = agility;
|
967
|
+
|
968
|
+
//////////////////////////////////////////////////////////////////////////
|
969
|
+
//
|
970
|
+
// Bundled plugin: persist
|
971
|
+
//
|
972
|
+
|
973
|
+
// Main initializer
|
974
|
+
agility.fn.persist = function(adapter, params){
|
975
|
+
var id = 'id'; // name of id attribute
|
976
|
+
|
977
|
+
this._data.persist = $.extend({adapter:adapter}, params);
|
978
|
+
this._data.persist.openRequests = 0;
|
979
|
+
if (params && params.id) {
|
980
|
+
id = params.id;
|
981
|
+
}
|
982
|
+
|
983
|
+
// Creates persist methods
|
984
|
+
|
985
|
+
// .save()
|
986
|
+
// Creates new model or update existing one, depending on whether model has 'id' property
|
987
|
+
this.save = function(){
|
988
|
+
var self = this;
|
989
|
+
if (this._data.persist.openRequests === 0) {
|
990
|
+
this.trigger('persist:start');
|
991
|
+
}
|
992
|
+
this._data.persist.openRequests++;
|
993
|
+
this._data.persist.adapter.call(this, {
|
994
|
+
type: this.model.get(id) ? 'PUT' : 'POST', // update vs. create
|
995
|
+
id: this.model.get(id),
|
996
|
+
data: this.model.get(),
|
997
|
+
complete: function(){
|
998
|
+
self._data.persist.openRequests--;
|
999
|
+
if (self._data.persist.openRequests === 0) {
|
1000
|
+
self.trigger('persist:stop');
|
1001
|
+
}
|
1002
|
+
},
|
1003
|
+
success: function(data, textStatus, jqXHR){
|
1004
|
+
if (data[id]) {
|
1005
|
+
// id in body
|
1006
|
+
self.model.set({id:data[id]}, {silent:true});
|
1007
|
+
}
|
1008
|
+
else if (jqXHR.getResponseHeader('Location')) {
|
1009
|
+
// parse id from Location
|
1010
|
+
self.model.set({ id: jqXHR.getResponseHeader('Location').match(/\/([0-9]+)$/)[1] }, {silent:true});
|
1011
|
+
}
|
1012
|
+
self.trigger('persist:save:success');
|
1013
|
+
},
|
1014
|
+
error: function(){
|
1015
|
+
self.trigger('persist:error');
|
1016
|
+
self.trigger('persist:save:error');
|
1017
|
+
}
|
1018
|
+
});
|
1019
|
+
|
1020
|
+
return this; // for chainable calls
|
1021
|
+
}; // save()
|
1022
|
+
|
1023
|
+
// .load()
|
1024
|
+
// Loads model with given id
|
1025
|
+
this.load = function(){
|
1026
|
+
var self = this;
|
1027
|
+
if (this.model.get(id) === undefined) throw 'agility.js: load() needs model id';
|
1028
|
+
|
1029
|
+
if (this._data.persist.openRequests === 0) {
|
1030
|
+
this.trigger('persist:start');
|
1031
|
+
}
|
1032
|
+
this._data.persist.openRequests++;
|
1033
|
+
this._data.persist.adapter.call(this, {
|
1034
|
+
type: 'GET',
|
1035
|
+
id: this.model.get(id),
|
1036
|
+
complete: function(){
|
1037
|
+
self._data.persist.openRequests--;
|
1038
|
+
if (self._data.persist.openRequests === 0) {
|
1039
|
+
self.trigger('persist:stop');
|
1040
|
+
}
|
1041
|
+
},
|
1042
|
+
success: function(data, textStatus, jqXHR){
|
1043
|
+
self.model.set(data);
|
1044
|
+
self.trigger('persist:load:success');
|
1045
|
+
},
|
1046
|
+
error: function(){
|
1047
|
+
self.trigger('persist:error');
|
1048
|
+
self.trigger('persist:load:error');
|
1049
|
+
}
|
1050
|
+
});
|
1051
|
+
|
1052
|
+
return this; // for chainable calls
|
1053
|
+
}; // load()
|
1054
|
+
|
1055
|
+
// .erase()
|
1056
|
+
// Erases model with given id
|
1057
|
+
this.erase = function(){
|
1058
|
+
var self = this;
|
1059
|
+
if (this.model.get(id) === undefined) throw 'agility.js: erase() needs model id';
|
1060
|
+
|
1061
|
+
if (this._data.persist.openRequests === 0) {
|
1062
|
+
this.trigger('persist:start');
|
1063
|
+
}
|
1064
|
+
this._data.persist.openRequests++;
|
1065
|
+
this._data.persist.adapter.call(this, {
|
1066
|
+
type: 'DELETE',
|
1067
|
+
id: this.model.get(id),
|
1068
|
+
complete: function(){
|
1069
|
+
self._data.persist.openRequests--;
|
1070
|
+
if (self._data.persist.openRequests === 0) {
|
1071
|
+
self.trigger('persist:stop');
|
1072
|
+
}
|
1073
|
+
},
|
1074
|
+
success: function(data, textStatus, jqXHR){
|
1075
|
+
self.destroy();
|
1076
|
+
self.trigger('persist:erase:success');
|
1077
|
+
},
|
1078
|
+
error: function(){
|
1079
|
+
self.trigger('persist:error');
|
1080
|
+
self.trigger('persist:erase:error');
|
1081
|
+
}
|
1082
|
+
});
|
1083
|
+
|
1084
|
+
return this; // for chainable calls
|
1085
|
+
}; // erase()
|
1086
|
+
|
1087
|
+
// .gather()
|
1088
|
+
// Loads collection and appends/prepends (depending on method) at selector. All persistence data including adapter comes from proto, not self
|
1089
|
+
this.gather = function(proto, method, selectorOrQuery, query){
|
1090
|
+
var selector, self = this;
|
1091
|
+
if (!proto) throw "agility.js plugin persist: gather() needs object prototype";
|
1092
|
+
if (!proto._data.persist) throw "agility.js plugin persist: prototype doesn't seem to contain persist() data";
|
1093
|
+
|
1094
|
+
// Determines arguments
|
1095
|
+
if (query) {
|
1096
|
+
selector = selectorOrQuery;
|
1097
|
+
}
|
1098
|
+
else {
|
1099
|
+
if (typeof selectorOrQuery === 'string') {
|
1100
|
+
selector = selectorOrQuery;
|
1101
|
+
}
|
1102
|
+
else {
|
1103
|
+
selector = undefined;
|
1104
|
+
query = selectorOrQuery;
|
1105
|
+
}
|
1106
|
+
}
|
1107
|
+
|
1108
|
+
if (this._data.persist.openRequests === 0) {
|
1109
|
+
this.trigger('persist:start');
|
1110
|
+
}
|
1111
|
+
this._data.persist.openRequests++;
|
1112
|
+
proto._data.persist.adapter.call(proto, {
|
1113
|
+
type: 'GET',
|
1114
|
+
data: query,
|
1115
|
+
complete: function(){
|
1116
|
+
self._data.persist.openRequests--;
|
1117
|
+
if (self._data.persist.openRequests === 0) {
|
1118
|
+
self.trigger('persist:stop');
|
1119
|
+
}
|
1120
|
+
},
|
1121
|
+
success: function(data){
|
1122
|
+
$.each(data, function(index, entry){
|
1123
|
+
var obj = $$(proto, entry);
|
1124
|
+
if (typeof method === 'string') {
|
1125
|
+
self[method](obj, selector);
|
1126
|
+
}
|
1127
|
+
});
|
1128
|
+
self.trigger('persist:gather:success', {data:data});
|
1129
|
+
},
|
1130
|
+
error: function(){
|
1131
|
+
self.trigger('persist:error');
|
1132
|
+
self.trigger('persist:gather:error');
|
1133
|
+
}
|
1134
|
+
});
|
1135
|
+
|
1136
|
+
return this; // for chainable calls
|
1137
|
+
}; // gather()
|
1138
|
+
|
1139
|
+
return this; // for chainable calls
|
1140
|
+
}; // fn.persist()
|
1141
|
+
|
1142
|
+
// Persistence adapters
|
1143
|
+
// These are functions. Required parameters:
|
1144
|
+
// {type: 'GET' || 'POST' || 'PUT' || 'DELETE'}
|
1145
|
+
agility.adapter = {};
|
1146
|
+
|
1147
|
+
// RESTful JSON adapter using jQuery's ajax()
|
1148
|
+
agility.adapter.restful = function(_params){
|
1149
|
+
var params = $.extend({
|
1150
|
+
dataType: 'json',
|
1151
|
+
url: (this._data.persist.baseUrl || 'api/') + this._data.persist.collection + (_params.id ? '/'+_params.id : '')
|
1152
|
+
}, _params);
|
1153
|
+
$.ajax(params);
|
1154
|
+
};
|
1155
|
+
|
1156
|
+
})(window);
|