plugin.hazelnut: fixed issue with filtering
[myslice.git] / plugins / hazelnut / static / js / hazelnut.js
index 91fe152..7c042b1 100644 (file)
@@ -6,10 +6,8 @@
 
 (function($){
 
-    // TEMP
-    var ELEMENT_KEY = 'resource_hrn';
     var debug=false;
-    debug=true
+//    debug=true
 
     var Hazelnut = Plugin.extend({
 
             this._super(options, element);
 
             /* Member variables */
-            // query status
-            this.received_all = false;
-            this.received_set = false;
-            this.in_set_buffer = Array();
+           // in general we expect 2 queries here
+           // query_uuid refers to a single object (typically a slice)
+           // query_all_uuid refers to a list (typically resources or users)
+           // these can return in any order so we keep track of which has been received yet
+            this.received_all_query = false;
+            this.received_query = false;
+
+            // We need to remember the active filter for datatables filtering
+            this.filters = Array(); 
+
+            // an internal buffer for records that are 'in' and thus need to be checked 
+            this.buffered_records_to_check = [];
+           // an internal buffer for keeping lines and display them in one call to fnAddData
+           this.buffered_lines = [];
 
             /* XXX Events XXX */
             // this.$element.on('show.Datatables', this.on_show);
-            this.el().on('show', this, this.on_show);
+            this.elmt().on('show', this, this.on_show);
             // Unbind all events using namespacing
             // TODO in destructor
             // $(window).unbind('Hazelnut');
             this.initialize_table();
         },
 
-        default_options: {
-            'checkboxes': false
-        },
-
         /* PLUGIN EVENTS */
 
         on_show: function(e)
         {
             /* Transforms the table into DataTable, and keep a pointer to it */
             var self = this;
-            actual_options = {
+            var actual_options = {
                 // Customize the position of Datatables elements (length,filter,button,...)
                 // we use a fluid row on top and another on the bottom, making sure we take 12 grid elt's each time
-                sDom: "<'row-fluid'<'span5'l><'span1'r><'span6'f>>t<'row-fluid'<'span5'i><'span7'p>>",
+                sDom: "<'row'<'col-xs-5'l><'col-xs-1'r><'col-xs-6'f>>t<'row'<'col-xs-5'i><'col-xs-7'p>>",
+               // XXX as of sept. 2013, I cannot locate a bootstrap3-friendly mode for now
+               // hopefully this would come with dataTables v1.10 ?
+               // in any case, search for 'sPaginationType' all over the code for more comments
                 sPaginationType: 'bootstrap',
                 // Handle the null values & the error : Datatables warning Requested unknown parameter
                 // http://datatables.net/forums/discussion/5331/datatables-warning-...-requested-unknown-parameter/p2
                 // XXX use $.proxy here !
             };
             // the intention here is that options.datatables_options as coming from the python object take precedence
-            //  XXX DISABLED by jordan: was causing errors in datatables.js     $.extend(actual_options, options.datatables_options );
-            this.table = this.el('table').dataTable(actual_options);
+           // xxx DISABLED by jordan: was causing errors in datatables.js
+           // xxx turned back on by Thierry - this is the code that takes python-provided options into account
+           // check your datatables_options tag instead 
+           // however, we have to accumulate in aoColumnDefs from here (above) 
+           // and from the python wrapper (checkboxes management, plus any user-provided aoColumnDefs)
+           if ( 'aoColumnDefs' in this.options.datatables_options) {
+               actual_options['aoColumnDefs']=this.options.datatables_options['aoColumnDefs'].concat(actual_options['aoColumnDefs']);
+               delete this.options.datatables_options['aoColumnDefs'];
+           }
+           $.extend(actual_options, this.options.datatables_options );
+            this.table = this.elmt('table').dataTable(actual_options);
 
             /* Setup the SelectAll button in the dataTable header */
             /* xxx not sure this is still working */
             var oSelectAll = $('#datatableSelectAll-'+ this.options.plugin_uuid);
-            oSelectAll.html("<span class='ui-icon ui-icon-check' style='float:right;display:inline-block;'></span>Select All");
+            oSelectAll.html("<span class='glyphicon glyphicon-ok' style='float:right;display:inline-block;'></span>Select All");
             oSelectAll.button();
             oSelectAll.css('font-size','11px');
             oSelectAll.css('float','right');
              */
             $.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) { 
                 /* No filtering if the table does not match */
-                if (oSettings.nTable.id != "hazelnut-" + self.options.plugin_uuid)
+                if (oSettings.nTable.id != self.options.plugin_uuid + '__table')
                     return true;
-                return this._hazelnut_filter.call(self, oSettings, aData, iDataIndex);
+                return self._hazelnut_filter.call(self, oSettings, aData, iDataIndex);
             });
 
             /* Processing hidden_columns */
             $.each(this.options.hidden_columns, function(i, field) {
+                //manifold.raise_event(self.options.query_all_uuid, FIELD_REMOVED, field);
                 self.hide_column(field);
             });
         }, // initialize_table
             return (tabIndex.length > 0) ? tabIndex[0] : -1;
         }, // getColIndex
 
- // UNUSED ? //         this.update_plugin = function(e, rows) {
- // UNUSED ? //             // e.data is what we passed in second argument to subscribe
- // UNUSED ? //             // so here it is the jquery object attached to the plugin <div>
- // UNUSED ? //             var $plugindiv=e.data;
- // UNUSED ? //             if (debug)
- // UNUSED ? //                 messages.debug("entering hazelnut.update_plugin on id '" + $plugindiv.attr('id') + "'");
- // UNUSED ? //             // clear the spinning wheel: look up an ancestor that has the need-spin class
- // UNUSED ? //             // do this before we might return
- // UNUSED ? //             $plugindiv.closest('.need-spin').spin(false);
- // UNUSED ? // 
- // UNUSED ? //             var options = this.options;
- // UNUSED ? //             var hazelnut = this;
- // UNUSED ? //     
- // UNUSED ? //             /* if we get no result, or an error, try to make that clear, and exit */
- // UNUSED ? //             if (rows.length==0) {
- // UNUSED ? //                 if (debug) 
- // UNUSED ? //                     messages.debug("Empty result on hazelnut " + this.options.domid);
- // UNUSED ? //                 var placeholder=$(this.table).find("td.dataTables_empty");
- // UNUSED ? //                 console.log("placeholder "+placeholder);
- // UNUSED ? //                 if (placeholder.length==1) 
- // UNUSED ? //                     placeholder.html(unfold.warning("Empty result"));
- // UNUSED ? //                 else
- // UNUSED ? //                     this.table.html(unfold.warning("Empty result"));
- // UNUSED ? //                     return;
- // UNUSED ? //             } else if (typeof(rows[0].error) != 'undefined') {
- // UNUSED ? //                 // we now should have another means to report errors that this inline/embedded hack
- // UNUSED ? //                 if (debug) 
- // UNUSED ? //                     messages.error ("undefined result on " + this.options.domid + " - should not happen anymore");
- // UNUSED ? //                 this.table.html(unfold.error(rows[0].error));
- // UNUSED ? //                 return;
- // UNUSED ? //             }
- // UNUSED ? // 
- // UNUSED ? //             /* 
- // UNUSED ? //              * fill the dataTables object
- // UNUSED ? //              * we cannot set html content directly here, need to use fnAddData
- // UNUSED ? //              */
- // UNUSED ? //             var lines = new Array();
- // UNUSED ? //     
- // UNUSED ? //             this.current_resources = Array();
- // UNUSED ? //     
- // UNUSED ? //             $.each(rows, function(index, row) {
- // UNUSED ? //                 // this models a line in dataTables, each element in the line describes a cell
- // UNUSED ? //                 line = new Array();
- // UNUSED ? //      
- // UNUSED ? //                 // go through table headers to get column names we want
- // UNUSED ? //                 // in order (we have temporarily hack some adjustments in names)
- // UNUSED ? //                 var cols = object.table.fnSettings().aoColumns;
- // UNUSED ? //                 var colnames = cols.map(function(x) {return x.sTitle})
- // UNUSED ? //                 var nb_col = cols.length;
- // UNUSED ? //                 /* if we've requested checkboxes, then forget about the checkbox column for now */
- // UNUSED ? //                 if (options.checkboxes) nb_col -= 1;
- // UNUSED ? // 
- // UNUSED ? //                 /* fill in stuff depending on the column name */
- // UNUSED ? //                 for (var j = 0; j < nb_col; j++) {
- // UNUSED ? //                     if (typeof colnames[j] == 'undefined') {
- // UNUSED ? //                         line.push('...');
- // UNUSED ? //                     } else if (colnames[j] == 'hostname') {
- // UNUSED ? //                         if (row['type'] == 'resource,link')
- // UNUSED ? //                             //TODO: we need to add source/destination for links
- // UNUSED ? //                             line.push('');
- // UNUSED ? //                         else
- // UNUSED ? //                             line.push(row['hostname']);
- // UNUSED ? //                     } else {
- // UNUSED ? //                         if (row[colnames[j]])
- // UNUSED ? //                             line.push(row[colnames[j]]);
- // UNUSED ? //                         else
- // UNUSED ? //                             line.push('');
- // UNUSED ? //                     }
- // UNUSED ? //                 }
- // UNUSED ? //     
- // UNUSED ? //                 /* catch up with the last column if checkboxes were requested */
- // UNUSED ? //                 if (options.checkboxes) {
- // UNUSED ? //                     var checked = '';
- // UNUSED ? //                     // xxx problem is, we don't get this 'sliver' thing set apparently
- // UNUSED ? //                     if (typeof(row['sliver']) != 'undefined') { /* It is equal to null when <sliver/> is present */
- // UNUSED ? //                         checked = 'checked ';
- // UNUSED ? //                         hazelnut.current_resources.push(row[ELEMENT_KEY]);
- // UNUSED ? //                     }
- // UNUSED ? //                     // Use a key instead of hostname (hard coded...)
- // UNUSED ? //                     line.push(hazelnut.checkbox(options.plugin_uuid, row[ELEMENT_KEY], row['type'], checked, false));
- // UNUSED ? //                 }
- // UNUSED ? //     
- // UNUSED ? //                 lines.push(line);
- // UNUSED ? //     
- // UNUSED ? //             });
- // UNUSED ? //     
- // UNUSED ? //             this.table.fnClearTable();
- // UNUSED ? //             if (debug)
- // UNUSED ? //                 messages.debug("hazelnut.update_plugin: total of " + lines.length + " rows");
- // UNUSED ? //             this.table.fnAddData(lines);
- // UNUSED ? //         
- // UNUSED ? //         }, // update_plugin
-
-        checkbox: function (plugin_uuid, header, field) //, selected_str, disabled_str)
+        checkbox_html : function (key, value)
         {
             var result="";
-            if (header === null)
-                header = '';
             // Prefix id with plugin_uuid
             result += "<input";
             result += " class='hazelnut-checkbox'";
-            result += " id='" + this.id('checkbox', this.id_from_key(this.key, unfold.get_value(header))) + "'";
-             //hazelnut-checkbox-" + plugin_uuid + "-" + unfold.get_value(header).replace(/\\/g, '')  + "'";
-            result += " name='" + unfold.get_value(field) + "'";
+            result += " id='" + this.flat_id(this.id('checkbox', value)) + "'";
+            result += " name='" + key + "'";
             result += " type='checkbox'";
-            //result += selected_str;
-            //result += disabled_str;
             result += " autocomplete='off'";
-            result += " value='" + unfold.get_value(header) + "'";
+            result += " value='" + value + "'";
             result += "></input>";
             return result;
         }, // checkbox
                         line.push('');
                     else
                         line.push(record['hostname']);
+
+                } else if (colnames[j] == 'hrn' && typeof(record) != 'undefined') {
+                    line.push('<a href="../resource/'+record['urn']+'"><span class="glyphicon glyphicon-search"></span></a> '+record['hrn']);
                 } else {
                     if (record[colnames[j]])
                         line.push(record[colnames[j]]);
                 }
             }
     
-            /* catch up with the last column if checkboxes were requested */
-            if (this.options.checkboxes)
+            // catch up with the last column if checkboxes were requested 
+            if (this.options.checkboxes) {
                 // Use a key instead of hostname (hard coded...)
-                // XXX remove the empty checked attribute
-                line.push(this.checkbox(this.options.plugin_uuid, record[ELEMENT_KEY], record['type']));
+                line.push(this.checkbox_html(this.key, record[this.key]));
+               }
     
-            // XXX Is adding an array of lines more efficient ?
-            this.table.fnAddData(line);
-
+           // adding an array in one call is *much* more efficient
+               // this.table.fnAddData(line);
+               this.buffered_lines.push(line);
         },
 
         clear_table: function()
 
         set_checkbox: function(record, checked)
         {
+            /* Default: checked = true */
+            if (checked === undefined) checked = true;
+
             var key_value;
             /* The function accepts both records and their key */
             switch (manifold.get_type(record)) {
             }
 
 
-            var checkbox_id = this.id('checkbox', this.id_from_key(this.key, key_value));
-            checkbox_id = '#' + checkbox_id.replace(/\./g, '\\.');
-
-            var element = $(checkbox_id, this.table.fnGetNodes());
-
-            /* Default: swap check status */
-            if (typeof checked === 'undefined')
-                checked = !(element.is(':checked'));
-
+            var checkbox_id = this.flat_id(this.id('checkbox', key_value));
+            // function escape_id(myid) is defined in portal/static/js/common.functions.js
+            checkbox_id = escape_id(checkbox_id);
+               // using dataTables's $ to search also in nodes that are not currently displayed
+            var element = this.table.$(checkbox_id);
+               if (debug) messages.debug("set_checkbox checked=" + checked + " id=" + checkbox_id + " matches=" + element.length);
             element.attr('checked', checked);
         },
 
 
         on_filter_added: function(filter)
         {
-            // XXX
+            this.filters.push(filter);
             this.redraw_table();
         },
 
         on_filter_removed: function(filter)
         {
-            // XXX
+            // Remove corresponding filters
+            this.filters = $.grep(this.filters, function(x) {
+                return x != filter;
+            });
             this.redraw_table();
         },
         
             alert('Hazelnut::clear_fields() not implemented');
         },
 
+        /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
+        /*************************** ALL QUERY HANDLER ****************************/
+
+        on_all_filter_added: function(filter)
+        {
+            // XXX
+            this.redraw_table();
+        },
+
+        on_all_filter_removed: function(filter)
+        {
+            // XXX
+            this.redraw_table();
+        },
+        
+        on_all_filter_clear: function()
+        {
+            // XXX
+            this.redraw_table();
+        },
+
+        on_all_field_added: function(field)
+        {
+            this.show_column(field);
+        },
+
+        on_all_field_removed: function(field)
+        {
+            this.hide_column(field);
+        },
+
+        on_all_field_clear: function()
+        {
+            alert('Hazelnut::clear_fields() not implemented');
+        },
+
+
         /*************************** RECORD HANDLER ***************************/
 
         on_new_record: function(record)
         {
-            /* NOTE in fact we are doing a join here */
-            if (this.received_all)
-                // update checkbox for record
+            if (this.received_all_query) {
+                       // if the 'all' query has been dealt with already we may turn on the checkbox
+                       if (debug) messages.debug("turning on checkbox for record "+record[this.key]);
                 this.set_checkbox(record, true);
-            else
-                // store for later update of checkboxes
-                this.in_set_buffer.push(record);
+               } else {
+                       // otherwise we need to remember that and do it later on
+                       if (debug) messages.debug("Remembering record to check " + record[this.key]);
+                this.buffered_records_to_check.push(record);
+               }
         },
 
         on_clear_records: function()
 
         on_query_done: function()
         {
-            if (this.received_all)
-                this.unspin();
-            this.received_set = true;
+            this.received_query = true;
+           // unspin once we have received both
+            if (this.received_all_query && this.received_query) this.unspin();
         },
         
         on_field_state_changed: function(data)
         {
             switch(data.request) {
                 case FIELD_REQUEST_ADD:
+                case FIELD_REQUEST_ADD_RESET:
                     this.set_checkbox(data.value, true);
                     break;
                 case FIELD_REQUEST_REMOVE:
+                case FIELD_REQUEST_REMOVE_RESET:
                     this.set_checkbox(data.value, false);
                     break;
-                case FIELD_REQUEST_RESET:
-                    this.set_checkbox(data.value); // swap
-                    break;
                 default:
                     break;
             }
         },
 
+        /* XXX TODO: make this generic a plugin has to subscribe to a set of Queries to avoid duplicated code ! */
         // all
+        on_all_field_state_changed: function(data)
+        {
+            switch(data.request) {
+                case FIELD_REQUEST_ADD:
+                case FIELD_REQUEST_ADD_RESET:
+                    this.set_checkbox(data.value, true);
+                    break;
+                case FIELD_REQUEST_REMOVE:
+                case FIELD_REQUEST_REMOVE_RESET:
+                    this.set_checkbox(data.value, false);
+                    break;
+                default:
+                    break;
+            }
+        },
 
         on_all_new_record: function(record)
         {
 
         on_all_query_done: function()
         {
+           if (debug) messages.debug("1-shot initializing dataTables content with " + this.buffered_lines.length + " lines");
+           this.table.fnAddData (this.buffered_lines);
+           this.buffered_lines=[];
+           
             var self = this;
-            if (this.received_set) {
-                /* XXX needed ? XXX We uncheck all checkboxes ... */
-                $("[id^='datatables-checkbox-" + this.options.plugin_uuid +"']").attr('checked', false);
-
-                /* ... and check the ones specified in the resource list */
-                $.each(this.in_set_buffer, function(i, record) {
-                    self.set_checkbox(record, true);
-                });
+           // if we've already received the slice query, we have not been able to set 
+           // checkboxes on the fly at that time (dom not yet created)
+            $.each(this.buffered_records_to_check, function(i, record) {
+               if (debug) messages.debug ("delayed turning on checkbox " + i + " record= " + record);
+                self.set_checkbox(record, true);
+            });
+           this.buffered_records_to_check = [];
 
-                this.unspin();
-            }
-            this.received_all = true;
+            this.received_all_query = true;
+           // unspin once we have received both
+            if (this.received_all_query && this.received_query) this.unspin();
 
         }, // on_all_query_done
 
          */
         _hazelnut_filter: function(oSettings, aData, iDataIndex)
         {
-            var cur_query = this.current_query;
-            if (!cur_query) return true;
             var ret = true;
-
-            /* We have an array of filters : a filter is an array (key op val) 
-             * field names (unless shortcut)    : oSettings.aoColumns  = [ sTitle ]
-             *     can we exploit the data property somewhere ?
-             * field values (unless formatting) : aData
-             *     formatting should leave original data available in a hidden field
-             *
-             * The current line should validate all filters
-             */
-            $.each (cur_query.filters, function(index, filter) { 
+            $.each (this.filters, function(index, filter) { 
                 /* XXX How to manage checkbox ? */
                 var key = filter[0]; 
                 var op = filter[1];
              * Handle clicks on checkboxes: reassociate checkbox click every time
              * the table is redrawn 
              */
-            this.els('hazelnut-checkbox').unbind('click').click(this, this._check_click);
+            this.elts('hazelnut-checkbox').unbind('click').click(this, this._check_click);
 
             if (!this.table)
                 return;
 
         _check_click: function(e) 
         {
+            e.stopPropagation();
 
             var self = e.data;
 
             // XXX this.value = key of object to be added... what about multiple keys ?
+           if (debug) messages.debug("hazelnut click handler checked=" + this.checked + " hrn=" + this.value);
             manifold.raise_event(self.options.query_uuid, this.checked?SET_ADD:SET_REMOVED, this.value);
             //return false; // prevent checkbox to be checked, waiting response from manifold plugin api
             
 
     $.plugin('Hazelnut', Hazelnut);
 
+  /* define the 'dom-checkbox' type for sorting in datatables 
+     http://datatables.net/examples/plug-ins/dom_sort.html
+     using trial and error I found that the actual column number
+     was in fact given as a third argument, and not second 
+     as the various online resources had it - go figure */
+    $.fn.dataTableExt.afnSortData['dom-checkbox'] = function  ( oSettings, _, iColumn ) {
+       return $.map( oSettings.oApi._fnGetTrNodes(oSettings), function (tr, i) {
+           return result=$('td:eq('+iColumn+') input', tr).prop('checked') ? '1' : '0';
+       } );
+    }
+
 })(jQuery);
+