76e1cb5268f455ce22c952ec34368ede9299308d
[unfold.git] / manifoldapi / static / js / plugin.js
1 // Common parts for angularjs plugins
2 // only one ng-app is allowed
3
4 var ManifoldApp = angular.module('ManifoldApp', []);
5 ManifoldApp.config(function ($interpolateProvider) {
6     $interpolateProvider.startSymbol('{[{').endSymbol('}]}');
7 });
8
9 ManifoldApp.factory('$exceptionHandler', function () {
10     return function (exception, cause) {
11         console.log(exception.message);
12     };
13 });
14
15 ManifoldApp.filter('offset', function() {
16   return function(input, start) {
17     start = parseInt(start, 10);
18     return input.slice(start);
19   };
20 });
21
22 // INHERITANCE
23 // http://alexsexton.com/blog/2010/02/using-inheritance-patterns-to-organize-large-jquery-applications/
24 // We will use John Resig's proposal
25
26 // http://pastie.org/517177
27
28 // NOTE: missing a destroy function
29
30 $.plugin = function(name, object) {
31     $.fn[name] = function(options) {
32         var args = Array.prototype.slice.call(arguments, 1);
33         return this.each(function() {
34             var instance = $.data(this, name);
35             if (instance) {
36                 instance[options].apply(instance, args);
37             } else {
38                 instance = $.data(this, name, new object(options, this));
39             }
40         });
41     };
42 };
43
44 // set to either
45 // * false or undefined or none : no debug
46 // * true : trace all event calls
47 // * [ 'in_progress', 'query_done' ] : would only trace to these events
48 var plugin_debug=false;
49 plugin_debug = [ 'in_progress', 'query_done' ];
50
51 var Plugin = Class.extend({
52
53     init: function(options, element) {
54         // Mix in the passed in options with the default options
55         this.options = $.extend({}, this.default_options, options);
56
57         // Save the element reference, both as a jQuery
58         // reference and a normal reference
59         this.element  = element;
60         this.$element = $(element);
61         // programmatically add specific class for publishing events
62         // used in manifold.js for triggering API events
63         if ( ! this.$element.hasClass('pubsub')) this.$element.addClass('pubsub');
64
65         // return this so we can chain/use the bridge with less code.
66         return this;
67     },
68
69     has_query_handler: function() {
70         return (typeof this.on_filter_added === 'function');
71     },
72
73     // do we need to log API calls ?
74     _is_in : function (obj, arr) {
75         for(var i=0; i<arr.length; i++) {
76             if (arr[i] == obj) return true;
77         }
78     },
79     _deserves_logging: function (event) {
80         if ( ! plugin_debug )                           return false;
81         else if ( plugin_debug === true)                return true;
82         else if (this._is_in (event, plugin_debug))     return true;
83         return false;
84     },
85
86     _query_handler: function(prefix, event_type, data) {
87         // We suppose this.query_handler_prefix has been defined if this
88         // callback is triggered    
89         var event, fn;
90         switch(event_type) {
91         case FILTER_ADDED:
92             event = 'filter_added';
93             break;
94         case FILTER_REMOVED:
95             event = 'filter_removed';
96             break;
97         case CLEAR_FILTERS:
98             event = 'filter_clear';
99             break;
100         case FIELD_ADDED:
101             event = 'field_added';
102             break;
103         case FIELD_REMOVED:
104             event = 'field_removed';
105             break;
106         case CLEAR_FIELDS:
107             event = 'field_clear';
108             break;
109         default:
110             return;
111         } // switch
112         
113         fn = 'on_' + prefix + event;
114         if (typeof this[fn] === 'function') {
115             if (this._deserves_logging (event)) {
116                 var classname=this.classname;
117                 messages.debug("Plugin._query_handler: calling "+fn+" on "+classname);
118             }
119             // call with data as parameter
120             // XXX implement anti loop
121             this[fn](data);
122         }
123     },
124
125     _record_handler: function(prefix, event_type, record) {
126         // We suppose this.query_handler_prefix has been defined if this
127         // callback is triggered    
128         var event, fn;
129         switch(event_type) {
130         case NEW_RECORD:
131             event = 'new_record';
132             break;
133         case CLEAR_RECORDS:
134             event = 'clear_records';
135             break;
136         case IN_PROGRESS:
137             event = 'query_in_progress';
138             break;
139         case DONE:
140             event = 'query_done';
141             break;
142         case FIELD_STATE_CHANGED:
143             event = 'field_state_changed';
144             break;
145         default:
146             return;
147         } // switch
148         
149         fn = 'on_' + prefix + event;
150         if (typeof this[fn] === 'function') {
151             if (this._deserves_logging (event)) {
152                 var classname=this.classname;
153                 messages.debug("Plugin._record_handler: calling "+fn+" on "+classname);
154             }
155             // call with data as parameter
156             // XXX implement anti loop
157             this[fn](record);
158         }
159     },
160
161     get_handler_function: function(type, prefix) {
162         
163         return $.proxy(function(e, event_type, record) {
164             return this['_' + type + '_handler'](prefix, event_type, record);
165         }, this);
166     },
167
168     listen_query: function(query_uuid, prefix) {
169         // default: prefix = ''
170         prefix = (typeof prefix === 'undefined') ? '' : (prefix + '_');
171
172         this.$element.on(manifold.get_channel('query', query_uuid),  this.get_handler_function('query',  prefix));
173         this.$element.on(manifold.get_channel('record', query_uuid),  this.get_handler_function('record', prefix));
174     },
175
176     default_options: {},
177
178     /* Helper functions for naming HTML elements (ID, classes), with support for filters and fields */
179
180     id: function() {
181         var ret = this.options.plugin_uuid;
182         for (var i = 0; i < arguments.length; i++) {
183             ret = ret + manifold.separator + arguments[i];
184         }
185         return ret;
186     },
187
188     elmt: function() {
189         if (arguments.length == 0) {
190             return $('#' + this.id());
191         } else {
192             // We make sure to search _inside_ the dom tag of the plugin
193             return $('#' + this.id.apply(this, arguments), this.elmt());
194         }
195     },
196
197     elts: function(cls) {
198         return $('.' + cls, this.elmt());
199     },
200
201     id_from_filter: function(filter, use_value) {
202         use_value = typeof use_value !== 'undefined' ? use_value : true;
203
204         var key    = filter[0];
205         var op     = filter[1];
206         var value  = filter[2];
207         var op_str = this.getOperatorLabel(op);
208         var s      = manifold.separator;
209
210         if (use_value) {
211             return 'filter' + s + key + s + op_str + s + value;
212         } else {
213             return 'filter' + s + key + s + op_str;
214         }
215     },
216
217     str_from_filter: function(filter) {
218         return filter[0] + ' ' + filter[1] + ' ' + filter[2];
219     },
220
221     array_from_id: function(id) {
222         var ret = id.split(manifold.separator);
223         ret.shift(); // remove plugin_uuid at the beginning
224         return ret;
225     },
226
227     id_from_field: function(field) {
228         return 'field' + manifold.separator + field;
229     },
230
231     field_from_id: function(id) {
232         var array;
233         if (typeof id === 'string') {
234             array = id.split(manifold.separator);
235         } else { // We suppose we have an array ('object')
236             array = id;
237         }
238         // array = ['field', FIELD_NAME]
239         return array[1];
240     },
241
242     id_from_key: function(key_field, value) {
243         
244         return key_field + manifold.separator + this.escape_id(value); //.replace(/\\/g, '');
245     },
246
247     // NOTE
248     // at some point in time we used to have a helper function named 'flat_id' here
249     // the goals was to sort of normalize id's but it turned out we can get rid of that
250     // in a nutshell, we would have an id (can be urn, hrn, whatever) and 
251     // we want to be able to retrieve a DOM element based on that (e.g. a checkbox)
252     // so we did something like <tag id="some-id-that-comes-from-the-db">
253     // and then $("#some-id-that-comes-from-the-db")
254     // however the syntax for that selector prevents from using some characters in id
255     // and so for some of our ids this won't work
256     // instead of 'flattening' we now do this instead
257     // <tag some_id="then!we:can+use.what$we!want">
258     // and to retrieve it
259     // $("[some_id='then!we:can+use.what$we!want']")
260     // which thanks to the quotes, works; and you can use this with id as well in fact
261     // of course if now we have quotes in the id it's going to squeak, but well..
262
263     // escape (read: backslashes) some meta-chars in input
264     escape_id: function(id) {
265         if( id !== undefined){
266             return id.replace( /(:|\.|\[|\])/g, "\\$1" );
267         }else{
268             return "undefined-id";
269         }
270     },
271
272     id_from_record: function(method, record) {
273         var keys = manifold.metadata.get_key(method);
274         if (!keys)
275             return;
276         if (keys.length > 1)
277             return;
278
279         var key = keys[0];
280         switch (Object.toType(key)) {
281         case 'string':
282             if (!(key in record))
283                 return null;
284             return this.id_from_key(key, record[key]);
285             
286         default:
287             throw 'Not implemented';
288         }
289     },
290
291     key_from_id: function(id) {
292         // NOTE this works only for simple keys
293
294         var array;
295         if (typeof id === 'string') {
296             array = id.split(manifold.separator);
297         } else { // We suppose we have an array ('object')
298             array = id;
299         }
300
301         // arguments has the initial id but lacks the key field name (see id_from_key), so we are even
302         // we finally add +1 for the plugin_uuid at the beginning
303         return array[arguments.length + 1];
304     },
305
306     // TOGGLE
307     // plugin-helper.js is about managing toggled state
308     // it would be beneficial to merge it in here
309     toggle_on: function () { return this.toggle("true"); },
310     toggle_off: function () { return this.toggle("false"); },
311     toggle: function (status) {
312         plugin_helper.set_toggle_status (this.options.plugin_uuid,status);
313     },
314
315     /* SPIN */
316     // use spin() to get our default spin settings (called presets)
317     // use spin(true) to get spin's builtin defaults
318     // you can also call spin_presets() yourself and tweak what you need to, like topmenuvalidation does
319     spin: function (presets) {
320         var presets = ( presets === undefined ) ? spin_presets() : presets;
321         try { this.$element.spin(presets); }
322         catch (err) { messages.debug("Cannot turn on spin " + err); }
323     },
324
325     unspin: function() {
326         try { this.$element.spin(false); }
327         catch (err) { messages.debug("Cannot turn off spin " + err); }
328     },
329
330     /* TEMPLATE */
331
332     load_template: function(name, ctx) {
333         return Mustache.render(this.elmt(name).html(), ctx);
334     },
335
336 });