query_editor plugin fix autocomplete with datatables pagination, search in objects...
[myslice.git] / plugins / query_editor / static / js / query_editor.js
1 /**
2  * Description: QueryEditor plugin
3  * Copyright (c) 2012-2013 UPMC Sorbonne Universite
4  * License: GPLv3
5  */
6
7 // XXX TODO This plugin will be interested in changes in metadata
8 // What if we remove a filter, is it removed in the right min/max field ???
9 //  -> no on_filter_removed is not yet implemented
10 // XXX if a plugin has not declared a handler, it might become inconsistent,
11 // and the interface should either reset or disable it
12 (function($){
13
14     var QueryEditor = Plugin.extend({
15
16         event_filter_added: function(op, suffix) {
17             suffix = (typeof suffix === 'undefined') ? '' : manifold.separator + suffix;
18             var self = this;
19             return function(e, ui) {
20                 var array = self.array_from_id(e.target.id);
21                 var key   = self.field_from_id(array); // No need to remove suffix...
22
23                 // using autocomplete ui
24                 if(typeof(ui) != "undefined"){
25                     var value = ui.item.value;
26                 }else{
27                     var value = e.target.value;
28                 }
29
30                 if (value) {
31                     // XXX This should be handled by manifold
32                     //manifold.raise_event(object.options.query_uuid, FILTER_UPDATED, [key, op, value]);
33                     manifold.raise_event(self.options.query_uuid, FILTER_ADDED, [key, op, value]);
34                 } else {
35                     // XXX This should be handled by manifold
36                     manifold.raise_event(self.options.query_uuid, FILTER_REMOVED, [key, op]);
37                 }
38             }
39         },
40
41         init: function(options, element) {
42             this._super(options, element);
43             this.listen_query(options.query_uuid);
44             // this one is the complete list of resources
45             // and will be bound to callbacks like on_all_new_record
46             this.listen_query(options.query_all_uuid, 'all');
47
48
49             this.elts('queryeditor-auto-filter').change(this.event_filter_added('='));
50             this.elts('queryeditor-filter').change(this.event_filter_added('='));
51             this.elts('queryeditor-filter-min').change(this.event_filter_added('>'));
52             this.elts('queryeditor-filter-max').change(this.event_filter_added('<'));
53
54             var self = this;
55             this.elts('queryeditor-check').click(function() { 
56                 manifold.raise_event(self.options.query_uuid, this.checked?FIELD_ADDED:FIELD_REMOVED, this.value);
57             });
58
59             /* The following code adds an expandable column for the table
60             // XXX Why isn't it done statically ?
61             var nCloneTh = document.createElement( 'th' );
62             var nCloneTd = document.createElement( 'td' );
63             nCloneTd.innerHTML = "<span class='glyphicon glyphicon-chevron-right' style='cursor:pointer'></span>";
64             //nCloneTd.innerHTML = '<img src="/components/com_tophat/images/details_open.png">';
65             nCloneTh.innerHTML = '<b>Info</b>';
66             nCloneTd.className = "center";
67             nCloneTh.className = "center";
68             this.elmt('table thead tr').each(function() {
69                 this.insertBefore(nCloneTh, this.childNodes[0]);
70             });
71             this.elmt('table tbody tr').each(function() {
72                 this.insertBefore(nCloneTd.cloneNode( true ), this.childNodes[0]);
73             });
74             */
75          
76             // We are currently using a DataTable display, but another browsing component could be better
77             //jQuery('#'+this.options.plugin_uuid+'-table').dataTable...
78             var  metaTable = this.elmt('table').dataTable({
79 // Thierry : I'm turning off all the dataTables options for now, so that
80 // the table displays more properly again, might need more tuning though
81 //                bFilter     : false,
82 //                bPaginate   : false,
83 //                bInfo       : false,
84 //                sScrollX    : '100%',         // Horizontal scrolling
85 //                sScrollY    : '200px',
86 //                //bJQueryUI   : true,           // Use jQuery UI
87 //                bProcessing : true,           // Loading
88 //                aaSorting   : [[ 1, "asc" ]], // sort by column fields on load
89 //                aoColumnDefs: [
90 //                    { 'bSortable': false, 'aTargets': [ 0 ]},
91 //                    { 'sWidth': '8px', 'aTargets': [ 0 ] },
92 //                    { 'sWidth': '8px', 'aTargets': [ 4 ] } // XXX NB OF COLS
93 //                ]
94             });
95             this.table = metaTable;
96
97             // Actions on the newly added fields
98             this.elmt('table tbody td span').on('click', function() {
99                 var nTr = this.parentNode.parentNode;
100                 // use jQuery UI instead of images to keep a common UI
101                 // class="glyphicon glyphicon-chevron-down treeclick tree-minus"
102                 // East oriented Triangle class="glyphicon-chevron-right"
103                 // South oriented Triangle class="glyphicon-chevron-down"
104                 
105                 if (this.hasClass("glyphicon-chevron-right")) {
106                     this.removeClass("glyphicon-chevron-right").addClass("glyphicon-chevron-down");
107                     // XXX ??????
108                     metaTable.fnOpen(nTr, this.fnFormatDetails(metaTable, nTr, self.options.plugin_uuid+'_div'), 'details' );
109                 } else {
110                     this.removeClass("glyphicon-chevron-down").addClass("glyphicon-chevron-right");
111                     metaTable.fnClose(nTr);
112                 }
113             });
114
115             this.elmt('table_wrapper').css({
116                 'padding-top'   : '0em',
117                 'padding-bottom': '0em'
118             });
119             
120             // autocomplete list of tags
121             this.availableTags = {};
122
123         }, // init
124
125         /* UI management */
126
127         check_field: function(field)
128         {
129             this.elmt('check', field).attr('checked', true);
130         },
131
132         uncheck_field: function(field)
133         {
134             this.elmt('check', field).attr('checked', false);
135         },
136
137         update_filter_value: function(filter, removed)
138         {
139             removed = !(typeof removed === 'undefined'); // default = False
140
141             var key   = filter[0];
142             var op    = filter[1];
143             var value = filter[2];
144
145             var id = this.id_from_field(key);
146
147             if (op == '=') {
148                 var element = this.elmt(id);
149             } else {
150                 var suffix;
151                 if (op == '<') {
152                     this.elmt(id, 'max').val(value);
153                 } else if (op == '>') {
154                     this.elmt(id, 'min').val(value);
155                 } else {
156                     return;
157                 }
158                 var element = this.elmt(id, suffix);
159             }
160
161             element.val(removed?null:value);
162
163         },
164
165         /* Events */
166
167         on_filter_added: function(filter)
168         {
169             this.update_filter_value(filter);
170         },
171
172         on_filter_removed: function(filter)
173         {
174             this.update_filter_value(filter, true);
175         },
176
177         on_field_added: function(field)
178         {
179             this.check_field(field);
180         },
181
182         on_field_removed: function(field)
183         {
184             this.uncheck_field(field);
185         },
186
187         /* RECORD HANDLERS */
188         on_query_done: function()
189         {
190             //console.log("Query_Editor: query_done!");
191             //console.log(this.availableTags);
192         },
193         /* Autocomplete based on query_all to get all the fields, where query get only the fields selected  */
194         on_all_new_record: function(record)
195         {
196             availableTags = this.availableTags;           
197             jQuery.each(record,function(key,value){
198                 value = unfold.get_value(value);
199                 if(!availableTags.hasOwnProperty(key)){availableTags[key]=new Array();}
200                 //availableTags[key].push(value);
201                 var currentArray = availableTags[key];
202                 if(value!=null){
203                     if(jQuery.inArray(value,currentArray)==-1){availableTags[key].push(value);}
204                 }
205            });
206            this.availableTags = availableTags;
207            this.update_autocomplete(availableTags);
208         },
209
210         /* Former code not used at the moment */
211
212         print_field_description: function(field_header, div_id) 
213         { 
214             //var selected = all_headers[field_header];
215             var selected = getMetadata_field('resource',field_header);
216
217             field_header = div_id+"_"+field_header;
218
219             var output = "<div id='desc"+field_header+"'>";
220
221             output += "<div id='divinfo"+field_header+"'>";
222             output += '<p><span class="column-title">'+selected['title']+'</span></p></span>'; 
223             output += '<p><span class="column-detail">'+selected['description']+'</span></p></span>'; 
224
225             var period_select = "<select id='selectperiod"+field_header+"'><option value='Now'> Now </option><option value='latest'> Latest  </option><option value=w> Week </option><option value=m> Month </option><option value=y> Year </option></select>";
226
227             if (selected['value_type'] == 'string') {
228
229                 var values_select = "<p><select id='selectvalues"+field_header+"' MULTIPLE size=3>";
230
231                 output += '<p>Values: ';
232
233                 var values_list = selected['allowed_values'].split(",");
234
235                 for (var value_index = 0; value_index < values_list.length ; value_index++) {
236                     var value_desc = values_list[value_index].split("-");
237                     if (value_index > 0)
238                         output += ', ';
239                     output += '<span class="bold">'+value_desc[0]+'</span>';
240                     values_select += "<option value ='"+value_desc[0]+"'>&nbsp;"+value_desc[0];
241                     if (value_desc[1]!='') 
242                         output += ' ('+value_desc[1]+')';
243
244                     values_select += "&nbsp;</option>";
245                 }
246                 values_select += "</select>";
247             }
248             else
249                 output+='<p>Unit: '+selected['unit'];
250
251             output+= '</p>';
252
253             output += '<p>Source: <a class="source-url" target="source_window" href="'+selected['platform_url']+'">'+selected['platform']+'</a>';
254
255             //if (selected['via'] != '') 
256                 //output += ' via <a class="source-url" target="source_window" href="http://'+selected['via_url']+'">'+selected['via']+'</a>';
257
258             output += '</p>';
259             output += "</div>";
260
261     /*
262             output += "<div id='divgroup"+field_header+"'>";
263             output += "<p>Group resources with the same value <input type=checkbox></input>";
264             output += "<p>Select aggregator : <select><option>Count</option><option selected=true>Average</option><option>Maximum</option><option>Minimum</option></select>";
265             output += "</div>";
266             output += "<div id='divtime"+field_header+"'>";
267             output += "<p>Select timestamp : ";
268             output += period_select;
269             output += "</div>";
270     */
271             output += "</div>";
272
273             return output;
274         },
275
276         update_autocomplete: function(availableTags)
277         {
278             var self = this;
279             var domid = this.options.plugin_uuid;
280             
281             jQuery.each(availableTags, function(key, value){
282                 value.sort();
283                 // using dataTables's $ to search also in nodes that are not currently displayed
284                 var element = self.table.$("#"+domid+"__field__"+key);
285
286                 element.autocomplete({
287                             source: value,
288                             selectFirst: true,
289                             minLength: 0, // allows to browse items with no value typed in
290                             select: self.event_filter_added('=')
291                 });
292             });                
293         }, // update_autocomplete     
294
295 /*
296         update_autocomplete: function(e, rows, current_query)
297         {
298             var d = data;
299             d.current_query = current_query;
300             var availableTags={};
301             jQuery.each (rows, function(index, obj) {                    
302                 jQuery.each(obj,function(key,value){                       
303                     value = unfold.get_value(value); 
304                     if(!availableTags.hasOwnProperty(key)){availableTags[key]=new Array();}
305                     //availableTags[key].push(value);
306                     var currentArray=availableTags[key];
307                     if(value!=null){
308                         if(jQuery.inArray(value,currentArray)==-1){availableTags[key].push(value);}
309                     }
310                 });                    
311             });
312             jQuery.each(availableTags, function(key, value){
313                 value.sort();
314                 jQuery("#"+options.plugin_uuid+"-filter-"+key).autocomplete({
315                             source: value,
316                             selectFirst: true,
317                             minLength: 0, // allows to browse items with no value typed in
318                             select: function(event, ui) {
319                                 var key=getKeySplitId(this.id,"-");
320                                 var op='=';
321                                 var val=ui.item.value;
322                                 
323                                 query=d.current_query;
324                                 query.update_filter(key,op,val);
325                                 // Publish the query changed, the other plugins with subscribe will get the changes
326                                 jQuery.publish('/query/' + query.uuid + '/changed', query);
327                                 //add_ActiveFilter(this.id,'=',ui.item.value,d);
328                             }
329                 });
330             });                
331         }, // update_autocomplete     
332 */
333         fnFormatDetails: function( metaTable, nTr, div_id ) 
334         {
335             var aData = metaTable.fnGetData( nTr );
336             var sOut = '<blockquote>';
337             //sOut += prepare_tab_description(aData[1].substr(21, aData[1].length-21-7), div_id);
338             sOut += this.print_field_description(aData[1].substring(3, aData[1].length-4), div_id);
339             sOut += '</blockquote>';
340          
341             return sOut;
342         }
343     });
344
345     $.plugin('QueryEditor', QueryEditor);
346
347 })(jQuery);