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