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