9eeb86cf13fe9d4e684db016de69d389d8ccd5e9
[myslice.git] / plugins / querytable / static / js / querytable.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     var debug=false;
10     debug=true
11     var debug_deep=false;
12 //    debug_deep=true;
13
14     var QueryTable = Plugin.extend({
15
16         init: function(options, element) {
17             this._super(options, element);
18
19             /* Member variables */
20             // in general we expect 2 queries here
21             // query_uuid refers to a single object (typically a slice)
22             // query_all_uuid refers to a list (typically resources or users)
23             // these can return in any order so we keep track of which has been received yet
24             this.received_all_query = false;
25             this.received_query = false;
26
27 //            // We need to remember the active filter for datatables filtering
28 //            this.filters = Array(); 
29
30             // an internal buffer for records that are 'in' and thus need to be checked 
31             this.buffered_records_to_check = [];
32
33             /* XXX Events XXX */
34             // this.$element.on('show.Datatables', this.on_show);
35             this.elmt().on('show', this, this.on_show);
36             // Unbind all events using namespacing
37             // TODO in destructor
38             // $(window).unbind('QueryTable');
39
40             var query = manifold.query_store.find_analyzed_query(this.options.query_uuid);
41             this.method = query.object;
42
43             // xxx beware that this.key needs to contain a key that all records will have
44             // in general query_all will return well populated records, but query
45             // returns records with only the fields displayed on startup. 
46             this.key = (this.options.id_key);
47             if (! this.key) {
48                 // if not specified by caller, decide from metadata
49                 var keys = manifold.metadata.get_key(this.method);
50                 this.key = (keys && keys.length == 1) ? keys[0] : null;
51             }
52             if (! this.key) messages.warning("querytable.init could not kind valid key");
53
54             if (debug) messages.debug("querytable: key="+this.key);
55
56             /* Setup query and record handlers */
57             this.listen_query(options.query_uuid);
58             this.listen_query(options.query_all_uuid, 'all');
59
60             /* GUI setup and event binding */
61             this.initialize_table();
62         },
63
64         /* PLUGIN EVENTS */
65
66         on_show: function(e) {
67             var self = e.data;
68             self.table.fnAdjustColumnSizing()
69         }, // on_show
70
71         /* GUI EVENTS */
72
73         /* GUI MANIPULATION */
74
75         initialize_table: function() {
76             // compute columns based on columns and hidden_columns
77             this.slick_columns = [];
78             var all_columns = this.options.columns; // .concat(this.options.hidden_columns)
79             // xxx would be helpful to support a column_renamings options arg
80             // for redefining some labels like 'network_hrn' that really are not meaningful
81             for (c in all_columns) {
82                 var column=all_columns[c];
83                 this.slick_columns.push ( {id:column, name:column, field:column, 
84                                            cssClass: "querytable-column-"+column,
85                                            width:100, minWidth:40, });
86             }
87
88             // xxx should be extensible from caller with this.options.slickgrid_options 
89             this.slick_options = {
90                 enableCellNavigation: false,
91                 enableColumnReorder: true,
92             };
93
94             this.slick_data=[];
95             
96             var selector="#grid-"+this.options.domid;
97             if (debug_deep) {
98                 messages.debug("slick grid selector is " + selector);
99                 for (c in this.slick_columns) {
100                     var col=this.slick_columns[c];
101                     var msg="";
102                     for (k in col) msg = msg+" col["+k+"]="+col[k];
103                     messages.debug("slick_column["+c+"]:"+msg);
104                 }
105             }
106             // add a checkbox column
107             var checkbox_selector = new Slick.CheckboxSelectColumn({
108                 cssClass: "slick-cell-checkboxsel"
109             });
110             this.slick_columns.push(checkbox_selector.getColumnDefinition());
111             this.slick_grid = new Slick.Grid(selector, this.slick_data, this.slick_columns, this.slick_options);
112             this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false}));
113             this.slick_grid.registerPlugin (checkbox_selector);
114             // autotooltips: for showing the full column name when ellipsed
115             var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true });
116             this.slick_grid.registerPlugin (auto_tooltips);
117             
118             this.columnpicker = new Slick.Controls.ColumnPicker (this.slick_columns, this.slick_grid, this.slick_options)
119
120         }, // initialize_table
121
122         // Determine index of key in the table columns 
123         getColIndex: function(key, cols) {
124             var tabIndex = $.map(cols, function(x, i) { if (x.sTitle == key) return i; });
125             return (tabIndex.length > 0) ? tabIndex[0] : -1;
126         }, // getColIndex
127
128         checkbox_html : function (key, value) {
129             if (debug_deep) messages.debug("checkbox_html, value="+value);
130             var result="";
131             // Prefix id with plugin_uuid
132             result += "<input";
133             result += " class='querytable-checkbox'";
134             result += " id='" + this.flat_id(this.id('checkbox', value)) + "'";
135             result += " name='" + key + "'";
136             result += " type='checkbox'";
137             result += " autocomplete='off'";
138             if (value === undefined) {
139                 messages.warning("querytable.checkbox_html - undefined value");
140             } else {
141                 result += " value='" + value + "'";
142             }
143             result += "></input>";
144             return result;
145         }, 
146
147         new_record: function(record) {
148             this.slick_data.push(record);
149         },
150
151         clear_table: function() {
152             console.log("clear_table not implemented");
153         },
154
155         redraw_table: function() {
156             this.table.fnDraw();
157         },
158
159         show_column: function(field) {
160             var oSettings = this.table.fnSettings();
161             var cols = oSettings.aoColumns;
162             var index = this.getColIndex(field,cols);
163             if (index != -1)
164                 this.table.fnSetColumnVis(index, true);
165         },
166
167         hide_column: function(field) {
168             console.log("hide_column not implemented - field="+field);
169         },
170
171         set_checkbox: function(record, checked) {
172             console.log("set_checkbox not yet implemented with slickgrid");
173             return;
174             /* Default: checked = true */
175             if (checked === undefined) checked = true;
176
177             var id;
178             /* The function accepts both records and their key */
179             switch (manifold.get_type(record)) {
180             case TYPE_VALUE:
181                 id = record;
182                 break;
183             case TYPE_RECORD:
184                 /* XXX Test the key before ? */
185                 id = record[this.key];
186                 break;
187             default:
188                 throw "Not implemented";
189                 break;
190             }
191
192
193             if (id === undefined) {
194                 messages.warning("querytable.set_checkbox record has no id to figure which line to tick");
195                 return;
196             }
197             var checkbox_id = this.flat_id(this.id('checkbox', id));
198             // function escape_id(myid) is defined in portal/static/js/common.functions.js
199             checkbox_id = escape_id(checkbox_id);
200             // using dataTables's $ to search also in nodes that are not currently displayed
201             var element = this.table.$(checkbox_id);
202             if (debug_deep) 
203                 messages.debug("set_checkbox checked=" + checked
204                                + " id=" + checkbox_id + " matches=" + element.length);
205             element.attr('checked', checked);
206         },
207
208         /*************************** QUERY HANDLER ****************************/
209
210         on_filter_added: function(filter) {
211             this.filters.push(filter);
212             this.redraw_table();
213         },
214
215         on_filter_removed: function(filter) {
216             // Remove corresponding filters
217             this.filters = $.grep(this.filters, function(x) {
218                 return x != filter;
219             });
220             this.redraw_table();
221         },
222         
223         on_filter_clear: function() {
224             this.redraw_table();
225         },
226
227         on_field_added: function(field) {
228             this.show_column(field);
229         },
230
231         on_field_removed: function(field) {
232             this.hide_column(field);
233         },
234
235         on_field_clear: function() {
236             alert('QueryTable::clear_fields() not implemented');
237         },
238
239         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
240         /*************************** ALL QUERY HANDLER ****************************/
241
242         on_all_filter_added: function(filter) {
243             // XXX
244             this.redraw_table();
245         },
246
247         on_all_filter_removed: function(filter) {
248             // XXX
249             this.redraw_table();
250         },
251         
252         on_all_filter_clear: function() {
253             // XXX
254             this.redraw_table();
255         },
256
257         on_all_field_added: function(field) {
258             this.show_column(field);
259         },
260
261         on_all_field_removed: function(field) {
262             this.hide_column(field);
263         },
264
265         on_all_field_clear: function() {
266             alert('QueryTable::clear_fields() not implemented');
267         },
268
269
270         /*************************** RECORD HANDLER ***************************/
271
272         on_new_record: function(record) {
273             if (this.received_all_query) {
274                 // if the 'all' query has been dealt with already we may turn on the checkbox
275                 this.set_checkbox(record, true);
276             } else {
277                 // otherwise we need to remember that and do it later on
278                 if (debug) messages.debug("Remembering record to check " + record[this.key]);
279                 this.buffered_records_to_check.push(record);
280             }
281         },
282
283         on_clear_records: function() {
284         },
285
286         // Could be the default in parent
287         on_query_in_progress: function() {
288             this.spin();
289         },
290
291         on_query_done: function() {
292             this.received_query = true;
293             // unspin once we have received both
294             if (this.received_all_query && this.received_query) this.unspin();
295         },
296         
297         on_field_state_changed: function(data) {
298             switch(data.request) {
299                 case FIELD_REQUEST_ADD:
300                 case FIELD_REQUEST_ADD_RESET:
301                     this.set_checkbox(data.value, true);
302                     break;
303                 case FIELD_REQUEST_REMOVE:
304                 case FIELD_REQUEST_REMOVE_RESET:
305                     this.set_checkbox(data.value, false);
306                     break;
307                 default:
308                     break;
309             }
310         },
311
312         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
313         // all
314         on_all_field_state_changed: function(data) {
315             switch(data.request) {
316                 case FIELD_REQUEST_ADD:
317                 case FIELD_REQUEST_ADD_RESET:
318                     this.set_checkbox(data.value, true);
319                     break;
320                 case FIELD_REQUEST_REMOVE:
321                 case FIELD_REQUEST_REMOVE_RESET:
322                     this.set_checkbox(data.value, false);
323                     break;
324                 default:
325                     break;
326             }
327         },
328
329         on_all_new_record: function(record) {
330             this.new_record(record);
331         },
332
333         on_all_clear_records: function() {
334             this.clear_table();
335
336         },
337
338         on_all_query_in_progress: function() {
339             // XXX parent
340             this.spin();
341         }, // on_all_query_in_progress
342
343         on_all_query_done: function() {
344             if (debug) messages.debug("1-shot initializing dataTables content with " + this.slick_data.length + " lines");
345             var start=new Date();
346             this.slick_grid.setData (this.slick_data, true);
347             this.slick_grid.autosizeColumns();
348             this.slick_grid.render();
349             var duration=new Date()-start;
350             if (debug) messages.debug("setData+render took " + duration + " ms");
351             if (debug_deep) {
352                 // show full contents of first row app
353                 for (k in this.slick_data[0]) messages.debug("slick_data[0]["+k+"]="+this.slick_data[0][k]);
354             }
355             
356             var self = this;
357             // if we've already received the slice query, we have not been able to set 
358             // checkboxes on the fly at that time (dom not yet created)
359             $.each(this.buffered_records_to_check, function(i, record) {
360                 if (debug) messages.debug ("delayed turning on checkbox " + i + " record= " + record);
361                 self.set_checkbox(record, true);
362             });
363             this.buffered_records_to_check = [];
364
365             this.received_all_query = true;
366             // unspin once we have received both
367             if (this.received_all_query && this.received_query) this.unspin();
368
369         }, // on_all_query_done
370
371         /************************** PRIVATE METHODS ***************************/
372
373         /** 
374          * @brief QueryTable filtering function
375          */
376         _querytable_filter: function(oSettings, aData, iDataIndex) {
377             var ret = true;
378             $.each (this.filters, function(index, filter) { 
379                 /* XXX How to manage checkbox ? */
380                 var key = filter[0]; 
381                 var op = filter[1];
382                 var value = filter[2];
383
384                 /* Determine index of key in the table columns */
385                 var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0];
386
387                 /* Unknown key: no filtering */
388                 if (typeof(col) == 'undefined')
389                     return;
390
391                 col_value=unfold.get_value(aData[col]);
392                 /* Test whether current filter is compatible with the column */
393                 if (op == '=' || op == '==') {
394                     if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a")
395                         ret = false;
396                 }else if (op == '!=') {
397                     if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a")
398                         ret = false;
399                 } else if(op=='<') {
400                     if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a")
401                         ret = false;
402                 } else if(op=='>') {
403                     if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a")
404                         ret = false;
405                 } else if(op=='<=' || op=='≤') {
406                     if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a")
407                         ret = false;
408                 } else if(op=='>=' || op=='≥') {
409                     if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a")
410                         ret = false;
411                 }else{
412                     // How to break out of a loop ?
413                     alert("filter not supported");
414                     return false;
415                 }
416
417             });
418             return ret;
419         },
420
421         _querytable_draw_callback: function() {
422             /* 
423              * Handle clicks on checkboxes: reassociate checkbox click every time
424              * the table is redrawn 
425              */
426             this.elts('querytable-checkbox').unbind('click').click(this, this._check_click);
427
428             if (!this.table)
429                 return;
430
431             /* Remove pagination if we show only a few results */
432             var wrapper = this.table; //.parent().parent().parent();
433             var rowsPerPage = this.table.fnSettings()._iDisplayLength;
434             var rowsToShow = this.table.fnSettings().fnRecordsDisplay();
435             var minRowsPerPage = this.table.fnSettings().aLengthMenu[0];
436
437             if ( rowsToShow <= rowsPerPage || rowsPerPage == -1 ) {
438                 $('.querytable_paginate', wrapper).css('visibility', 'hidden');
439             } else {
440                 $('.querytable_paginate', wrapper).css('visibility', 'visible');
441             }
442
443             if ( rowsToShow <= minRowsPerPage ) {
444                 $('.querytable_length', wrapper).css('visibility', 'hidden');
445             } else {
446                 $('.querytable_length', wrapper).css('visibility', 'visible');
447             }
448         },
449
450         _check_click: function(e) {
451             e.stopPropagation();
452
453             var self = e.data;
454
455             // XXX this.value = key of object to be added... what about multiple keys ?
456             if (debug) messages.debug("querytable click handler checked=" + this.checked + " hrn=" + this.value);
457             manifold.raise_event(self.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
458             //return false; // prevent checkbox to be checked, waiting response from manifold plugin api
459             
460         },
461
462         _selectAll: function() {
463             // requires jQuery id
464             var uuid=this.id.split("-");
465             var oTable=$("#querytable-"+uuid[1]).dataTable();
466             // Function available in QueryTable 1.9.x
467             // Filter : displayed data only
468             var filterData = oTable._('tr', {"filter":"applied"});   
469             /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */        
470             if(filterData.length<=100){
471                 $.each(filterData, function(index, obj) {
472                     var last=$(obj).last();
473                     var key_value=unfold.get_value(last[0]);
474                     if(typeof($(last[0]).attr('checked'))=="undefined"){
475                         $.publish('selected', 'add/'+key_value);
476                     }
477                 });
478             }
479         },
480
481     });
482
483     $.plugin('QueryTable', QueryTable);
484
485 //  /* define the 'dom-checkbox' type for sorting in datatables 
486 //     http://datatables.net/examples/plug-ins/dom_sort.html
487 //     using trial and error I found that the actual column number
488 //     was in fact given as a third argument, and not second 
489 //     as the various online resources had it - go figure */
490 //    $.fn.dataTableExt.afnSortData['dom-checkbox'] = function  ( oSettings, _, iColumn ) {
491 //      return $.map( oSettings.oApi._fnGetTrNodes(oSettings), function (tr, i) {
492 //          return result=$('td:eq('+iColumn+') input', tr).prop('checked') ? '1' : '0';
493 //      } );
494 //    }
495
496 })(jQuery);
497