5128a209e3d62292bcb9d3c63fdca326aa0e99bb
[myops.git] / web / query / vendor / couchapp / _attachments / jquery.evently.js
1 // $$ inspired by @wycats: http://yehudakatz.com/2009/04/20/evented-programming-with-jquery/
2 function $$(node) {
3   var data = $(node).data("$$");
4   if (data) {
5     return data;
6   } else {
7     data = {};
8     $(node).data("$$", data);
9     return data;
10   }
11 };
12
13 (function($) {
14   // utility functions used in the implementation
15   
16   function forIn(obj, fun) {
17     var name;
18     for (name in obj) {
19       if (obj.hasOwnProperty(name)) {
20         fun(name, obj[name]);
21       }
22     }
23   };
24   $.forIn = forIn;
25   function funViaString(fun, hint) {
26     if (fun && fun.match && fun.match(/^function/)) {
27       eval("var f = "+fun);
28       if (typeof f == "function") {
29         return function() {
30           try {
31             return f.apply(this, arguments);
32           } catch(e) {
33             // IF YOU SEE AN ERROR HERE IT HAPPENED WHEN WE TRIED TO RUN YOUR FUNCTION
34             $.log({"message": "Error in evently function.", "error": e, 
35               "src" : fun, "hint":hint});
36             throw(e);
37           }
38         };
39       }
40     }
41     return fun;
42   };
43   
44   function runIfFun(me, fun, args) {
45     // if the field is a function, call it, bound to the widget
46     var f = funViaString(fun, me);
47     if (typeof f == "function") {
48       return f.apply(me, args);
49     } else {
50       return fun;
51     }
52   }
53
54   $.evently = {
55     connect : function(source, target, events) {
56       events.forEach(function(ev) {
57         $(source).bind(ev, function() {
58           var args = $.makeArray(arguments);
59           // remove the original event to keep from stacking args extra deep
60           // it would be nice if jquery had a way to pass the original
61           // event to the trigger method.
62           args.shift();
63           $(target).trigger(ev, args);
64           return false;
65         });
66       });
67     },
68     paths : [],
69     changesDBs : {},
70     changesOpts : {}
71   };
72   
73   function extractFrom(name, evs) {
74     return evs[name];
75   };
76
77   function extractEvents(name, ddoc) {
78     // extract events from ddoc.evently and ddoc.vendor.*.evently
79     var events = [true, {}]
80       , vendor = ddoc.vendor || {}
81       , evently = ddoc.evently || {}
82       ;
83     $.forIn(vendor, function(k, v) {
84       if (v.evently && v.evently[name]) {
85         events.push(v.evently[name]);
86       }
87     });
88     if (evently[name]) {events.push(evently[name]);}
89     return $.extend.apply(null, events);
90   }
91
92   function extractPartials(ddoc) {
93     var partials = [true, {}]
94       , vendor = ddoc.vendor || {}
95       , evently = ddoc.evently || {}
96       ;
97     $.forIn(vendor, function(k, v) {
98       if (v.evently && v.evently._partials) {
99         partials.push(v.evently._partials);
100       }
101     });
102     if (evently._partials) {partials.push(evently._partials);}
103     return $.extend.apply(null, partials);
104   };
105
106   function applyCommon(events) {
107     if (events._common) {
108       $.forIn(events, function(k, v) {
109         events[k] = $.extend(true, {}, events._common, v);
110       });
111       delete events._common;
112       return events;
113     } else {
114       return events;
115     }
116   }
117
118   $.fn.evently = function(events, app, args) {
119     var elem = $(this);
120     // store the app on the element for later use
121     if (app) {
122       $$(elem).app = app;
123     }
124
125     if (typeof events == "string") {
126       events = extractEvents(events, app.ddoc);
127     }
128     events = applyCommon(events);
129     $$(elem).evently = events;
130     if (app && app.ddoc) {
131       $$(elem).partials = extractPartials(app.ddoc);
132     }
133     // setup the handlers onto elem
134     forIn(events, function(name, h) {
135       eventlyHandler(elem, name, h, args);
136     });
137     
138     if (events._init) {
139       elem.trigger("_init", args);
140     }
141     
142     if (app && events._changes) {
143       $("body").bind("evently-changes-"+app.db.name, function() {
144         elem.trigger("_changes");        
145       });
146       followChanges(app);
147       elem.trigger("_changes");
148     }
149   };
150   
151   // eventlyHandler applies the user's handler (h) to the 
152   // elem, bound to trigger based on name.
153   function eventlyHandler(elem, name, h, args) {
154     if ($.evently.log) {
155       elem.bind(name, function() {
156         $.log(elem, name);
157       });
158     }
159     if (h.path) {
160       elem.pathbinder(name, h.path);
161     }
162     var f = funViaString(h, name);
163     if (typeof f == "function") {
164       elem.bind(name, {args:args}, f); 
165     } else if (typeof f == "string") {
166       elem.bind(name, {args:args}, function() {
167         $(this).trigger(f, arguments);
168         return false;
169       });
170     } else if ($.isArray(h)) { 
171       // handle arrays recursively
172       for (var i=0; i < h.length; i++) {
173         eventlyHandler(elem, name, h[i], args);
174       }
175     } else {
176       // an object is using the evently / mustache template system
177       if (h.fun) {
178         throw("e.fun has been removed, please rename to e.before")
179       }
180       // templates, selectors, etc are intepreted
181       // when our named event is triggered.
182       elem.bind(name, {args:args}, function() {
183         renderElement($(this), h, arguments);
184         return false;
185       });
186     }
187   };
188   
189   $.fn.replace = function(elem) {
190     // $.log("Replace", this)
191     $(this).empty().append(elem);
192   };
193   
194   // todo: ability to call this
195   // to render and "prepend/append/etc" a new element to the host element (me)
196   // as well as call this in a way that replaces the host elements content
197   // this would be easy if there is a simple way to get at the element we just appended
198   // (as html) so that we can attache the selectors
199   function renderElement(me, h, args, qrun, arun) {
200     // if there's a query object we run the query,
201     // and then call the data function with the response.
202     if (h.before && (!qrun || !arun)) {
203       funViaString(h.before, me).apply(me, args);
204     }
205     if (h.async && !arun) {
206       runAsync(me, h, args)
207     } else if (h.query && !qrun) {
208       // $.log("query before renderElement", arguments)
209       runQuery(me, h, args)
210     } else {
211       // $.log("renderElement")
212       // $.log(me, h, args, qrun)
213       // otherwise we just render the template with the current args
214       var selectors = runIfFun(me, h.selectors, args);
215       var act = (h.render || "replace").replace(/\s/g,"");
216       var app = $$(me).app;
217       if (h.mustache) {
218         // $.log("rendering", h.mustache)
219         var newElem = mustachioed(me, h, args);
220         me[act](newElem);
221       }
222       if (selectors) {
223         if (act == "replace") {
224           var s = me;
225         } else {
226           var s = newElem;
227         }
228         forIn(selectors, function(selector, handlers) {
229           // $.log("selector", selector);
230           // $.log("selected", $(selector, s));
231           $(selector, s).evently(handlers, app, args);
232           // $.log("applied", selector);
233         });
234       }
235       if (h.after) {
236         runIfFun(me, h.after, args);
237       }
238     }    
239   };
240   
241   // todo this should return the new element
242   function mustachioed(me, h, args) {
243     var partials = $$(me).partials;
244     return $($.mustache(
245       runIfFun(me, h.mustache, args),
246       runIfFun(me, h.data, args), 
247       runIfFun(me, $.extend(true, partials, h.partials), args)));
248   };
249   
250   function runAsync(me, h, args) {  
251     // the callback is the first argument
252     funViaString(h.async, me).apply(me, [function() {
253       renderElement(me, h, 
254         $.argsToArray(arguments).concat($.argsToArray(args)), false, true);
255     }].concat($.argsToArray(args)));
256   };
257   
258   
259   function runQuery(me, h, args) {
260     // $.log("runQuery: args", args)
261     var app = $$(me).app;
262     var qu = runIfFun(me, h.query, args);
263     var qType = qu.type;
264     var viewName = qu.view;
265     var userSuccess = qu.success;
266     // $.log("qType", qType)
267     
268     var q = {};
269     forIn(qu, function(k, v) {
270       if (["type", "view"].indexOf(k) == -1) {
271         q[k] = v;
272       }
273     });
274     
275     if (qType == "newRows") {
276       q.success = function(resp) {
277         // $.log("runQuery newRows success", resp.rows.length, me, resp)
278         resp.rows.reverse().forEach(function(row) {
279           renderElement(me, h, [row].concat($.argsToArray(args)), true)
280         });
281         if (userSuccess) userSuccess(resp);
282       };
283       newRows(me, app, viewName, q);
284     } else {
285       q.success = function(resp) {
286         // $.log("runQuery success", resp)
287         renderElement(me, h, [resp].concat($.argsToArray(args)), true);
288         userSuccess && userSuccess(resp);
289       };
290       // $.log(app)
291       app.view(viewName, q);      
292     }
293   }
294   
295   // this is for the items handler
296   // var lastViewId, highKey, inFlight;
297   // this needs to key per elem
298   function newRows(elem, app, view, opts) {
299     // $.log("newRows", arguments);
300     // on success we'll set the top key
301     var thisViewId, successCallback = opts.success, full = false;
302     function successFun(resp) {
303       // $.log("newRows success", resp)
304       $$(elem).inFlight = false;
305       var JSONhighKey = JSON.stringify($$(elem).highKey);
306       resp.rows = resp.rows.filter(function(r) {
307         return JSON.stringify(r.key) != JSONhighKey;
308       });
309       if (resp.rows.length > 0) {
310         if (opts.descending) {
311           $$(elem).highKey = resp.rows[0].key;
312         } else {
313           $$(elem).highKey = resp.rows[resp.rows.length -1].key;
314         }
315       };
316       if (successCallback) {successCallback(resp, full)};
317     };
318     opts.success = successFun;
319     
320     if (opts.descending) {
321       thisViewId = view + (opts.startkey ? JSON.stringify(opts.startkey) : "");
322     } else {
323       thisViewId = view + (opts.endkey ? JSON.stringify(opts.endkey) : "");
324     }
325     // $.log(["thisViewId",thisViewId])
326     // for query we'll set keys
327     if (thisViewId == $$(elem).lastViewId) {
328       // we only want the rows newer than changesKey
329       var hk = $$(elem).highKey;
330       if (hk !== undefined) {
331         if (opts.descending) {
332           opts.endkey = hk;
333           // opts.inclusive_end = false;
334         } else {
335           opts.startkey = hk;
336         }
337       }
338       // $.log("add view rows", opts)
339       if (!$$(elem).inFlight) {
340         $$(elem).inFlight = true;
341         app.view(view, opts);
342       }
343     } else {
344       // full refresh
345       // $.log("new view stuff")
346       full = true;
347       $$(elem).lastViewId = thisViewId;
348       $$(elem).highKey = undefined;
349       $$(elem).inFlight = true;
350       app.view(view, opts);
351     }
352   };
353   
354   // only start one changes listener per db
355   function followChanges(app) {
356     var dbName = app.db.name, changeEvent = function(resp) {
357       $("body").trigger("evently-changes-"+dbName, [resp]);
358     };
359     if (!$.evently.changesDBs[dbName]) {
360       if (app.db.changes) {
361         // new api in jquery.couch.js 1.0
362         app.db.changes(null, $.evently.changesOpts).onChange(changeEvent);
363       } else {
364         // in case you are still on CouchDB 0.11 ;) deprecated.
365         connectToChanges(app, changeEvent);
366       }
367       $.evently.changesDBs[dbName] = true;
368     }
369   }
370   $.evently.followChanges = followChanges;
371   // deprecated. use db.changes() from jquery.couch.js
372   // this does not have an api for closing changes request.
373   function connectToChanges(app, fun, update_seq) {
374     function changesReq(seq) {
375       var url = app.db.uri+"_changes?heartbeat=10000&feed=longpoll&since="+seq;
376       if ($.evently.changesOpts.include_docs) {
377         url = url + "&include_docs=true";
378       }
379       $.ajax({
380         url: url,
381         contentType: "application/json",
382         dataType: "json",
383         complete: function(req) {
384           var resp = $.httpData(req, "json");
385           fun(resp);
386           connectToChanges(app, fun, resp.last_seq);
387         }
388       });
389     };
390     if (update_seq) {
391       changesReq(update_seq);
392     } else {
393       app.db.info({success: function(db_info) {
394         changesReq(db_info.update_seq);
395       }});
396     }
397   };
398   
399 })(jQuery);