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