rough repairs of slickgrid but this is still very far off
authorThierry Parmentelat <thierry.parmentelat@inria.fr>
Tue, 17 Dec 2013 13:45:30 +0000 (14:45 +0100)
committerThierry Parmentelat <thierry.parmentelat@inria.fr>
Tue, 17 Dec 2013 13:45:30 +0000 (14:45 +0100)
plugins/querygrid/__init__.py
plugins/querygrid/static/js/querygrid.js
plugins/querygrid/static/js/slick.unfolddataview.js [new file with mode: 0644]
plugins/querygrid/static/js/slick.unfoldselection.js [new file with mode: 0644]
plugins/querytable/static/js/querytable.js
portal/sliceview.py
sample/querygridview.py

index 22a2aab..587637a 100644 (file)
@@ -93,20 +93,21 @@ Current implementation makes the following assumptions
 # but triggers js errors when included - probably/maybe because of the jquery version ?
 # it might be responsible for not being able to select a row by clicking anywhere in it ?
 #                "http://mleibman.github.io/SlickGrid/lib/jquery-ui-1.8.16.custom.min.js",
-                "js/jquery.event.drag-2.2.js",   # from slickgrid/lib
+                "js/jquery.event.drag-2.2.js",          # from slickgrid/lib
                 "js/slick.core.js",
-                "js/slick.autotooltips.js",       # from slickgrid/plugins/
+                "js/slick.autotooltips.js",             # from slickgrid/plugins/
                 "js/slick.cellrangedecorator.js",       # from slickgrid/plugins/
-                "js/slick.cellrangeselector.js",       # from slickgrid/plugins/
-                "js/slick.cellcopymanager.js",       # from slickgrid/plugins/
-                "js/slick.cellselectionmodel.js",       # from slickgrid/plugins/
-                "js/slick.rowselectionmodel.js",       # from slickgrid/plugins/
-                "js/slick.checkboxselectcolumn.js",       # from slickgrid/plugins/
+                "js/slick.cellrangeselector.js",        # from slickgrid/plugins/
+                "js/slick.cellcopymanager.js",          # from slickgrid/plugins/
+#                "js/slick.cellselectionmodel.js",       # from slickgrid/plugins/
+                "js/slick.rowselectionmodel.js",        # from slickgrid/plugins/
+                "js/slick.checkboxselectcolumn.js",     # from slickgrid/plugins/
                 "js/slick.columnpicker.js",             # from slickgrid/controls/
                 "js/slick.formatters.js",
                 "js/slick.editors.js",
                 "js/slick.grid.js",
-                "js/slick.dataview.js",
+                "js/slick.unfoldselection.js",          # from plugins/querygrid
+                "js/slick.unfolddataview.js",           # from plugins/querygrid
 
 #                "js/dataTables.js", "js/dataTables.bootstrap.js", "js/with-datatables.js",
                 "js/manifold.js", "js/manifold-query.js", 
@@ -134,4 +135,4 @@ Current implementation makes the following assumptions
                 'query_uuid', 'query_all_uuid', 
                 'checkboxes', 'datatables_options', 
                 'columns','hidden_columns', 
-                'id_key',]
+                'init_key',]
index b4c7b29..f54d143 100644 (file)
@@ -1,5 +1,6 @@
+// -*- js-indent-tab:2 -*-
 /**
- * Description: display a query result in a slickgrid-powered <table>
+ * Description: display a query result in a slickgrid-powered table
  * Copyright (c) 2012-2013 UPMC Sorbonne Universite - INRIA
  * License: GPLv3
  */
@@ -9,9 +10,6 @@
  *
  * This is very rough for now and not deemed working
  * 
- * Also it still requires adaptation for the init_key / init_id / canonical_key / id business 
- * if the basic logic was to become usable
- * 
  * WARNINGS
  */
 
             this.elmt().on('show', this, this.on_show);
 
             var query = manifold.query_store.find_analyzed_query(this.options.query_uuid);
-            this.method = query.object;
-
-           // xxx beware that this.key needs to contain a key that all records will have
-           // in general query_all will return well populated records, but query
-           // returns records with only the fields displayed on startup. 
-           this.key = (this.options.id_key);
-           if (! this.key) {
-               // if not specified by caller, decide from metadata
-               var keys = manifold.metadata.get_key(this.method);
-               this.key = (keys && keys.length == 1) ? keys[0] : null;
-           }
-           if (! this.key) messages.warning("querygrid.init could not kind valid key");
-
-           if (debug) messages.debug("querygrid: key="+this.key);
+            this.object = query.object;
+
+           //// we need 2 different keys
+           // * canonical_key is the primary key as derived from metadata (typically: urn)
+           //   and is used to communicate about a given record with the other plugins
+           // * init_key is a key that both kinds of records 
+           //   (i.e. records returned by both queries) must have (typically: hrn or hostname)
+           //   in general query_all will return well populated records, but query
+           //   returns records with only the fields displayed on startup
+           var keys = manifold.metadata.get_key(this.object);
+           this.canonical_key = (keys && keys.length == 1) ? keys[0] : undefined;
+           // 
+           this.init_key = this.options.init_key;
+           // have init_key default to canonical_key
+           this.init_key = this.init_key || this.canonical_key;
+           // sanity check
+           if ( ! this.init_key ) messages.warning ("QueryGrid : cannot find init_key");
+           if ( ! this.canonical_key ) messages.warning ("QueryGrid : cannot find canonical_key");
+           if (debug) messages.debug("querygrid: canonical_key="+this.canonical_key+" init_key="+this.init_key);
 
             /* Setup query and record handlers */
             this.listen_query(options.query_uuid);
            };
 
            this.slick_data = [];
-           this.slick_dataview = new Slick.Data.DataView();
+           this.slick_dataview = new Slick.Data.UnfoldDataView();
+// capturing for debug
+window.dv=this.slick_dataview;
            var self=this;
            this.slick_dataview.onRowCountChanged.subscribe ( function (e,args) {
                self.slick_grid.updateRowCount();
                self.slick_grid.autosizeColumns();
                self.slick_grid.render();
            });
+         
            
            var selector="#grid-"+this.options.domid;
            if (debug_deep) {
            }
 
            this.slick_grid = new Slick.Grid(selector, this.slick_dataview, this.slick_columns, this.slick_options);
-           this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false}));
+//         this.slick_grid.setSelectionModel (new Slick.RowSelectionModel ({selectActiveRow: false}));
+           this.slick_grid.setSelectionModel (new Slick.UnfoldSelectionModel({selectActiveRow: false}));
            this.slick_grid.registerPlugin (checkbox_selector);
            // autotooltips: for showing the full column name when ellipsed
            var auto_tooltips = new Slick.AutoTooltips ({ enableForHeaderCells: true });
            
            this.columnpicker = new Slick.Controls.ColumnPicker (this.slick_columns, this.slick_grid, this.slick_options)
 
-           g=this.slick_grid;
-
         }, // initialize_table
 
         new_record: function(record) {
 
         clear_table: function() {
            this.slick_data=[];
-           this.slick_dataview.setItems(this.slick_data,this.key);
+         this.slick_dataview.setItems(this.slick_data,this.init_key,this.canonical_key);
         },
 
         redraw_table: function() {
         on_new_record: function(record) {
             if (this.received_all_query) {
                // if the 'all' query has been dealt with already we may turn on the checkbox
-                this._set_checkbox(record, true);
+                this._set_checkbox_from_record(record, true);
             } else {
                // otherwise we need to remember that and do it later on
-               if (debug) messages.debug("Remembering record to check " + record[this.key]);
+               if (debug) messages.debug("Remembering record to check, "+this.init_key+'='+ record[this.init_key]);
                 this.buffered_records_to_check.push(record);
             }
         },
             switch(data.request) {
                 case FIELD_REQUEST_ADD:
                 case FIELD_REQUEST_ADD_RESET:
-                    this._set_checkbox(data.value, true);
+                    this._set_checkbox_from_data(data.value, true);
                     break;
                 case FIELD_REQUEST_REMOVE:
                 case FIELD_REQUEST_REMOVE_RESET:
-                    this._set_checkbox(data.value, false);
+                    this._set_checkbox_from_data(data.value, false);
                     break;
                 default:
                     break;
             switch(data.request) {
                 case FIELD_REQUEST_ADD:
                 case FIELD_REQUEST_ADD_RESET:
-                    this._set_checkbox(data.value, true);
+                    this._set_checkbox_from_data(data.value, true);
                     break;
                 case FIELD_REQUEST_REMOVE:
                 case FIELD_REQUEST_REMOVE_RESET:
-                    this._set_checkbox(data.value, false);
+                    this._set_checkbox_from_data(data.value, false);
                     break;
                 default:
                     break;
         on_all_query_done: function() {
            var start=new Date();
            if (debug) messages.debug("1-shot initializing slickgrid content with " + this.slick_data.length + " lines");
-           // use this.key as the key for identifying rows
-           this.slick_dataview.setItems (this.slick_data, this.key);
+           // use this.init_key as the key for identifying rows
+         this.slick_dataview.setItems (this.slick_data, this.init_key,this.canonical_key);
            var duration=new Date()-start;
            if (debug) messages.debug("setItems " + duration + " ms");
            if (debug_deep) {
            // 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);
+                self._set_checkbox_from_record(record, true);
             });
            this.buffered_records_to_check = [];
 
 
         /************************** PRIVATE METHODS ***************************/
 
-        _set_checkbox: function(record, checked) {
-            /* Default: checked = true */
-            if (checked === undefined) checked = true;
-
-            var id;
-            /* The function accepts both records and their key */
-            switch (manifold.get_type(record)) {
-            case TYPE_VALUE:
-                id = record;
-                break;
-            case TYPE_RECORD:
-                /* XXX Test the key before ? */
-                id = record[this.key];
-                break;
-            default:
-                throw "Not implemented";
-                break;
-            }
+        _set_checkbox_from_record : function(record, checked) {
+           var init_id = record[this.init_key];
+           if (debug) messages.debug("querygrid.set_checkbox_from_record, init_id="+init_id);
+           var index = this.slick_dataview.getIdxById(init_id);
+            this._set_checkbox_from_index (index,checked);
+       },
 
+        _set_checkbox_from_data : function (id, checked) {
+           if (debug) messages.debug("querygrid.set_checkbox_from_data, id="+id);
+           // this is a local addition to mainstream dataview
+           // it's kind if slow in this first implementation (no hashing)
+           // but we should not notice that much
+           var index = this.slick_dataview.getIdxByIdKey(id,this.canonical_key);
+            this._set_checkbox_from_index (index,checked);
+        },
 
-           if (id === undefined) {
-               messages.warning("querygrid._set_checkbox record has no id to figure which line to tick");
-               return;
-           }
-           var index = this.slick_dataview.getIdxById(id);
+        _set_checkbox_from_index : function (index, checked) {
+         if (index === undefined) { messages.warn("querygrid.set_checkbox - cannot find index"); return;}
+            if (checked === undefined) checked = true;
            var selectedRows=this.slick_grid.getSelectedRows();
            if (checked) // add index in current list
                selectedRows=selectedRows.concat(index);
            else // remove index from current list
                selectedRows=selectedRows.filter(function(idx) {return idx!=index;});
+           // set new selection
            this.slick_grid.setSelectedRows(selectedRows);
-        },
+       },
 
 // initializing checkboxes
 // have tried 2 approaches, but none seems to work as we need it
diff --git a/plugins/querygrid/static/js/slick.unfolddataview.js b/plugins/querygrid/static/js/slick.unfolddataview.js
new file mode 100644 (file)
index 0000000..83dc040
--- /dev/null
@@ -0,0 +1,1080 @@
+// -*- js-indent-level:2 -*-
+// originally cloned from slick.dataview.js
+// actually I ended up just adding a method here (getIdxByIdKey)
+// so there probably are much smarter ways to achieve this
+(function ($) {
+  $.extend(true, window, {
+    Slick: {
+      Data: {
+        UnfoldDataView: UnfoldDataView,
+        Aggregators: {
+          Avg: AvgAggregator,
+          Min: MinAggregator,
+          Max: MaxAggregator,
+          Sum: SumAggregator
+        }
+      }
+    }
+  });
+
+
+  /***
+   * A sample Model implementation.
+   * Provides a filtered view of the underlying data.
+   *
+   * Relies on the data item having an "id" property uniquely identifying it.
+   */
+  function UnfoldDataView(options) {
+    var self = this;
+
+    var defaults = {
+      groupItemMetadataProvider: null,
+      inlineFilters: false
+    };
+
+
+    // private
+    var idProperty = "id";  // property holding a unique row id
+    var items = [];         // data by index
+    var rows = [];          // data by row
+    var idxById = {};       // indexes by id
+    var rowsById = null;    // rows by id; lazy-calculated
+    var filter = null;      // filter function
+    var updated = null;     // updated item ids
+    var suspend = false;    // suspends the recalculation
+    var sortAsc = true;
+    var fastSortField;
+    var sortComparer;
+    var refreshHints = {};
+    var prevRefreshHints = {};
+    var filterArgs;
+    var filteredItems = [];
+    var compiledFilter;
+    var compiledFilterWithCaching;
+    var filterCache = [];
+
+    // grouping
+    var groupingInfoDefaults = {
+      getter: null,
+      formatter: null,
+      comparer: function(a, b) { return a.value - b.value; },
+      predefinedValues: [],
+      aggregators: [],
+      aggregateEmpty: false,
+      aggregateCollapsed: false,
+      aggregateChildGroups: false,
+      collapsed: false,
+      displayTotalsRow: true
+    };
+    var groupingInfos = [];
+    var groups = [];
+    var toggledGroupsByLevel = [];
+    var groupingDelimiter = ':|:';
+
+    var pagesize = 0;
+    var pagenum = 0;
+    var totalRows = 0;
+
+    // events
+    var onRowCountChanged = new Slick.Event();
+    var onRowsChanged = new Slick.Event();
+    var onPagingInfoChanged = new Slick.Event();
+
+    options = $.extend(true, {}, defaults, options);
+
+
+    function beginUpdate() {
+      suspend = true;
+    }
+
+    function endUpdate() {
+      suspend = false;
+      refresh();
+    }
+
+    function setRefreshHints(hints) {
+      refreshHints = hints;
+    }
+
+    function setFilterArgs(args) {
+      filterArgs = args;
+    }
+
+    // the unfold addition is for spotting an index based on another key
+    // this is kind of slow for now but minimixes the changes to this mainstream code
+    function getIdxByIdKey(matching_id,matching_key) {
+      var id;
+      for (var i = 0, l = items.length; i < l; i++) {
+       id = items[i][matching_key];
+        if (id === undefined) { messages.warning ("ignoring record with no " + matching_key); continue; }
+       if (id == matching_id) return i;
+      }
+      return None;
+    }
+
+    function updateIdxById(startingIndex) {
+      startingIndex = startingIndex || 0;
+      var id;
+      for (var i = startingIndex, l = items.length; i < l; i++) {
+        id = items[i][idProperty];
+        if (id === undefined) {
+          throw "Each data element must implement a unique 'id' property";
+        }
+        idxById[id] = i;
+      }
+    }
+
+    function ensureIdUniqueness() {
+      var id;
+      for (var i = 0, l = items.length; i < l; i++) {
+        id = items[i][idProperty];
+        if (id === undefined || idxById[id] !== i) {
+          throw "Each data element must implement a unique 'id' property";
+        }
+      }
+    }
+
+    function getItems() {
+      return items;
+    }
+
+    function setItems(data, objectIdProperty) {
+      if (objectIdProperty !== undefined) {
+        idProperty = objectIdProperty;
+      }
+      items = filteredItems = data;
+      idxById = {};
+      updateIdxById();
+      ensureIdUniqueness();
+      refresh();
+    }
+
+    function setPagingOptions(args) {
+      if (args.pageSize != undefined) {
+        pagesize = args.pageSize;
+        pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
+      }
+
+      if (args.pageNum != undefined) {
+        pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
+      }
+
+      onPagingInfoChanged.notify(getPagingInfo(), null, self);
+
+      refresh();
+    }
+
+    function getPagingInfo() {
+      var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
+      return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages};
+    }
+
+    function sort(comparer, ascending) {
+      sortAsc = ascending;
+      sortComparer = comparer;
+      fastSortField = null;
+      if (ascending === false) {
+        items.reverse();
+      }
+      items.sort(comparer);
+      if (ascending === false) {
+        items.reverse();
+      }
+      idxById = {};
+      updateIdxById();
+      refresh();
+    }
+
+    /***
+     * Provides a workaround for the extremely slow sorting in IE.
+     * Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
+     * to return the value of that field and then doing a native Array.sort().
+     */
+    function fastSort(field, ascending) {
+      sortAsc = ascending;
+      fastSortField = field;
+      sortComparer = null;
+      var oldToString = Object.prototype.toString;
+      Object.prototype.toString = (typeof field == "function") ? field : function () {
+        return this[field]
+      };
+      // an extra reversal for descending sort keeps the sort stable
+      // (assuming a stable native sort implementation, which isn't true in some cases)
+      if (ascending === false) {
+        items.reverse();
+      }
+      items.sort();
+      Object.prototype.toString = oldToString;
+      if (ascending === false) {
+        items.reverse();
+      }
+      idxById = {};
+      updateIdxById();
+      refresh();
+    }
+
+    function reSort() {
+      if (sortComparer) {
+        sort(sortComparer, sortAsc);
+      } else if (fastSortField) {
+        fastSort(fastSortField, sortAsc);
+      }
+    }
+
+    function setFilter(filterFn) {
+      filter = filterFn;
+      if (options.inlineFilters) {
+        compiledFilter = compileFilter();
+        compiledFilterWithCaching = compileFilterWithCaching();
+      }
+      refresh();
+    }
+
+    function getGrouping() {
+      return groupingInfos;
+    }
+
+    function setGrouping(groupingInfo) {
+      if (!options.groupItemMetadataProvider) {
+        options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
+      }
+
+      groups = [];
+      toggledGroupsByLevel = [];
+      groupingInfo = groupingInfo || [];
+      groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
+
+      for (var i = 0; i < groupingInfos.length; i++) {
+        var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
+        gi.getterIsAFn = typeof gi.getter === "function";
+
+        // pre-compile accumulator loops
+        gi.compiledAccumulators = [];
+        var idx = gi.aggregators.length;
+        while (idx--) {
+          gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
+        }
+
+        toggledGroupsByLevel[i] = {};
+      }
+
+      refresh();
+    }
+
+    /**
+     * @deprecated Please use {@link setGrouping}.
+     */
+    function groupBy(valueGetter, valueFormatter, sortComparer) {
+      if (valueGetter == null) {
+        setGrouping([]);
+        return;
+      }
+
+      setGrouping({
+        getter: valueGetter,
+        formatter: valueFormatter,
+        comparer: sortComparer
+      });
+    }
+
+    /**
+     * @deprecated Please use {@link setGrouping}.
+     */
+    function setAggregators(groupAggregators, includeCollapsed) {
+      if (!groupingInfos.length) {
+        throw new Error("At least one grouping must be specified before calling setAggregators().");
+      }
+
+      groupingInfos[0].aggregators = groupAggregators;
+      groupingInfos[0].aggregateCollapsed = includeCollapsed;
+
+      setGrouping(groupingInfos);
+    }
+
+    function getItemByIdx(i) {
+      return items[i];
+    }
+
+    function getIdxById(id) {
+      return idxById[id];
+    }
+
+    function ensureRowsByIdCache() {
+      if (!rowsById) {
+        rowsById = {};
+        for (var i = 0, l = rows.length; i < l; i++) {
+          rowsById[rows[i][idProperty]] = i;
+        }
+      }
+    }
+
+    function getRowById(id) {
+      ensureRowsByIdCache();
+      return rowsById[id];
+    }
+
+    function getItemById(id) {
+      return items[idxById[id]];
+    }
+
+    function mapIdsToRows(idArray) {
+      var rows = [];
+      ensureRowsByIdCache();
+      for (var i = 0; i < idArray.length; i++) {
+        var row = rowsById[idArray[i]];
+        if (row != null) {
+          rows[rows.length] = row;
+        }
+      }
+      return rows;
+    }
+
+    function mapRowsToIds(rowArray) {
+      var ids = [];
+      for (var i = 0; i < rowArray.length; i++) {
+        if (rowArray[i] < rows.length) {
+          ids[ids.length] = rows[rowArray[i]][idProperty];
+        }
+      }
+      return ids;
+    }
+
+    function updateItem(id, item) {
+      if (idxById[id] === undefined || id !== item[idProperty]) {
+        throw "Invalid or non-matching id";
+      }
+      items[idxById[id]] = item;
+      if (!updated) {
+        updated = {};
+      }
+      updated[id] = true;
+      refresh();
+    }
+
+    function insertItem(insertBefore, item) {
+      items.splice(insertBefore, 0, item);
+      updateIdxById(insertBefore);
+      refresh();
+    }
+
+    function addItem(item) {
+      items.push(item);
+      updateIdxById(items.length - 1);
+      refresh();
+    }
+
+    function deleteItem(id) {
+      var idx = idxById[id];
+      if (idx === undefined) {
+        throw "Invalid id";
+      }
+      delete idxById[id];
+      items.splice(idx, 1);
+      updateIdxById(idx);
+      refresh();
+    }
+
+    function getLength() {
+      return rows.length;
+    }
+
+    function getItem(i) {
+      return rows[i];
+    }
+
+    function getItemMetadata(i) {
+      var item = rows[i];
+      if (item === undefined) {
+        return null;
+      }
+
+      // overrides for grouping rows
+      if (item.__group) {
+        return options.groupItemMetadataProvider.getGroupRowMetadata(item);
+      }
+
+      // overrides for totals rows
+      if (item.__groupTotals) {
+        return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
+      }
+
+      return null;
+    }
+
+    function expandCollapseAllGroups(level, collapse) {
+      if (level == null) {
+        for (var i = 0; i < groupingInfos.length; i++) {
+          toggledGroupsByLevel[i] = {};
+          groupingInfos[i].collapsed = collapse;
+        }
+      } else {
+        toggledGroupsByLevel[level] = {};
+        groupingInfos[level].collapsed = collapse;
+      }
+      refresh();
+    }
+
+    /**
+     * @param level {Number} Optional level to collapse.  If not specified, applies to all levels.
+     */
+    function collapseAllGroups(level) {
+      expandCollapseAllGroups(level, true);
+    }
+
+    /**
+     * @param level {Number} Optional level to expand.  If not specified, applies to all levels.
+     */
+    function expandAllGroups(level) {
+      expandCollapseAllGroups(level, false);
+    }
+
+    function expandCollapseGroup(level, groupingKey, collapse) {
+      toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
+      refresh();
+    }
+
+    /**
+     * @param varArgs Either a Slick.Group's "groupingKey" property, or a
+     *     variable argument list of grouping values denoting a unique path to the row.  For
+     *     example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
+     *     the 'high' setGrouping.
+     */
+    function collapseGroup(varArgs) {
+      var args = Array.prototype.slice.call(arguments);
+      var arg0 = args[0];
+      if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
+        expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
+      } else {
+        expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
+      }
+    }
+
+    /**
+     * @param varArgs Either a Slick.Group's "groupingKey" property, or a
+     *     variable argument list of grouping values denoting a unique path to the row.  For
+     *     example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
+     *     the 'high' setGrouping.
+     */
+    function expandGroup(varArgs) {
+      var args = Array.prototype.slice.call(arguments);
+      var arg0 = args[0];
+      if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
+        expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
+      } else {
+        expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
+      }
+    }
+
+    function getGroups() {
+      return groups;
+    }
+
+    function extractGroups(rows, parentGroup) {
+      var group;
+      var val;
+      var groups = [];
+      var groupsByVal = [];
+      var r;
+      var level = parentGroup ? parentGroup.level + 1 : 0;
+      var gi = groupingInfos[level];
+
+      for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
+        val = gi.predefinedValues[i];
+        group = groupsByVal[val];
+        if (!group) {
+          group = new Slick.Group();
+          group.value = val;
+          group.level = level;
+          group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
+          groups[groups.length] = group;
+          groupsByVal[val] = group;
+        }
+      }
+
+      for (var i = 0, l = rows.length; i < l; i++) {
+        r = rows[i];
+        val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
+        group = groupsByVal[val];
+        if (!group) {
+          group = new Slick.Group();
+          group.value = val;
+          group.level = level;
+          group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
+          groups[groups.length] = group;
+          groupsByVal[val] = group;
+        }
+
+        group.rows[group.count++] = r;
+      }
+
+      if (level < groupingInfos.length - 1) {
+        for (var i = 0; i < groups.length; i++) {
+          group = groups[i];
+          group.groups = extractGroups(group.rows, group);
+        }
+      }      
+
+      groups.sort(groupingInfos[level].comparer);
+
+      return groups;
+    }
+
+    // TODO:  lazy totals calculation
+    function calculateGroupTotals(group) {
+      // TODO:  try moving iterating over groups into compiled accumulator
+      var gi = groupingInfos[group.level];
+      var isLeafLevel = (group.level == groupingInfos.length);
+      var totals = new Slick.GroupTotals();
+      var agg, idx = gi.aggregators.length;
+      while (idx--) {
+        agg = gi.aggregators[idx];
+        agg.init();
+        gi.compiledAccumulators[idx].call(agg,
+            (!isLeafLevel && gi.aggregateChildGroups) ? group.groups : group.rows);
+        agg.storeResult(totals);
+      }
+      totals.group = group;
+      group.totals = totals;
+    }
+
+    function calculateTotals(groups, level) {
+      level = level || 0;
+      var gi = groupingInfos[level];
+      var idx = groups.length, g;
+      while (idx--) {
+        g = groups[idx];
+
+        if (g.collapsed && !gi.aggregateCollapsed) {
+          continue;
+        }
+
+        // Do a depth-first aggregation so that parent setGrouping aggregators can access subgroup totals.
+        if (g.groups) {
+          calculateTotals(g.groups, level + 1);
+        }
+
+        if (gi.aggregators.length && (
+            gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
+          calculateGroupTotals(g);
+        }
+      }
+    }
+
+    function finalizeGroups(groups, level) {
+      level = level || 0;
+      var gi = groupingInfos[level];
+      var groupCollapsed = gi.collapsed;
+      var toggledGroups = toggledGroupsByLevel[level];
+      var idx = groups.length, g;
+      while (idx--) {
+        g = groups[idx];
+        g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
+        g.title = gi.formatter ? gi.formatter(g) : g.value;
+
+        if (g.groups) {
+          finalizeGroups(g.groups, level + 1);
+          // Let the non-leaf setGrouping rows get garbage-collected.
+          // They may have been used by aggregates that go over all of the descendants,
+          // but at this point they are no longer needed.
+          g.rows = [];
+        }
+      }
+    }
+
+    function flattenGroupedRows(groups, level) {
+      level = level || 0;
+      var gi = groupingInfos[level];
+      var groupedRows = [], rows, gl = 0, g;
+      for (var i = 0, l = groups.length; i < l; i++) {
+        g = groups[i];
+        groupedRows[gl++] = g;
+
+        if (!g.collapsed) {
+          rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
+          for (var j = 0, jj = rows.length; j < jj; j++) {
+            groupedRows[gl++] = rows[j];
+          }
+        }
+
+        if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
+          groupedRows[gl++] = g.totals;
+        }
+      }
+      return groupedRows;
+    }
+
+    function getFunctionInfo(fn) {
+      var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
+      var matches = fn.toString().match(fnRegex);
+      return {
+        params: matches[1].split(","),
+        body: matches[2]
+      };
+    }
+
+    function compileAccumulatorLoop(aggregator) {
+      var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
+      var fn = new Function(
+          "_items",
+          "for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
+              accumulatorInfo.params[0] + " = _items[_i]; " +
+              accumulatorInfo.body +
+          "}"
+      );
+      fn.displayName = fn.name = "compiledAccumulatorLoop";
+      return fn;
+    }
+
+    function compileFilter() {
+      var filterInfo = getFunctionInfo(filter);
+
+      var filterBody = filterInfo.body
+          .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
+          .replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1")
+          .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
+          "{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
+
+      // This preserves the function template code after JS compression,
+      // so that replace() commands still work as expected.
+      var tpl = [
+        //"function(_items, _args) { ",
+        "var _retval = [], _idx = 0; ",
+        "var $item$, $args$ = _args; ",
+        "_coreloop: ",
+        "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
+        "$item$ = _items[_i]; ",
+        "$filter$; ",
+        "} ",
+        "return _retval; "
+        //"}"
+      ].join("");
+      tpl = tpl.replace(/\$filter\$/gi, filterBody);
+      tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
+      tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
+
+      var fn = new Function("_items,_args", tpl);
+      fn.displayName = fn.name = "compiledFilter";
+      return fn;
+    }
+
+    function compileFilterWithCaching() {
+      var filterInfo = getFunctionInfo(filter);
+
+      var filterBody = filterInfo.body
+          .replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
+          .replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1")
+          .replace(/return ([^;}]+?)\s*([;}]|$)/gi,
+          "{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
+
+      // This preserves the function template code after JS compression,
+      // so that replace() commands still work as expected.
+      var tpl = [
+        //"function(_items, _args, _cache) { ",
+        "var _retval = [], _idx = 0; ",
+        "var $item$, $args$ = _args; ",
+        "_coreloop: ",
+        "for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
+        "$item$ = _items[_i]; ",
+        "if (_cache[_i]) { ",
+        "_retval[_idx++] = $item$; ",
+        "continue _coreloop; ",
+        "} ",
+        "$filter$; ",
+        "} ",
+        "return _retval; "
+        //"}"
+      ].join("");
+      tpl = tpl.replace(/\$filter\$/gi, filterBody);
+      tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
+      tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
+
+      var fn = new Function("_items,_args,_cache", tpl);
+      fn.displayName = fn.name = "compiledFilterWithCaching";
+      return fn;
+    }
+
+    function uncompiledFilter(items, args) {
+      var retval = [], idx = 0;
+
+      for (var i = 0, ii = items.length; i < ii; i++) {
+        if (filter(items[i], args)) {
+          retval[idx++] = items[i];
+        }
+      }
+
+      return retval;
+    }
+
+    function uncompiledFilterWithCaching(items, args, cache) {
+      var retval = [], idx = 0, item;
+
+      for (var i = 0, ii = items.length; i < ii; i++) {
+        item = items[i];
+        if (cache[i]) {
+          retval[idx++] = item;
+        } else if (filter(item, args)) {
+          retval[idx++] = item;
+          cache[i] = true;
+        }
+      }
+
+      return retval;
+    }
+
+    function getFilteredAndPagedItems(items) {
+      if (filter) {
+        var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
+        var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
+
+        if (refreshHints.isFilterNarrowing) {
+          filteredItems = batchFilter(filteredItems, filterArgs);
+        } else if (refreshHints.isFilterExpanding) {
+          filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
+        } else if (!refreshHints.isFilterUnchanged) {
+          filteredItems = batchFilter(items, filterArgs);
+        }
+      } else {
+        // special case:  if not filtering and not paging, the resulting
+        // rows collection needs to be a copy so that changes due to sort
+        // can be caught
+        filteredItems = pagesize ? items : items.concat();
+      }
+
+      // get the current page
+      var paged;
+      if (pagesize) {
+        if (filteredItems.length < pagenum * pagesize) {
+          pagenum = Math.floor(filteredItems.length / pagesize);
+        }
+        paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
+      } else {
+        paged = filteredItems;
+      }
+
+      return {totalRows: filteredItems.length, rows: paged};
+    }
+
+    function getRowDiffs(rows, newRows) {
+      var item, r, eitherIsNonData, diff = [];
+      var from = 0, to = newRows.length;
+
+      if (refreshHints && refreshHints.ignoreDiffsBefore) {
+        from = Math.max(0,
+            Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
+      }
+
+      if (refreshHints && refreshHints.ignoreDiffsAfter) {
+        to = Math.min(newRows.length,
+            Math.max(0, refreshHints.ignoreDiffsAfter));
+      }
+
+      for (var i = from, rl = rows.length; i < to; i++) {
+        if (i >= rl) {
+          diff[diff.length] = i;
+        } else {
+          item = newRows[i];
+          r = rows[i];
+
+          if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
+              item.__group !== r.__group ||
+              item.__group && !item.equals(r))
+              || (eitherIsNonData &&
+              // no good way to compare totals since they are arbitrary DTOs
+              // deep object comparison is pretty expensive
+              // always considering them 'dirty' seems easier for the time being
+              (item.__groupTotals || r.__groupTotals))
+              || item[idProperty] != r[idProperty]
+              || (updated && updated[item[idProperty]])
+              ) {
+            diff[diff.length] = i;
+          }
+        }
+      }
+      return diff;
+    }
+
+    function recalc(_items) {
+      rowsById = null;
+
+      if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
+          refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
+        filterCache = [];
+      }
+
+      var filteredItems = getFilteredAndPagedItems(_items);
+      totalRows = filteredItems.totalRows;
+      var newRows = filteredItems.rows;
+
+      groups = [];
+      if (groupingInfos.length) {
+        groups = extractGroups(newRows);
+        if (groups.length) {
+          calculateTotals(groups);
+          finalizeGroups(groups);
+          newRows = flattenGroupedRows(groups);
+        }
+      }
+
+      var diff = getRowDiffs(rows, newRows);
+
+      rows = newRows;
+
+      return diff;
+    }
+
+    function refresh() {
+      if (suspend) {
+        return;
+      }
+
+      var countBefore = rows.length;
+      var totalRowsBefore = totalRows;
+
+      var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
+
+      // if the current page is no longer valid, go to last page and recalc
+      // we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
+      if (pagesize && totalRows < pagenum * pagesize) {
+        pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
+        diff = recalc(items, filter);
+      }
+
+      updated = null;
+      prevRefreshHints = refreshHints;
+      refreshHints = {};
+
+      if (totalRowsBefore != totalRows) {
+        onPagingInfoChanged.notify(getPagingInfo(), null, self);
+      }
+      if (countBefore != rows.length) {
+        onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
+      }
+      if (diff.length > 0) {
+        onRowsChanged.notify({rows: diff}, null, self);
+      }
+    }
+
+    function syncGridSelection(grid, preserveHidden) {
+      var self = this;
+      var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());;
+      var inHandler;
+
+      function update() {
+        if (selectedRowIds.length > 0) {
+          inHandler = true;
+          var selectedRows = self.mapIdsToRows(selectedRowIds);
+          if (!preserveHidden) {
+            selectedRowIds = self.mapRowsToIds(selectedRows);
+          }
+          grid.setSelectedRows(selectedRows);
+          inHandler = false;
+        }
+      }
+
+      grid.onSelectedRowsChanged.subscribe(function(e, args) {
+        if (inHandler) { return; }
+        selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
+      });
+
+      this.onRowsChanged.subscribe(update);
+
+      this.onRowCountChanged.subscribe(update);
+    }
+
+    function syncGridCellCssStyles(grid, key) {
+      var hashById;
+      var inHandler;
+
+      // since this method can be called after the cell styles have been set,
+      // get the existing ones right away
+      storeCellCssStyles(grid.getCellCssStyles(key));
+
+      function storeCellCssStyles(hash) {
+        hashById = {};
+        for (var row in hash) {
+          var id = rows[row][idProperty];
+          hashById[id] = hash[row];
+        }
+      }
+
+      function update() {
+        if (hashById) {
+          inHandler = true;
+          ensureRowsByIdCache();
+          var newHash = {};
+          for (var id in hashById) {
+            var row = rowsById[id];
+            if (row != undefined) {
+              newHash[row] = hashById[id];
+            }
+          }
+          grid.setCellCssStyles(key, newHash);
+          inHandler = false;
+        }
+      }
+
+      grid.onCellCssStylesChanged.subscribe(function(e, args) {
+        if (inHandler) { return; }
+        if (key != args.key) { return; }
+        if (args.hash) {
+          storeCellCssStyles(args.hash);
+        }
+      });
+
+      this.onRowsChanged.subscribe(update);
+
+      this.onRowCountChanged.subscribe(update);
+    }
+
+    $.extend(this, {
+      // methods
+      "beginUpdate": beginUpdate,
+      "endUpdate": endUpdate,
+      "setPagingOptions": setPagingOptions,
+      "getPagingInfo": getPagingInfo,
+      "getItems": getItems,
+      "setItems": setItems,
+      "setFilter": setFilter,
+      "sort": sort,
+      "fastSort": fastSort,
+      "reSort": reSort,
+      "setGrouping": setGrouping,
+      "getGrouping": getGrouping,
+      "groupBy": groupBy,
+      "setAggregators": setAggregators,
+      "collapseAllGroups": collapseAllGroups,
+      "expandAllGroups": expandAllGroups,
+      "collapseGroup": collapseGroup,
+      "expandGroup": expandGroup,
+      "getGroups": getGroups,
+      "getIdxById": getIdxById,
+      "getIdxByIdKey": getIdxByIdKey,
+      "getRowById": getRowById,
+      "getItemById": getItemById,
+      "getItemByIdx": getItemByIdx,
+      "mapRowsToIds": mapRowsToIds,
+      "mapIdsToRows": mapIdsToRows,
+      "setRefreshHints": setRefreshHints,
+      "setFilterArgs": setFilterArgs,
+      "refresh": refresh,
+      "updateItem": updateItem,
+      "insertItem": insertItem,
+      "addItem": addItem,
+      "deleteItem": deleteItem,
+      "syncGridSelection": syncGridSelection,
+      "syncGridCellCssStyles": syncGridCellCssStyles,
+
+      // data provider methods
+      "getLength": getLength,
+      "getItem": getItem,
+      "getItemMetadata": getItemMetadata,
+
+      // events
+      "onRowCountChanged": onRowCountChanged,
+      "onRowsChanged": onRowsChanged,
+      "onPagingInfoChanged": onPagingInfoChanged
+    });
+  }
+
+  function AvgAggregator(field) {
+    this.field_ = field;
+
+    this.init = function () {
+      this.count_ = 0;
+      this.nonNullCount_ = 0;
+      this.sum_ = 0;
+    };
+
+    this.accumulate = function (item) {
+      var val = item[this.field_];
+      this.count_++;
+      if (val != null && val !== "" && val !== NaN) {
+        this.nonNullCount_++;
+        this.sum_ += parseFloat(val);
+      }
+    };
+
+    this.storeResult = function (groupTotals) {
+      if (!groupTotals.avg) {
+        groupTotals.avg = {};
+      }
+      if (this.nonNullCount_ != 0) {
+        groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
+      }
+    };
+  }
+
+  function MinAggregator(field) {
+    this.field_ = field;
+
+    this.init = function () {
+      this.min_ = null;
+    };
+
+    this.accumulate = function (item) {
+      var val = item[this.field_];
+      if (val != null && val !== "" && val !== NaN) {
+        if (this.min_ == null || val < this.min_) {
+          this.min_ = val;
+        }
+      }
+    };
+
+    this.storeResult = function (groupTotals) {
+      if (!groupTotals.min) {
+        groupTotals.min = {};
+      }
+      groupTotals.min[this.field_] = this.min_;
+    }
+  }
+
+  function MaxAggregator(field) {
+    this.field_ = field;
+
+    this.init = function () {
+      this.max_ = null;
+    };
+
+    this.accumulate = function (item) {
+      var val = item[this.field_];
+      if (val != null && val !== "" && val !== NaN) {
+        if (this.max_ == null || val > this.max_) {
+          this.max_ = val;
+        }
+      }
+    };
+
+    this.storeResult = function (groupTotals) {
+      if (!groupTotals.max) {
+        groupTotals.max = {};
+      }
+      groupTotals.max[this.field_] = this.max_;
+    }
+  }
+
+  function SumAggregator(field) {
+    this.field_ = field;
+
+    this.init = function () {
+      this.sum_ = null;
+    };
+
+    this.accumulate = function (item) {
+      var val = item[this.field_];
+      if (val != null && val !== "" && val !== NaN) {
+        this.sum_ += parseFloat(val);
+      }
+    };
+
+    this.storeResult = function (groupTotals) {
+      if (!groupTotals.sum) {
+        groupTotals.sum = {};
+      }
+      groupTotals.sum[this.field_] = this.sum_;
+    }
+  }
+
+  // TODO:  add more built-in aggregators
+  // TODO:  merge common aggregators in one to prevent needles iterating
+
+})(jQuery);
diff --git a/plugins/querygrid/static/js/slick.unfoldselection.js b/plugins/querygrid/static/js/slick.unfoldselection.js
new file mode 100644 (file)
index 0000000..982fde0
--- /dev/null
@@ -0,0 +1,191 @@
+// -*- js-indent-level:2 -*-
+// originally cloned from slick.rowselectionmodel.js
+(function ($) {
+  // register namespace
+  $.extend(true, window, {
+    "Slick": {
+      "UnfoldSelectionModel": UnfoldSelectionModel
+    }
+  });
+  
+  function UnfoldSelectionModel(options) {
+    var _grid;
+    var _ranges = [];
+    var _self = this;
+    var _handler = new Slick.EventHandler();
+    var _inHandler;
+    var _options;
+    var _defaults = {
+      selectActiveRow: true
+    };
+
+    function init(grid) {
+      messages.debug ("init: incoming grid="+grid+" onclick="+grid.onClick);
+      _options = $.extend(true, {}, _defaults, options);
+      _grid = grid;
+      _handler.subscribe(_grid.onActiveCellChanged, 
+                         wrapHandler(handleActiveCellChange));
+      _handler.subscribe(_grid.onKeyDown, 
+                         wrapHandler(handleKeyDown));
+      _handler.subscribe(_grid.onClick, 
+                         wrapHandler(handleClick));
+    }
+
+    function destroy() {
+      _handler.unsubscribeAll();
+    }
+
+    function wrapHandler(handler) {
+      return function () {
+        if (!_inHandler) {
+          _inHandler = true;
+          handler.apply(this, arguments);
+          _inHandler = false;
+        }
+      };
+    }
+
+    function rangesToRows(ranges) {
+      var rows = [];
+      for (var i = 0; i < ranges.length; i++) {
+        for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
+          rows.push(j);
+        }
+      }
+      return rows;
+    }
+
+    function rowsToRanges(rows) {
+      var ranges = [];
+      var lastCell = _grid.getColumns().length - 1;
+      for (var i = 0; i < rows.length; i++) {
+        ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
+      }
+      return ranges;
+    }
+
+    function getRowsRange(from, to) {
+      var i, rows = [];
+      for (i = from; i <= to; i++) {
+        rows.push(i);
+      }
+      for (i = to; i < from; i++) {
+        rows.push(i);
+      }
+      return rows;
+    }
+
+    function getSelectedRows() {
+      return rangesToRows(_ranges);
+    }
+
+    function setSelectedRows(rows) {
+      setSelectedRanges(rowsToRanges(rows));
+    }
+
+    function setSelectedRanges(ranges) {
+      _ranges = ranges;
+      _self.onSelectedRangesChanged.notify(_ranges);
+    }
+
+    function getSelectedRanges() {
+      return _ranges;
+    }
+
+    function handleActiveCellChange(e, data) {
+      if (_options.selectActiveRow && data.row != null) {
+        setSelectedRanges([new Slick.Range(data.row, 0, data.row, _grid.getColumns().length - 1)]);
+      }
+    }
+
+    function handleKeyDown(e) {
+      var activeRow = _grid.getActiveCell();
+      if (activeRow && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && (e.which == 38 || e.which == 40)) {
+        var selectedRows = getSelectedRows();
+        selectedRows.sort(function (x, y) {
+          return x - y
+        });
+
+        if (!selectedRows.length) {
+          selectedRows = [activeRow.row];
+        }
+
+        var top = selectedRows[0];
+        var bottom = selectedRows[selectedRows.length - 1];
+        var active;
+
+        if (e.which == 40) {
+          active = activeRow.row < bottom || top == bottom ? ++bottom : ++top;
+        } else {
+          active = activeRow.row < bottom ? --bottom : --top;
+        }
+
+        if (active >= 0 && active < _grid.getDataLength()) {
+          _grid.scrollRowIntoView(active);
+          _ranges = rowsToRanges(getRowsRange(top, bottom));
+          setSelectedRanges(_ranges);
+        }
+
+        e.preventDefault();
+        e.stopPropagation();
+      }
+    }
+
+    function handleClick(e) {
+      messages.debug("UnfoldSelectionModel.handleClick");
+      var cell = _grid.getCellFromEvent(e);
+      if (!cell || !_grid.canCellBeActive(cell.row, cell.cell)) {
+        return false;
+      }
+
+      var selection = rangesToRows(_ranges);
+      var idx = $.inArray(cell.row, selection);
+
+      if (!e.ctrlKey && !e.shiftKey && !e.metaKey) {
+        return false;
+      }
+      else if (_grid.getOptions().multiSelect) {
+        if (idx === -1 && (e.ctrlKey || e.metaKey)) {
+          selection.push(cell.row);
+          _grid.setActiveCell(cell.row, cell.cell);
+        } else if (idx !== -1 && (e.ctrlKey || e.metaKey)) {
+          selection = $.grep(selection, function (o, i) {
+            return (o !== cell.row);
+          });
+          _grid.setActiveCell(cell.row, cell.cell);
+        } else if (selection.length && e.shiftKey) {
+          var last = selection.pop();
+          var from = Math.min(cell.row, last);
+          var to = Math.max(cell.row, last);
+          selection = [];
+          for (var i = from; i <= to; i++) {
+            if (i !== last) {
+              selection.push(i);
+            }
+          }
+          selection.push(last);
+          _grid.setActiveCell(cell.row, cell.cell);
+        }
+      }
+
+      _ranges = rowsToRanges(selection);
+      setSelectedRanges(_ranges);
+      e.stopImmediatePropagation();
+
+      return true;
+    }
+
+    $.extend(this, {
+      "getSelectedRows": getSelectedRows,
+      "setSelectedRows": setSelectedRows,
+
+      "getSelectedRanges": getSelectedRanges,
+      "setSelectedRanges": setSelectedRanges,
+
+      "init": init,
+      "destroy": destroy,
+
+      "onSelectedRangesChanged": new Slick.Event()
+    });
+  }
+})(jQuery);
index bcb8192..ca2c5da 100644 (file)
@@ -7,7 +7,7 @@
 (function($){
 
     var debug=false;
-    debug=true
+//    debug=true
 
     var QueryTable = Plugin.extend({
 
index db25e64..60a95d3 100644 (file)
@@ -224,7 +224,6 @@ class SliceView (LoginRequiredAutoLogoutView):
             # this is the query at the core of the slice list
             query      = sq_resource,
             query_all  = query_resource_all,
-            # use 'hrn' as the internal unique key for this plugin
             init_key     = main_query_init_key,
             checkboxes = True,
             datatables_options = { 
@@ -242,9 +241,7 @@ class SliceView (LoginRequiredAutoLogoutView):
                 # this is the query at the core of the slice list
                 query      = sq_resource,
                 query_all  = query_resource_all,
-                # use 'hrn' as the internal unique key for this plugin
-                # xxx todo on querygrid as well
-                # init_key     = main_query_init_key,
+                init_key     = main_query_init_key,
                 checkboxes = True,
                 )
 
index 0e2fc8e..abbecbb 100644 (file)
@@ -51,7 +51,7 @@ class QueryGridView (TemplateView):
             query      = sq_resource,
             query_all  = query_resource_all,
             # safer to use 'hrn' as the internal unique key for this plugin
-            id_key     = main_query_init_key,
+            init_key   = main_query_init_key,
             checkboxes = True,
             datatables_options = { 
                 'iDisplayLength': 25,