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