agilityjs-rails 0.1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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);