checkboxes behave as expected
[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                 showHeaderRow: true,
93                 syncColumnCellResize: true,
94             };
95
96             this.slick_data = [];
97             this.slick_dataview = new Slick.Data.DataView();
98             var self=this;
99             this.slick_dataview.onRowCountChanged.subscribe ( function (e,args) {
100                 self.slick_grid.updateRowCount();
101                 self.slick_grid.autosizeColumns();
102                 self.slick_grid.render();
103             });
104             
105             var selector="#grid-"+this.options.domid;
106             if (debug_deep) {
107                 messages.debug("slick grid selector is " + selector);
108                 for (c in this.slick_columns) {
109                     var col=this.slick_columns[c];
110                     var msg="";
111                     for (k in col) msg = msg+" col["+k+"]="+col[k];
112                     messages.debug("slick_column["+c+"]:"+msg);
113                 }
114             }
115             // add a checkbox column
116             var checkbox_selector = new Slick.CheckboxSelectColumn({
117                 cssClass: "slick-cell-checkboxsel"
118             });
119             this.slick_columns.push(checkbox_selector.getColumnDefinition());
120             this.slick_grid = new Slick.Grid(selector, this.slick_dataview, this.slick_columns, this.slick_options);
121             this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false}));
122             this.slick_grid.registerPlugin (checkbox_selector);
123             // autotooltips: for showing the full column name when ellipsed
124             var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true });
125             this.slick_grid.registerPlugin (auto_tooltips);
126             
127             this.columnpicker = new Slick.Controls.ColumnPicker (this.slick_columns, this.slick_grid, this.slick_options)
128
129
130         }, // initialize_table
131
132         new_record: function(record) {
133             this.slick_data.push(record);
134         },
135
136         clear_table: function() {
137             this.slick_data=[];
138             this.slick_dataview.setItems(this.slick_data,this.key);
139         },
140
141         redraw_table: function() {
142             this.slick_grid.autosizeColumns();
143             this.slick_grid.render();
144         },
145
146         show_column: function(field) {
147             console.log ("querytable.show_column not yet implemented with slickgrid - field="+field);
148         },
149
150         hide_column: function(field) {
151             console.log("querytable.hide_column not implemented with slickgrid - field="+field);
152         },
153
154         set_checkbox: function(record, checked) {
155             /* Default: checked = true */
156             if (checked === undefined) checked = true;
157
158             var id;
159             /* The function accepts both records and their key */
160             switch (manifold.get_type(record)) {
161             case TYPE_VALUE:
162                 id = record;
163                 break;
164             case TYPE_RECORD:
165                 /* XXX Test the key before ? */
166                 id = record[this.key];
167                 break;
168             default:
169                 throw "Not implemented";
170                 break;
171             }
172
173
174             if (id === undefined) {
175                 messages.warning("querytable.set_checkbox record has no id to figure which line to tick");
176                 return;
177             }
178             var index = this.slick_dataview.getIdxById(id);
179             var selectedRows=this.slick_grid.getSelectedRows();
180             if (checked) // add index in current list
181                 selectedRows=selectedRows.concat(index);
182             else
183                 selectedRows=selectedRows.filter(function(idx) {return idx!=index;});
184             this.slick_grid.setSelectedRows(selectedRows);
185         },
186
187         /*************************** QUERY HANDLER ****************************/
188
189         on_filter_added: function(filter) {
190             this.filters.push(filter);
191             this.redraw_table();
192         },
193
194         on_filter_removed: function(filter) {
195             // Remove corresponding filters
196             this.filters = $.grep(this.filters, function(x) {
197                 return x != filter;
198             });
199             this.redraw_table();
200         },
201         
202         on_filter_clear: function() {
203             this.redraw_table();
204         },
205
206         on_field_added: function(field) {
207             this.show_column(field);
208         },
209
210         on_field_removed: function(field) {
211             this.hide_column(field);
212         },
213
214         on_field_clear: function() {
215             alert('QueryTable::clear_fields() not implemented');
216         },
217
218         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
219         /*************************** ALL QUERY HANDLER ****************************/
220
221         on_all_filter_added: function(filter) {
222             // XXX
223             this.redraw_table();
224         },
225
226         on_all_filter_removed: function(filter) {
227             // XXX
228             this.redraw_table();
229         },
230         
231         on_all_filter_clear: function() {
232             // XXX
233             this.redraw_table();
234         },
235
236         on_all_field_added: function(field) {
237             this.show_column(field);
238         },
239
240         on_all_field_removed: function(field) {
241             this.hide_column(field);
242         },
243
244         on_all_field_clear: function() {
245             alert('QueryTable::clear_fields() not implemented');
246         },
247
248
249         /*************************** RECORD HANDLER ***************************/
250
251         on_new_record: function(record) {
252             if (this.received_all_query) {
253                 // if the 'all' query has been dealt with already we may turn on the checkbox
254                 this.set_checkbox(record, true);
255             } else {
256                 // otherwise we need to remember that and do it later on
257                 if (debug) messages.debug("Remembering record to check " + record[this.key]);
258                 this.buffered_records_to_check.push(record);
259             }
260         },
261
262         on_clear_records: function() {
263         },
264
265         // Could be the default in parent
266         on_query_in_progress: function() {
267             this.spin();
268         },
269
270         on_query_done: function() {
271             this.received_query = true;
272             // unspin once we have received both
273             if (this.received_all_query && this.received_query) this.unspin();
274         },
275         
276         on_field_state_changed: function(data) {
277             switch(data.request) {
278                 case FIELD_REQUEST_ADD:
279                 case FIELD_REQUEST_ADD_RESET:
280                     this.set_checkbox(data.value, true);
281                     break;
282                 case FIELD_REQUEST_REMOVE:
283                 case FIELD_REQUEST_REMOVE_RESET:
284                     this.set_checkbox(data.value, false);
285                     break;
286                 default:
287                     break;
288             }
289         },
290
291         /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
292         // all
293         on_all_field_state_changed: function(data) {
294             switch(data.request) {
295                 case FIELD_REQUEST_ADD:
296                 case FIELD_REQUEST_ADD_RESET:
297                     this.set_checkbox(data.value, true);
298                     break;
299                 case FIELD_REQUEST_REMOVE:
300                 case FIELD_REQUEST_REMOVE_RESET:
301                     this.set_checkbox(data.value, false);
302                     break;
303                 default:
304                     break;
305             }
306         },
307
308         on_all_new_record: function(record) {
309             this.new_record(record);
310         },
311
312         on_all_clear_records: function() {
313             this.clear_table();
314
315         },
316
317         on_all_query_in_progress: function() {
318             // XXX parent
319             this.spin();
320         }, // on_all_query_in_progress
321
322         on_all_query_done: function() {
323             if (debug) messages.debug("1-shot initializing dataTables content with " + this.slick_data.length + " lines");
324             var start=new Date();
325             // use this.key as the key for identifying rows
326             this.slick_dataview.setItems (this.slick_data, this.key);
327             var duration=new Date()-start;
328             if (debug) messages.debug("setItems " + duration + " ms");
329             if (debug_deep) {
330                 // show full contents of first row app
331                 for (k in this.slick_data[0]) messages.debug("slick_data[0]["+k+"]="+this.slick_data[0][k]);
332             }
333             
334             var self = this;
335             // if we've already received the slice query, we have not been able to set 
336             // checkboxes on the fly at that time (dom not yet created)
337             $.each(this.buffered_records_to_check, function(i, record) {
338                 if (debug) messages.debug ("delayed turning on checkbox " + i + " record= " + record);
339                 self.set_checkbox(record, true);
340             });
341             this.buffered_records_to_check = [];
342
343             this.received_all_query = true;
344             // unspin once we have received both
345             if (this.received_all_query && this.received_query) this.unspin();
346
347         }, // on_all_query_done
348
349         /************************** PRIVATE METHODS ***************************/
350
351         /** 
352          * @brief QueryTable filtering function
353          */
354         _querytable_filter: function(oSettings, aData, iDataIndex) {
355             var ret = true;
356             $.each (this.filters, function(index, filter) { 
357                 /* XXX How to manage checkbox ? */
358                 var key = filter[0]; 
359                 var op = filter[1];
360                 var value = filter[2];
361
362                 /* Determine index of key in the table columns */
363                 var col = $.map(oSettings.aoColumns, function(x, i) {if (x.sTitle == key) return i;})[0];
364
365                 /* Unknown key: no filtering */
366                 if (typeof(col) == 'undefined')
367                     return;
368
369                 col_value=unfold.get_value(aData[col]);
370                 /* Test whether current filter is compatible with the column */
371                 if (op == '=' || op == '==') {
372                     if ( col_value != value || col_value==null || col_value=="" || col_value=="n/a")
373                         ret = false;
374                 }else if (op == '!=') {
375                     if ( col_value == value || col_value==null || col_value=="" || col_value=="n/a")
376                         ret = false;
377                 } else if(op=='<') {
378                     if ( parseFloat(col_value) >= value || col_value==null || col_value=="" || col_value=="n/a")
379                         ret = false;
380                 } else if(op=='>') {
381                     if ( parseFloat(col_value) <= value || col_value==null || col_value=="" || col_value=="n/a")
382                         ret = false;
383                 } else if(op=='<=' || op=='≤') {
384                     if ( parseFloat(col_value) > value || col_value==null || col_value=="" || col_value=="n/a")
385                         ret = false;
386                 } else if(op=='>=' || op=='≥') {
387                     if ( parseFloat(col_value) < value || col_value==null || col_value=="" || col_value=="n/a")
388                         ret = false;
389                 }else{
390                     // How to break out of a loop ?
391                     alert("filter not supported");
392                     return false;
393                 }
394
395             });
396             return ret;
397         },
398
399         _querytable_draw_callback: function() {
400             /* 
401              * Handle clicks on checkboxes: reassociate checkbox click every time
402              * the table is redrawn 
403              */
404             this.elts('querytable-checkbox').unbind('click').click(this, this._check_click);
405
406             if (!this.table)
407                 return;
408
409             /* Remove pagination if we show only a few results */
410             var wrapper = this.table; //.parent().parent().parent();
411             var rowsPerPage = this.table.fnSettings()._iDisplayLength;
412             var rowsToShow = this.table.fnSettings().fnRecordsDisplay();
413             var minRowsPerPage = this.table.fnSettings().aLengthMenu[0];
414
415             if ( rowsToShow <= rowsPerPage || rowsPerPage == -1 ) {
416                 $('.querytable_paginate', wrapper).css('visibility', 'hidden');
417             } else {
418                 $('.querytable_paginate', wrapper).css('visibility', 'visible');
419             }
420
421             if ( rowsToShow <= minRowsPerPage ) {
422                 $('.querytable_length', wrapper).css('visibility', 'hidden');
423             } else {
424                 $('.querytable_length', wrapper).css('visibility', 'visible');
425             }
426         },
427
428         _check_click: function(e) {
429             e.stopPropagation();
430
431             var self = e.data;
432
433             // XXX this.value = key of object to be added... what about multiple keys ?
434             if (debug) messages.debug("querytable click handler checked=" + this.checked + " hrn=" + this.value);
435             manifold.raise_event(self.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
436             //return false; // prevent checkbox to be checked, waiting response from manifold plugin api
437             
438         },
439
440         _selectAll: function() {
441             // requires jQuery id
442             var uuid=this.id.split("-");
443             var oTable=$("#querytable-"+uuid[1]).dataTable();
444             // Function available in QueryTable 1.9.x
445             // Filter : displayed data only
446             var filterData = oTable._('tr', {"filter":"applied"});   
447             /* TODO: WARNING if too many nodes selected, use filters to reduce nuber of nodes */        
448             if(filterData.length<=100){
449                 $.each(filterData, function(index, obj) {
450                     var last=$(obj).last();
451                     var key_value=unfold.get_value(last[0]);
452                     if(typeof($(last[0]).attr('checked'))=="undefined"){
453                         $.publish('selected', 'add/'+key_value);
454                     }
455                 });
456             }
457         },
458
459     });
460
461     $.plugin('QueryTable', QueryTable);
462
463 //  /* define the 'dom-checkbox' type for sorting in datatables 
464 //     http://datatables.net/examples/plug-ins/dom_sort.html
465 //     using trial and error I found that the actual column number
466 //     was in fact given as a third argument, and not second 
467 //     as the various online resources had it - go figure */
468 //    $.fn.dataTableExt.afnSortData['dom-checkbox'] = function  ( oSettings, _, iColumn ) {
469 //      return $.map( oSettings.oApi._fnGetTrNodes(oSettings), function (tr, i) {
470 //          return result=$('td:eq('+iColumn+') input', tr).prop('checked') ? '1' : '0';
471 //      } );
472 //    }
473
474 })(jQuery);
475