Improved testbed plugin to support facility_name and testbed_name filters
[myslice.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 // http://stackoverflow.com/questions/19992090/angularjs-group-by-directive
23 ManifoldApp.filter('groupBy', ['$parse', function ($parse) {
24     return function (list, group_by) {
25
26         var filtered = [];
27         var prev_item = null;
28         var group_changed = false;
29         // this is a new field which is added to each item where we append "_CHANGED"
30         // to indicate a field change in the list
31         //was var new_field = group_by + '_CHANGED'; - JB 12/17/2013
32         var new_field = 'group_by_CHANGED';
33
34         // loop through each item in the list
35         angular.forEach(list, function (item) {
36
37             group_changed = false;
38
39             // if not the first item
40             if (prev_item !== null) {
41
42                 // check if any of the group by field changed
43
44                 //force group_by into Array
45                 group_by = angular.isArray(group_by) ? group_by : [group_by];
46
47                 //check each group by parameter
48                 for (var i = 0, len = group_by.length; i < len; i++) {
49                     if ($parse(group_by[i])(prev_item) !== $parse(group_by[i])(item)) {
50                         group_changed = true;
51                     }
52                 }
53
54
55             }// otherwise we have the first item in the list which is new
56             else {
57                 group_changed = true;
58             }
59
60             // if the group changed, then add a new field to the item
61             // to indicate this
62             if (group_changed) {
63                 item[new_field] = true;
64             } else {
65                 item[new_field] = false;
66             }
67
68             filtered.push(item);
69             prev_item = item;
70
71         });
72
73         return filtered;
74     };
75 }]);
76
77 // https://github.com/angular-ui/angular-ui-OLDREPO/blob/master/modules/filters/unique/unique.js
78 /**
79  * Filters out all duplicate items from an array by checking the specified key
80  * @param [key] {string} the name of the attribute of each object to compare for uniqueness
81  if the key is empty, the entire object will be compared
82  if the key === false then no filtering will be performed
83  * @return {array}
84  */
85 ManifoldApp.filter('unique', function () {
86
87   return function (items, filterOn) {
88
89     if (filterOn === false) {
90       return items;
91     }
92
93     if ((filterOn || angular.isUndefined(filterOn)) && angular.isArray(items)) {
94       var hashCheck = {}, newItems = [];
95
96       var extractValueToCompare = function (item) {
97         if (angular.isObject(item) && angular.isString(filterOn)) {
98           return item[filterOn];
99         } else {
100           return item;
101         }
102       };
103
104       angular.forEach(items, function (item) {
105         var valueToCheck, isDuplicate = false;
106
107         for (var i = 0; i < newItems.length; i++) {
108           if (angular.equals(extractValueToCompare(newItems[i]), extractValueToCompare(item))) {
109             isDuplicate = true;
110             break;
111           }
112         }
113         if (!isDuplicate) {
114           newItems.push(item);
115         }
116
117       });
118       items = newItems;
119     }
120     return items;
121   };
122 });
123
124 // INHERITANCE
125 // http://alexsexton.com/blog/2010/02/using-inheritance-patterns-to-organize-large-jquery-applications/
126 // We will use John Resig's proposal
127
128 // http://pastie.org/517177
129
130 // NOTE: missing a destroy function
131
132 $.plugin = function(name, object) {
133     $.fn[name] = function(options) {
134         var args = Array.prototype.slice.call(arguments, 1);
135         return this.each(function() {
136             var instance = $.data(this, name);
137             if (instance) {
138                 instance[options].apply(instance, args);
139             } else {
140                 instance = $.data(this, name, new object(options, this));
141             }
142         });
143     };
144 };
145
146 // set to either
147 // * false or undefined or none : no debug
148 // * true : trace all event calls
149 // * [ 'in_progress', 'query_done' ] : would only trace to these events
150 var plugin_debug=false;
151 plugin_debug = [ 'in_progress', 'query_done' ];
152
153 var Plugin = Class.extend({
154
155     init: function(options, element) {
156         // Mix in the passed in options with the default options
157         this.options = $.extend({}, this.default_options, options);
158
159         // Save the element reference, both as a jQuery
160         // reference and a normal reference
161         this.element  = element;
162         this.$element = $(element);
163         // programmatically add specific class for publishing events
164         // used in manifold.js for triggering API events
165         if ( ! this.$element.hasClass('pubsub')) this.$element.addClass('pubsub');
166
167         // return this so we can chain/use the bridge with less code.
168         return this;
169     },
170
171     has_query_handler: function() {
172         return (typeof this.on_filter_added === 'function');
173     },
174
175     // do we need to log API calls ?
176     _is_in : function (obj, arr) {
177         for(var i=0; i<arr.length; i++) {
178             if (arr[i] == obj) return true;
179         }
180     },
181     _deserves_logging: function (event) {
182         if ( ! plugin_debug )                           return false;
183         else if ( plugin_debug === true)                return true;
184         else if (this._is_in (event, plugin_debug))     return true;
185         return false;
186     },
187
188     _query_handler: function(prefix, event_type, data) {
189         // We suppose this.query_handler_prefix has been defined if this
190         // callback is triggered    
191         var event, fn;
192         switch(event_type) {
193         case FILTER_ADDED:
194             event = 'filter_added';
195             break;
196         case FILTER_REMOVED:
197             event = 'filter_removed';
198             break;
199         case CLEAR_FILTERS:
200             event = 'filter_clear';
201             break;
202         case FIELD_ADDED:
203             event = 'field_added';
204             break;
205         case FIELD_REMOVED:
206             event = 'field_removed';
207             break;
208         case CLEAR_FIELDS:
209             event = 'field_clear';
210             break;
211         default:
212             return;
213         } // switch
214         
215         fn = 'on_' + prefix + event;
216         if (typeof this[fn] === 'function') {
217             if (this._deserves_logging (event)) {
218                 var classname=this.classname;
219                 messages.debug("Plugin._query_handler: calling "+fn+" on "+classname);
220             }
221             // call with data as parameter
222             // XXX implement anti loop
223             this[fn](data);
224         }
225     },
226
227     _record_handler: function(prefix, event_type, record) {
228         // We suppose this.query_handler_prefix has been defined if this
229         // callback is triggered    
230         var event, fn;
231         switch(event_type) {
232         case NEW_RECORD:
233             event = 'new_record';
234             break;
235         case CLEAR_RECORDS:
236             event = 'clear_records';
237             break;
238         case IN_PROGRESS:
239             event = 'query_in_progress';
240             break;
241         case DONE:
242             event = 'query_done';
243             break;
244         case FIELD_STATE_CHANGED:
245             event = 'field_state_changed';
246             break;
247         default:
248             return;
249         } // switch
250         
251         fn = 'on_' + prefix + event;
252         if (typeof this[fn] === 'function') {
253             if (this._deserves_logging (event)) {
254                 var classname=this.classname;
255                 messages.debug("Plugin._record_handler: calling "+fn+" on "+classname);
256             }
257             // call with data as parameter
258             // XXX implement anti loop
259             this[fn](record);
260         }
261     },
262
263     get_handler_function: function(type, prefix) {
264         
265         return $.proxy(function(e, event_type, record) {
266             return this['_' + type + '_handler'](prefix, event_type, record);
267         }, this);
268     },
269
270     listen_query: function(query_uuid, prefix) {
271         // default: prefix = ''
272         prefix = (typeof prefix === 'undefined') ? '' : (prefix + '_');
273
274         this.$element.on(manifold.get_channel('query', query_uuid),  this.get_handler_function('query',  prefix));
275         this.$element.on(manifold.get_channel('record', query_uuid),  this.get_handler_function('record', prefix));
276     },
277
278     default_options: {},
279
280     /* Helper functions for naming HTML elements (ID, classes), with support for filters and fields */
281
282     id: function() {
283         var ret = this.options.plugin_uuid;
284         for (var i = 0; i < arguments.length; i++) {
285             ret = ret + manifold.separator + arguments[i];
286         }
287         return ret;
288     },
289
290     elmt: function() {
291         if (arguments.length == 0) {
292             return $('#' + this.id());
293         } else {
294             // We make sure to search _inside_ the dom tag of the plugin
295             return $('#' + this.id.apply(this, arguments), this.elmt());
296         }
297     },
298
299     elts: function(cls) {
300         return $('.' + cls, this.elmt());
301     },
302
303     id_from_filter: function(filter, use_value) {
304         use_value = typeof use_value !== 'undefined' ? use_value : true;
305
306         var key    = filter[0];
307         var op     = filter[1];
308         var value  = filter[2];
309         var op_str = this.getOperatorLabel(op);
310         var s      = manifold.separator;
311
312         if (use_value) {
313             return 'filter' + s + key + s + op_str + s + value;
314         } else {
315             return 'filter' + s + key + s + op_str;
316         }
317     },
318
319     str_from_filter: function(filter) {
320         return filter[0] + ' ' + filter[1] + ' ' + filter[2];
321     },
322
323     array_from_id: function(id) {
324         var ret = id.split(manifold.separator);
325         ret.shift(); // remove plugin_uuid at the beginning
326         return ret;
327     },
328
329     id_from_field: function(field) {
330         return 'field' + manifold.separator + field;
331     },
332
333     field_from_id: function(id) {
334         var array;
335         if (typeof id === 'string') {
336             array = id.split(manifold.separator);
337         } else { // We suppose we have an array ('object')
338             array = id;
339         }
340         // array = ['field', FIELD_NAME]
341         return array[1];
342     },
343
344     id_from_key: function(key_field, value) {
345         
346         return key_field + manifold.separator + this.escape_id(value); //.replace(/\\/g, '');
347     },
348
349     // NOTE
350     // at some point in time we used to have a helper function named 'flat_id' here
351     // the goals was to sort of normalize id's but it turned out we can get rid of that
352     // in a nutshell, we would have an id (can be urn, hrn, whatever) and 
353     // we want to be able to retrieve a DOM element based on that (e.g. a checkbox)
354     // so we did something like <tag id="some-id-that-comes-from-the-db">
355     // and then $("#some-id-that-comes-from-the-db")
356     // however the syntax for that selector prevents from using some characters in id
357     // and so for some of our ids this won't work
358     // instead of 'flattening' we now do this instead
359     // <tag some_id="then!we:can+use.what$we!want">
360     // and to retrieve it
361     // $("[some_id='then!we:can+use.what$we!want']")
362     // which thanks to the quotes, works; and you can use this with id as well in fact
363     // of course if now we have quotes in the id it's going to squeak, but well..
364
365     // escape (read: backslashes) some meta-chars in input
366     escape_id: function(id) {
367         if( id !== undefined){
368             return id.replace( /(:|\.|\[|\])/g, "\\$1" );
369         }else{
370             return "undefined-id";
371         }
372     },
373
374     id_from_record: function(method, record) {
375         var keys = manifold.metadata.get_key(method);
376         if (!keys)
377             return;
378         if (keys.length > 1)
379             return;
380
381         var key = keys[0];
382         switch (Object.toType(key)) {
383         case 'string':
384             if (!(key in record))
385                 return null;
386             return this.id_from_key(key, record[key]);
387             
388         default:
389             throw 'Not implemented';
390         }
391     },
392
393     key_from_id: function(id) {
394         // NOTE this works only for simple keys
395
396         var array;
397         if (typeof id === 'string') {
398             array = id.split(manifold.separator);
399         } else { // We suppose we have an array ('object')
400             array = id;
401         }
402
403         // arguments has the initial id but lacks the key field name (see id_from_key), so we are even
404         // we finally add +1 for the plugin_uuid at the beginning
405         return array[arguments.length + 1];
406     },
407
408     // TOGGLE
409     // plugin-helper.js is about managing toggled state
410     // it would be beneficial to merge it in here
411     toggle_on: function () { return this.toggle("true"); },
412     toggle_off: function () { return this.toggle("false"); },
413     toggle: function (status) {
414         plugin_helper.set_toggle_status (this.options.plugin_uuid,status);
415     },
416
417     /* SPIN */
418     // use spin() to get our default spin settings (called presets)
419     // use spin(true) to get spin's builtin defaults
420     // you can also call spin_presets() yourself and tweak what you need to, like topmenuvalidation does
421     spin: function (message) {
422         if (!message) {
423                 message = 'Please be patient, this operation can take a minute or two.';
424         }
425         $('div.loading').fadeIn('fast');
426         $('div.loading').find('.message').text(message);
427
428     },
429
430     unspin: function() {
431         $('div.loading').fadeOut('fast');
432
433     },
434
435     /* TEMPLATE */
436
437     load_template: function(name, ctx) {
438         return Mustache.render(this.elmt(name).html(), ctx);
439     },
440
441 });