af2dcfa0e65fd297a6b38e25fc11865b05c2d22b
[myslice.git] / plugins / hazelnut / static / js / hazelnut.js
1 /**
2  * Description: display a query result in a datatables-powered <table>
3  * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA
4  * License: GPLv3
5  */
6
7 (function($){
8
9     // TEMP
10     var ELEMENT_KEY = 'resource_hrn';
11     var debug=false;
12     debug=true
13
14     var Hazelnut = Plugin.extend({
15
16         init: function(options, element) 
17         {
18             this._super(options, element);
19
20             /* Member variables */
21             // query status
22             this.received_all = false;
23             this.received_set = false;
24             this.in_set_buffer = Array();
25
26             /* XXX Events XXX */
27             // this.$element.on('show.Datatables', this.on_show);
28             this.el().on('show', this, this.on_show);
29             // Unbind all events using namespacing
30             // TODO in destructor
31             // $(window).unbind('Hazelnut');
32
33             var query = manifold.query_store.find_analyzed_query(this.options.query_uuid);
34             this.method = query.object;
35
36             var keys = manifold.metadata.get_key(this.method);
37             this.key = (keys && keys.length == 1) ? keys[0] : null;
38
39             /* Setup query and record handlers */
40             this.listen_query(options.query_uuid);
41             this.listen_query(options.query_all_uuid, 'all');
42
43             /* GUI setup and event binding */
44             this.initialize_table();
45         },
46
47         default_options: {
48             'checkboxes': false
49         },
50
51         /* PLUGIN EVENTS */
52
53         on_show: function(e)
54         {
55             var self = e.data;
56
57             self.table.fnAdjustColumnSizing()
58         
59             /* Refresh dataTabeles if click on the menu to display it : fix dataTables 1.9.x Bug */        
60             /* temp disabled... useful ? -- jordan
61             $(this).each(function(i,elt) {
62                 if (jQuery(elt).hasClass('dataTables')) {
63                     var myDiv=jQuery('#hazelnut-' + this.id).parent();
64                     if(myDiv.height()==0) {
65                         var oTable=$('#hazelnut-' + this.id).dataTable();            
66                         oTable.fnDraw();
67                     }
68                 }
69             });
70             */
71         }, // on_show
72
73         /* GUI EVENTS */
74
75         /* GUI MANIPULATION */
76
77         initialize_table: function() 
78         {
79             /* Transforms the table into DataTable, and keep a pointer to it */
80             var self = this;
81             actual_options = {
82                 // Customize the position of Datatables elements (length,filter,button,...)
83                 // we use a fluid row on top and another on the bottom, making sure we take 12 grid elt's each time
84                 sDom: "<'row-fluid'<'span5'l><'span1'r><'span6'f>>t<'row-fluid'<'span5'i><'span7'p>>",
85                 sPaginationType: 'bootstrap',
86                 // Handle the null values & the error : Datatables warning Requested unknown parameter
87                 // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2
88                 aoColumnDefs: [{sDefaultContent: '',aTargets: [ '_all' ]}],
89                 // WARNING: this one causes tables in a 'tabs' that are not exposed at the time this is run to show up empty
90                 // sScrollX: '100%',       /* Horizontal scrolling */
91                 bProcessing: true,      /* Loading */
92                 fnDrawCallback: function() { self._hazelnut_draw_callback.call(self); }
93                 // XXX use $.proxy here !
94             };
95             // the intention here is that options.datatables_options as coming from the python object take precedence
96             //  XXX DISABLED by jordan: was causing errors in datatables.js     $.extend(actual_options, options.datatables_options );
97             this.table = this.el('table').dataTable(actual_options);
98
99             /* Setup the SelectAll button in the dataTable header */
100             /* xxx not sure this is still working */
101             var oSelectAll = $('#datatableSelectAll-'+ this.options.plugin_uuid);
102             oSelectAll.html("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>Select All");
103             oSelectAll.button();
104             oSelectAll.css('font-size','11px');
105             oSelectAll.css('float','right');
106             oSelectAll.css('margin-right','15px');
107             oSelectAll.css('margin-bottom','5px');
108             oSelectAll.unbind('click');
109             oSelectAll.click(this._selectAll);
110
111             /* Add a filtering function to the current table 
112              * Note: we use closure to get access to the 'options'
113              */
114             $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) { 
115                 /* No filtering if the table does not match */
116                 if (oSettings.nTable.id != "hazelnut-" + self.options.plugin_uuid)
117                     return true;
118                 return this._hazelnut_filter.call(self, oSettings, aData, iDataIndex);
119             });
120
121             /* Processing hidden_columns */
122             $.each(this.options.hidden_columns, function(i, field) {
123                 self.hide_column(field);
124             });
125         }, // initialize_table
126
127         /**
128          * @brief Determine index of key in the table columns 
129          * @param key
130          * @param cols
131          */
132         getColIndex: function(key, cols) {
133             var tabIndex = $.map(cols, function(x, i) { if (x.sTitle == key) return i; });
134             return (tabIndex.length > 0) ? tabIndex[0] : -1;
135         }, // getColIndex
136
137  // UNUSED ? //         this.update_plugin = function(e, rows) {
138  // UNUSED ? //             // e.data is what we passed in second argument to subscribe
139  // UNUSED ? //             // so here it is the jquery object attached to the plugin <div>
140  // UNUSED ? //             var $plugindiv=e.data;
141  // UNUSED ? //             if (debug)
142  // UNUSED ? //                 messages.debug("entering hazelnut.update_plugin on id '" + $plugindiv.attr('id') + "'");
143  // UNUSED ? //             // clear the spinning wheel: look up an ancestor that has the need-spin class
144  // UNUSED ? //             // do this before we might return
145  // UNUSED ? //             $plugindiv.closest('.need-spin').spin(false);
146  // UNUSED ? // 
147  // UNUSED ? //             var options = this.options;
148  // UNUSED ? //             var hazelnut = this;
149  // UNUSED ? //     
150  // UNUSED ? //             /* if we get no result, or an error, try to make that clear, and exit */
151  // UNUSED ? //             if (rows.length==0) {
152  // UNUSED ? //                 if (debug) 
153  // UNUSED ? //                     messages.debug("Empty result on hazelnut " + this.options.domid);
154  // UNUSED ? //                 var placeholder=$(this.table).find("td.dataTables_empty");
155  // UNUSED ? //                 console.log("placeholder "+placeholder);
156  // UNUSED ? //                 if (placeholder.length==1) 
157  // UNUSED ? //                     placeholder.html(unfold.warning("Empty result"));
158  // UNUSED ? //                 else
159  // UNUSED ? //                     this.table.html(unfold.warning("Empty result"));
160  // UNUSED ? //                     return;
161  // UNUSED ? //             } else if (typeof(rows[0].error) != 'undefined') {
162  // UNUSED ? //                 // we now should have another means to report errors that this inline/embedded hack
163  // UNUSED ? //                 if (debug) 
164  // UNUSED ? //                     messages.error ("undefined result on " + this.options.domid + " - should not happen anymore");
165  // UNUSED ? //                 this.table.html(unfold.error(rows[0].error));
166  // UNUSED ? //                 return;
167  // UNUSED ? //             }
168  // UNUSED ? // 
169  // UNUSED ? //             /* 
170  // UNUSED ? //              * fill the dataTables object
171  // UNUSED ? //              * we cannot set html content directly here, need to use fnAddData
172  // UNUSED ? //              */
173  // UNUSED ? //             var lines = new Array();
174  // UNUSED ? //     
175  // UNUSED ? //             this.current_resources = Array();
176  // UNUSED ? //     
177  // UNUSED ? //             $.each(rows, function(index, row) {
178  // UNUSED ? //                 // this models a line in dataTables, each element in the line describes a cell
179  // UNUSED ? //                 line = new Array();
180  // UNUSED ? //      
181  // UNUSED ? //                 // go through table headers to get column names we want
182  // UNUSED ? //                 // in order (we have temporarily hack some adjustments in names)
183  // UNUSED ? //                 var cols = object.table.fnSettings().aoColumns;
184  // UNUSED ? //                 var colnames = cols.map(function(x) {return x.sTitle})
185  // UNUSED ? //                 var nb_col = cols.length;
186  // UNUSED ? //                 /* if we've requested checkboxes, then forget about the checkbox column for now */
187  // UNUSED ? //                 if (options.checkboxes) nb_col -= 1;
188  // UNUSED ? // 
189  // UNUSED ? //                 /* fill in stuff depending on the column name */
190  // UNUSED ? //                 for (var j = 0; j < nb_col; j++) {
191  // UNUSED ? //                     if (typeof colnames[j] == 'undefined') {
192  // UNUSED ? //                         line.push('...');
193  // UNUSED ? //                     } else if (colnames[j] == 'hostname') {
194  // UNUSED ? //                         if (row['type'] == 'resource,link')
195  // UNUSED ? //                             //TODO: we need to add source/destination for links
196  // UNUSED ? //                             line.push('');
197  // UNUSED ? //                         else
198  // UNUSED ? //                             line.push(row['hostname']);
199  // UNUSED ? //                     } else {
200  // UNUSED ? //                         if (row[colnames[j]])
201  // UNUSED ? //                             line.push(row[colnames[j]]);
202  // UNUSED ? //                         else
203  // UNUSED ? //                             line.push('');
204  // UNUSED ? //                     }
205  // UNUSED ? //                 }
206  // UNUSED ? //     
207  // UNUSED ? //                 /* catch up with the last column if checkboxes were requested */
208  // UNUSED ? //                 if (options.checkboxes) {
209  // UNUSED ? //                     var checked = '';
210  // UNUSED ? //                     // xxx problem is, we don't get this 'sliver' thing set apparently
211  // UNUSED ? //                     if (typeof(row['sliver']) != 'undefined') { /* It is equal to null when <sliver/> is present */
212  // UNUSED ? //                         checked = 'checked ';
213  // UNUSED ? //                         hazelnut.current_resources.push(row[ELEMENT_KEY]);
214  // UNUSED ? //                     }
215  // UNUSED ? //                     // Use a key instead of hostname (hard coded...)
216  // UNUSED ? //                     line.push(hazelnut.checkbox(options.plugin_uuid, row[ELEMENT_KEY], row['type'], checked, false));
217  // UNUSED ? //                 }
218  // UNUSED ? //     
219  // UNUSED ? //                 lines.push(line);
220  // UNUSED ? //     
221  // UNUSED ? //             });
222  // UNUSED ? //     
223  // UNUSED ? //             this.table.fnClearTable();
224  // UNUSED ? //             if (debug)
225  // UNUSED ? //                 messages.debug("hazelnut.update_plugin: total of " + lines.length + " rows");
226  // UNUSED ? //             this.table.fnAddData(lines);
227  // UNUSED ? //         
228  // UNUSED ? //         }, // update_plugin
229
230         checkbox: function (plugin_uuid, header, field) //, selected_str, disabled_str)
231         {
232             var result="";
233             if (header === null)
234                 header = '';
235             // Prefix id with plugin_uuid
236             result += "<input";
237             result += " class='hazelnut-checkbox'";
238             result += " id='" + this.id('checkbox', this.id_from_key(this.key, unfold.get_value(header))) + "'";
239              //hazelnut-checkbox-" + plugin_uuid + "-" + unfold.get_value(header).replace(/\\/g, '')  + "'";
240             result += " name='" + unfold.get_value(field) + "'";
241             result += " type='checkbox'";
242             //result += selected_str;
243             //result += disabled_str;
244             result += " autocomplete='off'";
245             result += " value='" + unfold.get_value(header) + "'";
246             result += "></input>";
247             return result;
248         }, // checkbox
249
250
251         new_record: function(record)
252         {
253             // this models a line in dataTables, each element in the line describes a cell
254             line = new Array();
255      
256             // go through table headers to get column names we want
257             // in order (we have temporarily hack some adjustments in names)
258             var cols = this.table.fnSettings().aoColumns;
259             var colnames = cols.map(function(x) {return x.sTitle})
260             var nb_col = cols.length;
261             /* if we've requested checkboxes, then forget about the checkbox column for now */
262             if (this.options.checkboxes) nb_col -= 1;
263
264             /* fill in stuff depending on the column name */
265             for (var j = 0; j < nb_col; j++) {
266                 if (typeof colnames[j] == 'undefined') {
267                     line.push('...');
268                 } else if (colnames[j] == 'hostname') {
269                     if (record['type'] == 'resource,link')
270                         //TODO: we need to add source/destination for links
271                         line.push('');
272                     else
273                         line.push(record['hostname']);
274                 } else {
275                     if (record[colnames[j]])
276                         line.push(record[colnames[j]]);
277                     else
278                         line.push('');
279                 }
280             }
281     
282             /* catch up with the last column if checkboxes were requested */
283             if (this.options.checkboxes)
284                 // Use a key instead of hostname (hard coded...)
285                 // XXX remove the empty checked attribute
286                 line.push(this.checkbox(this.options.plugin_uuid, record[ELEMENT_KEY], record['type']));
287     
288             // XXX Is adding an array of lines more efficient ?
289             this.table.fnAddData(line);
290
291         },
292
293         clear_table: function()
294         {
295             this.table.fnClearTable();
296         },
297
298         redraw_table: function()
299         {
300             this.table.fnDraw();
301         },
302
303         show_column: function(field)
304         {
305             var oSettings = this.table.fnSettings();
306             var cols = oSettings.aoColumns;
307             var index = this.getColIndex(field,cols);
308             if (index != -1)
309                 this.table.fnSetColumnVis(index, true);
310         },
311
312         hide_column: function(field)
313         {
314             var oSettings = this.table.fnSettings();
315             var cols = oSettings.aoColumns;
316             var index = this.getColIndex(field,cols);
317             if (index != -1)
318                 this.table.fnSetColumnVis(index, false);
319         },
320
321         set_checkbox: function(record, checked)
322         {
323             /* Default: checked = true */
324             if (typeof checked === 'undefined')
325                 checked = true;
326
327             var key_value;
328             /* The function accepts both records and their key */
329             switch (manifold.get_type(record)) {
330                 case TYPE_VALUE:
331                     key_value = record;
332                     break;
333                 case TYPE_RECORD:
334                     /* XXX Test the key before ? */
335                     key_value = record[this.key];
336                     break;
337                 default:
338                     throw "Not implemented";
339                     break;
340             }
341
342
343             var checkbox_id = this.id('checkbox', this.id_from_key(this.key, key_value));
344             checkbox_id = '#' + checkbox_id.replace(/\./g, '\\.');
345
346             var element = $(checkbox_id, this.table.fnGetNodes());
347
348             element.attr('checked', checked);
349         },
350
351         /*************************** QUERY HANDLER ****************************/
352
353         on_filter_added: function(filter)
354         {
355             // XXX
356             this.redraw_table();
357         },
358
359         on_filter_removed: function(filter)
360         {
361             // XXX
362             this.redraw_table();
363         },
364         
365         on_filter_clear: function()
366         {
367             // XXX
368             this.redraw_table();
369         },
370
371         on_field_added: function(field)
372         {
373             this.show_column(field);
374         },
375
376         on_field_removed: function(field)
377         {
378             this.hide_column(field);
379         },
380
381         on_field_clear: function()
382         {
383             alert('Hazelnut::clear_fields() not implemented');
384         },
385
386         /*************************** RECORD HANDLER ***************************/
387
388         on_new_record: function(record)
389         {
390             /* NOTE in fact we are doing a join here */
391             if (this.received_all)
392                 // update checkbox for record
393                 this.set_checkbox(record, true);
394             else
395                 // store for later update of checkboxes
396                 this.in_set_buffer.push(record);
397         },
398
399         on_clear_records: function()
400         {
401         },
402
403         // Could be the default in parent
404         on_query_in_progress: function()
405         {
406             this.spin();
407         },
408
409         on_query_done: function()
410         {
411             if (this.received_all)
412                 this.unspin();
413             this.received_set = true;
414         },
415         
416         on_field_state_changed: function(data)
417         {
418             switch(data.request) {
419                 case FIELD_REQUEST_ADD:
420                 case FIELD_REQUEST_ADD_RESET:
421                     this.set_checkbox(data.value, true);
422                     break;
423                 case FIELD_REQUEST_REMOVE:
424                 case FIELD_REQUEST_REMOVE_RESET:
425                     this.set_checkbox(data.value, false);
426                     break;
427                 default:
428                     break;
429             }
430         },
431
432         // all
433
434         on_all_new_record: function(record)
435         {
436             this.new_record(record);
437         },
438
439         on_all_clear_records: function()
440         {
441             this.clear_table();
442
443         },
444
445         on_all_query_in_progress: function()
446         {
447             // XXX parent
448             this.spin();
449         }, // on_all_query_in_progress
450
451         on_all_query_done: function()
452         {
453             var self = this;
454             if (this.received_set) {
455                 /* XXX needed ? XXX We uncheck all checkboxes ... */
456                 $("[id^='datatables-checkbox-" + this.options.plugin_uuid +"']").attr('checked', false);
457
458                 /* ... and check the ones specified in the resource list */
459                 $.each(this.in_set_buffer, function(i, record) {
460                     self.set_checkbox(record, true);
461                 });
462
463                 this.unspin();
464             }
465             this.received_all = true;
466
467         }, // on_all_query_done
468
469         /************************** PRIVATE METHODS ***************************/
470
471         /** 
472          * @brief Hazelnut filtering function
473          */
474         _hazelnut_filter: function(oSettings, aData, iDataIndex)
475         {
476             var cur_query = this.current_query;
477             if (!cur_query) return true;
478             var ret = true;
479
480             /* We have an array of filters : a filter is an array (key op val) 
481              * field names (unless shortcut)    : oSettings.aoColumns  = [ sTitle ]
482              *     can we exploit the data property somewhere ?
483              * field values (unless formatting) : aData
484              *     formatting should leave original data available in a hidden field
485              *
486              * The current line should validate all filters
487              */
488             $.each (cur_query.filters, function(index, filter) { 
489                 /* XXX How to manage checkbox ? */
490                 var key = filter[0]; 
491                 var op = filter[1];
492                 var value = filter[2];
493
494                 /* Determine index of key in the table columns */
495                 var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0];
496
497                 /* Unknown key: no filtering */
498                 if (typeof(col) == 'undefined')
499                     return;
500
501                 col_value=unfold.get_value(aData[col]);
502                 /* Test whether current filter is compatible with the column */
503                 if (op == '=' || op == '==') {
504                     if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a")
505                         ret = false;
506                 }else if (op == '!=') {
507                     if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a")
508                         ret = false;
509                 } else if(op=='<') {
510                     if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a")
511                         ret = false;
512                 } else if(op=='>') {
513                     if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a")
514                         ret = false;
515                 } else if(op=='<=' || op=='≤') {
516                     if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a")
517                         ret = false;
518                 } else if(op=='>=' || op=='≥') {
519                     if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a")
520                         ret = false;
521                 }else{
522                     // How to break out of a loop ?
523                     alert("filter not supported");
524                     return false;
525                 }
526
527             });
528             return ret;
529         },
530
531         _hazelnut_draw_callback: function()
532         {
533             /* 
534              * Handle clicks on checkboxes: reassociate checkbox click every time
535              * the table is redrawn 
536              */
537             this.els('hazelnut-checkbox').unbind('click').click(this, this._check_click);
538
539             if (!this.table)
540                 return;
541
542             /* Remove pagination if we show only a few results */
543             var wrapper = this.table; //.parent().parent().parent();
544             var rowsPerPage = this.table.fnSettings()._iDisplayLength;
545             var rowsToShow = this.table.fnSettings().fnRecordsDisplay();
546             var minRowsPerPage = this.table.fnSettings().aLengthMenu[0];
547
548             if ( rowsToShow <= rowsPerPage || rowsPerPage == -1 ) {
549                 $('.hazelnut_paginate', wrapper).css('visibility', 'hidden');
550             } else {
551                 $('.hazelnut_paginate', wrapper).css('visibility', 'visible');
552             }
553
554             if ( rowsToShow <= minRowsPerPage ) {
555                 $('.hazelnut_length', wrapper).css('visibility', 'hidden');
556             } else {
557                 $('.hazelnut_length', wrapper).css('visibility', 'visible');
558             }
559         },
560
561         _check_click: function(e) 
562         {
563             e.stopPropagation();
564
565             var self = e.data;
566
567             // XXX this.value = key of object to be added... what about multiple keys ?
568             manifold.raise_event(self.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
569             //return false; // prevent checkbox to be checked, waiting response from manifold plugin api
570             
571         },
572
573         _selectAll: function() 
574         {
575             // requires jQuery id
576             var uuid=this.id.split("-");
577             var oTable=$("#hazelnut-"+uuid[1]).dataTable();
578             // Function available in Hazelnut 1.9.x
579             // Filter : displayed data only
580             var filterData = oTable._('tr', {"filter":"applied"});   
581             /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */        
582             if(filterData.length<=100){
583                 $.each(filterData, function(index, obj) {
584                     var last=$(obj).last();
585                     var key_value=unfold.get_value(last[0]);
586                     if(typeof($(last[0]).attr('checked'))=="undefined"){
587                         $.publish('selected', 'add/'+key_value);
588                     }
589                 });
590             }
591         },
592
593     });
594
595     $.plugin('Hazelnut', Hazelnut);
596
597 })(jQuery);