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.
@@ -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
@@ -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.
@@ -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,8 @@
1
+ require "agilityjs/rails/version"
2
+
3
+ module Agilityjs
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Agilityjs
2
+ module Rails
3
+ VERSION = "0.1.3.1"
4
+ end
5
+ end
@@ -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);