From 8778d5ff6426500382f9a6c78f37d7018db53e42 Mon Sep 17 00:00:00 2001
From: Joao Pereira and Sarah McAlear <pair+jpereira+smcalear@pivotal.io>
Date: Mon, 8 May 2017 17:28:00 -0400
Subject: [PATCH 1/4] Improves user's ability to select cells in query results

  - user can select columns
  - user can modify column or row selection with shift+arrow
  - user can select entire grid with ctrl+A or cmd+A
  - user can copy from grid using keyboard shortcuts
---
 libraries.txt                                      |    2 +-
 .../tables/tests/test_column_properties_sql.py     |    2 +-
 .../copy_selected_query_results_feature_test.py    |   71 +-
 .../static/js/selection/active_cell_capture.js     |  152 +
 web/pgadmin/static/js/selection/column_selector.js |   45 +-
 web/pgadmin/static/js/selection/copy_data.js       |   13 +-
 web/pgadmin/static/js/selection/grid_selector.js   |   39 +-
 .../js/selection/range_boundary_navigator.js       |    7 +-
 .../static/js/selection/range_selection_helper.js  |   12 +-
 web/pgadmin/static/js/selection/row_selector.js    |    5 +-
 web/pgadmin/static/js/slickgrid/cell_selector.js   |   18 +
 .../handle_query_output_keyboard_event.js          |   21 +
 web/pgadmin/static/vendor/slickgrid/README         |    9 -
 web/pgadmin/static/vendor/slickgrid/README.md      |    9 +
 .../slickgrid/controls/slick.columnpicker.js       |    9 +-
 .../static/vendor/slickgrid/images/CheckboxN.png   |  Bin 0 -> 257 bytes
 .../static/vendor/slickgrid/images/CheckboxY.png   |  Bin 0 -> 361 bytes
 .../vendor/slickgrid/plugins/slick.autotooltips.js |   10 +-
 .../slickgrid/plugins/slick.cellcopymanager.js     |    6 +-
 .../plugins/slick.cellexternalcopymanager.js       |  449 ++
 .../slickgrid/plugins/slick.cellrangeselector.js   |    9 +-
 .../slickgrid/plugins/slick.cellselectionmodel.js  |   46 +-
 .../slickgrid/plugins/slick.headerbuttons.js       |    6 +-
 .../vendor/slickgrid/plugins/slick.headermenu.js   |    8 +-
 .../slickgrid/plugins/slick.rowselectionmodel.js   |    8 +-
 .../vendor/slickgrid/slick-default-theme.css       |   22 +-
 web/pgadmin/static/vendor/slickgrid/slick.core.js  |   15 +-
 .../static/vendor/slickgrid/slick.dataview.js      |   40 +-
 .../static/vendor/slickgrid/slick.editors.js       |   67 +-
 .../static/vendor/slickgrid/slick.formatters.js    |   12 +-
 web/pgadmin/static/vendor/slickgrid/slick.grid.css |   43 +-
 web/pgadmin/static/vendor/slickgrid/slick.grid.js  | 7400 ++++++++++----------
 .../slickgrid/slick.groupitemmetadataprovider.js   |  158 +
 .../vendor/slickgrid/slick.remotemodel-yahoo.js    |  206 +
 .../static/vendor/slickgrid/slick.remotemodel.js   |  169 +
 .../tools/sqleditor/static/css/sqleditor.css       |   34 +-
 .../sqleditor/templates/sqleditor/js/sqleditor.js  |   76 +-
 .../utils/tests/test_versioned_template_loader.py  |   25 +-
 .../selection/active_cell_capture_spec.js          |  231 +
 .../javascript/selection/column_selector_spec.js   |  291 +-
 .../javascript/selection/copy_data_spec.js         |    9 +
 .../javascript/selection/grid_selector_spec.js     |    4 +-
 .../selection/range_boundary_navigator_spec.js     |   19 +-
 .../javascript/selection/row_selector_spec.js      |   75 +-
 .../javascript/slickgrid/cell_selector_spec.js     |   77 +
 .../handle_query_output_keyboard_event_spec.js     |  145 +
 web/regression/javascript/test-main.js             |    3 +
 web/regression/python_test_utils/test_utils.py     |    8 +-
 48 files changed, 6087 insertions(+), 3998 deletions(-)
 create mode 100644 web/pgadmin/static/js/selection/active_cell_capture.js
 create mode 100644 web/pgadmin/static/js/slickgrid/cell_selector.js
 create mode 100644 web/pgadmin/static/js/slickgrid/event_handlers/handle_query_output_keyboard_event.js
 delete mode 100644 web/pgadmin/static/vendor/slickgrid/README
 create mode 100644 web/pgadmin/static/vendor/slickgrid/README.md
 create mode 100644 web/pgadmin/static/vendor/slickgrid/images/CheckboxN.png
 create mode 100644 web/pgadmin/static/vendor/slickgrid/images/CheckboxY.png
 create mode 100644 web/pgadmin/static/vendor/slickgrid/plugins/slick.cellexternalcopymanager.js
 create mode 100644 web/pgadmin/static/vendor/slickgrid/slick.groupitemmetadataprovider.js
 create mode 100644 web/pgadmin/static/vendor/slickgrid/slick.remotemodel-yahoo.js
 create mode 100644 web/pgadmin/static/vendor/slickgrid/slick.remotemodel.js
 create mode 100644 web/regression/javascript/selection/active_cell_capture_spec.js
 create mode 100644 web/regression/javascript/slickgrid/cell_selector_spec.js
 create mode 100644 web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js

diff --git a/libraries.txt b/libraries.txt
index 7e0bd5b7..a8c634c8 100644
--- a/libraries.txt
+++ b/libraries.txt
@@ -31,6 +31,6 @@ dropzone            4e20bd4     MIT          https://github.com/enyo/dropzone
 Filemanager         7e060c2     MIT          https://github.com/simogeo/Filemanager
 Unit (Length)       d8e6237     MIT          https://github.com/heygrady/Units/blob/master/Length.min.js
 Natural Sort        9565816     MIT          https://github.com/javve/natural-sort/blob/master/index.js
-SlickGrid           2.2.4       MIT          https://github.com/6pac/SlickGrid
+SlickGrid          2.3.6-53ee34 MIT          https://github.com/6pac/SlickGrid
 jQuery-UI           1.11.3      MIT          https://jqueryui.com/
 BigNumber           3.0.1       MIT          http://mikemcl.github.io/bignumber.js
\ No newline at end of file
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_column_properties_sql.py b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_column_properties_sql.py
index ce6fa972..47c7221f 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_column_properties_sql.py
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/tests/test_column_properties_sql.py
@@ -45,7 +45,7 @@ class TestColumnPropertiesSql(SQLTemplateTestBase):
 
         self.assertEqual('some_column', first_row['name'])
         self.assertEqual('character varying', first_row['cltype'])
-        self.assertEqual(2, len(fetch_result))
+        self.assertEqual(3, len(fetch_result))
 
     @staticmethod
     def get_template_file(version, filename):
diff --git a/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py
index 338e5f71..08fc5556 100644
--- a/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py
+++ b/web/pgadmin/feature_tests/copy_selected_query_results_feature_test.py
@@ -11,6 +11,7 @@ import pyperclip
 import time
 
 from selenium.webdriver import ActionChains
+from selenium.webdriver.common.keys import Keys
 
 from regression.python_test_utils import test_utils
 from regression.feature_utils.base_feature_test import BaseFeatureTest
@@ -21,9 +22,8 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
     Tests various ways to copy data from the query results grid.
     """
 
-
     scenarios = [
-        ("Test Copying Query Results", dict())
+        ("Copy rows, column using button and keyboard shortcut", dict())
     ]
 
     def before(self):
@@ -50,27 +50,82 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
         self.page.driver.switch_to_frame(self.page.driver.find_element_by_tag_name("iframe"))
         self.page.find_by_id("btn-flash").click()
 
+        time.sleep(5)
         self._copies_rows()
         self._copies_columns()
+        self._copies_row_using_keyboard_shortcut()
+        self._copies_column_using_keyboard_shortcut()
+        self._shift_resizes_rectangular_selection()
+        self._shift_resizes_column_selection()
 
     def _copies_rows(self):
         pyperclip.copy("old clipboard contents")
-        time.sleep(5)
-        self.page.find_by_xpath("//*[contains(@class, 'sr')]/*[1]/input[@type='checkbox']").click()
+        self.page.find_by_xpath("//*[contains(@class, 'slick-row')]/*[1]").click()
+
         self.page.find_by_xpath("//*[@id='btn-copy-row']").click()
 
-        self.assertEqual("'Some-Name','6'",
+        self.assertEqual("'Some-Name','6','some info'",
                          pyperclip.paste())
 
     def _copies_columns(self):
         pyperclip.copy("old clipboard contents")
-
-        self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]/input").click()
+        self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]").click()
         self.page.find_by_xpath("//*[@id='btn-copy-row']").click()
 
         self.assertEqual(
             """'Some-Name'
-'Some-Other-Name'""",
+'Some-Other-Name'
+'Yet-Another-Name'""",
+            pyperclip.paste())
+
+    def _copies_row_using_keyboard_shortcut(self):
+        pyperclip.copy("old clipboard contents")
+        self.page.find_by_xpath("//*[contains(@class, 'slick-row')]/*[1]/input[@type='checkbox']").click()
+
+        ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        self.assertEqual("'Some-Name','6','some info'",
+                         pyperclip.paste())
+
+    def _copies_column_using_keyboard_shortcut(self):
+        pyperclip.copy("old clipboard contents")
+        self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'some_column')]").click()
+
+        ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        self.assertEqual(
+            """'Some-Name'
+'Some-Other-Name'
+'Yet-Another-Name'""",
+            pyperclip.paste())
+
+    def _shift_resizes_rectangular_selection(self):
+        pyperclip.copy("old clipboard contents")
+
+        top_left_cell = self.page.find_by_xpath("//div[contains(@class, 'slick-cell') and contains(., 'Some-Other-Name')]")
+        initial_bottom_right_cell = self.page.find_by_xpath("//div[contains(@class, 'slick-cell') and contains(., '14')]")
+        ActionChains(self.page.driver).click_and_hold(top_left_cell).move_to_element(initial_bottom_right_cell)\
+            .release(initial_bottom_right_cell).perform()
+
+        ActionChains(self.page.driver).key_down(Keys.SHIFT).send_keys(Keys.ARROW_RIGHT).key_up(Keys.SHIFT).perform()
+        ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        self.assertEqual("""'Some-Other-Name','22','some other info'
+'Yet-Another-Name','14','cool info'""", pyperclip.paste())
+
+    def _shift_resizes_column_selection(self):
+        pyperclip.copy("old clipboard contents")
+
+        self.page.find_by_xpath("//*[@data-test='output-column-header' and contains(., 'value')]").click()
+        ActionChains(self.page.driver).key_down(Keys.SHIFT).send_keys(Keys.ARROW_LEFT)\
+            .key_up(Keys.SHIFT).perform()
+
+        ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+        self.assertEqual(
+            """'Some-Name','6'
+'Some-Other-Name','22'
+'Yet-Another-Name','14'""",
             pyperclip.paste())
 
     def after(self):
diff --git a/web/pgadmin/static/js/selection/active_cell_capture.js b/web/pgadmin/static/js/selection/active_cell_capture.js
new file mode 100644
index 00000000..fe8cbd81
--- /dev/null
+++ b/web/pgadmin/static/js/selection/active_cell_capture.js
@@ -0,0 +1,152 @@
+define([
+  'jquery',
+  'sources/selection/range_selection_helper'
+], function ($, rangeSelectionHelper) {
+
+  var ActiveCellCapture = function () {
+    var KEY_RIGHT = 39;
+    var KEY_LEFT = 37;
+    var KEY_UP = 38;
+    var KEY_DOWN = 40;
+
+    var bypassDefaultActiveCellRangeChange = false;
+    var grid;
+
+    var init = function (slickGrid) {
+      grid = slickGrid;
+      grid.onDragEnd.subscribe(onDragEndHandler);
+      grid.onHeaderClick.subscribe(onHeaderClickHandler);
+      grid.onClick.subscribe(onClickHandler);
+      grid.onActiveCellChanged.subscribe(onActiveCellChangedHandler);
+      grid.onKeyDown.subscribe(onKeyDownHandler);
+    };
+
+    var destroy = function () {
+      grid.onDragEnd.unsubscribe(onDragEndHandler);
+      grid.onHeaderClick.unsubscribe(onHeaderClickHandler);
+      grid.onActiveCellChanged.unsubscribe(onActiveCellChangedHandler);
+      grid.onKeyDown.unsubscribe(onKeyDownHandler);
+    };
+
+    $.extend(this, {
+      "init": init,
+      "destroy": destroy,
+    });
+
+    function onDragEndHandler(event, dragData) {
+      bypassDefaultActiveCellRangeChange = true;
+      grid.setActiveCell(dragData.range.start.row, dragData.range.start.cell);
+    }
+
+    function onHeaderClickHandler(event, args) {
+      bypassDefaultActiveCellRangeChange = true;
+
+      var clickedColumn = args.column.pos + 1;
+      if (isClickingLastClickedHeader(0, clickedColumn)) {
+        if (isSingleRangeSelected()) {
+          grid.resetActiveCell();
+        } else {
+          grid.setActiveCell(0, retrievePreviousSelectedRange().fromCell);
+        }
+      } else if (!isClickingInSelectedColumn(clickedColumn)) {
+        grid.setActiveCell(0, clickedColumn);
+      }
+    }
+
+    function onClickHandler(event, args) {
+      if (isRowHeader(args.cell)) {
+        bypassDefaultActiveCellRangeChange = true;
+        var rowClicked = args.row;
+
+        if (isClickingLastClickedHeader(rowClicked, 1)) {
+          if (isSingleRangeSelected()) {
+            grid.resetActiveCell();
+          } else {
+            grid.setActiveCell(retrievePreviousSelectedRange().fromRow, 1);
+          }
+        } else if (!isClickingInSelectedRow(rowClicked)) {
+          grid.setActiveCell(rowClicked, 1);
+        }
+      }
+    }
+
+    function onActiveCellChangedHandler(event, args) {
+      if (bypassDefaultActiveCellRangeChange) {
+        bypassDefaultActiveCellRangeChange = false;
+        event.stopPropagation();
+      }
+    }
+
+    function onKeyDownHandler(event) {
+      if (hasActiveCell() && isShiftArrowKey(event)) {
+        selectOnlyRangeOfActiveCell();
+      }
+    }
+
+    function isClickingLastClickedHeader(clickedRow, clickedColumn) {
+      return hasActiveCell() && grid.getActiveCell().row === clickedRow && grid.getActiveCell().cell === clickedColumn;
+    }
+
+    function isClickingInSelectedColumn(clickedColumn) {
+      var column = rangeSelectionHelper.rangeForColumn(grid, clickedColumn);
+      var cellSelectionModel = grid.getSelectionModel();
+      var ranges = cellSelectionModel.getSelectedRanges();
+      return rangeSelectionHelper.isRangeSelected(ranges, column);
+    }
+
+    function isRowHeader(cellClicked) {
+      return grid.getColumns()[cellClicked].id === 'row-header-column';
+    }
+
+    function isClickingInSelectedRow(rowClicked) {
+      var row = rangeSelectionHelper.rangeForRow(grid, rowClicked);
+      var cellSelectionModel = grid.getSelectionModel();
+      var ranges = cellSelectionModel.getSelectedRanges();
+      return rangeSelectionHelper.isRangeSelected(ranges, row);
+    }
+
+    function isSingleRangeSelected() {
+      var cellSelectionModel = grid.getSelectionModel();
+      var ranges = cellSelectionModel.getSelectedRanges();
+      return ranges.length === 1;
+    }
+
+    function retrievePreviousSelectedRange() {
+      var cellSelectionModel = grid.getSelectionModel();
+      var ranges = cellSelectionModel.getSelectedRanges();
+      return ranges[ranges.length - 2];
+    }
+
+    function isArrowKey(event) {
+      return event.which === KEY_RIGHT
+        || event.which === KEY_UP
+        || event.which === KEY_LEFT
+        || event.which === KEY_DOWN;
+    }
+
+    function isModifiedByShiftOnly(event) {
+      return event.shiftKey
+        && !event.ctrlKey
+        && !event.altKey;
+    }
+
+    function isShiftArrowKey(event) {
+      return isModifiedByShiftOnly(event) && isArrowKey(event);
+    }
+
+    function hasActiveCell() {
+      return !!grid.getActiveCell();
+    }
+
+    function selectOnlyRangeOfActiveCell() {
+      var cellSelectionModel = grid.getSelectionModel();
+      var ranges = cellSelectionModel.getSelectedRanges();
+
+      if (ranges.length > 1) {
+        cellSelectionModel.setSelectedRanges([ranges.pop()]);
+      }
+    }
+  };
+
+  return ActiveCellCapture;
+});
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js
index c89b3fa8..2923a796 100644
--- a/web/pgadmin/static/js/selection/column_selector.js
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -1,20 +1,29 @@
 define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, rangeSelectionHelper) {
   var ColumnSelector = function () {
+    var Slick = window.Slick;
+    var gridEventBus = new Slick.EventHandler();
+
     var init = function (grid) {
-      grid.onHeaderClick.subscribe(function (event, eventArgument) {
-          var column = eventArgument.column;
+      gridEventBus.subscribe(grid.onHeaderClick, function (event, eventArgument) {
+        var columnDefinition = eventArgument.column;
 
-          if (column.selectable !== false) {
+        grid.focus();
 
-            if (!clickedCheckbox(event)) {
-              var $checkbox = $("[data-id='checkbox-" + column.id + "']");
-              toggleCheckbox($checkbox);
-            }
+        if (isColumnSelectable(columnDefinition)) {
+          var $columnHeader = $(event.target);
+          if(hasClickedChildOfColumnHeader(event)) {
+            $columnHeader = $(event.target).parents(".slick-header-column");
+          }
+          $columnHeader.toggleClass('selected');
 
-            updateRanges(grid, column.id);
+          if (!hasClickedOnCheckbox(event)) {
+            var $checkbox = $("[data-id='checkbox-" + columnDefinition.id + "']");
+            toggleCheckbox($checkbox);
           }
+
+          updateRanges(grid, columnDefinition.id);
         }
-      );
+      });
       grid.getSelectionModel().onSelectedRangesChanged
         .subscribe(handleSelectedRangesChanged.bind(null, grid));
     };
@@ -24,7 +33,8 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
         .each(function (index, checkbox) {
           var $checkbox = $(checkbox);
           var columnIndex = grid.getColumnIndex($checkbox.data('column-id'));
-          var isStillSelected = rangeSelectionHelper.isRangeSelected(ranges, rangeSelectionHelper.rangeForColumn(grid, columnIndex));
+          var isStillSelected = rangeSelectionHelper.isRangeSelected(ranges,
+            rangeSelectionHelper.rangeForColumn(grid, columnIndex));
           if (!isStillSelected) {
             toggleCheckbox($checkbox);
           }
@@ -51,8 +61,13 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
       selectionModel.setSelectedRanges(newRanges);
     };
 
-    var clickedCheckbox = function (e) {
-      return e.target.type == "checkbox"
+    var hasClickedOnCheckbox = function (event) {
+      return event.target.type == "checkbox"
+    };
+
+
+    var hasClickedChildOfColumnHeader = function (event) {
+      return !$(event.target).hasClass("slick-header-column");
     };
 
     var toggleCheckbox = function (checkbox) {
@@ -63,9 +78,13 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
       }
     };
 
+    var isColumnSelectable = function (columnDefinition) {
+      return columnDefinition.selectable !== false;
+    };
+
     var getColumnDefinitionsWithCheckboxes = function (columnDefinitions) {
       return _.map(columnDefinitions, function (columnDefinition) {
-        if (columnDefinition.selectable !== false) {
+        if (isColumnSelectable(columnDefinition)) {
           var name =
             "<span data-cell-type='column-header-row' " +
             "       data-test='output-column-header'>" +
diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js
index 018efead..166219ba 100644
--- a/web/pgadmin/static/js/selection/copy_data.js
+++ b/web/pgadmin/static/js/selection/copy_data.js
@@ -3,8 +3,9 @@ define([
   'underscore',
   'sources/selection/clipboard',
   'sources/selection/range_selection_helper',
-  'sources/selection/range_boundary_navigator'],
-  function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
+  'sources/selection/range_boundary_navigator'
+  ],
+function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
   var copyData = function () {
     var self = this;
 
@@ -14,7 +15,6 @@ define([
     var data = grid.getData();
     var rows = grid.getSelectedRows();
 
-
     if (allTheRangesAreFullRows(selectedRanges, columnDefinitions)) {
       self.copied_rows = rows.map(function (rowIndex) {
         return data[rowIndex];
@@ -24,7 +24,6 @@ define([
       self.copied_rows = [];
       setPasteRowButtonEnablement(self.can_edit, false);
     }
-
     var csvText = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, selectedRanges);
     if (csvText) {
       clipboard.copyTextToClipboard(csvText);
@@ -45,8 +44,8 @@ define([
     if(RangeSelectionHelper.isFirstColumnData(columnDefinitions)) {
       return _.isEqual(_.union.apply(null, colRangeBounds), [0, columnDefinitions.length - 1]);
     }
-    return _.isEqual(_.union.apply(null, colRangeBounds), [1, columnDefinitions.length - 1]);
+    return _.isEqual(_.union.apply(null, colRangeBounds), [0, columnDefinitions.length - 1]);
   };
 
-  return copyData;
-});
+  return copyData
+});
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/grid_selector.js b/web/pgadmin/static/js/selection/grid_selector.js
index 31aee69f..90eeacf8 100644
--- a/web/pgadmin/static/js/selection/grid_selector.js
+++ b/web/pgadmin/static/js/selection/grid_selector.js
@@ -1,5 +1,8 @@
-define(['jquery', 'sources/selection/column_selector', 'sources/selection/row_selector'],
-  function ($, ColumnSelector, RowSelector) {
+define(['jquery',
+    'sources/selection/column_selector',
+    'sources/selection/row_selector',
+    'sources/selection/range_selection_helper'],
+  function ($, ColumnSelector, RowSelector, RangeSelectionHelper) {
     var Slick = window.Slick;
 
     var GridSelector = function (columnDefinitions) {
@@ -31,44 +34,22 @@ define(['jquery', 'sources/selection/column_selector', 'sources/selection/row_se
       };
 
       function handleSelectedRangesChanged(grid) {
-        $("[data-id='checkbox-select-all']").prop("checked", isEntireGridSelected(grid));
-      }
-
-      function isEntireGridSelected(grid) {
-        var selectionModel = grid.getSelectionModel();
-        var selectedRanges = selectionModel.getSelectedRanges();
-        return selectedRanges.length == 1 && isSameRange(selectedRanges[0], getRangeOfWholeGrid(grid));
+        $("[data-id='checkbox-select-all']").prop("checked", RangeSelectionHelper.isEntireGridSelected(grid));
       }
 
       function toggleSelectAll(grid) {
-        if (isEntireGridSelected(grid)) {
-          deselect(grid);
+        if (RangeSelectionHelper.isEntireGridSelected(grid)) {
+          selectNone(grid);
         } else {
-          selectAll(grid)
+          RangeSelectionHelper.selectAll(grid);
         }
       }
 
-      var isSameRange = function (range, otherRange) {
-        return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
-          range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
-      };
-
-      function getRangeOfWholeGrid(grid) {
-        return new Slick.Range(0, 1, grid.getDataLength() - 1, grid.getColumns().length - 1);
-      }
-
-      function deselect(grid) {
+      function selectNone(grid) {
         var selectionModel = grid.getSelectionModel();
         selectionModel.setSelectedRanges([]);
       }
 
-      function selectAll(grid) {
-        var range = getRangeOfWholeGrid(grid);
-        var selectionModel = grid.getSelectionModel();
-
-        selectionModel.setSelectedRanges([range]);
-      }
-
       $.extend(this, {
         "init": init,
         "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
diff --git a/web/pgadmin/static/js/selection/range_boundary_navigator.js b/web/pgadmin/static/js/selection/range_boundary_navigator.js
index a268d245..f88a6533 100644
--- a/web/pgadmin/static/js/selection/range_boundary_navigator.js
+++ b/web/pgadmin/static/js/selection/range_boundary_navigator.js
@@ -1,4 +1,5 @@
-define(['sources/selection/range_selection_helper'], function (RangeSelectionHelper) {
+define(['sources/selection/range_selection_helper'],
+function (RangeSelectionHelper) {
   return {
     getUnion: function (allRanges) {
       if (_.isEmpty(allRanges)) {
@@ -77,6 +78,10 @@ define(['sources/selection/range_selection_helper'], function (RangeSelectionHel
     removeFirstColumn: function (colRangeBounds) {
       var unionedColRanges = this.getUnion(colRangeBounds);
 
+      if(unionedColRanges.length == 0) {
+        return [];
+      }
+
       var firstSubrangeStartsAt0 = function () {
         return unionedColRanges[0][0] == 0;
       };
diff --git a/web/pgadmin/static/js/selection/range_selection_helper.js b/web/pgadmin/static/js/selection/range_selection_helper.js
index 31ad3bf7..6c69c390 100644
--- a/web/pgadmin/static/js/selection/range_selection_helper.js
+++ b/web/pgadmin/static/js/selection/range_selection_helper.js
@@ -45,7 +45,7 @@ define(['slickgrid'], function () {
     return new Slick.Range(rowId, 1, rowId, grid.getColumns().length - 1);
   };
 
-  function rangeForColumn(grid, columnIndex) {
+  var rangeForColumn = function (grid, columnIndex) {
     return new Slick.Range(0, columnIndex, grid.getDataLength() - 1, columnIndex)
   };
 
@@ -63,6 +63,13 @@ define(['slickgrid'], function () {
     return !_.isUndefined(columnDefinitions[0].pos);
   };
 
+  function selectAll(grid) {
+    var range = getRangeOfWholeGrid(grid);
+    var selectionModel = grid.getSelectionModel();
+
+    selectionModel.setSelectedRanges([range]);
+  }
+
   return {
     addRange: addRange,
     removeRange: removeRange,
@@ -73,6 +80,7 @@ define(['slickgrid'], function () {
     rangeForColumn: rangeForColumn,
     isEntireGridSelected: isEntireGridSelected,
     getRangeOfWholeGrid: getRangeOfWholeGrid,
-    isFirstColumnData: isFirstColumnData
+    isFirstColumnData: isFirstColumnData,
+    selectAll: selectAll,
   }
 });
\ No newline at end of file
diff --git a/web/pgadmin/static/js/selection/row_selector.js b/web/pgadmin/static/js/selection/row_selector.js
index 76a8c1a7..8124b5cb 100644
--- a/web/pgadmin/static/js/selection/row_selector.js
+++ b/web/pgadmin/static/js/selection/row_selector.js
@@ -19,7 +19,7 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
         }
         updateRanges(grid, args.row);
       }
-    }
+    };
 
     var handleSelectedRangesChanged = function (grid, event, ranges) {
       $('[data-cell-type="row-header-checkbox"]:checked')
@@ -32,7 +32,7 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
             toggleCheckbox($checkbox);
           }
         });
-    }
+    };
 
     var updateRanges = function (grid, rowId) {
       var selectionModel = grid.getSelectionModel();
@@ -81,5 +81,6 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
       "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
     });
   };
+
   return RowSelector;
 });
diff --git a/web/pgadmin/static/js/slickgrid/cell_selector.js b/web/pgadmin/static/js/slickgrid/cell_selector.js
new file mode 100644
index 00000000..a709351e
--- /dev/null
+++ b/web/pgadmin/static/js/slickgrid/cell_selector.js
@@ -0,0 +1,18 @@
+define(["slickgrid"], function () {
+  var Slick = window.Slick;
+
+  return function () {
+    this.init = function (grid) {
+      grid.onActiveCellChanged.subscribe(function (event, slickEvent) {
+        grid.getSelectionModel().setSelectedRanges([
+          new Slick.Range(
+            slickEvent.row,
+            slickEvent.cell,
+            slickEvent.row,
+            slickEvent.cell
+          )
+        ]);
+      });
+    }
+  }
+});
\ No newline at end of file
diff --git a/web/pgadmin/static/js/slickgrid/event_handlers/handle_query_output_keyboard_event.js b/web/pgadmin/static/js/slickgrid/event_handlers/handle_query_output_keyboard_event.js
new file mode 100644
index 00000000..2a319407
--- /dev/null
+++ b/web/pgadmin/static/js/slickgrid/event_handlers/handle_query_output_keyboard_event.js
@@ -0,0 +1,21 @@
+define([
+  'sources/selection/copy_data',
+  'sources/selection/range_selection_helper'
+  ],
+function (copyData, RangeSelectionHelper) {
+  return function handleQueryOutputKeyboardEvent(event, args) {
+    var KEY_C = 67;
+    var KEY_A = 65;
+    var modifiedKey = event.keyCode;
+    var isModifierDown = event.ctrlKey || event.metaKey;
+    this.slickgrid = args.grid;
+
+    if (isModifierDown && modifiedKey == KEY_C) {
+      copyData.apply(this);
+    }
+
+    if (isModifierDown && modifiedKey == KEY_A) {
+      RangeSelectionHelper.selectAll(this.slickgrid);
+    }
+  }
+});
\ No newline at end of file
diff --git a/web/pgadmin/static/vendor/slickgrid/README b/web/pgadmin/static/vendor/slickgrid/README
deleted file mode 100644
index cf583515..00000000
--- a/web/pgadmin/static/vendor/slickgrid/README
+++ /dev/null
@@ -1,9 +0,0 @@
-WARNING!!
-
-The following changes have been made to SlickGrid. These must be re-applied if updating the code from upstream!
-
-- The CSS class 'slick-row' has been renamed to 'sr'
-
-- The CSS class 'slick-cell' has been renamed to 'sc'
-
-The intent of these changes is to minimise memory usage by the grid, by saving a few bytes per row/cell.
\ No newline at end of file
diff --git a/web/pgadmin/static/vendor/slickgrid/README.md b/web/pgadmin/static/vendor/slickgrid/README.md
new file mode 100644
index 00000000..e8a1ad7a
--- /dev/null
+++ b/web/pgadmin/static/vendor/slickgrid/README.md
@@ -0,0 +1,9 @@
+## This is the 6pac SlickGrid repo
+
+This is the acknowledged most active non-customised fork of SlickGrid.
+
+It aims to be a viable alternative master repo, building on the legacy of the mleibman/SlickGrid master branch, keeping libraries up to date and applying small, safe core patches and enhancements without turning into a personalised build.
+
+Check out the [examples](https://github.com/6pac/SlickGrid/wiki/Examples) for examples demonstrating new features and use cases, such as dynamic grid creation and editors with third party controls.
+
+Also check out the [wiki](https://github.com/6pac/SlickGrid/wiki) for news and documentation.
\ No newline at end of file
diff --git a/web/pgadmin/static/vendor/slickgrid/controls/slick.columnpicker.js b/web/pgadmin/static/vendor/slickgrid/controls/slick.columnpicker.js
index dc167209..93a6be5b 100644
--- a/web/pgadmin/static/vendor/slickgrid/controls/slick.columnpicker.js
+++ b/web/pgadmin/static/vendor/slickgrid/controls/slick.columnpicker.js
@@ -12,12 +12,12 @@
       grid.onColumnsReordered.subscribe(updateColumnOrder);
       options = $.extend({}, defaults, options);
 
-      $menu = $("<span class='slick-columnpicker' style='display:none;position:absolute;z-index:20;' />").appendTo(document.body);
+      $menu = $("<span class='slick-columnpicker' style='display:none;position:absolute;z-index:20;overflow-y:scroll;' />").appendTo(document.body);
 
-      $menu.bind("mouseleave", function (e) {
+      $menu.on("mouseleave", function (e) {
         $(this).fadeOut(options.fadeSpeed)
       });
-      $menu.bind("click", updateColumn);
+      $menu.on("click", updateColumn);
 
     }
 
@@ -44,7 +44,7 @@
         }
 
         $("<label />")
-            .text(columns[i].name)
+            .html(columns[i].name)
             .prepend($input)
             .appendTo($li);
       }
@@ -73,6 +73,7 @@
       $menu
           .css("top", e.pageY - 10)
           .css("left", e.pageX - 10)
+          .css("max-height", $(window).height() - e.pageY -10)
           .fadeIn(options.fadeSpeed);
     }
 
diff --git a/web/pgadmin/static/vendor/slickgrid/images/CheckboxN.png b/web/pgadmin/static/vendor/slickgrid/images/CheckboxN.png
new file mode 100644
index 0000000000000000000000000000000000000000..072ecbb96435c4822374cb3116fca5819194363e
GIT binary patch
diff --git a/web/pgadmin/static/vendor/slickgrid/images/CheckboxY.png b/web/pgadmin/static/vendor/slickgrid/images/CheckboxY.png
new file mode 100644
index 0000000000000000000000000000000000000000..467f3010896f438d0ede54760ba1aec7da3161ed
GIT binary patch
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.autotooltips.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.autotooltips.js
index 6633bb70..ffa93e0d 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.autotooltips.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.autotooltips.js
@@ -21,7 +21,7 @@
       enableForHeaderCells: false,
       maxToolTipLength: null
     };
-
+    
     /**
      * Initialize plugin.
      */
@@ -31,7 +31,7 @@
       if (options.enableForCells) _grid.onMouseEnter.subscribe(handleMouseEnter);
       if (options.enableForHeaderCells) _grid.onHeaderMouseEnter.subscribe(handleHeaderMouseEnter);
     }
-
+    
     /**
      * Destroy plugin.
      */
@@ -39,7 +39,7 @@
       if (options.enableForCells) _grid.onMouseEnter.unsubscribe(handleMouseEnter);
       if (options.enableForHeaderCells) _grid.onHeaderMouseEnter.unsubscribe(handleHeaderMouseEnter);
     }
-
+    
     /**
      * Handle mouse entering grid cell to add/remove tooltip.
      * @param {jQuery.Event} e - The event
@@ -60,7 +60,7 @@
         $node.attr("title", text);
       }
     }
-
+    
     /**
      * Handle mouse entering header cell to add/remove tooltip.
      * @param {jQuery.Event} e     - The event
@@ -73,7 +73,7 @@
         $node.attr("title", ($node.innerWidth() < $node[0].scrollWidth) ? column.name : "");
       }
     }
-
+    
     // Public API
     $.extend(this, {
       "init": init,
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellcopymanager.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellcopymanager.js
index c74018de..66c2376c 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellcopymanager.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellcopymanager.js
@@ -46,10 +46,12 @@
         if (e.which == 86 && (e.ctrlKey || e.metaKey)) {
           if (_copiedRanges) {
             e.preventDefault();
-            clearCopySelection();
             ranges = _grid.getSelectionModel().getSelectedRanges();
             _self.onPasteCells.notify({from: _copiedRanges, to: ranges});
-            _copiedRanges = null;
+            if (!_grid.getOptions().preserveCopiedSelectionOnPaste) {
+              clearCopySelection();
+              _copiedRanges = null;
+            }
           }
         }
       }
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellexternalcopymanager.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellexternalcopymanager.js
new file mode 100644
index 00000000..fec7cb2a
--- /dev/null
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellexternalcopymanager.js
@@ -0,0 +1,449 @@
+(function ($) {
+  // register namespace
+  $.extend(true, window, {
+    "Slick": {
+      "CellExternalCopyManager": CellExternalCopyManager
+    }
+  });
+
+
+  function CellExternalCopyManager(options) {
+    /*
+      This manager enables users to copy/paste data from/to an external Spreadsheet application
+      such as MS-Excel® or OpenOffice-Spreadsheet.
+      
+      Since it is not possible to access directly the clipboard in javascript, the plugin uses
+      a trick to do it's job. After detecting the keystroke, we dynamically create a textarea
+      where the browser copies/pastes the serialized data. 
+      
+      options:
+        copiedCellStyle : sets the css className used for copied cells. default : "copied"
+        copiedCellStyleLayerKey : sets the layer key for setting css values of copied cells. default : "copy-manager"
+        dataItemColumnValueExtractor : option to specify a custom column value extractor function
+        dataItemColumnValueSetter : option to specify a custom column value setter function
+        clipboardCommandHandler : option to specify a custom handler for paste actions
+        includeHeaderWhenCopying : set to true and the plugin will take the name property from each column (which is usually what appears in your header) and put that as the first row of the text that's copied to the clipboard
+        bodyElement: option to specify a custom DOM element which to will be added the hidden textbox. It's useful if the grid is inside a modal dialog.
+        onCopyInit: optional handler to run when copy action initializes
+        onCopySuccess: optional handler to run when copy action is complete
+        newRowCreator: function to add rows to table if paste overflows bottom of table
+        readOnlyMode: suppresses paste
+    */
+    var _grid;
+    var _self = this;
+    var _copiedRanges;
+    var _options = options || {};
+    var _copiedCellStyleLayerKey = _options.copiedCellStyleLayerKey || "copy-manager";
+    var _copiedCellStyle = _options.copiedCellStyle || "copied";
+    var _clearCopyTI = 0;
+    var _bodyElement = _options.bodyElement || document.body;
+    var _onCopyInit = _options.onCopyInit || null;
+    var _onCopySuccess = _options.onCopySuccess || null;
+    
+    var keyCodes = {
+      'C': 67,
+      'V': 86,
+      'ESC': 27,
+      'INSERT': 45
+    };
+
+    function init(grid) {
+      _grid = grid;
+      _grid.onKeyDown.subscribe(handleKeyDown);
+      
+      // we need a cell selection model
+      var cellSelectionModel = grid.getSelectionModel();
+      if (!cellSelectionModel){
+        throw new Error("Selection model is mandatory for this plugin. Please set a selection model on the grid before adding this plugin: grid.setSelectionModel(new Slick.CellSelectionModel())");
+      }
+      // we give focus on the grid when a selection is done on it.
+      // without this, if the user selects a range of cell without giving focus on a particular cell, the grid doesn't get the focus and key stroke handles (ctrl+c) don't work
+      cellSelectionModel.onSelectedRangesChanged.subscribe(function(e, args){
+        _grid.focus();
+      });
+    }
+
+    function destroy() {
+      _grid.onKeyDown.unsubscribe(handleKeyDown);
+    }
+    
+    function getDataItemValueForColumn(item, columnDef) {
+      if (_options.dataItemColumnValueExtractor) {
+        var dataItemColumnValueExtractorValue = _options.dataItemColumnValueExtractor(item, columnDef);
+
+        if (dataItemColumnValueExtractorValue)
+          return dataItemColumnValueExtractorValue;
+      }
+
+      var retVal = '';
+
+      // if a custom getter is not defined, we call serializeValue of the editor to serialize
+      if (columnDef.editor){
+        var editorArgs = {
+          'container':$("<p>"),  // a dummy container
+          'column':columnDef,
+          'position':{'top':0, 'left':0},  // a dummy position required by some editors
+          'grid':_grid
+        };
+        var editor = new columnDef.editor(editorArgs);
+        editor.loadValue(item);
+        retVal = editor.serializeValue();
+        editor.destroy();
+      }
+      else {
+        retVal = item[columnDef.field];
+      }
+
+      return retVal;
+    }
+    
+    function setDataItemValueForColumn(item, columnDef, value) {
+      if (_options.dataItemColumnValueSetter) {
+        return _options.dataItemColumnValueSetter(item, columnDef, value);
+      }
+
+      // if a custom setter is not defined, we call applyValue of the editor to unserialize
+      if (columnDef.editor){
+        var editorArgs = {
+          'container':$("body"),  // a dummy container
+          'column':columnDef,
+          'position':{'top':0, 'left':0},  // a dummy position required by some editors
+          'grid':_grid
+        };
+        var editor = new columnDef.editor(editorArgs);
+        editor.loadValue(item);
+        editor.applyValue(item, value);
+        editor.destroy();
+      }
+    }
+    
+    
+    function _createTextBox(innerText){
+      var ta = document.createElement('textarea');
+      ta.style.position = 'absolute';
+      ta.style.left = '-1000px';
+      ta.style.top = document.body.scrollTop + 'px';
+      ta.value = innerText;
+      _bodyElement.appendChild(ta);
+      ta.select();
+      
+      return ta;
+    }
+    
+    function _decodeTabularData(_grid, ta){
+      var columns = _grid.getColumns();
+      var clipText = ta.value;
+      var clipRows = clipText.split(/[\n\f\r]/);
+      // trim trailing CR if present
+      if (clipRows[clipRows.length - 1]=="") { clipRows.pop(); }
+      
+      var clippedRange = [];
+      var j = 0;
+      
+      _bodyElement.removeChild(ta);
+      for (var i=0; i<clipRows.length; i++) {
+        if (clipRows[i]!="")
+          clippedRange[j++] = clipRows[i].split("\t");
+          else
+          clippedRange[i] = [""];
+      }
+      var selectedCell = _grid.getActiveCell();
+      var ranges = _grid.getSelectionModel().getSelectedRanges();
+      var selectedRange = ranges && ranges.length ? ranges[0] : null;   // pick only one selection
+      var activeRow = null;
+      var activeCell = null;
+      
+      if (selectedRange){
+        activeRow = selectedRange.fromRow;
+        activeCell = selectedRange.fromCell;
+      } else if (selectedCell){
+        activeRow = selectedCell.row;
+        activeCell = selectedCell.cell;
+      } else {
+        // we don't know where to paste
+        return;
+      }
+      
+      var oneCellToMultiple = false;
+      var destH = clippedRange.length;
+      var destW = clippedRange.length ? clippedRange[0].length : 0;
+      if (clippedRange.length == 1 && clippedRange[0].length == 1 && selectedRange){
+        oneCellToMultiple = true;
+        destH = selectedRange.toRow - selectedRange.fromRow +1;
+        destW = selectedRange.toCell - selectedRange.fromCell +1;
+      }
+      var availableRows = _grid.getData().length - activeRow;
+      var addRows = 0;
+      if(availableRows < destH)
+      {
+        var d = _grid.getData();
+        for(addRows = 1; addRows <= destH - availableRows; addRows++)
+          d.push({});
+        _grid.setData(d);
+        _grid.render();
+      }
+
+      var overflowsBottomOfGrid = activeRow + destH > _grid.getDataLength();
+
+      if (_options.newRowCreator && overflowsBottomOfGrid) {
+
+        var newRowsNeeded = activeRow + destH - _grid.getDataLength();
+
+        _options.newRowCreator(newRowsNeeded);
+
+      }
+
+      var clipCommand = {
+
+        isClipboardCommand: true,
+        clippedRange: clippedRange,
+        oldValues: [],
+        cellExternalCopyManager: _self,
+        _options: _options,
+        setDataItemValueForColumn: setDataItemValueForColumn,
+        markCopySelection: markCopySelection,
+        oneCellToMultiple: oneCellToMultiple,
+        activeRow: activeRow,
+        activeCell: activeCell,
+        destH: destH,
+        destW: destW,
+        maxDestY: _grid.getDataLength(),
+        maxDestX: _grid.getColumns().length,
+        h: 0,
+        w: 0,
+          
+        execute: function() {
+          this.h=0;
+          for (var y = 0; y < this.destH; y++){
+            this.oldValues[y] = [];
+            this.w=0;
+            this.h++;
+            for (var x = 0; x < this.destW; x++){
+              this.w++;
+              var desty = activeRow + y;
+              var destx = activeCell + x;
+              
+              if (desty < this.maxDestY && destx < this.maxDestX ) {
+                var nd = _grid.getCellNode(desty, destx);
+                var dt = _grid.getDataItem(desty);
+                this.oldValues[y][x] = dt[columns[destx]['field']];
+                if (oneCellToMultiple)
+                  this.setDataItemValueForColumn(dt, columns[destx], clippedRange[0][0]);
+                else
+                  this.setDataItemValueForColumn(dt, columns[destx], clippedRange[y] ? clippedRange[y][x] : '');
+                _grid.updateCell(desty, destx);
+                _grid.onCellChange.notify({
+                    row: desty,
+                    cell: destx,
+                    item: dt,
+                    grid: _grid
+                });
+
+              }
+            }
+          }
+          
+          var bRange = {
+            'fromCell': activeCell,
+            'fromRow': activeRow,
+            'toCell': activeCell+this.w-1,
+            'toRow': activeRow+this.h-1
+          }
+
+          this.markCopySelection([bRange]);
+          _grid.getSelectionModel().setSelectedRanges([bRange]);
+          this.cellExternalCopyManager.onPasteCells.notify({ranges: [bRange]});
+        },
+
+        undo: function() {
+          for (var y = 0; y < this.destH; y++){
+            for (var x = 0; x < this.destW; x++){
+              var desty = activeRow + y;
+              var destx = activeCell + x;
+              
+              if (desty < this.maxDestY && destx < this.maxDestX ) {
+                var nd = _grid.getCellNode(desty, destx);
+                var dt = _grid.getDataItem(desty);
+                if (oneCellToMultiple)
+                  this.setDataItemValueForColumn(dt, columns[destx], this.oldValues[0][0]);
+                else
+                  this.setDataItemValueForColumn(dt, columns[destx], this.oldValues[y][x]);
+                _grid.updateCell(desty, destx);
+                _grid.onCellChange.notify({
+                    row: desty,
+                    cell: destx,
+                    item: dt,
+                    grid: _grid
+                });
+              }
+            }
+          }
+          
+          var bRange = {
+            'fromCell': activeCell,
+            'fromRow': activeRow,
+            'toCell': activeCell+this.w-1,
+            'toRow': activeRow+this.h-1
+          }
+
+          this.markCopySelection([bRange]);
+          _grid.getSelectionModel().setSelectedRanges([bRange]);
+          this.cellExternalCopyManager.onPasteCells.notify({ranges: [bRange]});
+          
+          if(addRows > 1){            
+            var d = _grid.getData();
+            for(; addRows > 1; addRows--)
+              d.splice(d.length - 1, 1);
+            _grid.setData(d);
+            _grid.render();
+          }
+        }
+      };
+
+      if(_options.clipboardCommandHandler) {
+        _options.clipboardCommandHandler(clipCommand);
+      }
+      else {
+        clipCommand.execute();
+      }
+    }
+    
+    
+    function handleKeyDown(e, args) {
+      var ranges;
+      if (!_grid.getEditorLock().isActive() || _grid.getOptions().autoEdit) {
+        if (e.which == keyCodes.ESC) {
+          if (_copiedRanges) {
+            e.preventDefault();
+            clearCopySelection();
+            _self.onCopyCancelled.notify({ranges: _copiedRanges});
+            _copiedRanges = null;
+          }
+        }
+
+        if ((e.which === keyCodes.C || e.which === keyCodes.INSERT) && (e.ctrlKey || e.metaKey) && !e.shiftKey) {    // CTRL+C or CTRL+INS
+          if (_onCopyInit) {
+            _onCopyInit.call();
+          }
+          ranges = _grid.getSelectionModel().getSelectedRanges();
+          if (ranges.length != 0) {
+            _copiedRanges = ranges;
+            markCopySelection(ranges);
+            _self.onCopyCells.notify({ranges: ranges});
+            
+            var columns = _grid.getColumns();
+            var clipText = "";
+
+            for (var rg = 0; rg < ranges.length; rg++){
+                var range = ranges[rg];
+                var clipTextRows = [];
+                for (var i=range.fromRow; i< range.toRow+1 ; i++){
+                    var clipTextCells = [];
+                    var dt = _grid.getDataItem(i);
+                    
+                    if (clipTextRows == "" && _options.includeHeaderWhenCopying) {
+                        var clipTextHeaders = [];
+                        for (var j = range.fromCell; j < range.toCell + 1 ; j++) {
+                            if (columns[j].name.length > 0)
+                                clipTextHeaders.push(columns[j].name);
+                        }
+                        clipTextRows.push(clipTextHeaders.join("\t"));
+                    }
+
+                    for (var j=range.fromCell; j< range.toCell+1 ; j++){
+                        clipTextCells.push(getDataItemValueForColumn(dt, columns[j]));
+                    }
+                    clipTextRows.push(clipTextCells.join("\t"));
+                }
+                clipText += clipTextRows.join("\r\n") + "\r\n";
+            }
+            
+            if(window.clipboardData) {
+                window.clipboardData.setData("Text", clipText);
+                return true;
+            }
+            else {
+                var focusEl = document.activeElement;
+
+                var ta = _createTextBox(clipText);
+
+                ta.focus();
+                
+                setTimeout(function(){
+                     _bodyElement.removeChild(ta);
+                    // restore focus
+                    if (focusEl)
+                        focusEl.focus();
+                    else
+                        console.log("Not element to restore focus to after copy?");
+
+                }, 100);
+
+                if (_onCopySuccess) {
+                    var rowCount = 0;
+                    // If it's cell selection, use the toRow/fromRow fields
+                    if (ranges.length === 1) {
+                        rowCount = (ranges[0].toRow + 1) - ranges[0].fromRow;
+                    }
+                    else {
+                        rowCount = ranges.length;
+                    }
+                    _onCopySuccess.call(this, rowCount);
+                }
+
+                return false;
+            }
+          }
+        }
+
+        if (!_options.readOnlyMode && (
+         (e.which === keyCodes.V && (e.ctrlKey || e.metaKey) && !e.shiftKey)
+          || (e.which === keyCodes.INSERT && e.shiftKey && !e.ctrlKey)
+         )) {    // CTRL+V or Shift+INS
+            var ta = _createTextBox('');
+            
+            setTimeout(function(){
+                _decodeTabularData(_grid, ta);
+            }, 100);
+            
+            return false;
+        }
+      }
+    }
+
+    function markCopySelection(ranges) {
+      clearCopySelection();
+      
+      var columns = _grid.getColumns();
+      var hash = {};
+      for (var i = 0; i < ranges.length; i++) {
+        for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
+          hash[j] = {};
+          for (var k = ranges[i].fromCell; k <= ranges[i].toCell && k<columns.length; k++) {
+            hash[j][columns[k].id] = _copiedCellStyle;
+          }
+        }
+      }
+      _grid.setCellCssStyles(_copiedCellStyleLayerKey, hash);
+      clearTimeout(_clearCopyTI);
+      _clearCopyTI = setTimeout(function(){
+        _self.clearCopySelection();
+      }, 2000);
+    }
+
+    function clearCopySelection() {
+      _grid.removeCellCssStyles(_copiedCellStyleLayerKey);
+    }
+
+    $.extend(this, {
+      "init": init,
+      "destroy": destroy,
+      "clearCopySelection": clearCopySelection,
+      "handleKeyDown":handleKeyDown,
+      
+      "onCopyCells": new Slick.Event(),
+      "onCopyCancelled": new Slick.Event(),
+      "onPasteCells": new Slick.Event()
+    });
+  }
+})(jQuery);
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellrangeselector.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellrangeselector.js
index 520b17f3..8d97d6bf 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellrangeselector.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellrangeselector.js
@@ -10,6 +10,7 @@
   function CellRangeSelector(options) {
     var _grid;
     var _canvas;
+    var _currentlySelectedRange;
     var _dragging;
     var _decorator;
     var _self = this;
@@ -61,7 +62,7 @@
           dd.startY - $(_canvas).offset().top);
 
       dd.range = {start: start, end: {}};
-
+      _currentlySelectedRange = dd.range;
       return _decorator.show(new Slick.Range(start.row, start.cell));
     }
 
@@ -80,6 +81,7 @@
       }
 
       dd.range.end = end;
+      _currentlySelectedRange = dd.range;
       _decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell));
     }
 
@@ -102,9 +104,14 @@
       });
     }
 
+    function getCurrentRange() {
+      return _currentlySelectedRange;
+    }
+
     $.extend(this, {
       "init": init,
       "destroy": destroy,
+      "getCurrentRange": getCurrentRange,
 
       "onBeforeCellRangeSelected": new Slick.Event(),
       "onCellRangeSelected": new Slick.Event()
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellselectionmodel.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellselectionmodel.js
index 07453ed1..47cb0cf6 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellselectionmodel.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.cellselectionmodel.js
@@ -56,7 +56,7 @@
     }
 
     function setSelectedRanges(ranges) {
-      // simle check for: empty selection didn't change, prevent firing onSelectedRangesChanged
+      // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
       if ((!_ranges || _ranges.length === 0) && (!ranges || ranges.length === 0)) { return; }
 
       _ranges = removeInvalidRanges(ranges);
@@ -75,6 +75,7 @@
     }
 
     function handleCellRangeSelected(e, args) {
+      _grid.setActiveCell(args.range.fromRow, args.range.fromCell, false, false, true);
       setSelectedRanges([args.range]);
     }
 
@@ -83,49 +84,50 @@
         setSelectedRanges([new Slick.Range(args.row, args.cell)]);
       }
     }
-
+    
     function handleKeyDown(e) {
       /***
        * Кey codes
        * 37 left
        * 38 up
        * 39 right
-       * 40 down
-       */
+       * 40 down                     
+       */                                         
       var ranges, last;
       var active = _grid.getActiveCell();
+      var metaKey = e.ctrlKey || e.metaKey;
 
-      if ( active && e.shiftKey && !e.ctrlKey && !e.altKey &&
+      if ( active && e.shiftKey && !metaKey && !e.altKey &&
           (e.which == 37 || e.which == 39 || e.which == 38 || e.which == 40) ) {
-
+      
         ranges = getSelectedRanges();
         if (!ranges.length)
          ranges.push(new Slick.Range(active.row, active.cell));
-
-        // keyboard can work with last range only
+         
+        // keyboard can work with last range only          
         last = ranges.pop();
-
+        
         // can't handle selection out of active cell
         if (!last.contains(active.row, active.cell))
           last = new Slick.Range(active.row, active.cell);
-
+        
         var dRow = last.toRow - last.fromRow,
             dCell = last.toCell - last.fromCell,
             // walking direction
             dirRow = active.row == last.fromRow ? 1 : -1,
             dirCell = active.cell == last.fromCell ? 1 : -1;
-
+                 
         if (e.which == 37) {
-          dCell -= dirCell;
+          dCell -= dirCell; 
         } else if (e.which == 39) {
-          dCell += dirCell ;
+          dCell += dirCell ; 
         } else if (e.which == 38) {
-          dRow -= dirRow;
+          dRow -= dirRow; 
         } else if (e.which == 40) {
-          dRow += dirRow;
+          dRow += dirRow; 
         }
-
-        // define new selection range
+        
+        // define new selection range 
         var new_last = new Slick.Range(active.row, active.cell, active.row + dirRow*dRow, active.cell + dirCell*dCell);
         if (removeInvalidRanges([new_last]).length) {
           ranges.push(new_last);
@@ -134,14 +136,14 @@
          _grid.scrollRowIntoView(viewRow);
          _grid.scrollCellIntoView(viewRow, viewCell);
         }
-        else
+        else 
           ranges.push(last);
 
-        setSelectedRanges(ranges);
-
+        setSelectedRanges(ranges);  
+       
         e.preventDefault();
-        e.stopPropagation();
-      }
+        e.stopPropagation();        
+      }           
     }
 
     $.extend(this, {
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.headerbuttons.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.headerbuttons.js
index 8e612735..d7c06195 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.headerbuttons.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.headerbuttons.js
@@ -122,11 +122,11 @@
           }
 
           if (button.handler) {
-            btn.bind("click", button.handler);
+            btn.on("click", button.handler);
           }
 
           btn
-            .bind("click", handleButtonClick)
+            .on("click", handleButtonClick)
             .appendTo(args.node);
         }
       }
@@ -174,4 +174,4 @@
       "onCommand": new Slick.Event()
     });
   }
-})(jQuery);
\ No newline at end of file
+})(jQuery);
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.headermenu.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.headermenu.js
index ec8244da..c4032301 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.headermenu.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.headermenu.js
@@ -100,13 +100,13 @@
       _grid.setColumns(_grid.getColumns());
 
       // Hide the menu on outside click.
-      $(document.body).bind("mousedown", handleBodyMouseDown);
+      $(document.body).on("mousedown", handleBodyMouseDown);
     }
 
 
     function destroy() {
       _handler.unsubscribeAll();
-      $(document.body).unbind("mousedown", handleBodyMouseDown);
+      $(document.body).off("mousedown", handleBodyMouseDown);
     }
 
 
@@ -149,7 +149,7 @@
         }
 
         $el
-          .bind("click", showMenu)
+          .on("click", showMenu)
           .appendTo(args.node);
       }
     }
@@ -195,7 +195,7 @@
           .data("command", item.command || '')
           .data("column", columnDef)
           .data("item", item)
-          .bind("click", handleMenuItemClick)
+          .on("click", handleMenuItemClick)
           .appendTo($menu);
 
         if (item.disabled) {
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
index 190b0f71..8553d050 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
@@ -121,8 +121,8 @@
 
         if (active >= 0 && active < _grid.getDataLength()) {
           _grid.scrollRowIntoView(active);
-          _ranges = rowsToRanges(getRowsRange(top, bottom));
-          setSelectedRanges(_ranges);
+          var tempRanges = rowsToRanges(getRowsRange(top, bottom));
+          setSelectedRanges(tempRanges);
         }
 
         e.preventDefault();
@@ -166,8 +166,8 @@
         _grid.setActiveCell(cell.row, cell.cell);
       }
 
-      _ranges = rowsToRanges(selection);
-      setSelectedRanges(_ranges);
+      var tempRanges = rowsToRanges(selection);
+      setSelectedRanges(tempRanges);
       e.stopImmediatePropagation();
 
       return true;
diff --git a/web/pgadmin/static/vendor/slickgrid/slick-default-theme.css b/web/pgadmin/static/vendor/slickgrid/slick-default-theme.css
index 2338d6e7..efc74154 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick-default-theme.css
+++ b/web/pgadmin/static/vendor/slickgrid/slick-default-theme.css
@@ -29,23 +29,23 @@ classes should alter those!
   height: 100%;
 }
 
-.sr.ui-state-active {
+.slick-row.ui-state-active {
   background: #F5F7D7;
 }
 
-.sr {
+.slick-row {
   position: absolute;
   background: white;
   border: 0px;
   line-height: 20px;
 }
 
-.sr.selected {
+.slick-row.selected {
   z-index: 10;
   background: #DFE8F6;
 }
 
-.sc {
+.slick-cell {
   padding-left: 4px;
   padding-right: 4px;
 }
@@ -73,11 +73,11 @@ classes should alter those!
   background: white;
 }
 
-.sc.selected {
+.slick-cell.selected {
   background-color: beige;
 }
 
-.sc.active {
+.slick-cell.active {
   border-color: gray;
   border-style: solid;
 }
@@ -86,25 +86,25 @@ classes should alter those!
   background: silver !important;
 }
 
-.sr.odd {
+.slick-row.odd {
   background: #fafafa;
 }
 
-.sr.ui-state-active {
+.slick-row.ui-state-active {
   background: #F5F7D7;
 }
 
-.sr.loading {
+.slick-row.loading {
   opacity: 0.5;
   filter: alpha(opacity = 50);
 }
 
-.sc.invalid {
+.slick-cell.invalid {
   border-color: red;
   -moz-animation-duration: 0.2s;
   -webkit-animation-duration: 0.2s;
   -moz-animation-name: slickgrid-invalid-hilite;
-  -webkit-animation-name: slickgrid-invalid-hilite;
+  -webkit-animation-name: slickgrid-invalid-hilite;  
 }
 
 @-moz-keyframes slickgrid-invalid-hilite {
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.core.js b/web/pgadmin/static/vendor/slickgrid/slick.core.js
index c4f04ec4..1846a50a 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.core.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.core.js
@@ -40,7 +40,8 @@
         RIGHT: 39,
         TAB: 9,
         UP: 38
-      }
+      },
+      "preClickClassName" : "slick-edit-preclick"
     }
   });
 
@@ -423,7 +424,7 @@
 
     /***
      * Sets the specified edit controller as the active edit controller (acquire edit lock).
-     * If another edit controller is already active, and exception will be thrown.
+     * If another edit controller is already active, and exception will be throw new Error(.
      * @method activate
      * @param editController {EditController} edit controller acquiring the lock
      */
@@ -432,26 +433,26 @@
         return;
       }
       if (activeEditController !== null) {
-        throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController";
+        throw new Error("SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController");
       }
       if (!editController.commitCurrentEdit) {
-        throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()";
+        throw new Error("SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()");
       }
       if (!editController.cancelCurrentEdit) {
-        throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()";
+        throw new Error("SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()");
       }
       activeEditController = editController;
     };
 
     /***
      * Unsets the specified edit controller as the active edit controller (release edit lock).
-     * If the specified edit controller is not the active one, an exception will be thrown.
+     * If the specified edit controller is not the active one, an exception will be throw new Error(.
      * @method deactivate
      * @param editController {EditController} edit controller releasing the lock
      */
     this.deactivate = function (editController) {
       if (activeEditController !== editController) {
-        throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one";
+        throw new Error("SlickGrid.EditorLock.deactivate: specified editController is not the currently active one");
       }
       activeEditController = null;
     };
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.dataview.js b/web/pgadmin/static/vendor/slickgrid/slick.dataview.js
index f62df669..b004b800 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.dataview.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.dataview.js
@@ -107,7 +107,7 @@
       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";
+          throw new Error("Each data element must implement a unique 'id' property");
         }
         idxById[id] = i;
       }
@@ -118,7 +118,7 @@
       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";
+          throw new Error("Each data element must implement a unique 'id' property");
         }
       }
     }
@@ -210,6 +210,15 @@
       }
     }
 
+    function getFilteredItems(){
+      return filteredItems;
+    }
+
+
+    function getFilter(){
+      return filter;
+    }
+    
     function setFilter(filterFn) {
       filter = filterFn;
       if (options.inlineFilters) {
@@ -330,7 +339,7 @@
 
     function updateItem(id, item) {
       if (idxById[id] === undefined || id !== item[idProperty]) {
-        throw "Invalid or non-matching id";
+        throw new Error("Invalid or non-matching id");
       }
       items[idxById[id]] = item;
       if (!updated) {
@@ -355,7 +364,7 @@
     function deleteItem(id) {
       var idx = idxById[id];
       if (idx === undefined) {
-        throw "Invalid id";
+        throw new Error("Invalid id");
       }
       delete idxById[id];
       items.splice(idx, 1);
@@ -516,7 +525,7 @@
           group = groups[i];
           group.groups = extractGroups(group.rows, group);
         }
-      }
+      }      
 
       groups.sort(groupingInfos[level].comparer);
 
@@ -566,7 +575,7 @@
       level = level || 0;
       var gi = groupingInfos[level];
       var groupCollapsed = gi.collapsed;
-      var toggledGroups = toggledGroupsByLevel[level];
+      var toggledGroups = toggledGroupsByLevel[level];      
       var idx = groups.length, g;
       while (idx--) {
         g = groups[idx];
@@ -588,7 +597,7 @@
         g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
         g.title = gi.formatter ? gi.formatter(g) : g.value;
       }
-    }
+    } 
 
     function flattenGroupedRows(groups, level) {
       level = level || 0;
@@ -762,14 +771,17 @@
       // get the current page
       var paged;
       if (pagesize) {
-        if (filteredItems.length < pagenum * pagesize) {
-          pagenum = Math.floor(filteredItems.length / pagesize);
+        if (filteredItems.length <= pagenum * pagesize) {
+          if (filteredItems.length === 0) {
+            pagenum = 0;
+          } else {
+            pagenum = Math.floor((filteredItems.length - 1) / pagesize);
+          }
         }
         paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
       } else {
         paged = filteredItems;
       }
-
       return {totalRows: filteredItems.length, rows: paged};
     }
 
@@ -916,7 +928,7 @@
           inHandler = true;
           var selectedRows = self.mapIdsToRows(selectedRowIds);
           if (!preserveHidden) {
-            setSelectedRowIds(self.mapRowsToIds(selectedRows));
+            setSelectedRowIds(self.mapRowsToIds(selectedRows));       
           }
           grid.setSelectedRows(selectedRows);
           inHandler = false;
@@ -980,6 +992,10 @@
         if (key != args.key) { return; }
         if (args.hash) {
           storeCellCssStyles(args.hash);
+        } else {
+          grid.onCellCssStylesChanged.unsubscribe(styleChanged);
+          self.onRowsChanged.unsubscribe(update);
+          self.onRowCountChanged.unsubscribe(update);          
         }
       });
 
@@ -997,6 +1013,8 @@
       "getItems": getItems,
       "setItems": setItems,
       "setFilter": setFilter,
+      "getFilter": getFilter,
+      "getFilteredItems": getFilteredItems,
       "sort": sort,
       "fastSort": fastSort,
       "reSort": reSort,
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.editors.js b/web/pgadmin/static/vendor/slickgrid/slick.editors.js
index e852592c..33b4e266 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.editors.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.editors.js
@@ -11,7 +11,7 @@
       "Editors": {
         "Text": TextEditor,
         "Integer": IntegerEditor,
-		"Float": FloatEditor,
+        "Float": FloatEditor,
         "Date": DateEditor,
         "YesNoSelect": YesNoSelectEditor,
         "Checkbox": CheckboxEditor,
@@ -29,7 +29,7 @@
     this.init = function () {
       $input = $("<INPUT type=text class='editor-text' />")
           .appendTo(args.container)
-          .bind("keydown.nav", function (e) {
+          .on("keydown.nav", function (e) {
             if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
               e.stopImmediatePropagation();
             }
@@ -98,7 +98,7 @@
     this.init = function () {
       $input = $("<INPUT type=text class='editor-text' />");
 
-      $input.bind("keydown.nav", function (e) {
+      $input.on("keydown.nav", function (e) {
         if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
           e.stopImmediatePropagation();
         }
@@ -167,7 +167,7 @@
     this.init = function () {
       $input = $("<INPUT type=text class='editor-text' />");
 
-      $input.bind("keydown.nav", function (e) {
+      $input.on("keydown.nav", function (e) {
         if (e.keyCode === $.ui.keyCode.LEFT || e.keyCode === $.ui.keyCode.RIGHT) {
           e.stopImmediatePropagation();
         }
@@ -185,24 +185,24 @@
       $input.focus();
     };
 
-	function getDecimalPlaces() {
-		// returns the number of fixed decimal places or null
-		var rtn = args.column.editorFixedDecimalPlaces;
-		if (typeof rtn == 'undefined') {
-			rtn = FloatEditor.DefaultDecimalPlaces;
-		}
-		return (!rtn && rtn!==0 ? null : rtn);
-	}
+    function getDecimalPlaces() {
+        // returns the number of fixed decimal places or null
+        var rtn = args.column.editorFixedDecimalPlaces;
+        if (typeof rtn == 'undefined') {
+            rtn = FloatEditor.DefaultDecimalPlaces;
+        }
+        return (!rtn && rtn!==0 ? null : rtn);
+    }
 
     this.loadValue = function (item) {
       defaultValue = item[args.column.field];
 
-	  var decPlaces = getDecimalPlaces();
-	  if (decPlaces !== null
-	  && (defaultValue || defaultValue===0)
-	  && defaultValue.toFixed) {
-		defaultValue = defaultValue.toFixed(decPlaces);
-	  }
+      var decPlaces = getDecimalPlaces();
+      if (decPlaces !== null
+      && (defaultValue || defaultValue===0)
+      && defaultValue.toFixed) {
+        defaultValue = defaultValue.toFixed(decPlaces);
+      }
 
       $input.val(defaultValue);
       $input[0].defaultValue = defaultValue;
@@ -210,14 +210,14 @@
     };
 
     this.serializeValue = function () {
-	  var rtn = parseFloat($input.val()) || 0;
+      var rtn = parseFloat($input.val()) || 0;
 
-	  var decPlaces = getDecimalPlaces();
-	  if (decPlaces !== null
-	  && (rtn || rtn===0)
-	  && rtn.toFixed) {
-		rtn = parseFloat(rtn.toFixed(decPlaces));
-	  }
+      var decPlaces = getDecimalPlaces();
+      if (decPlaces !== null
+      && (rtn || rtn===0)
+      && rtn.toFixed) {
+        rtn = parseFloat(rtn.toFixed(decPlaces));
+      }
 
       return rtn;
     };
@@ -269,8 +269,7 @@
       $input.datepicker({
         showOn: "button",
         buttonImageOnly: true,
-        buttonImage: "../images/calendar.gif",
-        beforeShow: function () {
+         beforeShow: function () {
           calendarOpen = true
         },
         onClose: function () {
@@ -422,6 +421,10 @@
       }
     };
 
+    this.preClick = function () {
+        $select.prop('checked', !$select.prop('checked'));
+    };
+
     this.serializeValue = function () {
       return $select.prop('checked');
     };
@@ -470,7 +473,7 @@
         }
       });
 
-      $picker.find(".editor-percentcomplete-buttons button").bind("click", function (e) {
+      $picker.find(".editor-percentcomplete-buttons button").on("click", function (e) {
         $input.val($(this).attr("val"));
         $picker.find(".editor-percentcomplete-slider").slider("value", $(this).attr("val"));
       })
@@ -535,15 +538,15 @@
       $wrapper = $("<DIV style='z-index:10000;position:absolute;background:white;padding:5px;border:3px solid gray; -moz-border-radius:10px; border-radius:10px;'/>")
           .appendTo($container);
 
-      $input = $("<TEXTAREA hidefocus rows=5 style='backround:white;width:250px;height:80px;border:0;outline:0'>")
+      $input = $("<TEXTAREA hidefocus rows=5 style='background:white;width:250px;height:80px;border:0;outline:0'>")
           .appendTo($wrapper);
 
       $("<DIV style='text-align:right'><BUTTON>Save</BUTTON><BUTTON>Cancel</BUTTON></DIV>")
           .appendTo($wrapper);
 
-      $wrapper.find("button:first").bind("click", this.save);
-      $wrapper.find("button:last").bind("click", this.cancel);
-      $input.bind("keydown", this.handleKeyDown);
+      $wrapper.find("button:first").on("click", this.save);
+      $wrapper.find("button:last").on("click", this.cancel);
+      $input.on("keydown", this.handleKeyDown);
 
       scope.position(args.position);
       $input.focus().select();
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.formatters.js b/web/pgadmin/static/vendor/slickgrid/slick.formatters.js
index 302b6f31..62bb8870 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.formatters.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.formatters.js
@@ -1,9 +1,9 @@
 /***
  * Contains basic SlickGrid formatters.
- *
+ * 
  * NOTE:  These are merely examples.  You will most likely need to implement something more
  *        robust/extensible/localizable/etc. for your use!
- *
+ * 
  * @module Formatters
  * @namespace Slick
  */
@@ -16,7 +16,9 @@
         "PercentComplete": PercentCompleteFormatter,
         "PercentCompleteBar": PercentCompleteBarFormatter,
         "YesNo": YesNoFormatter,
-        "Checkmark": CheckmarkFormatter
+        "Checkmark": CheckmarkFormatter,
+        "Checkbox": CheckboxFormatter
+
       }
     }
   });
@@ -53,6 +55,10 @@
     return value ? "Yes" : "No";
   }
 
+  function CheckboxFormatter(row, cell, value, columnDef, dataContext) {
+    return '<img class="slick-edit-preclick" src="../images/' + (value ? "CheckboxY" : "CheckboxN") + '.png">';
+  }
+
   function CheckmarkFormatter(row, cell, value, columnDef, dataContext) {
     return value ? "<img src='../images/tick.png'>" : "";
   }
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.grid.css b/web/pgadmin/static/vendor/slickgrid/slick.grid.css
index a628c029..ce085eff 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.grid.css
+++ b/web/pgadmin/static/vendor/slickgrid/slick.grid.css
@@ -5,13 +5,13 @@ No built-in (selected, editable, highlight, flashing, invalid, loading, :focus)
 classes should alter those!
 */
 
-.slick-header.ui-state-default, .slick-headerrow.ui-state-default {
+.slick-header.ui-state-default, .slick-headerrow.ui-state-default, .slick-footerrow.ui-state-default  {
   width: 100%;
   overflow: hidden;
   border-left: 0px !important;
 }
 
-.slick-header-columns, .slick-headerrow-columns {
+.slick-header-columns, .slick-headerrow-columns, .slick-footerrow-columns {
   position: relative;
   white-space: nowrap;
   cursor: default;
@@ -21,7 +21,7 @@ classes should alter those!
 .slick-header-column.ui-state-default {
   position: relative;
   display: inline-block;
-  /*box-sizing: content-box !important; */
+ /*box-sizing: content-box !important; use this for Firefox! */ 
   overflow: hidden;
   -o-text-overflow: ellipsis;
   text-overflow: ellipsis;
@@ -36,7 +36,7 @@ classes should alter those!
   float: left;
 }
 
-.slick-headerrow-column.ui-state-default {
+.slick-headerrow-column.ui-state-default, .slick-footerrow-column.ui-state-default {
   padding: 4px;
 }
 
@@ -53,6 +53,21 @@ classes should alter those!
   float: left;
 }
 
+.slick-sort-indicator-numbered {
+  display: inline-block;
+  width: 8px;
+  height: 5px;
+  margin-left: 4px;
+  margin-top: 0;
+  padding-left: 1px;
+  line-height: 20px;
+  float: left;
+  font-family: Arial;
+  font-style: normal;
+  font-weight: bold;
+  color: #6190CD;
+}
+
 .slick-sort-indicator-desc {
   background: url(images/sort-desc.gif);
 }
@@ -81,13 +96,13 @@ classes should alter those!
   outline: 0;
 }
 
-.sr.ui-widget-content, .sr.ui-state-active {
+.slick-row.ui-widget-content, .slick-row.ui-state-active {
   position: absolute;
   border: 0px;
   width: 100%;
 }
 
-.sc, .slick-headerrow-column {
+.slick-cell, .slick-headerrow-column , .slick-footerrow-column{
   position: absolute;
   border: 1px solid transparent;
   border-right: 1px dotted silver;
@@ -102,7 +117,13 @@ classes should alter those!
   white-space: nowrap;
   cursor: default;
 }
-
+.slick-cell, .slick-headerrow-column{
+  border-bottom-color: silver;
+}
+.slick-footerrow-column {
+  border-top-color: silver;
+}
+ 
 .slick-group {
 }
 
@@ -110,7 +131,7 @@ classes should alter those!
   display: inline-block;
 }
 
-.sc.highlighted {
+.slick-cell.highlighted {
   background: lightskyblue;
   background: rgba(0, 0, 255, 0.2);
   -webkit-transition: all 0.5s;
@@ -119,11 +140,11 @@ classes should alter those!
   transition: all 0.5s;
 }
 
-.sc.flashing {
+.slick-cell.flashing {
   border: 1px solid red !important;
 }
 
-.sc.editable {
+.slick-cell.editable {
   z-index: 11;
   overflow: visible;
   background: white;
@@ -131,7 +152,7 @@ classes should alter those!
   border-style: solid;
 }
 
-.sc:focus {
+.slick-cell:focus {
   outline: none;
 }
 
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.grid.js b/web/pgadmin/static/vendor/slickgrid/slick.grid.js
index 29d07e4f..8ebfd68b 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.grid.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.grid.js
@@ -1,3607 +1,3793 @@
-/**
- * @license
- * (c) 2009-2013 Michael Leibman
- * michael{dot}leibman{at}gmail{dot}com
- * http://github.com/mleibman/slickgrid
- *
- * Distributed under MIT license.
- * All rights reserved.
- *
- * SlickGrid v2.2
- *
- * NOTES:
- *     Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
- *     This increases the speed dramatically, but can only be done safely because there are no event handlers
- *     or data associated with any cell/row DOM nodes.  Cell editors must make sure they implement .destroy()
- *     and do proper cleanup.
- */
-
-// make sure required JavaScript modules are loaded
-if (typeof jQuery === "undefined") {
-  throw "SlickGrid requires jquery module to be loaded";
-}
-if (!jQuery.fn.drag) {
-  throw "SlickGrid requires jquery.event.drag module to be loaded";
-}
-if (typeof Slick === "undefined") {
-  throw "slick.core.js not loaded";
-}
-
-
-(function ($) {
-  // Slick.Grid
-  $.extend(true, window, {
-    Slick: {
-      Grid: SlickGrid
-    }
-  });
-
-  // shared across all grids on the page
-  var scrollbarDimensions;
-  var maxSupportedCssHeight;  // browser's breaking point
-
-  //////////////////////////////////////////////////////////////////////////////////////////////
-  // SlickGrid class implementation (available as Slick.Grid)
-
-  /**
-   * Creates a new instance of the grid.
-   * @class SlickGrid
-   * @constructor
-   * @param {Node}              container   Container node to create the grid in.
-   * @param {Array,Object}      data        An array of objects for databinding.
-   * @param {Array}             columns     An array of column definitions.
-   * @param {Object}            options     Grid options.
-   **/
-  function SlickGrid(container, data, columns, options) {
-    // settings
-    var defaults = {
-      explicitInitialization: false,
-      rowHeight: 25,
-      defaultColumnWidth: 80,
-      enableAddRow: false,
-      leaveSpaceForNewRows: false,
-      editable: false,
-      autoEdit: true,
-      enableCellNavigation: true,
-      enableColumnReorder: true,
-      asyncEditorLoading: false,
-      asyncEditorLoadDelay: 100,
-      forceFitColumns: false,
-      enableAsyncPostRender: false,
-      asyncPostRenderDelay: 50,
-      enableAsyncPostRenderCleanup: false,
-      asyncPostRenderCleanupDelay: 40,
-      autoHeight: false,
-      editorLock: Slick.GlobalEditorLock,
-      showHeaderRow: false,
-      headerRowHeight: 25,
-      showTopPanel: false,
-      topPanelHeight: 25,
-      formatterFactory: null,
-      editorFactory: null,
-      cellFlashingCssClass: "flashing",
-      selectedCellCssClass: "selected",
-      multiSelect: true,
-      enableTextSelectionOnCells: false,
-      dataItemColumnValueExtractor: null,
-      fullWidthRows: false,
-      multiColumnSort: false,
-      defaultFormatter: defaultFormatter,
-      forceSyncScrolling: false,
-      addNewRowCssClass: "new-row"
-    };
-
-    var columnDefaults = {
-      name: "",
-      resizable: true,
-      sortable: false,
-      minWidth: 30,
-      rerenderOnResize: false,
-      headerCssClass: null,
-      defaultSortAsc: true,
-      focusable: true,
-      selectable: true
-    };
-
-    // scroller
-    var th;   // virtual height
-    var h;    // real scrollable height
-    var ph;   // page height
-    var n;    // number of pages
-    var cj;   // "jumpiness" coefficient
-
-    var page = 0;       // current page
-    var offset = 0;     // current page offset
-    var vScrollDir = 1;
-
-    // private
-    var initialized = false;
-    var $container;
-    var uid = "slickgrid_" + Math.round(1000000 * Math.random());
-    var self = this;
-    var $focusSink, $focusSink2;
-    var $headerScroller;
-    var $headers;
-    var $headerRow, $headerRowScroller, $headerRowSpacer;
-    var $topPanelScroller;
-    var $topPanel;
-    var $viewport;
-    var $canvas;
-    var $style;
-    var $boundAncestors;
-    var stylesheet, columnCssRulesL, columnCssRulesR;
-    var viewportH, viewportW;
-    var canvasWidth;
-    var viewportHasHScroll, viewportHasVScroll;
-    var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding
-        cellWidthDiff = 0, cellHeightDiff = 0, jQueryNewWidthBehaviour = false;
-    var absoluteColumnMinWidth;
-
-    var tabbingDirection = 1;
-    var activePosX;
-    var activeRow, activeCell;
-    var activeCellNode = null;
-    var currentEditor = null;
-    var serializedEditorValue;
-    var editController;
-
-    var rowsCache = {};
-    var renderedRows = 0;
-    var numVisibleRows;
-    var prevScrollTop = 0;
-    var scrollTop = 0;
-    var lastRenderedScrollTop = 0;
-    var lastRenderedScrollLeft = 0;
-    var prevScrollLeft = 0;
-    var scrollLeft = 0;
-
-    var selectionModel;
-    var selectedRows = [];
-
-    var plugins = [];
-    var cellCssClasses = {};
-
-    var columnsById = {};
-    var sortColumns = [];
-    var columnPosLeft = [];
-    var columnPosRight = [];
-
-
-    // async call handles
-    var h_editorLoader = null;
-    var h_render = null;
-    var h_postrender = null;
-    var h_postrenderCleanup = null;
-    var postProcessedRows = {};
-    var postProcessToRow = null;
-    var postProcessFromRow = null;
-    var postProcessedCleanupQueue = [];
-    var postProcessgroupId = 0;
-
-    // perf counters
-    var counter_rows_rendered = 0;
-    var counter_rows_removed = 0;
-
-    // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac.
-    // See http://crbug.com/312427.
-    var rowNodeFromLastMouseWheelEvent;  // this node must not be deleted while inertial scrolling
-    var zombieRowNodeFromLastMouseWheelEvent;  // node that was hidden instead of getting deleted
-    var zombieRowCacheFromLastMouseWheelEvent;  // row cache for above node
-    var zombieRowPostProcessedFromLastMouseWheelEvent;  // post processing references for above node
-
-    // store css attributes if display:none is active in container or parent
-    var cssShow = { position: 'absolute', visibility: 'hidden', display: 'block' };
-    var $hiddenParents;
-    var oldProps = [];
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Initialization
-
-    function init() {
-      $container = $(container);
-      if ($container.length < 1) {
-        throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
-      }
-
-      cacheCssForHiddenInit();
-
-      // calculate these only once and share between grid instances
-      maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
-      scrollbarDimensions = scrollbarDimensions || measureScrollbar();
-
-      options = $.extend({}, defaults, options);
-      validateAndEnforceOptions();
-      columnDefaults.width = options.defaultColumnWidth;
-
-      columnsById = {};
-      for (var i = 0; i < columns.length; i++) {
-        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
-        columnsById[m.id] = i;
-        if (m.minWidth && m.width < m.minWidth) {
-          m.width = m.minWidth;
-        }
-        if (m.maxWidth && m.width > m.maxWidth) {
-          m.width = m.maxWidth;
-        }
-      }
-
-      // validate loaded JavaScript modules against requested options
-      if (options.enableColumnReorder && !$.fn.sortable) {
-        throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded");
-      }
-
-      editController = {
-        "commitCurrentEdit": commitCurrentEdit,
-        "cancelCurrentEdit": cancelCurrentEdit
-      };
-
-      $container
-          .empty()
-          .css("overflow", "hidden")
-          .css("outline", 0)
-          .addClass(uid)
-          .addClass("ui-widget");
-
-      // set up a positioning container if needed
-      if (!/relative|absolute|fixed/.test($container.css("position"))) {
-        $container.css("position", "relative");
-      }
-
-      $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
-
-      $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
-      $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
-      $headers.width(getHeadersWidth());
-
-      $headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
-      $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
-      $headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
-          .css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
-          .appendTo($headerRowScroller);
-
-      $topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
-      $topPanel = $("<div class='slick-top-panel' style='width:10000px' />").appendTo($topPanelScroller);
-
-      if (!options.showTopPanel) {
-        $topPanelScroller.hide();
-      }
-
-      if (!options.showHeaderRow) {
-        $headerRowScroller.hide();
-      }
-
-      $viewport = $("<div class='slick-viewport' style='width:100%;overflow:auto;outline:0;position:relative;;'>").appendTo($container);
-      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
-
-      $canvas = $("<div class='grid-canvas' />").appendTo($viewport);
-
-      $focusSink2 = $focusSink.clone().appendTo($container);
-
-      if (!options.explicitInitialization) {
-        finishInitialization();
-      }
-    }
-
-    function finishInitialization() {
-      if (!initialized) {
-        initialized = true;
-
-        viewportW = parseFloat($.css($container[0], "width", true));
-
-        // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?)
-        // calculate the diff so we can set consistent sizes
-        measureCellPaddingAndBorder();
-
-        // for usability reasons, all text selection in SlickGrid is disabled
-        // with the exception of input and textarea elements (selection must
-        // be enabled there so that editors work as expected); note that
-        // selection in grid cells (grid body) is already unavailable in
-        // all browsers except IE
-        disableSelection($headers); // disable all text selection in header (including input and textarea)
-
-        if (!options.enableTextSelectionOnCells) {
-          // disable text selection in grid cells except in input and textarea elements
-          // (this is IE-specific, because selectstart event will only fire in IE)
-          $viewport.bind("selectstart.ui", function (event) {
-            return $(event.target).is("input,textarea");
-          });
-        }
-
-        updateColumnCaches();
-        createColumnHeaders();
-        setupColumnSort();
-        createCssRules();
-        resizeCanvas();
-        bindAncestorScrollEvents();
-
-        $container
-            .bind("resize.slickgrid", resizeCanvas);
-        $viewport
-            //.bind("click", handleClick)
-            .bind("scroll", handleScroll);
-        $headerScroller
-            .bind("contextmenu", handleHeaderContextMenu)
-            .bind("click", handleHeaderClick)
-            .delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter)
-            .delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave);
-        $headerRowScroller
-            .bind("scroll", handleHeaderRowScroll);
-        $focusSink.add($focusSink2)
-            .bind("keydown", handleKeyDown);
-        $canvas
-            .bind("keydown", handleKeyDown)
-            .bind("click", handleClick)
-            .bind("dblclick", handleDblClick)
-            .bind("contextmenu", handleContextMenu)
-            .bind("draginit", handleDragInit)
-            .bind("dragstart", {distance: 3}, handleDragStart)
-            .bind("drag", handleDrag)
-            .bind("dragend", handleDragEnd)
-            .delegate(".sc", "mouseenter", handleMouseEnter)
-            .delegate(".sc", "mouseleave", handleMouseLeave);
-
-        // Work around http://crbug.com/312427.
-        if (navigator.userAgent.toLowerCase().match(/webkit/) &&
-            navigator.userAgent.toLowerCase().match(/macintosh/)) {
-          $canvas.bind("mousewheel", handleMouseWheel);
-        }
-        restoreCssFromHiddenInit();
-      }
-    }
-
-    function cacheCssForHiddenInit() {
-      // handle display:none on container or container parents
-      $hiddenParents = $container.parents().andSelf().not(':visible');
-      $hiddenParents.each(function() {
-        var old = {};
-        for ( var name in cssShow ) {
-          old[ name ] = this.style[ name ];
-          this.style[ name ] = cssShow[ name ];
-        }
-        oldProps.push(old);
-      });
-    }
-
-    function restoreCssFromHiddenInit() {
-      // finish handle display:none on container or container parents
-      // - put values back the way they were
-      $hiddenParents.each(function(i) {
-        var old = oldProps[i];
-        for ( var name in cssShow ) {
-          this.style[ name ] = old[ name ];
-        }
-      });
-    }
-
-    function registerPlugin(plugin) {
-      plugins.unshift(plugin);
-      plugin.init(self);
-    }
-
-    function unregisterPlugin(plugin) {
-      for (var i = plugins.length; i >= 0; i--) {
-        if (plugins[i] === plugin) {
-          if (plugins[i].destroy) {
-            plugins[i].destroy();
-          }
-          plugins.splice(i, 1);
-          break;
-        }
-      }
-    }
-
-    function setSelectionModel(model) {
-      if (selectionModel) {
-        selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged);
-        if (selectionModel.destroy) {
-          selectionModel.destroy();
-        }
-      }
-
-      selectionModel = model;
-      if (selectionModel) {
-        selectionModel.init(self);
-        selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged);
-      }
-    }
-
-    function getSelectionModel() {
-      return selectionModel;
-    }
-
-    function getCanvasNode() {
-      return $canvas[0];
-    }
-
-    function measureScrollbar() {
-      var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
-      var dim = {
-        width: $c.width() - $c[0].clientWidth,
-        height: $c.height() - $c[0].clientHeight
-      };
-      $c.remove();
-      return dim;
-    }
-
-    function getHeadersWidth() {
-      var headersWidth = 0;
-      for (var i = 0, ii = columns.length; i < ii; i++) {
-        var width = columns[i].width;
-        headersWidth += width;
-      }
-      headersWidth += scrollbarDimensions.width;
-      return Math.max(headersWidth, viewportW) + 1000;
-    }
-
-    function getCanvasWidth() {
-      var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
-      var rowWidth = 0;
-      var i = columns.length;
-      while (i--) {
-        rowWidth += columns[i].width;
-      }
-      return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
-    }
-
-    function updateCanvasWidth(forceColumnWidthsUpdate) {
-      var oldCanvasWidth = canvasWidth;
-      canvasWidth = getCanvasWidth();
-
-      if (canvasWidth != oldCanvasWidth) {
-        $canvas.width(canvasWidth);
-        $headerRow.width(canvasWidth);
-        $headers.width(getHeadersWidth());
-        viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
-      }
-
-      $headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0));
-
-      if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
-        applyColumnWidths();
-      }
-    }
-
-    function disableSelection($target) {
-      if ($target && $target.jquery) {
-        $target
-            .attr("unselectable", "on")
-            .css("MozUserSelect", "none")
-            .bind("selectstart.ui", function () {
-              return false;
-            }); // from jquery:ui.core.js 1.7.2
-      }
-    }
-
-    function getMaxSupportedCssHeight() {
-      var supportedHeight = 1000000;
-      // FF reports the height back but still renders blank after ~6M px
-      var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000;
-      var div = $("<div style='display:none' />").appendTo(document.body);
-
-      while (true) {
-        var test = supportedHeight * 2;
-        div.css("height", test);
-        if (test > testUpTo || div.height() !== test) {
-          break;
-        } else {
-          supportedHeight = test;
-        }
-      }
-
-      div.remove();
-      return supportedHeight;
-    }
-
-    // TODO:  this is static.  need to handle page mutation.
-    function bindAncestorScrollEvents() {
-      var elem = $canvas[0];
-      while ((elem = elem.parentNode) != document.body && elem != null) {
-        // bind to scroll containers only
-        if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) {
-          var $elem = $(elem);
-          if (!$boundAncestors) {
-            $boundAncestors = $elem;
-          } else {
-            $boundAncestors = $boundAncestors.add($elem);
-          }
-          $elem.bind("scroll." + uid, handleActiveCellPositionChange);
-        }
-      }
-    }
-
-    function unbindAncestorScrollEvents() {
-      if (!$boundAncestors) {
-        return;
-      }
-      $boundAncestors.unbind("scroll." + uid);
-      $boundAncestors = null;
-    }
-
-    function updateColumnHeader(columnId, title, toolTip) {
-      if (!initialized) { return; }
-      var idx = getColumnIndex(columnId);
-      if (idx == null) {
-        return;
-      }
-
-      var columnDef = columns[idx];
-      var $header = $headers.children().eq(idx);
-      if ($header) {
-        if (title !== undefined) {
-          columns[idx].name = title;
-        }
-        if (toolTip !== undefined) {
-          columns[idx].toolTip = toolTip;
-        }
-
-        trigger(self.onBeforeHeaderCellDestroy, {
-          "node": $header[0],
-          "column": columnDef,
-          "grid": self
-        });
-
-        $header
-            .attr("title", toolTip || "")
-            .children().eq(0).html(title);
-
-        trigger(self.onHeaderCellRendered, {
-          "node": $header[0],
-          "column": columnDef,
-          "grid": self
-        });
-      }
-    }
-
-    function getHeaderRow() {
-      return $headerRow[0];
-    }
-
-    function getHeaderRowColumn(columnId) {
-      var idx = getColumnIndex(columnId);
-      var $header = $headerRow.children().eq(idx);
-      return $header && $header[0];
-    }
-
-    function createColumnHeaders() {
-      function onMouseEnter() {
-        $(this).addClass("ui-state-hover");
-      }
-
-      function onMouseLeave() {
-        $(this).removeClass("ui-state-hover");
-      }
-
-      $headers.find(".slick-header-column")
-        .each(function() {
-          var columnDef = $(this).data("column");
-          if (columnDef) {
-            trigger(self.onBeforeHeaderCellDestroy, {
-              "node": this,
-              "column": columnDef,
-              "grid": self
-            });
-          }
-        });
-      $headers.empty();
-      $headers.width(getHeadersWidth());
-
-      $headerRow.find(".slick-headerrow-column")
-        .each(function() {
-          var columnDef = $(this).data("column");
-          if (columnDef) {
-            trigger(self.onBeforeHeaderRowCellDestroy, {
-              "node": this,
-              "column": columnDef,
-              "grid": self
-            });
-          }
-        });
-      $headerRow.empty();
-
-      for (var i = 0; i < columns.length; i++) {
-        var m = columns[i];
-
-        var header = $("<div class='ui-state-default slick-header-column' />")
-            .html("<span class='slick-column-name'>" + m.name + "</span>")
-            .width(m.width - headerColumnWidthDiff)
-            .attr("id", "" + uid + m.id)
-            .attr("title", m.toolTip || "")
-            .data("column", m)
-            .addClass(m.headerCssClass || "")
-            .appendTo($headers);
-
-        if (options.enableColumnReorder || m.sortable) {
-          header
-            .on('mouseenter', onMouseEnter)
-            .on('mouseleave', onMouseLeave);
-        }
-
-        if (m.sortable) {
-          header.addClass("slick-header-sortable");
-          header.append("<span class='slick-sort-indicator' />");
-        }
-
-        trigger(self.onHeaderCellRendered, {
-          "node": header[0],
-          "column": m,
-          "grid": self
-        });
-
-        if (options.showHeaderRow) {
-          var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>")
-              .data("column", m)
-              .appendTo($headerRow);
-
-          trigger(self.onHeaderRowCellRendered, {
-            "node": headerRowCell[0],
-            "column": m,
-            "grid": self
-          });
-        }
-      }
-
-      setSortColumns(sortColumns);
-      setupColumnResize();
-      if (options.enableColumnReorder) {
-        setupColumnReorder();
-      }
-    }
-
-    function setupColumnSort() {
-      $headers.click(function (e) {
-        // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328)
-        e.metaKey = e.metaKey || e.ctrlKey;
-
-        if ($(e.target).hasClass("slick-resizable-handle")) {
-          return;
-        }
-
-        var $col = $(e.target).closest(".slick-header-column");
-        if (!$col.length) {
-          return;
-        }
-
-        var column = $col.data("column");
-        if (column.sortable) {
-          if (!getEditorLock().commitCurrentEdit()) {
-            return;
-          }
-
-          var sortOpts = null;
-          var i = 0;
-          for (; i < sortColumns.length; i++) {
-            if (sortColumns[i].columnId == column.id) {
-              sortOpts = sortColumns[i];
-              sortOpts.sortAsc = !sortOpts.sortAsc;
-              break;
-            }
-          }
-
-          if (e.metaKey && options.multiColumnSort) {
-            if (sortOpts) {
-              sortColumns.splice(i, 1);
-            }
-          }
-          else {
-            if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
-              sortColumns = [];
-            }
-
-            if (!sortOpts) {
-              sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
-              sortColumns.push(sortOpts);
-            } else if (sortColumns.length == 0) {
-              sortColumns.push(sortOpts);
-            }
-          }
-
-          setSortColumns(sortColumns);
-
-          if (!options.multiColumnSort) {
-            trigger(self.onSort, {
-              multiColumnSort: false,
-              sortCol: column,
-              sortAsc: sortOpts.sortAsc,
-              grid: self}, e);
-          } else {
-            trigger(self.onSort, {
-              multiColumnSort: true,
-              sortCols: $.map(sortColumns, function(col) {
-                return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
-              }),
-              grid: self}, e);
-          }
-        }
-      });
-    }
-
-    function setupColumnReorder() {
-      $headers.filter(":ui-sortable").sortable("destroy");
-      $headers.sortable({
-        containment: "parent",
-        distance: 3,
-        axis: "x",
-        cursor: "default",
-        tolerance: "intersection",
-        helper: "clone",
-        placeholder: "slick-sortable-placeholder ui-state-default slick-header-column",
-        start: function (e, ui) {
-          ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff);
-          $(ui.helper).addClass("slick-header-column-active");
-        },
-        beforeStop: function (e, ui) {
-          $(ui.helper).removeClass("slick-header-column-active");
-        },
-        stop: function (e) {
-          if (!getEditorLock().commitCurrentEdit()) {
-            $(this).sortable("cancel");
-            return;
-          }
-
-          var reorderedIds = $headers.sortable("toArray");
-          var reorderedColumns = [];
-          for (var i = 0; i < reorderedIds.length; i++) {
-            reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]);
-          }
-          setColumns(reorderedColumns);
-
-          trigger(self.onColumnsReordered, {grid: self});
-          e.stopPropagation();
-          setupColumnResize();
-        }
-      });
-    }
-
-    function setupColumnResize() {
-      var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable;
-      columnElements = $headers.children();
-      columnElements.find(".slick-resizable-handle").remove();
-      columnElements.each(function (i, e) {
-        if (columns[i].resizable) {
-          if (firstResizable === undefined) {
-            firstResizable = i;
-          }
-          lastResizable = i;
-        }
-      });
-      if (firstResizable === undefined) {
-        return;
-      }
-      columnElements.each(function (i, e) {
-        if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
-          return;
-        }
-        $col = $(e);
-        $("<div class='slick-resizable-handle' />")
-            .appendTo(e)
-            .bind("dragstart", function (e, dd) {
-              if (!getEditorLock().commitCurrentEdit()) {
-                return false;
-              }
-              pageX = e.pageX;
-              $(this).parent().addClass("slick-header-column-active");
-              var shrinkLeewayOnRight = null, stretchLeewayOnRight = null;
-              // lock each column's width option to current width
-              columnElements.each(function (i, e) {
-                columns[i].previousWidth = $(e).outerWidth();
-              });
-              if (options.forceFitColumns) {
-                shrinkLeewayOnRight = 0;
-                stretchLeewayOnRight = 0;
-                // colums on right affect maxPageX/minPageX
-                for (j = i + 1; j < columnElements.length; j++) {
-                  c = columns[j];
-                  if (c.resizable) {
-                    if (stretchLeewayOnRight !== null) {
-                      if (c.maxWidth) {
-                        stretchLeewayOnRight += c.maxWidth - c.previousWidth;
-                      } else {
-                        stretchLeewayOnRight = null;
-                      }
-                    }
-                    shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
-                  }
-                }
-              }
-              var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
-              for (j = 0; j <= i; j++) {
-                // columns on left only affect minPageX
-                c = columns[j];
-                if (c.resizable) {
-                  if (stretchLeewayOnLeft !== null) {
-                    if (c.maxWidth) {
-                      stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
-                    } else {
-                      stretchLeewayOnLeft = null;
-                    }
-                  }
-                  shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
-                }
-              }
-              if (shrinkLeewayOnRight === null) {
-                shrinkLeewayOnRight = 100000;
-              }
-              if (shrinkLeewayOnLeft === null) {
-                shrinkLeewayOnLeft = 100000;
-              }
-              if (stretchLeewayOnRight === null) {
-                stretchLeewayOnRight = 100000;
-              }
-              if (stretchLeewayOnLeft === null) {
-                stretchLeewayOnLeft = 100000;
-              }
-              maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
-              minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
-            })
-            .bind("drag", function (e, dd) {
-              var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x;
-              if (d < 0) { // shrink column
-                x = d;
-                for (j = i; j >= 0; j--) {
-                  c = columns[j];
-                  if (c.resizable) {
-                    actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
-                    if (x && c.previousWidth + x < actualMinWidth) {
-                      x += c.previousWidth - actualMinWidth;
-                      c.width = actualMinWidth;
-                    } else {
-                      c.width = c.previousWidth + x;
-                      x = 0;
-                    }
-                  }
-                }
-
-                if (options.forceFitColumns) {
-                  x = -d;
-                  for (j = i + 1; j < columnElements.length; j++) {
-                    c = columns[j];
-                    if (c.resizable) {
-                      if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
-                        x -= c.maxWidth - c.previousWidth;
-                        c.width = c.maxWidth;
-                      } else {
-                        c.width = c.previousWidth + x;
-                        x = 0;
-                      }
-                    }
-                  }
-                }
-              } else { // stretch column
-                x = d;
-                for (j = i; j >= 0; j--) {
-                  c = columns[j];
-                  if (c.resizable) {
-                    if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
-                      x -= c.maxWidth - c.previousWidth;
-                      c.width = c.maxWidth;
-                    } else {
-                      c.width = c.previousWidth + x;
-                      x = 0;
-                    }
-                  }
-                }
-
-                if (options.forceFitColumns) {
-                  x = -d;
-                  for (j = i + 1; j < columnElements.length; j++) {
-                    c = columns[j];
-                    if (c.resizable) {
-                      actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
-                      if (x && c.previousWidth + x < actualMinWidth) {
-                        x += c.previousWidth - actualMinWidth;
-                        c.width = actualMinWidth;
-                      } else {
-                        c.width = c.previousWidth + x;
-                        x = 0;
-                      }
-                    }
-                  }
-                }
-              }
-              applyColumnHeaderWidths();
-              if (options.syncColumnCellResize) {
-                applyColumnWidths();
-              }
-            })
-            .bind("dragend", function (e, dd) {
-              var newWidth;
-              $(this).parent().removeClass("slick-header-column-active");
-              for (j = 0; j < columnElements.length; j++) {
-                c = columns[j];
-                newWidth = $(columnElements[j]).outerWidth();
-
-                if (c.previousWidth !== newWidth && c.rerenderOnResize) {
-                  invalidateAllRows();
-                }
-              }
-              updateCanvasWidth(true);
-              render();
-              trigger(self.onColumnsResized, {grid: self});
-            });
-      });
-    }
-
-    function getVBoxDelta($el) {
-      var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
-      var delta = 0;
-      $.each(p, function (n, val) {
-        delta += parseFloat($el.css(val)) || 0;
-      });
-      return delta;
-    }
-
-    function measureCellPaddingAndBorder() {
-      var el;
-      var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"];
-      var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
-
-	  // jquery prior to version 1.8 handles .width setter/getter as a direct css write/read
-	  // jquery 1.8 changed .width to read the true inner element width if box-sizing is set to border-box, and introduced a setter for .outerWidth
-	  // so for equivalent functionality, prior to 1.8 use .width, and after use .outerWidth
-	  var verArray = $.fn.jquery.split('.');
-	  jQueryNewWidthBehaviour = (verArray[0]==1 && verArray[1]>=8) ||  verArray[0] >=2;
-
-      el = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers);
-      headerColumnWidthDiff = headerColumnHeightDiff = 0;
-      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
-        $.each(h, function (n, val) {
-          headerColumnWidthDiff += parseFloat(el.css(val)) || 0;
-        });
-        $.each(v, function (n, val) {
-          headerColumnHeightDiff += parseFloat(el.css(val)) || 0;
-        });
-      }
-      el.remove();
-
-      var r = $("<div class='sr' />").appendTo($canvas);
-      el = $("<div class='sc' id='' style='visibility:hidden'>-</div>").appendTo(r);
-      cellWidthDiff = cellHeightDiff = 0;
-      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
-        $.each(h, function (n, val) {
-          cellWidthDiff += parseFloat(el.css(val)) || 0;
-        });
-        $.each(v, function (n, val) {
-          cellHeightDiff += parseFloat(el.css(val)) || 0;
-        });
-      }
-      r.remove();
-
-      absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff);
-    }
-
-    function createCssRules() {
-      $style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
-      var rowHeight = (options.rowHeight - cellHeightDiff);
-      var rules = [
-        "." + uid + " .slick-header-column { left: 1000px; }",
-        "." + uid + " .slick-top-panel { height:" + options.topPanelHeight + "px; }",
-        "." + uid + " .slick-headerrow-columns { height:" + options.headerRowHeight + "px; }",
-        "." + uid + " .sc { height:" + rowHeight + "px; }",
-        "." + uid + " .sr { height:" + options.rowHeight + "px; }"
-      ];
-
-      for (var i = 0; i < columns.length; i++) {
-        rules.push("." + uid + " .l" + i + " { }");
-        rules.push("." + uid + " .r" + i + " { }");
-      }
-
-      if ($style[0].styleSheet) { // IE
-        $style[0].styleSheet.cssText = rules.join(" ");
-      } else {
-        $style[0].appendChild(document.createTextNode(rules.join(" ")));
-      }
-    }
-
-    function getColumnCssRules(idx) {
-      if (!stylesheet) {
-        var sheets = document.styleSheets;
-        for (var i = 0; i < sheets.length; i++) {
-          if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) {
-            stylesheet = sheets[i];
-            break;
-          }
-        }
-
-        if (!stylesheet) {
-          throw new Error("Cannot find stylesheet.");
-        }
-
-        // find and cache column CSS rules
-        columnCssRulesL = [];
-        columnCssRulesR = [];
-        var cssRules = (stylesheet.cssRules || stylesheet.rules);
-        var matches, columnIdx;
-        for (var i = 0; i < cssRules.length; i++) {
-          var selector = cssRules[i].selectorText;
-          if (matches = /\.l\d+/.exec(selector)) {
-            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
-            columnCssRulesL[columnIdx] = cssRules[i];
-          } else if (matches = /\.r\d+/.exec(selector)) {
-            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
-            columnCssRulesR[columnIdx] = cssRules[i];
-          }
-        }
-      }
-
-      return {
-        "left": columnCssRulesL[idx],
-        "right": columnCssRulesR[idx]
-      };
-    }
-
-    function removeCssRules() {
-      $style.remove();
-      stylesheet = null;
-    }
-
-    function destroy() {
-      getEditorLock().cancelCurrentEdit();
-
-      trigger(self.onBeforeDestroy, {grid: self});
-
-      var i = plugins.length;
-      while(i--) {
-        unregisterPlugin(plugins[i]);
-      }
-
-      if (options.enableColumnReorder) {
-          $headers.filter(":ui-sortable").sortable("destroy");
-      }
-
-      unbindAncestorScrollEvents();
-      $container.unbind(".slickgrid");
-      removeCssRules();
-
-      $canvas.unbind("draginit dragstart dragend drag");
-      $container.empty().removeClass(uid);
-    }
-
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // General
-
-    function trigger(evt, args, e) {
-      e = e || new Slick.EventData();
-      args = args || {};
-      args.grid = self;
-      return evt.notify(args, e, self);
-    }
-
-    function getEditorLock() {
-      return options.editorLock;
-    }
-
-    function getEditController() {
-      return editController;
-    }
-
-    function getColumnIndex(id) {
-      return columnsById[id];
-    }
-
-    function autosizeColumns() {
-      var i, c,
-          widths = [],
-          shrinkLeeway = 0,
-          total = 0,
-          prevTotal,
-          availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
-
-      for (i = 0; i < columns.length; i++) {
-        c = columns[i];
-        widths.push(c.width);
-        total += c.width;
-        if (c.resizable) {
-          shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth);
-        }
-      }
-
-      // shrink
-      prevTotal = total;
-      while (total > availWidth && shrinkLeeway) {
-        var shrinkProportion = (total - availWidth) / shrinkLeeway;
-        for (i = 0; i < columns.length && total > availWidth; i++) {
-          c = columns[i];
-          var width = widths[i];
-          if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) {
-            continue;
-          }
-          var absMinWidth = Math.max(c.minWidth, absoluteColumnMinWidth);
-          var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1;
-          shrinkSize = Math.min(shrinkSize, width - absMinWidth);
-          total -= shrinkSize;
-          shrinkLeeway -= shrinkSize;
-          widths[i] -= shrinkSize;
-        }
-        if (prevTotal <= total) {  // avoid infinite loop
-          break;
-        }
-        prevTotal = total;
-      }
-
-      // grow
-      prevTotal = total;
-      while (total < availWidth) {
-        var growProportion = availWidth / total;
-        for (i = 0; i < columns.length && total < availWidth; i++) {
-          c = columns[i];
-          var currentWidth = widths[i];
-          var growSize;
-
-          if (!c.resizable || c.maxWidth <= currentWidth) {
-            growSize = 0;
-          } else {
-            growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1;
-          }
-          total += growSize;
-          widths[i] += (total <= availWidth ? growSize : 0);
-        }
-        if (prevTotal >= total) {  // avoid infinite loop
-          break;
-        }
-        prevTotal = total;
-      }
-
-      var reRender = false;
-      for (i = 0; i < columns.length; i++) {
-        if (columns[i].rerenderOnResize && columns[i].width != widths[i]) {
-          reRender = true;
-        }
-        columns[i].width = widths[i];
-      }
-
-      applyColumnHeaderWidths();
-      updateCanvasWidth(true);
-      if (reRender) {
-        invalidateAllRows();
-        render();
-      }
-    }
-
-    function applyColumnHeaderWidths() {
-      if (!initialized) { return; }
-      var h;
-      for (var i = 0, headers = $headers.children(), ii = headers.length; i < ii; i++) {
-        h = $(headers[i]);
-		if (jQueryNewWidthBehaviour) {
-			if (h.outerWidth() !== columns[i].width) {
-			  h.outerWidth(columns[i].width);
-			}
-		} else {
-			if (h.width() !== columns[i].width - headerColumnWidthDiff) {
-			  h.width(columns[i].width - headerColumnWidthDiff);
-			}
-		}
-      }
-
-      updateColumnCaches();
-    }
-
-    function applyColumnWidths() {
-      var x = 0, w, rule;
-      for (var i = 0; i < columns.length; i++) {
-        w = columns[i].width;
-
-        rule = getColumnCssRules(i);
-        rule.left.style.left = x + "px";
-        rule.right.style.right = (canvasWidth - x - w) + "px";
-
-        x += columns[i].width;
-      }
-    }
-
-    function setSortColumn(columnId, ascending) {
-      setSortColumns([{ columnId: columnId, sortAsc: ascending}]);
-    }
-
-    function setSortColumns(cols) {
-      sortColumns = cols;
-
-      var headerColumnEls = $headers.children();
-      headerColumnEls
-          .removeClass("slick-header-column-sorted")
-          .find(".slick-sort-indicator")
-              .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
-
-      $.each(sortColumns, function(i, col) {
-        if (col.sortAsc == null) {
-          col.sortAsc = true;
-        }
-        var columnIndex = getColumnIndex(col.columnId);
-        if (columnIndex != null) {
-          headerColumnEls.eq(columnIndex)
-              .addClass("slick-header-column-sorted")
-              .find(".slick-sort-indicator")
-                  .addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
-        }
-      });
-    }
-
-    function getSortColumns() {
-      return sortColumns;
-    }
-
-    function handleSelectedRangesChanged(e, ranges) {
-      selectedRows = [];
-      var hash = {};
-      for (var i = 0; i < ranges.length; i++) {
-        for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
-          if (!hash[j]) {  // prevent duplicates
-            selectedRows.push(j);
-            hash[j] = {};
-          }
-          for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) {
-            if (canCellBeSelected(j, k)) {
-              hash[j][columns[k].id] = options.selectedCellCssClass;
-            }
-          }
-        }
-      }
-
-      setCellCssStyles(options.selectedCellCssClass, hash);
-
-      trigger(self.onSelectedRowsChanged, {rows: getSelectedRows(), grid: self}, e);
-    }
-
-    function getColumns() {
-      return columns;
-    }
-
-    function updateColumnCaches() {
-      // Pre-calculate cell boundaries.
-      columnPosLeft = [];
-      columnPosRight = [];
-      var x = 0;
-      for (var i = 0, ii = columns.length; i < ii; i++) {
-        columnPosLeft[i] = x;
-        columnPosRight[i] = x + columns[i].width;
-        x += columns[i].width;
-      }
-    }
-
-    function setColumns(columnDefinitions) {
-      columns = columnDefinitions;
-
-      columnsById = {};
-      for (var i = 0; i < columns.length; i++) {
-        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
-        columnsById[m.id] = i;
-        if (m.minWidth && m.width < m.minWidth) {
-          m.width = m.minWidth;
-        }
-        if (m.maxWidth && m.width > m.maxWidth) {
-          m.width = m.maxWidth;
-        }
-      }
-
-      updateColumnCaches();
-
-      if (initialized) {
-        invalidateAllRows();
-        createColumnHeaders();
-        removeCssRules();
-        createCssRules();
-        resizeCanvas();
-        applyColumnWidths();
-        handleScroll();
-      }
-    }
-
-    function getOptions() {
-      return options;
-    }
-
-    function setOptions(args) {
-      if (!getEditorLock().commitCurrentEdit()) {
-        return;
-      }
-
-      makeActiveCellNormal();
-
-      if (options.enableAddRow !== args.enableAddRow) {
-        invalidateRow(getDataLength());
-      }
-
-      options = $.extend(options, args);
-      validateAndEnforceOptions();
-
-      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
-      render();
-    }
-
-    function validateAndEnforceOptions() {
-      if (options.autoHeight) {
-        options.leaveSpaceForNewRows = false;
-      }
-    }
-
-    function setData(newData, scrollToTop) {
-      data = newData;
-      invalidateAllRows();
-      updateRowCount();
-      if (scrollToTop) {
-        scrollTo(0);
-      }
-    }
-
-    function getData() {
-      return data;
-    }
-
-    function getDataLength() {
-      if (data.getLength) {
-        return data.getLength();
-      } else {
-        return data.length;
-      }
-    }
-
-    function getDataLengthIncludingAddNew() {
-      return getDataLength() + (options.enableAddRow ? 2 : 1);
-    }
-
-    function getDataItem(i) {
-      if (data.getItem) {
-        return data.getItem(i);
-      } else {
-        return data[i];
-      }
-    }
-
-    function getTopPanel() {
-      return $topPanel[0];
-    }
-
-    function setTopPanelVisibility(visible) {
-      if (options.showTopPanel != visible) {
-        options.showTopPanel = visible;
-        if (visible) {
-          $topPanelScroller.slideDown("fast", resizeCanvas);
-        } else {
-          $topPanelScroller.slideUp("fast", resizeCanvas);
-        }
-      }
-    }
-
-    function setHeaderRowVisibility(visible) {
-      if (options.showHeaderRow != visible) {
-        options.showHeaderRow = visible;
-        if (visible) {
-          $headerRowScroller.slideDown("fast", resizeCanvas);
-        } else {
-          $headerRowScroller.slideUp("fast", resizeCanvas);
-        }
-      }
-    }
-
-    function getContainerNode() {
-      return $container.get(0);
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Rendering / Scrolling
-
-    function getRowTop(row) {
-      return options.rowHeight * row - offset;
-    }
-
-    function getRowFromPosition(y) {
-      return Math.floor((y + offset) / options.rowHeight);
-    }
-
-    function scrollTo(y) {
-      y = Math.max(y, 0);
-      y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
-
-      var oldOffset = offset;
-
-      page = Math.min(n - 1, Math.floor(y / ph));
-      offset = Math.round(page * cj);
-      var newScrollTop = y - offset;
-
-      if (offset != oldOffset) {
-        var range = getVisibleRange(newScrollTop);
-        cleanupRows(range);
-        updateRowPositions();
-      }
-
-      if (prevScrollTop != newScrollTop) {
-        vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
-        $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
-
-        trigger(self.onViewportChanged, {grid: self});
-      }
-    }
-
-    function defaultFormatter(row, cell, value, columnDef, dataContext) {
-      if (value == null) {
-        return "";
-      } else {
-        return (value + "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
-      }
-    }
-
-    function getFormatter(row, column) {
-      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
-
-      // look up by id, then index
-      var columnOverrides = rowMetadata &&
-          rowMetadata.columns &&
-          (rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
-
-      return (columnOverrides && columnOverrides.formatter) ||
-          (rowMetadata && rowMetadata.formatter) ||
-          column.formatter ||
-          (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
-          options.defaultFormatter;
-    }
-
-    function getEditor(row, cell) {
-      var column = columns[cell];
-      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
-      var columnMetadata = rowMetadata && rowMetadata.columns;
-
-      if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
-        return columnMetadata[column.id].editor;
-      }
-      if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
-        return columnMetadata[cell].editor;
-      }
-
-      return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
-    }
-
-    function getDataItemValueForColumn(item, columnDef) {
-      if (options.dataItemColumnValueExtractor) {
-        return options.dataItemColumnValueExtractor(item, columnDef);
-      }
-      return item[columnDef.field];
-    }
-
-    function appendRowHtml(stringArray, row, range, dataLength) {
-      var d = getDataItem(row);
-      var dataLoading = row < dataLength && !d;
-      var diff = dataLength - row;
-      var rowCss = "sr" +
-          (dataLoading ? " loading" : "") +
-          (row === activeRow ? " active" : "") +
-          (((options.enableAddRow && diff < 0) || diff == 0) ? "" : (row % 2 == 1 ? " odd" : " even"));
-
-      if (!d && options.enableAddRow && diff == 0) {
-        rowCss += " " + options.addNewRowCssClass;
-      }
-
-      var metadata = data.getItemMetadata && data.getItemMetadata(row);
-
-      if (metadata && metadata.cssClasses) {
-        rowCss += " " + metadata.cssClasses;
-      }
-
-      stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
-
-      if (options.enableAddRow ? diff < 0 : diff == 0) {
-        stringArray.push("</div>");
-        return;
-      }
-
-      var colspan, m;
-      for (var i = 0, ii = columns.length; i < ii; i++) {
-        m = columns[i];
-        colspan = 1;
-        if (metadata && metadata.columns) {
-          var columnData = metadata.columns[m.id] || metadata.columns[i];
-          colspan = (columnData && columnData.colspan) || 1;
-          if (colspan === "*") {
-            colspan = ii - i;
-          }
-        }
-
-        // Do not render cells outside of the viewport.
-        if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
-          if (columnPosLeft[i] > range.rightPx) {
-            // All columns to the right are outside the range.
-            break;
-          }
-
-          appendCellHtml(stringArray, row, i, colspan, d);
-        }
-
-        if (colspan > 1) {
-          i += (colspan - 1);
-        }
-      }
-
-      stringArray.push("</div>");
-    }
-
-    function appendCellHtml(stringArray, row, cell, colspan, item) {
-      // stringArray: stringBuilder containing the HTML parts
-      // row, cell: row and column index
-      // colspan: HTML colspan
-      // item: grid data for row
-
-      var m = columns[cell];
-      var cellCss = "sc l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
-          (m.cssClass ? " " + m.cssClass : "");
-      if (row === activeRow && cell === activeCell) {
-        cellCss += (" active");
-      }
-
-      if (!item) {
-          var diff = getDataLength() - row;
-          if (options.enableAddRow ? diff < 0 : diff == 0)
-            return;
-      }
-
-      // TODO:  merge them together in the setter
-      for (var key in cellCssClasses) {
-        if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
-          cellCss += (" " + cellCssClasses[key][row][m.id]);
-        }
-      }
-
-      stringArray.push("<div class='" + cellCss + "'>");
-
-      // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
-      if (item) {
-        var value = getDataItemValueForColumn(item, m);
-        stringArray.push(getFormatter(row, m)(row, cell, value, m, item));
-      }
-
-      stringArray.push("</div>");
-
-      rowsCache[row].cellRenderQueue.push(cell);
-      rowsCache[row].cellColSpans[cell] = colspan;
-    }
-
-
-    function cleanupRows(rangeToKeep) {
-      for (var i in rowsCache) {
-        if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
-          removeRowFromCache(i);
-        }
-      }
-      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
-    }
-
-    function invalidate() {
-      updateRowCount();
-      invalidateAllRows();
-      render();
-    }
-
-    function invalidateAllRows() {
-      if (currentEditor) {
-        makeActiveCellNormal();
-      }
-      for (var row in rowsCache) {
-        removeRowFromCache(row);
-      }
-      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
-    }
-
-    function queuePostProcessedRowForCleanup(cacheEntry, postProcessedRow, rowIdx) {
-      postProcessgroupId++;
-
-      // store and detach node for later async cleanup
-      for (var columnIdx in postProcessedRow) {
-        if (postProcessedRow.hasOwnProperty(columnIdx)) {
-          postProcessedCleanupQueue.push({
-            actionType: 'C',
-            groupId: postProcessgroupId,
-            node: cacheEntry.cellNodesByColumnIdx[ columnIdx | 0],
-            columnIdx: columnIdx | 0,
-            rowIdx: rowIdx
-          });
-        }
-      }
-      postProcessedCleanupQueue.push({
-        actionType: 'R',
-        groupId: postProcessgroupId,
-        node: cacheEntry.rowNode
-      });
-      $(cacheEntry.rowNode).detach();
-    }
-
-    function queuePostProcessedCellForCleanup(cellnode, columnIdx, rowIdx) {
-      postProcessedCleanupQueue.push({
-        actionType: 'C',
-        groupId: postProcessgroupId,
-        node: cellnode,
-        columnIdx: columnIdx,
-        rowIdx: rowIdx
-      });
-      $(cellnode).detach();
-    }
-
-    function removeRowFromCache(row) {
-      var cacheEntry = rowsCache[row];
-      if (!cacheEntry) {
-        return;
-      }
-
-      if (rowNodeFromLastMouseWheelEvent === cacheEntry.rowNode) {
-        cacheEntry.rowNode.style.display = 'none';
-        zombieRowNodeFromLastMouseWheelEvent = rowNodeFromLastMouseWheelEvent;
-        zombieRowCacheFromLastMouseWheelEvent = cacheEntry;
-        zombieRowPostProcessedFromLastMouseWheelEvent = postProcessedRows[row];
-        // ignore post processing cleanup in this case - it will be dealt with later
-      } else {
-        if (options.enableAsyncPostRenderCleanup && postProcessedRows[row]) {
-          queuePostProcessedRowForCleanup(cacheEntry, postProcessedRows[row], row);
-        } else {
-          $canvas[0].removeChild(cacheEntry.rowNode);
-        }
-      }
-
-      delete rowsCache[row];
-      delete postProcessedRows[row];
-      renderedRows--;
-      counter_rows_removed++;
-    }
-
-    function invalidateRows(rows) {
-      var i, rl;
-      if (!rows || !rows.length) {
-        return;
-      }
-      vScrollDir = 0;
-      for (i = 0, rl = rows.length; i < rl; i++) {
-        if (currentEditor && activeRow === rows[i]) {
-          makeActiveCellNormal();
-        }
-        if (rowsCache[rows[i]]) {
-          removeRowFromCache(rows[i]);
-        }
-      }
-      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
-
-    }
-
-    function invalidateRow(row) {
-      invalidateRows([row]);
-    }
-
-    function updateCell(row, cell) {
-      var cellNode = getCellNode(row, cell);
-      if (!cellNode) {
-        return;
-      }
-
-      var m = columns[cell], d = getDataItem(row);
-      if (currentEditor && activeRow === row && activeCell === cell) {
-        currentEditor.loadValue(d);
-      } else {
-        cellNode.innerHTML = d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
-        invalidatePostProcessingResults(row);
-      }
-    }
-
-    function updateRow(row) {
-      var cacheEntry = rowsCache[row];
-      if (!cacheEntry) {
-        return;
-      }
-
-      ensureCellNodesInRowsCache(row);
-
-      var d = getDataItem(row);
-
-      for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
-        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
-          continue;
-        }
-
-        columnIdx = columnIdx | 0;
-        var m = columns[columnIdx],
-            node = cacheEntry.cellNodesByColumnIdx[columnIdx];
-
-        if (row === activeRow && columnIdx === activeCell && currentEditor) {
-          currentEditor.loadValue(d);
-        } else if (d) {
-          node.innerHTML = getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
-        } else {
-          node.innerHTML = "";
-        }
-      }
-
-      invalidatePostProcessingResults(row);
-    }
-
-    function getViewportHeight() {
-      return parseFloat($.css($container[0], "height", true)) -
-          parseFloat($.css($container[0], "paddingTop", true)) -
-          parseFloat($.css($container[0], "paddingBottom", true)) -
-          parseFloat($.css($headerScroller[0], "height")) - getVBoxDelta($headerScroller) -
-          (options.showTopPanel ? options.topPanelHeight + getVBoxDelta($topPanelScroller) : 0) -
-          (options.showHeaderRow ? options.headerRowHeight + getVBoxDelta($headerRowScroller) : 0);
-    }
-
-    function resizeCanvas() {
-      if (!initialized) { return; }
-      if (options.autoHeight) {
-        viewportH = options.rowHeight * getDataLengthIncludingAddNew();
-      } else {
-        viewportH = getViewportHeight();
-      }
-
-      numVisibleRows = Math.ceil(viewportH / options.rowHeight);
-      viewportW = parseFloat($.css($container[0], "width", true));
-      if (!options.autoHeight) {
-        $viewport.height(viewportH);
-      }
-
-      if (options.forceFitColumns) {
-        autosizeColumns();
-      }
-
-      updateRowCount();
-      handleScroll();
-      // Since the width has changed, force the render() to reevaluate virtually rendered cells.
-      lastRenderedScrollLeft = -1;
-      render();
-    }
-
-    function updateRowCount() {
-      if (!initialized) { return; }
-
-      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
-      var numberOfRows = dataLengthIncludingAddNew +
-          (options.leaveSpaceForNewRows ? numVisibleRows - 1 : 0);
-
-      var oldViewportHasVScroll = viewportHasVScroll;
-      // with autoHeight, we do not need to accommodate the vertical scroll bar
-      viewportHasVScroll = !options.autoHeight && (numberOfRows * options.rowHeight > viewportH);
-
-      makeActiveCellNormal();
-
-      // remove the rows that are now outside of the data range
-      // this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
-      var l = dataLengthIncludingAddNew - 1;
-      for (var i in rowsCache) {
-        if (i >= l) {
-          removeRowFromCache(i);
-        }
-      }
-      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
-
-      if (activeCellNode && activeRow > l) {
-        resetActiveCell();
-      }
-
-      var oldH = h;
-      th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
-      if (th < maxSupportedCssHeight) {
-        // just one page
-        h = ph = th;
-        n = 1;
-        cj = 0;
-      } else {
-        // break into pages
-        h = maxSupportedCssHeight;
-        ph = h / 100;
-        n = Math.floor(th / ph);
-        cj = (th - h) / (n - 1);
-      }
-
-      if (h !== oldH) {
-        $canvas.css("height", h);
-        scrollTop = $viewport[0].scrollTop;
-      }
-
-      var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
-
-      if (th == 0 || scrollTop == 0) {
-        page = offset = 0;
-      } else if (oldScrollTopInRange) {
-        // maintain virtual position
-        scrollTo(scrollTop + offset);
-      } else {
-        // scroll to bottom
-        scrollTo(th - viewportH);
-      }
-
-      if (h != oldH && options.autoHeight) {
-        resizeCanvas();
-      }
-
-      if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
-        autosizeColumns();
-      }
-      updateCanvasWidth(false);
-    }
-
-    function getVisibleRange(viewportTop, viewportLeft) {
-      if (viewportTop == null) {
-        viewportTop = scrollTop;
-      }
-      if (viewportLeft == null) {
-        viewportLeft = scrollLeft;
-      }
-
-      return {
-        top: getRowFromPosition(viewportTop),
-        bottom: getRowFromPosition(viewportTop + viewportH) + 1,
-        leftPx: viewportLeft,
-        rightPx: viewportLeft + viewportW
-      };
-    }
-
-    function getRenderedRange(viewportTop, viewportLeft) {
-      var range = getVisibleRange(viewportTop, viewportLeft);
-      var buffer = Math.round(viewportH / options.rowHeight);
-      var minBuffer = 3;
-
-      if (vScrollDir == -1) {
-        range.top -= buffer;
-        range.bottom += minBuffer;
-      } else if (vScrollDir == 1) {
-        range.top -= minBuffer;
-        range.bottom += buffer;
-      } else {
-        range.top -= minBuffer;
-        range.bottom += minBuffer;
-      }
-
-      range.top = Math.max(0, range.top);
-      range.bottom = Math.min(getDataLengthIncludingAddNew() - 1, range.bottom);
-
-      range.leftPx -= viewportW;
-      range.rightPx += viewportW;
-
-      range.leftPx = Math.max(0, range.leftPx);
-      range.rightPx = Math.min(canvasWidth, range.rightPx);
-
-      return range;
-    }
-
-    function ensureCellNodesInRowsCache(row) {
-      var cacheEntry = rowsCache[row];
-      if (cacheEntry) {
-        if (cacheEntry.cellRenderQueue.length) {
-          var lastChild = cacheEntry.rowNode.lastChild;
-          while (cacheEntry.cellRenderQueue.length) {
-            var columnIdx = cacheEntry.cellRenderQueue.pop();
-            cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
-            lastChild = lastChild.previousSibling;
-          }
-        }
-      }
-    }
-
-    function cleanUpCells(range, row) {
-      var totalCellsRemoved = 0;
-      var cacheEntry = rowsCache[row];
-
-      // Remove cells outside the range.
-      var cellsToRemove = [];
-      for (var i in cacheEntry.cellNodesByColumnIdx) {
-        // I really hate it when people mess with Array.prototype.
-        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
-          continue;
-        }
-
-        // This is a string, so it needs to be cast back to a number.
-        i = i | 0;
-
-        var colspan = cacheEntry.cellColSpans[i];
-        if (columnPosLeft[i] > range.rightPx ||
-          columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
-          if (!(row == activeRow && i == activeCell)) {
-            cellsToRemove.push(i);
-          }
-        }
-      }
-
-      var cellToRemove, node;
-      postProcessgroupId++;
-      while ((cellToRemove = cellsToRemove.pop()) != null) {
-        node = cacheEntry.cellNodesByColumnIdx[cellToRemove];
-        if (options.enableAsyncPostRenderCleanup && postProcessedRows[row] && postProcessedRows[row][cellToRemove]) {
-          queuePostProcessedCellForCleanup(node, cellToRemove, row);
-        } else {
-          cacheEntry.rowNode.removeChild(node);
-        }
-
-        delete cacheEntry.cellColSpans[cellToRemove];
-        delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
-        if (postProcessedRows[row]) {
-          delete postProcessedRows[row][cellToRemove];
-        }
-        totalCellsRemoved++;
-      }
-    }
-
-    function cleanUpAndRenderCells(range) {
-      var cacheEntry;
-      var stringArray = [];
-      var processedRows = [];
-      var cellsAdded;
-      var totalCellsAdded = 0;
-      var colspan;
-
-      for (var row = range.top, btm = range.bottom; row <= btm; row++) {
-        cacheEntry = rowsCache[row];
-        if (!cacheEntry) {
-          continue;
-        }
-
-        // cellRenderQueue populated in renderRows() needs to be cleared first
-        ensureCellNodesInRowsCache(row);
-
-        cleanUpCells(range, row);
-
-        // Render missing cells.
-        cellsAdded = 0;
-
-        var metadata = data.getItemMetadata && data.getItemMetadata(row);
-        metadata = metadata && metadata.columns;
-
-        var d = getDataItem(row);
-
-        // TODO:  shorten this loop (index? heuristics? binary search?)
-        for (var i = 0, ii = columns.length; i < ii; i++) {
-          // Cells to the right are outside the range.
-          if (columnPosLeft[i] > range.rightPx) {
-            break;
-          }
-
-          // Already rendered.
-          if ((colspan = cacheEntry.cellColSpans[i]) != null) {
-            i += (colspan > 1 ? colspan - 1 : 0);
-            continue;
-          }
-
-          colspan = 1;
-          if (metadata) {
-            var columnData = metadata[columns[i].id] || metadata[i];
-            colspan = (columnData && columnData.colspan) || 1;
-            if (colspan === "*") {
-              colspan = ii - i;
-            }
-          }
-
-          if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
-            appendCellHtml(stringArray, row, i, colspan, d);
-            cellsAdded++;
-          }
-
-          i += (colspan > 1 ? colspan - 1 : 0);
-        }
-
-        if (cellsAdded) {
-          totalCellsAdded += cellsAdded;
-          processedRows.push(row);
-        }
-      }
-
-      if (!stringArray.length) {
-        return;
-      }
-
-      var x = document.createElement("div");
-      x.innerHTML = stringArray.join("");
-
-      var processedRow;
-      var node;
-      while ((processedRow = processedRows.pop()) != null) {
-        cacheEntry = rowsCache[processedRow];
-        var columnIdx;
-        while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
-          node = x.lastChild;
-          cacheEntry.rowNode.appendChild(node);
-          cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
-        }
-      }
-    }
-
-    function renderRows(range) {
-      var parentNode = $canvas[0],
-          stringArray = [],
-          rows = [],
-          needToReselectCell = false,
-          dataLength = getDataLength();
-
-      for (var i = range.top, ii = range.bottom; i <= ii; i++) {
-        if (rowsCache[i]) {
-          continue;
-        }
-        renderedRows++;
-        rows.push(i);
-
-        // Create an entry right away so that appendRowHtml() can
-        // start populatating it.
-        rowsCache[i] = {
-          "rowNode": null,
-
-          // ColSpans of rendered cells (by column idx).
-          // Can also be used for checking whether a cell has been rendered.
-          "cellColSpans": [],
-
-          // Cell nodes (by column idx).  Lazy-populated by ensureCellNodesInRowsCache().
-          "cellNodesByColumnIdx": [],
-
-          // Column indices of cell nodes that have been rendered, but not yet indexed in
-          // cellNodesByColumnIdx.  These are in the same order as cell nodes added at the
-          // end of the row.
-          "cellRenderQueue": []
-        };
-
-        appendRowHtml(stringArray, i, range, dataLength);
-        if (activeCellNode && activeRow === i) {
-          needToReselectCell = true;
-        }
-        counter_rows_rendered++;
-      }
-
-      if (!rows.length) { return; }
-
-      var x = document.createElement("div");
-      x.innerHTML = stringArray.join("");
-
-      for (var i = 0, ii = rows.length; i < ii; i++) {
-        rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
-      }
-
-      if (needToReselectCell) {
-        activeCellNode = getCellNode(activeRow, activeCell);
-      }
-    }
-
-    function startPostProcessing() {
-      if (!options.enableAsyncPostRender) {
-        return;
-      }
-      clearTimeout(h_postrender);
-      h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
-    }
-
-    function startPostProcessingCleanup() {
-      if (!options.enableAsyncPostRenderCleanup) {
-        return;
-      }
-      clearTimeout(h_postrenderCleanup);
-      h_postrenderCleanup = setTimeout(asyncPostProcessCleanupRows, options.asyncPostRenderCleanupDelay);
-    }
-
-    function invalidatePostProcessingResults(row) {
-      // change status of columns to be re-rendered
-      for (var columnIdx in postProcessedRows[row]) {
-        if (postProcessedRows[row].hasOwnProperty(columnIdx)) {
-          postProcessedRows[row][columnIdx] = 'C';
-        }
-      }
-      postProcessFromRow = Math.min(postProcessFromRow, row);
-      postProcessToRow = Math.max(postProcessToRow, row);
-      startPostProcessing();
-    }
-
-    function updateRowPositions() {
-      for (var row in rowsCache) {
-        rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
-      }
-    }
-
-    function render() {
-      if (!initialized) { return; }
-      var visible = getVisibleRange();
-      var rendered = getRenderedRange();
-
-      // remove rows no longer in the viewport
-      cleanupRows(rendered);
-
-      // add new rows & missing cells in existing rows
-      if (lastRenderedScrollLeft != scrollLeft) {
-        cleanUpAndRenderCells(rendered);
-      }
-
-      // render missing rows
-      renderRows(rendered);
-
-      postProcessFromRow = visible.top;
-      postProcessToRow = Math.min(getDataLengthIncludingAddNew() - 1, visible.bottom);
-      startPostProcessing();
-
-      lastRenderedScrollTop = scrollTop;
-      lastRenderedScrollLeft = scrollLeft;
-      h_render = null;
-    }
-
-    function handleHeaderRowScroll() {
-      var scrollLeft = $headerRowScroller[0].scrollLeft;
-      if (scrollLeft != $viewport[0].scrollLeft) {
-        $viewport[0].scrollLeft = scrollLeft;
-      }
-    }
-
-    function handleScroll() {
-      scrollTop = $viewport[0].scrollTop;
-      scrollLeft = $viewport[0].scrollLeft;
-      var vScrollDist = Math.abs(scrollTop - prevScrollTop);
-      var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
-
-      if (hScrollDist) {
-        prevScrollLeft = scrollLeft;
-        $headerScroller[0].scrollLeft = scrollLeft;
-        $topPanelScroller[0].scrollLeft = scrollLeft;
-        $headerRowScroller[0].scrollLeft = scrollLeft;
-      }
-
-      if (vScrollDist) {
-        vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
-        prevScrollTop = scrollTop;
-
-        // switch virtual pages if needed
-        if (vScrollDist < viewportH) {
-          scrollTo(scrollTop + offset);
-        } else {
-          var oldOffset = offset;
-          if (h == viewportH) {
-            page = 0;
-          } else {
-            page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
-          }
-          offset = Math.round(page * cj);
-          if (oldOffset != offset) {
-            invalidateAllRows();
-          }
-        }
-      }
-
-      if (hScrollDist || vScrollDist) {
-        if (h_render) {
-          clearTimeout(h_render);
-        }
-
-        if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
-            Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
-          if (options.forceSyncScrolling || (
-              Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
-              Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW)) {
-            render();
-          } else {
-            h_render = setTimeout(render, 50);
-          }
-
-          trigger(self.onViewportChanged, {grid: self});
-        }
-      }
-
-      trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop, grid: self});
-    }
-
-    function asyncPostProcessRows() {
-      var dataLength = getDataLength();
-      while (postProcessFromRow <= postProcessToRow) {
-        var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
-        var cacheEntry = rowsCache[row];
-        if (!cacheEntry || row >= dataLength) {
-          continue;
-        }
-
-        if (!postProcessedRows[row]) {
-          postProcessedRows[row] = {};
-        }
-
-        ensureCellNodesInRowsCache(row);
-        for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
-          if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
-            continue;
-          }
-
-          columnIdx = columnIdx | 0;
-
-          var m = columns[columnIdx];
-          var processedStatus = postProcessedRows[row][columnIdx]; // C=cleanup and re-render, R=rendered
-          if (m.asyncPostRender && processedStatus !== 'R') {
-            var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
-            if (node) {
-              m.asyncPostRender(node, row, getDataItem(row), m, (processedStatus === 'C'));
-            }
-            postProcessedRows[row][columnIdx] = 'R';
-          }
-        }
-
-        h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
-        return;
-      }
-    }
-
-    function asyncPostProcessCleanupRows() {
-      if (postProcessedCleanupQueue.length > 0) {
-        var groupId = postProcessedCleanupQueue[0].groupId;
-
-        // loop through all queue members with this groupID
-        while (postProcessedCleanupQueue.length > 0 && postProcessedCleanupQueue[0].groupId == groupId) {
-          var entry = postProcessedCleanupQueue.shift();
-          if (entry.actionType == 'R') {
-            $(entry.node).remove();
-          }
-          if (entry.actionType == 'C') {
-            var column = columns[entry.columnIdx];
-            if (column.asyncPostRenderCleanup && entry.node) {
-              // cleanup must also remove element
-              column.asyncPostRenderCleanup(entry.node, entry.rowIdx, column);
-            }
-          }
-        }
-
-        // call this function again after the specified delay
-        h_postrenderCleanup = setTimeout(asyncPostProcessCleanupRows, options.asyncPostRenderCleanupDelay);
-      }
-    }
-
-    function updateCellCssStylesOnRenderedRows(addedHash, removedHash) {
-      var node, columnId, addedRowHash, removedRowHash;
-      for (var row in rowsCache) {
-        removedRowHash = removedHash && removedHash[row];
-        addedRowHash = addedHash && addedHash[row];
-
-        if (removedRowHash) {
-          for (columnId in removedRowHash) {
-            if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
-              node = getCellNode(row, getColumnIndex(columnId));
-              if (node) {
-                $(node).removeClass(removedRowHash[columnId]);
-              }
-            }
-          }
-        }
-
-        if (addedRowHash) {
-          for (columnId in addedRowHash) {
-            if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
-              node = getCellNode(row, getColumnIndex(columnId));
-              if (node) {
-                $(node).addClass(addedRowHash[columnId]);
-              }
-            }
-          }
-        }
-      }
-    }
-
-    function addCellCssStyles(key, hash) {
-      if (cellCssClasses[key]) {
-        throw "addCellCssStyles: cell CSS hash with key '" + key + "' already exists.";
-      }
-
-      cellCssClasses[key] = hash;
-      updateCellCssStylesOnRenderedRows(hash, null);
-
-      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash, "grid": self });
-    }
-
-    function removeCellCssStyles(key) {
-      if (!cellCssClasses[key]) {
-        return;
-      }
-
-      updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
-      delete cellCssClasses[key];
-
-      trigger(self.onCellCssStylesChanged, { "key": key, "hash": null, "grid": self });
-    }
-
-    function setCellCssStyles(key, hash) {
-      var prevHash = cellCssClasses[key];
-
-      cellCssClasses[key] = hash;
-      updateCellCssStylesOnRenderedRows(hash, prevHash);
-
-      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash, "grid": self });
-    }
-
-    function getCellCssStyles(key) {
-      return cellCssClasses[key];
-    }
-
-    function flashCell(row, cell, speed) {
-      speed = speed || 100;
-      if (rowsCache[row]) {
-        var $cell = $(getCellNode(row, cell));
-
-        function toggleCellClass(times) {
-          if (!times) {
-            return;
-          }
-          setTimeout(function () {
-                $cell.queue(function () {
-                  $cell.toggleClass(options.cellFlashingCssClass).dequeue();
-                  toggleCellClass(times - 1);
-                });
-              },
-              speed);
-        }
-
-        toggleCellClass(4);
-      }
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Interactivity
-
-    function handleMouseWheel(e) {
-      var rowNode = $(e.target).closest(".sr")[0];
-      if (rowNode != rowNodeFromLastMouseWheelEvent) {
-        if (zombieRowNodeFromLastMouseWheelEvent && zombieRowNodeFromLastMouseWheelEvent != rowNode) {
-          if (options.enableAsyncPostRenderCleanup && zombieRowPostProcessedFromLastMouseWheelEvent) {
-            queuePostProcessedRowForCleanup(zombieRowCacheFromLastMouseWheelEvent,
-              zombieRowPostProcessedFromLastMouseWheelEvent);
-          } else {
-            $canvas[0].removeChild(zombieRowNodeFromLastMouseWheelEvent);
-          }
-          zombieRowNodeFromLastMouseWheelEvent = null;
-          zombieRowCacheFromLastMouseWheelEvent = null;
-          zombieRowPostProcessedFromLastMouseWheelEvent = null;
-
-          if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
-        }
-        rowNodeFromLastMouseWheelEvent = rowNode;
-      }
-    }
-
-    function handleDragInit(e, dd) {
-      var cell = getCellFromEvent(e);
-      if (!cell || !cellExists(cell.row, cell.cell)) {
-        return false;
-      }
-
-      var retval = trigger(self.onDragInit, dd, e);
-      if (e.isImmediatePropagationStopped()) {
-        return retval;
-      }
-
-      // if nobody claims to be handling drag'n'drop by stopping immediate propagation,
-      // cancel out of it
-      return false;
-    }
-
-    function handleDragStart(e, dd) {
-      var cell = getCellFromEvent(e);
-      if (!cell || !cellExists(cell.row, cell.cell)) {
-        return false;
-      }
-
-      var retval = trigger(self.onDragStart, dd, e);
-      if (e.isImmediatePropagationStopped()) {
-        return retval;
-      }
-
-      return false;
-    }
-
-    function handleDrag(e, dd) {
-      return trigger(self.onDrag, dd, e);
-    }
-
-    function handleDragEnd(e, dd) {
-      trigger(self.onDragEnd, dd, e);
-    }
-
-    function handleKeyDown(e) {
-      trigger(self.onKeyDown, {row: activeRow, cell: activeCell, grid: self}, e);
-      var handled = e.isImmediatePropagationStopped();
-      var keyCode = Slick.keyCode;
-
-      if (!handled) {
-        if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
-          if (e.which == keyCode.ESCAPE) {
-            if (!getEditorLock().isActive()) {
-              return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
-            }
-            cancelEditAndSetFocus();
-          } else if (e.which == keyCode.PAGEDOWN) {
-            navigatePageDown();
-            handled = true;
-          } else if (e.which == keyCode.PAGEUP) {
-            navigatePageUp();
-            handled = true;
-          } else if (e.which == keyCode.LEFT) {
-            handled = navigateLeft();
-          } else if (e.which == keyCode.RIGHT) {
-            handled = navigateRight();
-          } else if (e.which == keyCode.UP) {
-            handled = navigateUp();
-          } else if (e.which == keyCode.DOWN) {
-            handled = navigateDown();
-          } else if (e.which == keyCode.TAB) {
-            handled = navigateNext();
-          } else if (e.which == keyCode.ENTER) {
-            if (options.editable) {
-              if (currentEditor) {
-                // adding new row
-                if (activeRow === getDataLength()) {
-                  navigateDown();
-                } else {
-                  commitEditAndSetFocus();
-                }
-              } else {
-                if (getEditorLock().commitCurrentEdit()) {
-                  makeActiveCellEditable();
-                }
-              }
-            }
-            handled = true;
-          }
-        } else if (e.which == keyCode.TAB && e.shiftKey && !e.ctrlKey && !e.altKey) {
-          handled = navigatePrev();
-        }
-      }
-
-      if (handled) {
-        // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
-        e.stopPropagation();
-        e.preventDefault();
-        try {
-          e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
-        }
-        // ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl"
-        // (hitting control key only, nothing else), "Shift" (maybe others)
-        catch (error) {
-        }
-      }
-    }
-
-    function handleClick(e) {
-      if (!currentEditor) {
-        // if this click resulted in some cell child node getting focus,
-        // don't steal it back - keyboard events will still bubble up
-        // IE9+ seems to default DIVs to tabIndex=0 instead of -1, so check for cell clicks directly.
-        if (e.target != document.activeElement || $(e.target).hasClass("sc")) {
-          setFocus();
-        }
-      }
-
-      var cell = getCellFromEvent(e);
-      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
-        return;
-      }
-
-      trigger(self.onClick, {row: cell.row, cell: cell.cell, grid: self}, e);
-      if (e.isImmediatePropagationStopped()) {
-        return;
-      }
-
-      if ((activeCell != cell.cell || activeRow != cell.row) && canCellBeActive(cell.row, cell.cell)) {
-        if (!getEditorLock().isActive() || getEditorLock().commitCurrentEdit()) {
-          scrollRowIntoView(cell.row, false);
-          setActiveCellInternal(getCellNode(cell.row, cell.cell));
-        }
-      }
-    }
-
-    function handleContextMenu(e) {
-      var $cell = $(e.target).closest(".sc", $canvas);
-      if ($cell.length === 0) {
-        return;
-      }
-
-      // are we editing this cell?
-      if (activeCellNode === $cell[0] && currentEditor !== null) {
-        return;
-      }
-
-      trigger(self.onContextMenu, {grid: self}, e);
-    }
-
-    function handleDblClick(e) {
-      var cell = getCellFromEvent(e);
-      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
-        return;
-      }
-
-      trigger(self.onDblClick, {row: cell.row, cell: cell.cell, grid: self}, e);
-      if (e.isImmediatePropagationStopped()) {
-        return;
-      }
-
-      if (options.editable) {
-        gotoCell(cell.row, cell.cell, true);
-      }
-    }
-
-    function handleHeaderMouseEnter(e) {
-      trigger(self.onHeaderMouseEnter, {
-        "column": $(this).data("column"),
-        "grid": self
-      }, e);
-    }
-
-    function handleHeaderMouseLeave(e) {
-      trigger(self.onHeaderMouseLeave, {
-        "column": $(this).data("column"),
-        "grid": self
-      }, e);
-    }
-
-    function handleHeaderContextMenu(e) {
-      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
-      var column = $header && $header.data("column");
-      trigger(self.onHeaderContextMenu, {column: column, grid: self}, e);
-    }
-
-    function handleHeaderClick(e) {
-      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
-      var column = $header && $header.data("column");
-      if (column) {
-        trigger(self.onHeaderClick, {column: column, grid: self}, e);
-      }
-    }
-
-    function handleMouseEnter(e) {
-      trigger(self.onMouseEnter, {grid: self}, e);
-    }
-
-    function handleMouseLeave(e) {
-      trigger(self.onMouseLeave, {grid: self}, e);
-    }
-
-    function cellExists(row, cell) {
-      return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
-    }
-
-    function getCellFromPoint(x, y) {
-      var row = getRowFromPosition(y);
-      var cell = 0;
-
-      var w = 0;
-      for (var i = 0; i < columns.length && w < x; i++) {
-        w += columns[i].width;
-        cell++;
-      }
-
-      if (cell < 0) {
-        cell = 0;
-      }
-
-      return {row: row, cell: cell - 1};
-    }
-
-    function getCellFromNode(cellNode) {
-      // read column number from .l<columnNumber> CSS class
-      var cls = /l\d+/.exec(cellNode.className);
-      if (!cls) {
-        throw "getCellFromNode: cannot get cell - " + cellNode.className;
-      }
-      return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
-    }
-
-    function getRowFromNode(rowNode) {
-      for (var row in rowsCache) {
-        if (rowsCache[row].rowNode === rowNode) {
-          return row | 0;
-        }
-      }
-
-      return null;
-    }
-
-    function getCellFromEvent(e) {
-      var $cell = $(e.target).closest(".sc", $canvas);
-      if (!$cell.length) {
-        return null;
-      }
-
-      var row = getRowFromNode($cell[0].parentNode);
-      var cell = getCellFromNode($cell[0]);
-
-      if (row == null || cell == null) {
-        return null;
-      } else {
-        return {
-          "row": row,
-          "cell": cell
-        };
-      }
-    }
-
-    function getCellNodeBox(row, cell) {
-      if (!cellExists(row, cell)) {
-        return null;
-      }
-
-      var y1 = getRowTop(row);
-      var y2 = y1 + options.rowHeight - 1;
-      var x1 = 0;
-      for (var i = 0; i < cell; i++) {
-        x1 += columns[i].width;
-      }
-      var x2 = x1 + columns[cell].width;
-
-      return {
-        top: y1,
-        left: x1,
-        bottom: y2,
-        right: x2
-      };
-    }
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Cell switching
-
-    function resetActiveCell() {
-      setActiveCellInternal(null, false);
-    }
-
-    function setFocus() {
-      if (tabbingDirection == -1) {
-        $focusSink[0].focus();
-      } else {
-        $focusSink2[0].focus();
-      }
-    }
-
-    function scrollCellIntoView(row, cell, doPaging) {
-      scrollRowIntoView(row, doPaging);
-
-      var colspan = getColspan(row, cell);
-      var left = columnPosLeft[cell],
-        right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
-        scrollRight = scrollLeft + viewportW;
-
-      if (left < scrollLeft) {
-        $viewport.scrollLeft(left);
-        handleScroll();
-        render();
-      } else if (right > scrollRight) {
-        $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
-        handleScroll();
-        render();
-      }
-    }
-
-    function setActiveCellInternal(newCell, opt_editMode) {
-      if (activeCellNode !== null) {
-        makeActiveCellNormal();
-        $(activeCellNode).removeClass("active");
-        if (rowsCache[activeRow]) {
-          $(rowsCache[activeRow].rowNode).removeClass("active");
-        }
-      }
-
-      var activeCellChanged = (activeCellNode !== newCell);
-      activeCellNode = newCell;
-
-      if (activeCellNode != null) {
-        activeRow = getRowFromNode(activeCellNode.parentNode);
-        activeCell = activePosX = getCellFromNode(activeCellNode);
-
-        if (opt_editMode == null) {
-          opt_editMode = (activeRow == getDataLength()) || options.autoEdit;
-        }
-
-        $(activeCellNode).addClass("active");
-        $(rowsCache[activeRow].rowNode).addClass("active");
-
-        if (options.editable && opt_editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
-          clearTimeout(h_editorLoader);
-
-          if (options.asyncEditorLoading) {
-            h_editorLoader = setTimeout(function () {
-              makeActiveCellEditable();
-            }, options.asyncEditorLoadDelay);
-          } else {
-            makeActiveCellEditable();
-          }
-        }
-      } else {
-        activeRow = activeCell = null;
-      }
-
-      if (activeCellChanged) {
-        trigger(self.onActiveCellChanged, getActiveCell());
-      }
-    }
-
-    function clearTextSelection() {
-      if (document.selection && document.selection.empty) {
-        try {
-          //IE fails here if selected element is not in dom
-          document.selection.empty();
-        } catch (e) { }
-      } else if (window.getSelection) {
-        var sel = window.getSelection();
-        if (sel && sel.removeAllRanges) {
-          sel.removeAllRanges();
-        }
-      }
-    }
-
-    function isCellPotentiallyEditable(row, cell) {
-      var dataLength = getDataLength();
-      // is the data for this row loaded?
-      if (row < dataLength && !getDataItem(row)) {
-        return false;
-      }
-
-      // are we in the Add New row?  can we create new from this cell?
-      if (columns[cell].cannotTriggerInsert && row >= dataLength) {
-        return false;
-      }
-
-      // does this cell have an editor?
-      if (!getEditor(row, cell)) {
-        return false;
-      }
-
-      return true;
-    }
-
-    function makeActiveCellNormal() {
-      if (!currentEditor) {
-        return;
-      }
-      trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor, grid: self});
-      currentEditor.destroy();
-      currentEditor = null;
-
-      if (activeCellNode) {
-        var d = getDataItem(activeRow);
-        $(activeCellNode).removeClass("editable invalid");
-        if (d) {
-          var column = columns[activeCell];
-          var formatter = getFormatter(activeRow, column);
-          activeCellNode.innerHTML = formatter(activeRow, activeCell, getDataItemValueForColumn(d, column), column, d, self);
-          invalidatePostProcessingResults(activeRow);
-        }
-      }
-
-      // if there previously was text selected on a page (such as selected text in the edit cell just removed),
-      // IE can't set focus to anything else correctly
-      if (navigator.userAgent.toLowerCase().match(/msie/)) {
-        clearTextSelection();
-      }
-
-      getEditorLock().deactivate(editController);
-    }
-
-    function makeActiveCellEditable(editor) {
-      if (!activeCellNode) {
-        return;
-      }
-      if (!options.editable) {
-        throw "Grid : makeActiveCellEditable : should never get called when options.editable is false";
-      }
-
-      // cancel pending async call if there is one
-      clearTimeout(h_editorLoader);
-
-      if (!isCellPotentiallyEditable(activeRow, activeCell)) {
-        return;
-      }
-
-      var columnDef = columns[activeCell];
-      var item = getDataItem(activeRow);
-
-      if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef, grid: self}) === false) {
-        setFocus();
-        return;
-      }
-
-      getEditorLock().activate(editController);
-      $(activeCellNode).addClass("editable");
-
-	  var useEditor = editor || getEditor(activeRow, activeCell);
-
-      // don't clear the cell if a custom editor is passed through
-      if (!editor && !useEditor.suppressClearOnEdit) {
-        activeCellNode.innerHTML = "";
-      }
-
-      currentEditor = new useEditor({
-        grid: self,
-        gridPosition: absBox($container[0]),
-        position: absBox(activeCellNode),
-        container: activeCellNode,
-        column: columnDef,
-        item: item || {},
-        commitChanges: commitEditAndSetFocus,
-        cancelChanges: cancelEditAndSetFocus
-      });
-
-      if (item) {
-        currentEditor.loadValue(item);
-      }
-
-      serializedEditorValue = currentEditor.serializeValue();
-
-      if (currentEditor.position) {
-        handleActiveCellPositionChange();
-      }
-    }
-
-    function commitEditAndSetFocus() {
-      // if the commit fails, it would do so due to a validation error
-      // if so, do not steal the focus from the editor
-      if (getEditorLock().commitCurrentEdit()) {
-        setFocus();
-        if (options.autoEdit) {
-          navigateDown();
-        }
-      }
-    }
-
-    function cancelEditAndSetFocus() {
-      if (getEditorLock().cancelCurrentEdit()) {
-        setFocus();
-      }
-    }
-
-    function absBox(elem) {
-      var box = {
-        top: elem.offsetTop,
-        left: elem.offsetLeft,
-        bottom: 0,
-        right: 0,
-        width: $(elem).outerWidth(),
-        height: $(elem).outerHeight(),
-        visible: true};
-      box.bottom = box.top + box.height;
-      box.right = box.left + box.width;
-
-      // walk up the tree
-      var offsetParent = elem.offsetParent;
-      while ((elem = elem.parentNode) != document.body) {
-		if (elem == null) break;
-
-        if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible") {
-          box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
-        }
-
-        if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible") {
-          box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
-        }
-
-        box.left -= elem.scrollLeft;
-        box.top -= elem.scrollTop;
-
-        if (elem === offsetParent) {
-          box.left += elem.offsetLeft;
-          box.top += elem.offsetTop;
-          offsetParent = elem.offsetParent;
-        }
-
-        box.bottom = box.top + box.height;
-        box.right = box.left + box.width;
-      }
-
-      return box;
-    }
-
-    function getActiveCellPosition() {
-      return absBox(activeCellNode);
-    }
-
-    function getGridPosition() {
-      return absBox($container[0])
-    }
-
-    function handleActiveCellPositionChange() {
-      if (!activeCellNode) {
-        return;
-      }
-
-      trigger(self.onActiveCellPositionChanged, {grid: self});
-
-      if (currentEditor) {
-        var cellBox = getActiveCellPosition();
-        if (currentEditor.show && currentEditor.hide) {
-          if (!cellBox.visible) {
-            currentEditor.hide();
-          } else {
-            currentEditor.show();
-          }
-        }
-
-        if (currentEditor.position) {
-          currentEditor.position(cellBox);
-        }
-      }
-    }
-
-    function getCellEditor() {
-      return currentEditor;
-    }
-
-    function getActiveCell() {
-      if (!activeCellNode) {
-        return null;
-      } else {
-        return {row: activeRow, cell: activeCell, grid: self};
-      }
-    }
-
-    function getActiveCellNode() {
-      return activeCellNode;
-    }
-
-    function scrollRowIntoView(row, doPaging) {
-      var rowAtTop = row * options.rowHeight;
-      var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
-
-      // need to page down?
-      if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
-        scrollTo(doPaging ? rowAtTop : rowAtBottom);
-        render();
-      }
-      // or page up?
-      else if (row * options.rowHeight < scrollTop + offset) {
-        scrollTo(doPaging ? rowAtBottom : rowAtTop);
-        render();
-      }
-    }
-
-    function scrollRowToTop(row) {
-      scrollTo(row * options.rowHeight);
-      render();
-    }
-
-    function scrollPage(dir) {
-      var deltaRows = dir * numVisibleRows;
-      scrollTo((getRowFromPosition(scrollTop) + deltaRows) * options.rowHeight);
-      render();
-
-      if (options.enableCellNavigation && activeRow != null) {
-        var row = activeRow + deltaRows;
-        var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
-        if (row >= dataLengthIncludingAddNew) {
-          row = dataLengthIncludingAddNew - 1;
-        }
-        if (row < 0) {
-          row = 0;
-        }
-
-        var cell = 0, prevCell = null;
-        var prevActivePosX = activePosX;
-        while (cell <= activePosX) {
-          if (canCellBeActive(row, cell)) {
-            prevCell = cell;
-          }
-          cell += getColspan(row, cell);
-        }
-
-        if (prevCell !== null) {
-          setActiveCellInternal(getCellNode(row, prevCell));
-          activePosX = prevActivePosX;
-        } else {
-          resetActiveCell();
-        }
-      }
-    }
-
-    function navigatePageDown() {
-      scrollPage(1);
-    }
-
-    function navigatePageUp() {
-      scrollPage(-1);
-    }
-
-    function getColspan(row, cell) {
-      var metadata = data.getItemMetadata && data.getItemMetadata(row);
-      if (!metadata || !metadata.columns) {
-        return 1;
-      }
-
-      var columnData = metadata.columns[columns[cell].id] || metadata.columns[cell];
-      var colspan = (columnData && columnData.colspan);
-      if (colspan === "*") {
-        colspan = columns.length - cell;
-      } else {
-        colspan = colspan || 1;
-      }
-
-      return colspan;
-    }
-
-    function findFirstFocusableCell(row) {
-      var cell = 0;
-      while (cell < columns.length) {
-        if (canCellBeActive(row, cell)) {
-          return cell;
-        }
-        cell += getColspan(row, cell);
-      }
-      return null;
-    }
-
-    function findLastFocusableCell(row) {
-      var cell = 0;
-      var lastFocusableCell = null;
-      while (cell < columns.length) {
-        if (canCellBeActive(row, cell)) {
-          lastFocusableCell = cell;
-        }
-        cell += getColspan(row, cell);
-      }
-      return lastFocusableCell;
-    }
-
-    function gotoRight(row, cell, posX) {
-      if (cell >= columns.length) {
-        return null;
-      }
-
-      do {
-        cell += getColspan(row, cell);
-      }
-      while (cell < columns.length && !canCellBeActive(row, cell));
-
-      if (cell < columns.length) {
-        return {
-          "row": row,
-          "cell": cell,
-          "posX": cell
-        };
-      }
-      return null;
-    }
-
-    function gotoLeft(row, cell, posX) {
-      if (cell <= 0) {
-        return null;
-      }
-
-      var firstFocusableCell = findFirstFocusableCell(row);
-      if (firstFocusableCell === null || firstFocusableCell >= cell) {
-        return null;
-      }
-
-      var prev = {
-        "row": row,
-        "cell": firstFocusableCell,
-        "posX": firstFocusableCell
-      };
-      var pos;
-      while (true) {
-        pos = gotoRight(prev.row, prev.cell, prev.posX);
-        if (!pos) {
-          return null;
-        }
-        if (pos.cell >= cell) {
-          return prev;
-        }
-        prev = pos;
-      }
-    }
-
-    function gotoDown(row, cell, posX) {
-      var prevCell;
-      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
-      while (true) {
-        if (++row >= dataLengthIncludingAddNew) {
-          return null;
-        }
-
-        prevCell = cell = 0;
-        while (cell <= posX) {
-          prevCell = cell;
-          cell += getColspan(row, cell);
-        }
-
-        if (canCellBeActive(row, prevCell)) {
-          return {
-            "row": row,
-            "cell": prevCell,
-            "posX": posX
-          };
-        }
-      }
-    }
-
-    function gotoUp(row, cell, posX) {
-      var prevCell;
-      while (true) {
-        if (--row < 0) {
-          return null;
-        }
-
-        prevCell = cell = 0;
-        while (cell <= posX) {
-          prevCell = cell;
-          cell += getColspan(row, cell);
-        }
-
-        if (canCellBeActive(row, prevCell)) {
-          return {
-            "row": row,
-            "cell": prevCell,
-            "posX": posX
-          };
-        }
-      }
-    }
-
-    function gotoNext(row, cell, posX) {
-      if (row == null && cell == null) {
-        row = cell = posX = 0;
-        if (canCellBeActive(row, cell)) {
-          return {
-            "row": row,
-            "cell": cell,
-            "posX": cell
-          };
-        }
-      }
-
-      var pos = gotoRight(row, cell, posX);
-      if (pos) {
-        return pos;
-      }
-
-      var firstFocusableCell = null;
-      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
-      while (++row < dataLengthIncludingAddNew) {
-        firstFocusableCell = findFirstFocusableCell(row);
-        if (firstFocusableCell !== null) {
-          return {
-            "row": row,
-            "cell": firstFocusableCell,
-            "posX": firstFocusableCell
-          };
-        }
-      }
-      return null;
-    }
-
-    function gotoPrev(row, cell, posX) {
-      if (row == null && cell == null) {
-        row = getDataLengthIncludingAddNew() - 1;
-        cell = posX = columns.length - 1;
-        if (canCellBeActive(row, cell)) {
-          return {
-            "row": row,
-            "cell": cell,
-            "posX": cell
-          };
-        }
-      }
-
-      var pos;
-      var lastSelectableCell;
-      while (!pos) {
-        pos = gotoLeft(row, cell, posX);
-        if (pos) {
-          break;
-        }
-        if (--row < 0) {
-          return null;
-        }
-
-        cell = 0;
-        lastSelectableCell = findLastFocusableCell(row);
-        if (lastSelectableCell !== null) {
-          pos = {
-            "row": row,
-            "cell": lastSelectableCell,
-            "posX": lastSelectableCell
-          };
-        }
-      }
-      return pos;
-    }
-
-    function navigateRight() {
-      return navigate("right");
-    }
-
-    function navigateLeft() {
-      return navigate("left");
-    }
-
-    function navigateDown() {
-      return navigate("down");
-    }
-
-    function navigateUp() {
-      return navigate("up");
-    }
-
-    function navigateNext() {
-      return navigate("next");
-    }
-
-    function navigatePrev() {
-      return navigate("prev");
-    }
-
-    /**
-     * @param {string} dir Navigation direction.
-     * @return {boolean} Whether navigation resulted in a change of active cell.
-     */
-    function navigate(dir) {
-      if (!options.enableCellNavigation) {
-        return false;
-      }
-
-      if (!activeCellNode && dir != "prev" && dir != "next") {
-        return false;
-      }
-
-      if (!getEditorLock().commitCurrentEdit()) {
-        return true;
-      }
-      setFocus();
-
-      var tabbingDirections = {
-        "up": -1,
-        "down": 1,
-        "left": -1,
-        "right": 1,
-        "prev": -1,
-        "next": 1
-      };
-      tabbingDirection = tabbingDirections[dir];
-
-      var stepFunctions = {
-        "up": gotoUp,
-        "down": gotoDown,
-        "left": gotoLeft,
-        "right": gotoRight,
-        "prev": gotoPrev,
-        "next": gotoNext
-      };
-      var stepFn = stepFunctions[dir];
-      var pos = stepFn(activeRow, activeCell, activePosX);
-      if (pos) {
-        var isAddNewRow = (pos.row == getDataLength());
-        scrollCellIntoView(pos.row, pos.cell, !isAddNewRow);
-        setActiveCellInternal(getCellNode(pos.row, pos.cell));
-        activePosX = pos.posX;
-        return true;
-      } else {
-        setActiveCellInternal(getCellNode(activeRow, activeCell));
-        return false;
-      }
-    }
-
-    function getCellNode(row, cell) {
-      if (rowsCache[row]) {
-        ensureCellNodesInRowsCache(row);
-        return rowsCache[row].cellNodesByColumnIdx[cell];
-      }
-      return null;
-    }
-
-    function setActiveCell(row, cell) {
-      if (!initialized) { return; }
-      if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
-        return;
-      }
-
-      if (!options.enableCellNavigation) {
-        return;
-      }
-
-      scrollCellIntoView(row, cell, false);
-      setActiveCellInternal(getCellNode(row, cell), false);
-    }
-
-    function canCellBeActive(row, cell) {
-      if (!options.enableCellNavigation || row >= getDataLengthIncludingAddNew() ||
-          row < 0 || cell >= columns.length || cell < 0) {
-        return false;
-      }
-
-      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
-      if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
-        return rowMetadata.focusable;
-      }
-
-      var columnMetadata = rowMetadata && rowMetadata.columns;
-      if (columnMetadata && columnMetadata[columns[cell].id] && typeof columnMetadata[columns[cell].id].focusable === "boolean") {
-        return columnMetadata[columns[cell].id].focusable;
-      }
-      if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
-        return columnMetadata[cell].focusable;
-      }
-
-      return columns[cell].focusable;
-    }
-
-    function canCellBeSelected(row, cell) {
-      if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
-        return false;
-      }
-
-      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
-      if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
-        return rowMetadata.selectable;
-      }
-
-      var columnMetadata = rowMetadata && rowMetadata.columns && (rowMetadata.columns[columns[cell].id] || rowMetadata.columns[cell]);
-      if (columnMetadata && typeof columnMetadata.selectable === "boolean") {
-        return columnMetadata.selectable;
-      }
-
-      return columns[cell].selectable;
-    }
-
-    function gotoCell(row, cell, forceEdit) {
-      if (!initialized) { return; }
-      if (!canCellBeActive(row, cell)) {
-        return;
-      }
-
-      if (!getEditorLock().commitCurrentEdit()) {
-        return;
-      }
-
-      scrollCellIntoView(row, cell, false);
-
-      var newCell = getCellNode(row, cell);
-
-      // if selecting the 'add new' row, start editing right away
-      setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
-
-      // if no editor was created, set the focus back on the grid
-      if (!currentEditor) {
-        setFocus();
-      }
-    }
-
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // IEditor implementation for the editor lock
-
-    function commitCurrentEdit() {
-      var item = getDataItem(activeRow);
-      var column = columns[activeCell];
-
-      if (currentEditor) {
-        if (currentEditor.isValueChanged()) {
-          var validationResults = currentEditor.validate();
-
-          if (validationResults.valid) {
-            if (activeRow < getDataLength()) {
-              var editCommand = {
-                row: activeRow,
-                cell: activeCell,
-                editor: currentEditor,
-                serializedValue: currentEditor.serializeValue(),
-                prevSerializedValue: serializedEditorValue,
-                execute: function () {
-                  this.editor.applyValue(item, this.serializedValue);
-                  updateRow(this.row);
-                  trigger(self.onCellChange, {
-                    row: activeRow,
-                    cell: activeCell,
-                    item: item,
-                    grid: self
-                  });
-                },
-                undo: function () {
-                  this.editor.applyValue(item, this.prevSerializedValue);
-                  updateRow(this.row);
-                  trigger(self.onCellChange, {
-                    row: activeRow,
-                    cell: activeCell,
-                    item: item,
-                    grid: self
-                  });
-                }
-              };
-
-              if (options.editCommandHandler) {
-                makeActiveCellNormal();
-                options.editCommandHandler(item, column, editCommand);
-              } else {
-                editCommand.execute();
-                makeActiveCellNormal();
-              }
-
-            } else {
-              var newItem = {};
-              currentEditor.applyValue(newItem, currentEditor.serializeValue());
-              makeActiveCellNormal();
-              trigger(self.onAddNewRow, {item: newItem, column: column, grid: self});
-            }
-
-            // check whether the lock has been re-acquired by event handlers
-            return !getEditorLock().isActive();
-          } else {
-            // Re-add the CSS class to trigger transitions, if any.
-            $(activeCellNode).removeClass("invalid");
-            $(activeCellNode).width();  // force layout
-            $(activeCellNode).addClass("invalid");
-
-            trigger(self.onValidationError, {
-              editor: currentEditor,
-              cellNode: activeCellNode,
-              validationResults: validationResults,
-              row: activeRow,
-              cell: activeCell,
-              column: column,
-              grid: self
-            });
-
-            currentEditor.focus();
-            return false;
-          }
-        }
-
-        makeActiveCellNormal();
-      }
-      return true;
-    }
-
-    function cancelCurrentEdit() {
-      makeActiveCellNormal();
-      return true;
-    }
-
-    function rowsToRanges(rows) {
-      var ranges = [];
-      var lastCell = columns.length - 1;
-      for (var i = 0; i < rows.length; i++) {
-        ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
-      }
-      return ranges;
-    }
-
-    function getSelectedRows() {
-      if (!selectionModel) {
-        throw "Selection model is not set";
-      }
-      return selectedRows;
-    }
-
-    function setSelectedRows(rows) {
-      if (!selectionModel) {
-        throw "Selection model is not set";
-      }
-      selectionModel.setSelectedRanges(rowsToRanges(rows));
-    }
-
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Debug
-
-    this.debug = function () {
-      var s = "";
-
-      s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);
-      s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);
-      s += ("\n" + "renderedRows:  " + renderedRows);
-      s += ("\n" + "numVisibleRows:  " + numVisibleRows);
-      s += ("\n" + "maxSupportedCssHeight:  " + maxSupportedCssHeight);
-      s += ("\n" + "n(umber of pages):  " + n);
-      s += ("\n" + "(current) page:  " + page);
-      s += ("\n" + "page height (ph):  " + ph);
-      s += ("\n" + "vScrollDir:  " + vScrollDir);
-
-      alert(s);
-    };
-
-    // a debug helper to be able to access private members
-    this.eval = function (expr) {
-      return eval(expr);
-    };
-
-    //////////////////////////////////////////////////////////////////////////////////////////////
-    // Public API
-
-    $.extend(this, {
-      "slickGridVersion": "2.2.4",
-
-      // Events
-      "onScroll": new Slick.Event(),
-      "onSort": new Slick.Event(),
-      "onHeaderMouseEnter": new Slick.Event(),
-      "onHeaderMouseLeave": new Slick.Event(),
-      "onHeaderContextMenu": new Slick.Event(),
-      "onHeaderClick": new Slick.Event(),
-      "onHeaderCellRendered": new Slick.Event(),
-      "onBeforeHeaderCellDestroy": new Slick.Event(),
-      "onHeaderRowCellRendered": new Slick.Event(),
-      "onBeforeHeaderRowCellDestroy": new Slick.Event(),
-      "onMouseEnter": new Slick.Event(),
-      "onMouseLeave": new Slick.Event(),
-      "onClick": new Slick.Event(),
-      "onDblClick": new Slick.Event(),
-      "onContextMenu": new Slick.Event(),
-      "onKeyDown": new Slick.Event(),
-      "onAddNewRow": new Slick.Event(),
-      "onValidationError": new Slick.Event(),
-      "onViewportChanged": new Slick.Event(),
-      "onColumnsReordered": new Slick.Event(),
-      "onColumnsResized": new Slick.Event(),
-      "onCellChange": new Slick.Event(),
-      "onBeforeEditCell": new Slick.Event(),
-      "onBeforeCellEditorDestroy": new Slick.Event(),
-      "onBeforeDestroy": new Slick.Event(),
-      "onActiveCellChanged": new Slick.Event(),
-      "onActiveCellPositionChanged": new Slick.Event(),
-      "onDragInit": new Slick.Event(),
-      "onDragStart": new Slick.Event(),
-      "onDrag": new Slick.Event(),
-      "onDragEnd": new Slick.Event(),
-      "onSelectedRowsChanged": new Slick.Event(),
-      "onCellCssStylesChanged": new Slick.Event(),
-
-      // Methods
-      "registerPlugin": registerPlugin,
-      "unregisterPlugin": unregisterPlugin,
-      "getColumns": getColumns,
-      "setColumns": setColumns,
-      "getColumnIndex": getColumnIndex,
-      "updateColumnHeader": updateColumnHeader,
-      "setSortColumn": setSortColumn,
-      "setSortColumns": setSortColumns,
-      "getSortColumns": getSortColumns,
-      "autosizeColumns": autosizeColumns,
-      "getOptions": getOptions,
-      "setOptions": setOptions,
-      "getData": getData,
-      "getDataLength": getDataLength,
-      "getDataItem": getDataItem,
-      "setData": setData,
-      "getSelectionModel": getSelectionModel,
-      "setSelectionModel": setSelectionModel,
-      "getSelectedRows": getSelectedRows,
-      "setSelectedRows": setSelectedRows,
-      "getContainerNode": getContainerNode,
-
-      "render": render,
-      "invalidate": invalidate,
-      "invalidateRow": invalidateRow,
-      "invalidateRows": invalidateRows,
-      "invalidateAllRows": invalidateAllRows,
-      "updateCell": updateCell,
-      "updateRow": updateRow,
-      "getViewport": getVisibleRange,
-      "getRenderedRange": getRenderedRange,
-      "resizeCanvas": resizeCanvas,
-      "updateRowCount": updateRowCount,
-      "scrollRowIntoView": scrollRowIntoView,
-      "scrollRowToTop": scrollRowToTop,
-      "scrollCellIntoView": scrollCellIntoView,
-      "getCanvasNode": getCanvasNode,
-      "focus": setFocus,
-
-      "getCellFromPoint": getCellFromPoint,
-      "getCellFromEvent": getCellFromEvent,
-      "getActiveCell": getActiveCell,
-      "setActiveCell": setActiveCell,
-      "getActiveCellNode": getActiveCellNode,
-      "getActiveCellPosition": getActiveCellPosition,
-      "resetActiveCell": resetActiveCell,
-      "editActiveCell": makeActiveCellEditable,
-      "getCellEditor": getCellEditor,
-      "getCellNode": getCellNode,
-      "getCellNodeBox": getCellNodeBox,
-      "canCellBeSelected": canCellBeSelected,
-      "canCellBeActive": canCellBeActive,
-      "navigatePrev": navigatePrev,
-      "navigateNext": navigateNext,
-      "navigateUp": navigateUp,
-      "navigateDown": navigateDown,
-      "navigateLeft": navigateLeft,
-      "navigateRight": navigateRight,
-      "navigatePageUp": navigatePageUp,
-      "navigatePageDown": navigatePageDown,
-      "gotoCell": gotoCell,
-      "getTopPanel": getTopPanel,
-      "setTopPanelVisibility": setTopPanelVisibility,
-      "setHeaderRowVisibility": setHeaderRowVisibility,
-      "getHeaderRow": getHeaderRow,
-      "getHeaderRowColumn": getHeaderRowColumn,
-      "getGridPosition": getGridPosition,
-      "flashCell": flashCell,
-      "addCellCssStyles": addCellCssStyles,
-      "setCellCssStyles": setCellCssStyles,
-      "removeCellCssStyles": removeCellCssStyles,
-      "getCellCssStyles": getCellCssStyles,
-
-      "init": finishInitialization,
-      "destroy": destroy,
-
-      // IEditor implementation
-      "getEditorLock": getEditorLock,
-      "getEditController": getEditController
-    });
-
-    init();
-  }
-}(jQuery));
+/**
+ * @license
+ * (c) 2009-2016 Michael Leibman
+ * michael{dot}leibman{at}gmail{dot}com
+ * http://github.com/mleibman/slickgrid
+ *
+ * Distributed under MIT license.
+ * All rights reserved.
+ *
+ * SlickGrid v2.3
+ *
+ * NOTES:
+ *     Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
+ *     This increases the speed dramatically, but can only be done safely because there are no event handlers
+ *     or data associated with any cell/row DOM nodes.  Cell editors must make sure they implement .destroy()
+ *     and do proper cleanup.
+ */
+
+// make sure required JavaScript modules are loaded
+if (typeof jQuery === "undefined") {
+  throw new Error("SlickGrid requires jquery module to be loaded");
+}
+if (!jQuery.fn.drag) {
+  throw new Error("SlickGrid requires jquery.event.drag module to be loaded");
+}
+if (typeof Slick === "undefined") {
+  throw new Error("slick.core.js not loaded");
+}
+
+
+(function ($) {
+  // Slick.Grid
+  $.extend(true, window, {
+    Slick: {
+      Grid: SlickGrid
+    }
+  });
+
+  // shared across all grids on the page
+  var scrollbarDimensions;
+  var maxSupportedCssHeight;  // browser's breaking point
+
+  //////////////////////////////////////////////////////////////////////////////////////////////
+  // SlickGrid class implementation (available as Slick.Grid)
+
+  /**
+   * Creates a new instance of the grid.
+   * @class SlickGrid
+   * @constructor
+   * @param {Node}              container   Container node to create the grid in.
+   * @param {Array,Object}      data        An array of objects for databinding.
+   * @param {Array}             columns     An array of column definitions.
+   * @param {Object}            options     Grid options.
+   **/
+  function SlickGrid(container, data, columns, options) {
+    // settings
+    var defaults = {
+      explicitInitialization: false,
+      rowHeight: 25,
+      defaultColumnWidth: 80,
+      enableAddRow: false,
+      leaveSpaceForNewRows: false,
+      editable: false,
+      autoEdit: true,
+      enableCellNavigation: true,
+      enableColumnReorder: true,
+      asyncEditorLoading: false,
+      asyncEditorLoadDelay: 100,
+      forceFitColumns: false,
+      enableAsyncPostRender: false,
+      asyncPostRenderDelay: 50,
+      enableAsyncPostRenderCleanup: false,
+      asyncPostRenderCleanupDelay: 40,
+      autoHeight: false,
+      editorLock: Slick.GlobalEditorLock,
+      showHeaderRow: false,
+      headerRowHeight: 25,
+      createFooterRow: false,
+      showFooterRow: false,
+      footerRowHeight: 25,
+      showTopPanel: false,
+      topPanelHeight: 25,
+      formatterFactory: null,
+      editorFactory: null,
+      cellFlashingCssClass: "flashing",
+      selectedCellCssClass: "selected",
+      multiSelect: true,
+      enableTextSelectionOnCells: false,
+      dataItemColumnValueExtractor: null,
+      fullWidthRows: false,
+      multiColumnSort: false,
+      numberedMultiColumnSort: false,
+      tristateMultiColumnSort: false,
+      defaultFormatter: defaultFormatter,
+      forceSyncScrolling: false,
+      addNewRowCssClass: "new-row",
+      preserveCopiedSelectionOnPaste: false
+    };
+
+    var columnDefaults = {
+      name: "",
+      resizable: true,
+      sortable: false,
+      minWidth: 30,
+      rerenderOnResize: false,
+      headerCssClass: null,
+      defaultSortAsc: true,
+      focusable: true,
+      selectable: true
+    };
+
+    // scroller
+    var th;   // virtual height
+    var h;    // real scrollable height
+    var ph;   // page height
+    var n;    // number of pages
+    var cj;   // "jumpiness" coefficient
+
+    var page = 0;       // current page
+    var offset = 0;     // current page offset
+    var vScrollDir = 1;
+
+    // private
+    var initialized = false;
+    var $container;
+    var uid = "slickgrid_" + Math.round(1000000 * Math.random());
+    var self = this;
+    var $focusSink, $focusSink2;
+    var $headerScroller;
+    var $headers;
+    var $headerRow, $headerRowScroller, $headerRowSpacer;
+    var $footerRow, $footerRowScroller, $footerRowSpacer;
+    var $topPanelScroller;
+    var $topPanel;
+    var $viewport;
+    var $canvas;
+    var $style;
+    var $boundAncestors;
+    var stylesheet, columnCssRulesL, columnCssRulesR;
+    var viewportH, viewportW;
+    var canvasWidth;
+    var viewportHasHScroll, viewportHasVScroll;
+    var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding
+        cellWidthDiff = 0, cellHeightDiff = 0, jQueryNewWidthBehaviour = false;
+    var absoluteColumnMinWidth;
+    var sortIndicatorCssClass = "slick-sort-indicator";
+    
+    var tabbingDirection = 1;
+    var activePosX;
+    var activeRow, activeCell;
+    var activeCellNode = null;
+    var currentEditor = null;
+    var serializedEditorValue;
+    var editController;
+
+    var rowsCache = {};
+    var renderedRows = 0;
+    var numVisibleRows;
+    var prevScrollTop = 0;
+    var scrollTop = 0;
+    var lastRenderedScrollTop = 0;
+    var lastRenderedScrollLeft = 0;
+    var prevScrollLeft = 0;
+    var scrollLeft = 0;
+
+    var selectionModel;
+    var selectedRows = [];
+
+    var plugins = [];
+    var cellCssClasses = {};
+
+    var columnsById = {};
+    var sortColumns = [];
+    var columnPosLeft = [];
+    var columnPosRight = [];
+
+    var pagingActive = false;
+    var pagingIsLastPage = false;
+    
+    // async call handles
+    var h_editorLoader = null;
+    var h_render = null;
+    var h_postrender = null;
+    var h_postrenderCleanup = null;
+    var postProcessedRows = {};
+    var postProcessToRow = null;
+    var postProcessFromRow = null;
+    var postProcessedCleanupQueue = [];
+    var postProcessgroupId = 0;
+
+    // perf counters
+    var counter_rows_rendered = 0;
+    var counter_rows_removed = 0;
+
+    // These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac.
+    // See http://crbug.com/312427.
+    var rowNodeFromLastMouseWheelEvent;  // this node must not be deleted while inertial scrolling
+    var zombieRowNodeFromLastMouseWheelEvent;  // node that was hidden instead of getting deleted
+    var zombieRowCacheFromLastMouseWheelEvent;  // row cache for above node
+    var zombieRowPostProcessedFromLastMouseWheelEvent;  // post processing references for above node
+
+    // store css attributes if display:none is active in container or parent
+    var cssShow = { position: 'absolute', visibility: 'hidden', display: 'block' };
+    var $hiddenParents;
+    var oldProps = [];
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Initialization
+
+    function init() {
+      if (container instanceof jQuery) { 
+        $container = container; 
+      } else { 
+        $container = $(container); 
+      }
+      if ($container.length < 1) {
+        throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
+      }
+
+      cacheCssForHiddenInit();
+
+      // calculate these only once and share between grid instances
+      maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
+      scrollbarDimensions = scrollbarDimensions || measureScrollbar();
+
+      options = $.extend({}, defaults, options);
+      validateAndEnforceOptions();
+      columnDefaults.width = options.defaultColumnWidth;
+
+      columnsById = {};
+      for (var i = 0; i < columns.length; i++) {
+        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
+        columnsById[m.id] = i;
+        if (m.minWidth && m.width < m.minWidth) {
+          m.width = m.minWidth;
+        }
+        if (m.maxWidth && m.width > m.maxWidth) {
+          m.width = m.maxWidth;
+        }
+      }
+
+      // validate loaded JavaScript modules against requested options
+      if (options.enableColumnReorder && !$.fn.sortable) {
+        throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded");
+      }
+
+      editController = {
+        "commitCurrentEdit": commitCurrentEdit,
+        "cancelCurrentEdit": cancelCurrentEdit
+      };
+
+      $container
+          .empty()
+          .css("overflow", "hidden")
+          .css("outline", 0)
+          .addClass(uid)
+          .addClass("ui-widget");
+
+      // set up a positioning container if needed
+      if (!/relative|absolute|fixed/.test($container.css("position"))) {
+        $container.css("position", "relative");
+      }
+
+      $focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
+
+      $headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+      $headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
+      $headers.width(getHeadersWidth());
+
+      $headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+      $headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
+      $headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
+          .css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
+          .appendTo($headerRowScroller);
+
+      $topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+      $topPanel = $("<div class='slick-top-panel' style='width:10000px' />").appendTo($topPanelScroller);
+
+      if (!options.showTopPanel) {
+        $topPanelScroller.hide();
+      }
+
+      if (!options.showHeaderRow) {
+        $headerRowScroller.hide();
+      }
+
+      $viewport = $("<div class='slick-viewport' style='width:100%;overflow:auto;outline:0;position:relative;;'>").appendTo($container);
+      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
+
+      $canvas = $("<div class='grid-canvas' />").appendTo($viewport);
+
+      if (options.createFooterRow) {
+        $footerRowScroller = $("<div class='slick-footerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
+        $footerRow = $("<div class='slick-footerrow-columns' />").appendTo($footerRowScroller);
+        $footerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
+            .css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
+            .appendTo($footerRowScroller);
+
+        if (!options.showFooterRow) {
+          $footerRowScroller.hide();
+        }
+      }
+
+      if (options.numberedMultiColumnSort) { sortIndicatorCssClass = "slick-sort-indicator-numbered"; }
+
+      $focusSink2 = $focusSink.clone().appendTo($container);
+
+      if (!options.explicitInitialization) {
+        finishInitialization();
+      }
+    }
+
+    function finishInitialization() {
+      if (!initialized) {
+        initialized = true;
+
+        viewportW = parseFloat($.css($container[0], "width", true));
+
+        // header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?)
+        // calculate the diff so we can set consistent sizes
+        measureCellPaddingAndBorder();
+
+        // for usability reasons, all text selection in SlickGrid is disabled
+        // with the exception of input and textarea elements (selection must
+        // be enabled there so that editors work as expected); note that
+        // selection in grid cells (grid body) is already unavailable in
+        // all browsers except IE
+        disableSelection($headers); // disable all text selection in header (including input and textarea)
+
+        if (!options.enableTextSelectionOnCells) {
+          // disable text selection in grid cells except in input and textarea elements
+          // (this is IE-specific, because selectstart event will only fire in IE)
+          $viewport.on("selectstart.ui", function (event) {
+            return $(event.target).is("input,textarea");
+          });
+        }
+
+        updateColumnCaches();
+        createColumnHeaders();
+        setupColumnSort();
+        createCssRules();
+        resizeCanvas();
+        bindAncestorScrollEvents();
+
+        $container
+            .on("resize.slickgrid", resizeCanvas);
+        $viewport
+            //.on("click", handleClick)
+            .on("scroll", handleScroll);
+        $headerScroller
+            .on("contextmenu", handleHeaderContextMenu)
+            .on("click", handleHeaderClick)
+            .on("mouseenter", ".slick-header-column", handleHeaderMouseEnter)
+            .on("mouseleave", ".slick-header-column", handleHeaderMouseLeave);
+        $headerRowScroller
+            .on("scroll", handleHeaderRowScroll);
+
+        if (options.createFooterRow) {
+          $footerRowScroller
+              .on("scroll", handleFooterRowScroll);
+        }
+
+        $focusSink.add($focusSink2)
+            .on("keydown", handleKeyDown);
+        $canvas
+            .on("keydown", handleKeyDown)
+            .on("click", handleClick)
+            .on("dblclick", handleDblClick)
+            .on("contextmenu", handleContextMenu)
+            .on("draginit", handleDragInit)
+            .on("dragstart", {distance: 3}, handleDragStart)
+            .on("drag", handleDrag)
+            .on("dragend", handleDragEnd)
+            .on("mouseenter", ".slick-cell", handleMouseEnter)
+            .on("mouseleave", ".slick-cell", handleMouseLeave);
+
+        // Work around http://crbug.com/312427.
+        if (navigator.userAgent.toLowerCase().match(/webkit/) &&
+            navigator.userAgent.toLowerCase().match(/macintosh/)) {
+          $canvas.on("mousewheel", handleMouseWheel);
+        }
+        restoreCssFromHiddenInit();
+      }
+    }
+
+    function cacheCssForHiddenInit() {
+      // handle display:none on container or container parents
+      $hiddenParents = $container.parents().addBack().not(':visible');
+      $hiddenParents.each(function() {
+        var old = {};
+        for ( var name in cssShow ) {
+          old[ name ] = this.style[ name ];
+          this.style[ name ] = cssShow[ name ];
+        }
+        oldProps.push(old);
+      });
+    }
+
+    function restoreCssFromHiddenInit() {
+      // finish handle display:none on container or container parents
+      // - put values back the way they were
+      $hiddenParents.each(function(i) {
+        var old = oldProps[i];
+        for ( var name in cssShow ) {
+          this.style[ name ] = old[ name ];
+        }
+      });
+    }
+
+    function registerPlugin(plugin) {
+      plugins.unshift(plugin);
+      plugin.init(self);
+    }
+
+    function unregisterPlugin(plugin) {
+      for (var i = plugins.length; i >= 0; i--) {
+        if (plugins[i] === plugin) {
+          if (plugins[i].destroy) {
+            plugins[i].destroy();
+          }
+          plugins.splice(i, 1);
+          break;
+        }
+      }
+    }
+
+    function setSelectionModel(model) {
+      if (selectionModel) {
+        selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged);
+        if (selectionModel.destroy) {
+          selectionModel.destroy();
+        }
+      }
+
+      selectionModel = model;
+      if (selectionModel) {
+        selectionModel.init(self);
+        selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged);
+      }
+    }
+
+    function getSelectionModel() {
+      return selectionModel;
+    }
+
+    function getCanvasNode() {
+      return $canvas[0];
+    }
+
+    function measureScrollbar() {
+      var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
+      var dim = {
+        width: $c.width() - $c[0].clientWidth,
+        height: $c.height() - $c[0].clientHeight
+      };
+      $c.remove();
+      return dim;
+    }
+
+    function getColumnTotalWidth(includeScrollbar) {
+      var totalWidth = 0;
+      for (var i = 0, ii = columns.length; i < ii; i++) {
+        var width = columns[i].width;
+        totalWidth += width;
+      }
+      if (includeScrollbar) {
+        totalWidth += scrollbarDimensions.width;
+      }
+      return totalWidth;
+    }
+
+    function getHeadersWidth() {
+      var headersWidth = getColumnTotalWidth(true);
+      return Math.max(headersWidth, viewportW) + 1000;
+    }
+
+    function getCanvasWidth() {
+      var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
+      var rowWidth = 0;
+      var i = columns.length;
+      while (i--) {
+        rowWidth += columns[i].width;
+      }
+      return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
+    }
+
+    function updateCanvasWidth(forceColumnWidthsUpdate) {
+      var oldCanvasWidth = canvasWidth;
+      canvasWidth = getCanvasWidth();
+
+      if (canvasWidth != oldCanvasWidth) {
+        $canvas.width(canvasWidth);
+        $headerRow.width(canvasWidth);
+        if (options.createFooterRow) { $footerRow.width(canvasWidth); }
+        $headers.width(getHeadersWidth());
+        viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
+      }
+
+       var w=canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0);
+       $headerRowSpacer.width(w);
+       if (options.createFooterRow) { $footerRowSpacer.width(w); }
+
+      if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
+        applyColumnWidths();
+      }
+    }
+
+    function disableSelection($target) {
+      if ($target && $target.jquery) {
+        $target
+            .attr("unselectable", "on")
+            .css("MozUserSelect", "none")
+            .on("selectstart.ui", function () {
+              return false;
+            }); // from jquery:ui.core.js 1.7.2
+      }
+    }
+
+    function getMaxSupportedCssHeight() {
+      var supportedHeight = 1000000;
+      // FF reports the height back but still renders blank after ~6M px
+      var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000;
+      var div = $("<div style='display:none' />").appendTo(document.body);
+
+      while (true) {
+        var test = supportedHeight * 2;
+        div.css("height", test);
+        if (test > testUpTo || div.height() !== test) {
+          break;
+        } else {
+          supportedHeight = test;
+        }
+      }
+
+      div.remove();
+      return supportedHeight;
+    }
+
+    // TODO:  this is static.  need to handle page mutation.
+    function bindAncestorScrollEvents() {
+      var elem = $canvas[0];
+      while ((elem = elem.parentNode) != document.body && elem != null) {
+        // bind to scroll containers only
+        if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) {
+          var $elem = $(elem);
+          if (!$boundAncestors) {
+            $boundAncestors = $elem;
+          } else {
+            $boundAncestors = $boundAncestors.add($elem);
+          }
+          $elem.on("scroll." + uid, handleActiveCellPositionChange);
+        }
+      }
+    }
+
+    function unbindAncestorScrollEvents() {
+      if (!$boundAncestors) {
+        return;
+      }
+      $boundAncestors.off("scroll." + uid);
+      $boundAncestors = null;
+    }
+
+    function updateColumnHeader(columnId, title, toolTip) {
+      if (!initialized) { return; }
+      var idx = getColumnIndex(columnId);
+      if (idx == null) {
+        return;
+      }
+
+      var columnDef = columns[idx];
+      var $header = $headers.children().eq(idx);
+      if ($header) {
+        if (title !== undefined) {
+          columns[idx].name = title;
+        }
+        if (toolTip !== undefined) {
+          columns[idx].toolTip = toolTip;
+        }
+
+        trigger(self.onBeforeHeaderCellDestroy, {
+          "node": $header[0],
+          "column": columnDef,
+          "grid": self
+        });
+
+        $header
+            .attr("title", toolTip || "")
+            .children().eq(0).html(title);
+
+        trigger(self.onHeaderCellRendered, {
+          "node": $header[0],
+          "column": columnDef,
+          "grid": self
+        });
+      }
+    }
+
+    function getHeaderRow() {
+      return $headerRow[0];
+    }
+
+    function getFooterRow() {
+      return $footerRow[0];
+    }
+
+    function getHeaderRowColumn(columnId) {
+      var idx = getColumnIndex(columnId);
+      var $header = $headerRow.children().eq(idx);
+      return $header && $header[0];
+    }
+
+    function getFooterRowColumn(columnId) {
+      var idx = getColumnIndex(columnId);
+      var $footer = $footerRow.children().eq(idx);
+      return $footer && $footer[0];
+    }
+
+    function createColumnHeaders() {
+      function onMouseEnter() {
+        $(this).addClass("ui-state-hover");
+      }
+
+      function onMouseLeave() {
+        $(this).removeClass("ui-state-hover");
+      }
+
+      $headers.find(".slick-header-column")
+        .each(function() {
+          var columnDef = $(this).data("column");
+          if (columnDef) {
+            trigger(self.onBeforeHeaderCellDestroy, {
+              "node": this,
+              "column": columnDef,
+              "grid": self
+            });
+          }
+        });
+      $headers.empty();
+      $headers.width(getHeadersWidth());
+
+      $headerRow.find(".slick-headerrow-column")
+        .each(function() {
+          var columnDef = $(this).data("column");
+          if (columnDef) {
+            trigger(self.onBeforeHeaderRowCellDestroy, {
+              "node": this,
+              "column": columnDef,
+              "grid": self
+            });
+          }
+        });
+      $headerRow.empty();
+
+      if (options.createFooterRow) {
+        $footerRow.find(".slick-footerrow-column")
+          .each(function() {
+            var columnDef = $(this).data("column");
+            if (columnDef) {
+              trigger(self.onBeforeFooterRowCellDestroy, {
+                "node": this,
+                "column": columnDef
+              });
+            }
+          });
+        $footerRow.empty();
+      }
+
+      for (var i = 0; i < columns.length; i++) {
+        var m = columns[i];
+
+        var header = $("<div class='ui-state-default slick-header-column' />")
+            .html("<span class='slick-column-name'>" + m.name + "</span>")
+            .width(m.width - headerColumnWidthDiff)
+            .attr("id", "" + uid + m.id)
+            .attr("title", m.toolTip || "")
+            .data("column", m)
+            .addClass(m.headerCssClass || "")
+            .appendTo($headers);
+
+        if (options.enableColumnReorder || m.sortable) {
+          header
+            .on('mouseenter', onMouseEnter)
+            .on('mouseleave', onMouseLeave);
+        }
+
+        if (m.sortable) {
+          header.addClass("slick-header-sortable");
+          header.append("<span class='" + sortIndicatorCssClass + "' />");
+        }
+
+        trigger(self.onHeaderCellRendered, {
+          "node": header[0],
+          "column": m,
+          "grid": self
+        });
+
+        if (options.showHeaderRow) {
+          var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>")
+              .data("column", m)
+              .appendTo($headerRow);
+
+          trigger(self.onHeaderRowCellRendered, {
+            "node": headerRowCell[0],
+            "column": m,
+            "grid": self
+          });
+        }
+        if (options.createFooterRow && options.showFooterRow) {
+          var footerRowCell = $("<div class='ui-state-default slick-footerrow-column l" + i   + " r" + i + "'></div>")
+              .data("column", m)
+              .appendTo($footerRow);
+
+          trigger(self.onFooterRowCellRendered, {
+            "node": footerRowCell[0],
+            "column": m
+          });
+        }
+      }
+
+      setSortColumns(sortColumns);
+      setupColumnResize();
+      if (options.enableColumnReorder) {
+        if (typeof options.enableColumnReorder == 'function') {
+            options.enableColumnReorder(self, $headers, headerColumnWidthDiff, setColumns, setupColumnResize, columns, getColumnIndex, uid, trigger);
+        } else {
+            setupColumnReorder();
+        }
+      }
+    }
+    
+    function setupColumnSort() {
+      $headers.click(function (e) {
+        // temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328)
+        e.metaKey = e.metaKey || e.ctrlKey;
+
+        if ($(e.target).hasClass("slick-resizable-handle")) {
+          return;
+        }
+
+        var $col = $(e.target).closest(".slick-header-column");
+        if (!$col.length) {
+          return;
+        }
+
+        var column = $col.data("column");
+        if (column.sortable) {
+          if (!getEditorLock().commitCurrentEdit()) {
+            return;
+          }
+
+          var sortColumn = null;
+          var i = 0;
+          for (; i < sortColumns.length; i++) {
+            if (sortColumns[i].columnId == column.id) {
+              sortColumn = sortColumns[i];
+              sortColumn.sortAsc = !sortColumn.sortAsc;
+              break;
+            }
+          }
+          var hadSortCol = !!sortColumn;
+          
+          if (options.tristateMultiColumnSort) {
+              if (!sortColumn) { 
+                sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc }; 
+              }
+              if (hadSortCol && sortColumn.sortAsc) {
+                // three state: remove sort rather than go back to ASC
+                sortColumns.splice(i, 1);
+                sortColumn = null;
+              }
+              if (!options.multiColumnSort) { sortColumns = []; }
+              if (sortColumn && (!hadSortCol || !options.multiColumnSort)) { 
+                sortColumns.push(sortColumn); 
+              }
+          } else {
+              // legacy behaviour
+              if (e.metaKey && options.multiColumnSort) {
+                if (sortColumn) {
+                  sortColumns.splice(i, 1);
+                }
+              }
+              else {
+                if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
+                  sortColumns = [];
+                }
+
+                if (!sortColumn) {
+                  sortColumn = { columnId: column.id, sortAsc: column.defaultSortAsc };
+                  sortColumns.push(sortColumn);
+                } else if (sortColumns.length == 0) {
+                  sortColumns.push(sortColumn);
+                }
+              }              
+          }
+          
+          setSortColumns(sortColumns);
+
+          if (sortColumns.length > 0) { 
+              if (!options.multiColumnSort) {
+                trigger(self.onSort, {
+                  multiColumnSort: false,
+                  sortCol: column,
+                  sortAsc: sortColumns[0].sortAsc,
+                  grid: self}, e);
+              } else {
+                trigger(self.onSort, {
+                  multiColumnSort: true,
+                  sortCols: $.map(sortColumns, function(col) {
+                    return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
+                  }),
+                  grid: self}, e);
+              }
+          }
+        }
+      });
+    }
+
+    function setupColumnReorder() {
+      $headers.filter(":ui-sortable").sortable("destroy");
+      $headers.sortable({
+        containment: "parent",
+        distance: 3,
+        axis: "x",
+        cursor: "default",
+        tolerance: "intersection",
+        helper: "clone",
+        placeholder: "slick-sortable-placeholder ui-state-default slick-header-column",
+        start: function (e, ui) {
+          ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff);
+          $(ui.helper).addClass("slick-header-column-active");
+        },
+        beforeStop: function (e, ui) {
+          $(ui.helper).removeClass("slick-header-column-active");
+        },
+        stop: function (e) {
+          if (!getEditorLock().commitCurrentEdit()) {
+            $(this).sortable("cancel");
+            return;
+          }
+
+          var reorderedIds = $headers.sortable("toArray");
+          var reorderedColumns = [];
+          for (var i = 0; i < reorderedIds.length; i++) {
+            reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]);
+          }
+          setColumns(reorderedColumns);
+
+          trigger(self.onColumnsReordered, {grid: self});
+          e.stopPropagation();
+          setupColumnResize();
+        }
+      });
+    }
+
+    function setupColumnResize() {
+      var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable;
+      columnElements = $headers.children();
+      columnElements.find(".slick-resizable-handle").remove();
+      columnElements.each(function (i, e) {
+        if (i >= columns.length) { return; }
+        if (columns[i].resizable) {
+          if (firstResizable === undefined) {
+            firstResizable = i;
+          }
+          lastResizable = i;
+        }
+      });
+      if (firstResizable === undefined) {
+        return;
+      }
+      columnElements.each(function (i, e) {
+        if (i >= columns.length) { return; }
+        if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
+          return;
+        }
+        $col = $(e);
+        $("<div class='slick-resizable-handle' />")
+            .appendTo(e)
+            .on("dragstart", function (e, dd) {
+              if (!getEditorLock().commitCurrentEdit()) {
+                return false;
+              }
+              pageX = e.pageX;
+              $(this).parent().addClass("slick-header-column-active");
+              var shrinkLeewayOnRight = null, stretchLeewayOnRight = null;
+              // lock each column's width option to current width
+              columnElements.each(function (i, e) {
+                if (i >= columns.length) { return; }
+                columns[i].previousWidth = $(e).outerWidth();
+              });
+              if (options.forceFitColumns) {
+                shrinkLeewayOnRight = 0;
+                stretchLeewayOnRight = 0;
+                // colums on right affect maxPageX/minPageX
+                for (j = i + 1; j < columns.length; j++) {
+                  c = columns[j];
+                  if (c.resizable) {
+                    if (stretchLeewayOnRight !== null) {
+                      if (c.maxWidth) {
+                        stretchLeewayOnRight += c.maxWidth - c.previousWidth;
+                      } else {
+                        stretchLeewayOnRight = null;
+                      }
+                    }
+                    shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                  }
+                }
+              }
+              var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
+              for (j = 0; j <= i; j++) {
+                // columns on left only affect minPageX
+                c = columns[j];
+                if (c.resizable) {
+                  if (stretchLeewayOnLeft !== null) {
+                    if (c.maxWidth) {
+                      stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
+                    } else {
+                      stretchLeewayOnLeft = null;
+                    }
+                  }
+                  shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                }
+              }
+              if (shrinkLeewayOnRight === null) {
+                shrinkLeewayOnRight = 100000;
+              }
+              if (shrinkLeewayOnLeft === null) {
+                shrinkLeewayOnLeft = 100000;
+              }
+              if (stretchLeewayOnRight === null) {
+                stretchLeewayOnRight = 100000;
+              }
+              if (stretchLeewayOnLeft === null) {
+                stretchLeewayOnLeft = 100000;
+              }
+              maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
+              minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
+            })
+            .on("drag", function (e, dd) {
+              var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x;
+              if (d < 0) { // shrink column
+                x = d;
+                for (j = i; j >= 0; j--) {
+                  c = columns[j];
+                  if (c.resizable) {
+                    actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                    if (x && c.previousWidth + x < actualMinWidth) {
+                      x += c.previousWidth - actualMinWidth;
+                      c.width = actualMinWidth;
+                    } else {
+                      c.width = c.previousWidth + x;
+                      x = 0;
+                    }
+                  }
+                }
+
+                if (options.forceFitColumns) {
+                  x = -d;
+                  for (j = i + 1; j < columns.length; j++) {
+                    c = columns[j];
+                    if (c.resizable) {
+                      if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
+                        x -= c.maxWidth - c.previousWidth;
+                        c.width = c.maxWidth;
+                      } else {
+                        c.width = c.previousWidth + x;
+                        x = 0;
+                      }
+                    }
+                  }
+                }
+              } else { // stretch column
+                x = d;
+                for (j = i; j >= 0; j--) {
+                  c = columns[j];
+                  if (c.resizable) {
+                    if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
+                      x -= c.maxWidth - c.previousWidth;
+                      c.width = c.maxWidth;
+                    } else {
+                      c.width = c.previousWidth + x;
+                      x = 0;
+                    }
+                  }
+                }
+
+                if (options.forceFitColumns) {
+                  x = -d;
+                  for (j = i + 1; j < columns.length; j++) {
+                    c = columns[j];
+                    if (c.resizable) {
+                      actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
+                      if (x && c.previousWidth + x < actualMinWidth) {
+                        x += c.previousWidth - actualMinWidth;
+                        c.width = actualMinWidth;
+                      } else {
+                        c.width = c.previousWidth + x;
+                        x = 0;
+                      }
+                    }
+                  }
+                }
+              }
+              applyColumnHeaderWidths();
+              if (options.syncColumnCellResize) {
+                applyColumnWidths();
+              }
+            })
+            .on("dragend", function (e, dd) {
+              var newWidth;
+              $(this).parent().removeClass("slick-header-column-active");
+              for (j = 0; j < columns.length; j++) {
+                c = columns[j];
+                newWidth = $(columnElements[j]).outerWidth();
+
+                if (c.previousWidth !== newWidth && c.rerenderOnResize) {
+                  invalidateAllRows();
+                }
+              }
+              updateCanvasWidth(true);
+              render();
+              trigger(self.onColumnsResized, {grid: self});
+            });
+      });
+    }
+
+    function getVBoxDelta($el) {
+      var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
+      var delta = 0;
+      $.each(p, function (n, val) {
+        delta += parseFloat($el.css(val)) || 0;
+      });
+      return delta;
+    }
+
+    function measureCellPaddingAndBorder() {
+      var el;
+      var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"];
+      var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
+
+      // jquery prior to version 1.8 handles .width setter/getter as a direct css write/read
+      // jquery 1.8 changed .width to read the true inner element width if box-sizing is set to border-box, and introduced a setter for .outerWidth
+      // so for equivalent functionality, prior to 1.8 use .width, and after use .outerWidth
+      var verArray = $.fn.jquery.split('.');
+      jQueryNewWidthBehaviour = (verArray[0]==1 && verArray[1]>=8) ||  verArray[0] >=2;
+
+      el = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers);
+      headerColumnWidthDiff = headerColumnHeightDiff = 0;
+      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
+        $.each(h, function (n, val) {
+          headerColumnWidthDiff += parseFloat(el.css(val)) || 0;
+        });
+        $.each(v, function (n, val) {
+          headerColumnHeightDiff += parseFloat(el.css(val)) || 0;
+        });
+      }
+      el.remove();
+
+      var r = $("<div class='slick-row' />").appendTo($canvas);
+      el = $("<div class='slick-cell' id='' style='visibility:hidden'>-</div>").appendTo(r);
+      cellWidthDiff = cellHeightDiff = 0;
+      if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
+        $.each(h, function (n, val) {
+          cellWidthDiff += parseFloat(el.css(val)) || 0;
+        });
+        $.each(v, function (n, val) {
+          cellHeightDiff += parseFloat(el.css(val)) || 0;
+        });
+      }
+      r.remove();
+
+      absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff);
+    }
+
+    function createCssRules() {
+      $style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
+      var rowHeight = (options.rowHeight - cellHeightDiff);
+      var rules = [
+        "." + uid + " .slick-header-column { left: 1000px; }",
+        "." + uid + " .slick-top-panel { height:" + options.topPanelHeight + "px; }",
+        "." + uid + " .slick-headerrow-columns { height:" + options.headerRowHeight + "px; }",
+        "." + uid + " .slick-footerrow-columns { height:" + options.footerRowHeight + "px; }",
+        "." + uid + " .slick-cell { height:" + rowHeight + "px; }",
+        "." + uid + " .slick-row { height:" + options.rowHeight + "px; }"
+      ];
+
+      for (var i = 0; i < columns.length; i++) {
+        rules.push("." + uid + " .l" + i + " { }");
+        rules.push("." + uid + " .r" + i + " { }");
+      }
+
+      if ($style[0].styleSheet) { // IE
+        $style[0].styleSheet.cssText = rules.join(" ");
+      } else {
+        $style[0].appendChild(document.createTextNode(rules.join(" ")));
+      }
+    }
+
+    function getColumnCssRules(idx) {
+      var i;
+      if (!stylesheet) {
+        var sheets = document.styleSheets;
+        for (i = 0; i < sheets.length; i++) {
+          if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) {
+            stylesheet = sheets[i];
+            break;
+          }
+        }
+
+        if (!stylesheet) {
+          throw new Error("Cannot find stylesheet.");
+        }
+
+        // find and cache column CSS rules
+        columnCssRulesL = [];
+        columnCssRulesR = [];
+        var cssRules = (stylesheet.cssRules || stylesheet.rules);
+        var matches, columnIdx;
+        for (i = 0; i < cssRules.length; i++) {
+          var selector = cssRules[i].selectorText;
+          if (matches = /\.l\d+/.exec(selector)) {
+            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
+            columnCssRulesL[columnIdx] = cssRules[i];
+          } else if (matches = /\.r\d+/.exec(selector)) {
+            columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
+            columnCssRulesR[columnIdx] = cssRules[i];
+          }
+        }
+      }
+
+      return {
+        "left": columnCssRulesL[idx],
+        "right": columnCssRulesR[idx]
+      };
+    }
+
+    function removeCssRules() {
+      $style.remove();
+      stylesheet = null;
+    }
+
+    function destroy() {
+      getEditorLock().cancelCurrentEdit();
+
+      trigger(self.onBeforeDestroy, {grid: self});
+
+      var i = plugins.length;
+      while(i--) {
+        unregisterPlugin(plugins[i]);
+      }
+
+      if (options.enableColumnReorder) {
+          $headers.filter(":ui-sortable").sortable("destroy");
+      }
+
+      unbindAncestorScrollEvents();
+      $container.off(".slickgrid");
+      removeCssRules();
+
+      $canvas.off("draginit dragstart dragend drag");
+      $container.empty().removeClass(uid);
+    }
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // General
+
+    function trigger(evt, args, e) {
+      e = e || new Slick.EventData();
+      args = args || {};
+      args.grid = self;
+      return evt.notify(args, e, self);
+    }
+
+    function getEditorLock() {
+      return options.editorLock;
+    }
+
+    function getEditController() {
+      return editController;
+    }
+
+    function getColumnIndex(id) {
+      return columnsById[id];
+    }
+
+    function autosizeColumns() {
+      var i, c,
+          widths = [],
+          shrinkLeeway = 0,
+          total = 0,
+          prevTotal,
+          availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
+
+      for (i = 0; i < columns.length; i++) {
+        c = columns[i];
+        widths.push(c.width);
+        total += c.width;
+        if (c.resizable) {
+          shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth);
+        }
+      }
+
+      // shrink
+      prevTotal = total;
+      while (total > availWidth && shrinkLeeway) {
+        var shrinkProportion = (total - availWidth) / shrinkLeeway;
+        for (i = 0; i < columns.length && total > availWidth; i++) {
+          c = columns[i];
+          var width = widths[i];
+          if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) {
+            continue;
+          }
+          var absMinWidth = Math.max(c.minWidth, absoluteColumnMinWidth);
+          var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1;
+          shrinkSize = Math.min(shrinkSize, width - absMinWidth);
+          total -= shrinkSize;
+          shrinkLeeway -= shrinkSize;
+          widths[i] -= shrinkSize;
+        }
+        if (prevTotal <= total) {  // avoid infinite loop
+          break;
+        }
+        prevTotal = total;
+      }
+
+      // grow
+      prevTotal = total;
+      while (total < availWidth) {
+        var growProportion = availWidth / total;
+        for (i = 0; i < columns.length && total < availWidth; i++) {
+          c = columns[i];
+          var currentWidth = widths[i];
+          var growSize;
+
+          if (!c.resizable || c.maxWidth <= currentWidth) {
+            growSize = 0;
+          } else {
+            growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1;
+          }
+          total += growSize;
+          widths[i] += (total <= availWidth ? growSize : 0);
+        }
+        if (prevTotal >= total) {  // avoid infinite loop
+          break;
+        }
+        prevTotal = total;
+      }
+
+      var reRender = false;
+      for (i = 0; i < columns.length; i++) {
+        if (columns[i].rerenderOnResize && columns[i].width != widths[i]) {
+          reRender = true;
+        }
+        columns[i].width = widths[i];
+      }
+
+      applyColumnHeaderWidths();
+      updateCanvasWidth(true);
+      if (reRender) {
+        invalidateAllRows();
+        render();
+      }
+    }
+
+    function applyColumnHeaderWidths() {
+      if (!initialized) { return; }
+      var h;
+
+      for (var i = 0, headers = $headers.children(), ii = columns.length; i < ii; i++) {
+        h = $(headers[i]);
+        if (jQueryNewWidthBehaviour) {
+            if (h.outerWidth() !== columns[i].width) {
+              h.outerWidth(columns[i].width);
+            }
+        } else {
+            if (h.width() !== columns[i].width - headerColumnWidthDiff) {
+              h.width(columns[i].width - headerColumnWidthDiff);
+            }
+        }
+      }
+
+      updateColumnCaches();
+    }
+
+    function applyColumnWidths() {
+      var x = 0, w, rule;
+      for (var i = 0; i < columns.length; i++) {
+        w = columns[i].width;
+
+        rule = getColumnCssRules(i);
+        rule.left.style.left = x + "px";
+        rule.right.style.right = (canvasWidth - x - w) + "px";
+
+        x += columns[i].width;
+      }
+    }
+
+    function setSortColumn(columnId, ascending) {
+      setSortColumns([{ columnId: columnId, sortAsc: ascending}]);
+    }
+
+    function setSortColumns(cols) {
+      sortColumns = cols;
+      var numberCols = options.numberedMultiColumnSort && sortColumns.length > 1;
+      var headerColumnEls = $headers.children();
+      var sortIndicatorEl = headerColumnEls
+          .removeClass("slick-header-column-sorted")
+          .find("." + sortIndicatorCssClass)
+              .removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
+      if (numberCols) { sortIndicatorEl.text(''); }
+
+      $.each(sortColumns, function(i, col) {
+        if (col.sortAsc == null) {
+          col.sortAsc = true;
+        }
+        var columnIndex = getColumnIndex(col.columnId);
+        if (columnIndex != null) {
+          sortIndicatorEl = headerColumnEls.eq(columnIndex)
+              .addClass("slick-header-column-sorted")
+              .find("." + sortIndicatorCssClass)
+                  .addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
+          if (numberCols) { sortIndicatorEl.text(i+1); }
+        }
+      });
+    }
+
+    function getSortColumns() {
+      return sortColumns;
+    }
+
+    function handleSelectedRangesChanged(e, ranges) {
+      selectedRows = [];
+      var hash = {};
+      for (var i = 0; i < ranges.length; i++) {
+        for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
+          if (!hash[j]) {  // prevent duplicates
+            selectedRows.push(j);
+            hash[j] = {};
+          }
+          for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) {
+            if (canCellBeSelected(j, k)) {
+              hash[j][columns[k].id] = options.selectedCellCssClass;
+            }
+          }
+        }
+      }
+
+      setCellCssStyles(options.selectedCellCssClass, hash);
+
+      trigger(self.onSelectedRowsChanged, {rows: getSelectedRows(), grid: self}, e);
+    }
+
+    function getColumns() {
+      return columns;
+    }
+
+    function updateColumnCaches() {
+      // Pre-calculate cell boundaries.
+      columnPosLeft = [];
+      columnPosRight = [];
+      var x = 0;
+      for (var i = 0, ii = columns.length; i < ii; i++) {
+        columnPosLeft[i] = x;
+        columnPosRight[i] = x + columns[i].width;
+        x += columns[i].width;
+      }
+    }
+
+    function setColumns(columnDefinitions) {
+      columns = columnDefinitions;
+
+      columnsById = {};
+      for (var i = 0; i < columns.length; i++) {
+        var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
+        columnsById[m.id] = i;
+        if (m.minWidth && m.width < m.minWidth) {
+          m.width = m.minWidth;
+        }
+        if (m.maxWidth && m.width > m.maxWidth) {
+          m.width = m.maxWidth;
+        }
+      }
+
+      updateColumnCaches();
+
+      if (initialized) {
+        invalidateAllRows();
+        createColumnHeaders();
+        removeCssRules();
+        createCssRules();
+        resizeCanvas();
+        applyColumnWidths();
+        handleScroll();
+      }
+    }
+
+    function getOptions() {
+      return options;
+    }
+
+    function setOptions(args, suppressRender) {
+      if (!getEditorLock().commitCurrentEdit()) {
+        return;
+      }
+
+      makeActiveCellNormal();
+
+      if (options.enableAddRow !== args.enableAddRow) {
+        invalidateRow(getDataLength());
+      }
+
+      options = $.extend(options, args);
+      validateAndEnforceOptions();
+
+      $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
+      if (!suppressRender) { render(); }
+    }
+
+    function validateAndEnforceOptions() {
+      if (options.autoHeight) {
+        options.leaveSpaceForNewRows = false;
+      }
+    }
+
+    function setData(newData, scrollToTop) {
+      data = newData;
+      invalidateAllRows();
+      updateRowCount();
+      if (scrollToTop) {
+        scrollTo(0);
+      }
+    }
+
+    function getData() {
+      return data;
+    }
+
+    function getDataLength() {
+      if (data.getLength) {
+        return data.getLength();
+      } else {
+        return data.length;
+      }
+    }
+
+    function getDataLengthIncludingAddNew() {
+      return getDataLength() + (!options.enableAddRow ? 0
+        : (!pagingActive || pagingIsLastPage ? 1 : 0)
+      );
+    }
+
+    function getDataItem(i) {
+      if (data.getItem) {
+        return data.getItem(i);
+      } else {
+        return data[i];
+      }
+    }
+
+    function getTopPanel() {
+      return $topPanel[0];
+    }
+
+    function setTopPanelVisibility(visible) {
+      if (options.showTopPanel != visible) {
+        options.showTopPanel = visible;
+        if (visible) {
+          $topPanelScroller.slideDown("fast", resizeCanvas);
+        } else {
+          $topPanelScroller.slideUp("fast", resizeCanvas);
+        }
+      }
+    }
+
+    function setHeaderRowVisibility(visible) {
+      if (options.showHeaderRow != visible) {
+        options.showHeaderRow = visible;
+        if (visible) {
+          $headerRowScroller.slideDown("fast", resizeCanvas);
+        } else {
+          $headerRowScroller.slideUp("fast", resizeCanvas);
+        }
+      }
+    }
+
+    function setFooterRowVisibility(visible) {
+      if (options.showFooterRow != visible) {
+        options.showFooterRow = visible;
+        if (visible) {
+          $footerRowScroller.slideDown("fast", resizeCanvas);
+        } else {
+          $footerRowScroller.slideUp("fast", resizeCanvas);
+        }
+      }
+    }
+
+    function getContainerNode() {
+      return $container.get(0);
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Rendering / Scrolling
+
+    function getRowTop(row) {
+      return options.rowHeight * row - offset;
+    }
+
+    function getRowFromPosition(y) {
+      return Math.floor((y + offset) / options.rowHeight);
+    }
+
+    function scrollTo(y) {
+      y = Math.max(y, 0);
+      y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
+
+      var oldOffset = offset;
+
+      page = Math.min(n - 1, Math.floor(y / ph));
+      offset = Math.round(page * cj);
+      var newScrollTop = y - offset;
+
+      if (offset != oldOffset) {
+        var range = getVisibleRange(newScrollTop);
+        cleanupRows(range);
+        updateRowPositions();
+      }
+
+      if (prevScrollTop != newScrollTop) {
+        vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
+        $viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
+
+        trigger(self.onViewportChanged, {grid: self});
+      }
+    }
+
+    function defaultFormatter(row, cell, value, columnDef, dataContext) {
+      if (value == null) {
+        return "";
+      } else {
+        return (value + "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
+      }
+    }
+
+    function getFormatter(row, column) {
+      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
+
+      // look up by id, then index
+      var columnOverrides = rowMetadata &&
+          rowMetadata.columns &&
+          (rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
+
+      return (columnOverrides && columnOverrides.formatter) ||
+          (rowMetadata && rowMetadata.formatter) ||
+          column.formatter ||
+          (options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
+          options.defaultFormatter;
+    }
+
+    function getEditor(row, cell) {
+      var column = columns[cell];
+      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
+      var columnMetadata = rowMetadata && rowMetadata.columns;
+
+      if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
+        return columnMetadata[column.id].editor;
+      }
+      if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
+        return columnMetadata[cell].editor;
+      }
+
+      return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
+    }
+
+    function getDataItemValueForColumn(item, columnDef) {
+      if (options.dataItemColumnValueExtractor) {
+        return options.dataItemColumnValueExtractor(item, columnDef);
+      }
+      return item[columnDef.field];
+    }
+
+    function appendRowHtml(stringArray, row, range, dataLength) {
+      var d = getDataItem(row);
+      var dataLoading = row < dataLength && !d;
+      var rowCss = "slick-row" +
+          (dataLoading ? " loading" : "") +
+          (row === activeRow ? " active" : "") +
+          (row % 2 == 1 ? " odd" : " even");
+
+      if (!d) {
+        rowCss += " " + options.addNewRowCssClass;
+      }
+
+      var metadata = data.getItemMetadata && data.getItemMetadata(row);
+
+      if (metadata && metadata.cssClasses) {
+        rowCss += " " + metadata.cssClasses;
+      }
+
+      stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
+
+      var colspan, m;
+      for (var i = 0, ii = columns.length; i < ii; i++) {
+        m = columns[i];
+        colspan = 1;
+        if (metadata && metadata.columns) {
+          var columnData = metadata.columns[m.id] || metadata.columns[i];
+          colspan = (columnData && columnData.colspan) || 1;
+          if (colspan === "*") {
+            colspan = ii - i;
+          }
+        }
+
+        // Do not render cells outside of the viewport.
+        if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
+          if (columnPosLeft[i] > range.rightPx) {
+            // All columns to the right are outside the range.
+            break;
+          }
+
+          appendCellHtml(stringArray, row, i, colspan, d);
+        }
+
+        if (colspan > 1) {
+          i += (colspan - 1);
+        }
+      }
+
+      stringArray.push("</div>");
+    }
+
+    function appendCellHtml(stringArray, row, cell, colspan, item) {
+      // stringArray: stringBuilder containing the HTML parts
+      // row, cell: row and column index
+      // colspan: HTML colspan
+      // item: grid data for row
+
+      var m = columns[cell];
+      var cellCss = "slick-cell l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
+          (m.cssClass ? " " + m.cssClass : "");
+      if (row === activeRow && cell === activeCell) {
+        cellCss += (" active");
+      }
+
+      // TODO:  merge them together in the setter
+      for (var key in cellCssClasses) {
+        if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
+          cellCss += (" " + cellCssClasses[key][row][m.id]);
+        }
+      }
+
+      var value = null;
+      if (item) { value = getDataItemValueForColumn(item, m); }
+      var formatterResult =  getFormatter(row, m)(row, cell, value, m, item);
+      
+      // get addl css class names from object type formatter return and from string type return of onBeforeAppendCell
+      var addlCssClasses = trigger(self.onBeforeAppendCell, { row: row, cell: cell, grid: self, value: value, dataContext: item }) || '';
+      addlCssClasses += (formatterResult.addClasses ? (addlCssClasses ? ' ' : '') + formatterResult.addClasses : '');
+      
+      stringArray.push("<div class='" + cellCss + (addlCssClasses ? ' ' + addlCssClasses : '') + "'>");
+
+      // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
+      if (item) {
+        stringArray.push(typeof formatterResult === 'string' ? formatterResult : formatterResult.text);
+      }
+      
+      stringArray.push("</div>");
+
+      rowsCache[row].cellRenderQueue.push(cell);
+      rowsCache[row].cellColSpans[cell] = colspan;
+    }
+
+
+    function cleanupRows(rangeToKeep) {
+      for (var i in rowsCache) {
+        if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
+          removeRowFromCache(i);
+        }
+      }
+      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
+    }
+
+    function invalidate() {
+      updateRowCount();
+      invalidateAllRows();
+      render();
+    }
+
+    function invalidateAllRows() {
+      if (currentEditor) {
+        makeActiveCellNormal();
+      }
+      for (var row in rowsCache) {
+        removeRowFromCache(row);
+      }
+      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
+    }
+
+    function queuePostProcessedRowForCleanup(cacheEntry, postProcessedRow, rowIdx) {
+      postProcessgroupId++;
+
+      // store and detach node for later async cleanup
+      for (var columnIdx in postProcessedRow) {
+        if (postProcessedRow.hasOwnProperty(columnIdx)) {
+          postProcessedCleanupQueue.push({
+            actionType: 'C',
+            groupId: postProcessgroupId,
+            node: cacheEntry.cellNodesByColumnIdx[ columnIdx | 0],
+            columnIdx: columnIdx | 0,
+            rowIdx: rowIdx
+          });
+        }
+      }
+      postProcessedCleanupQueue.push({
+        actionType: 'R',
+        groupId: postProcessgroupId,
+        node: cacheEntry.rowNode
+      });
+      $(cacheEntry.rowNode).detach();
+    }
+
+    function queuePostProcessedCellForCleanup(cellnode, columnIdx, rowIdx) {
+      postProcessedCleanupQueue.push({
+        actionType: 'C',
+        groupId: postProcessgroupId,
+        node: cellnode,
+        columnIdx: columnIdx,
+        rowIdx: rowIdx
+      });
+      $(cellnode).detach();
+    }
+
+    function removeRowFromCache(row) {
+      var cacheEntry = rowsCache[row];
+      if (!cacheEntry) {
+        return;
+      }
+
+      if (rowNodeFromLastMouseWheelEvent === cacheEntry.rowNode) {
+        cacheEntry.rowNode.style.display = 'none';
+        zombieRowNodeFromLastMouseWheelEvent = rowNodeFromLastMouseWheelEvent;
+        zombieRowCacheFromLastMouseWheelEvent = cacheEntry;
+        zombieRowPostProcessedFromLastMouseWheelEvent = postProcessedRows[row];
+        // ignore post processing cleanup in this case - it will be dealt with later
+      } else {
+        if (options.enableAsyncPostRenderCleanup && postProcessedRows[row]) {
+          queuePostProcessedRowForCleanup(cacheEntry, postProcessedRows[row], row);
+        } else {
+          $canvas[0].removeChild(cacheEntry.rowNode);
+        }
+      }
+
+      delete rowsCache[row];
+      delete postProcessedRows[row];
+      renderedRows--;
+      counter_rows_removed++;
+    }
+
+    function invalidateRows(rows) {
+      var i, rl;
+      if (!rows || !rows.length) {
+        return;
+      }
+      vScrollDir = 0;
+      rl = rows.length;
+      for (i = 0;  i < rl; i++) {
+        if (currentEditor && activeRow === rows[i]) {
+          makeActiveCellNormal();
+        }
+        if (rowsCache[rows[i]]) {
+          removeRowFromCache(rows[i]);
+        }
+      }
+      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
+
+    }
+
+    function invalidateRow(row) {
+      invalidateRows([row]);
+    }
+
+    function applyFormatResultToCellNode(formatterResult, cellNode, suppressRemove) {
+        if (typeof formatterResult === 'string') { 
+            cellNode.innerHTML = formatterResult;
+            return;
+        }
+        cellNode.innerHTML = formatterResult.text;
+        if (formatterResult.removeClasses && !suppressRemove) { 
+            cellNode.removeClass(formatterResult.removeClasses); 
+        }
+        if (formatterResult.addClasses) { 
+            cellNode.addClass(formatterResult.addClasses); 
+        }
+    }
+
+    function updateCell(row, cell) {
+      var cellNode = getCellNode(row, cell);
+      if (!cellNode) {
+        return;
+      }
+
+      var m = columns[cell], d = getDataItem(row);
+      if (currentEditor && activeRow === row && activeCell === cell) {
+        currentEditor.loadValue(d);
+      } else {
+        var formatterResult =  d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
+        applyFormatResultToCellNode(formatterResult, cellNode);
+        invalidatePostProcessingResults(row);
+      }
+    }
+
+    function updateRow(row) {
+      var cacheEntry = rowsCache[row];
+      if (!cacheEntry) {
+        return;
+      }
+
+      ensureCellNodesInRowsCache(row);
+
+      var formatterResult, d = getDataItem(row);
+    
+      for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
+        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
+          continue;
+        }
+
+        columnIdx = columnIdx | 0;
+        var m = columns[columnIdx],
+            node = cacheEntry.cellNodesByColumnIdx[columnIdx];
+
+        if (row === activeRow && columnIdx === activeCell && currentEditor) {
+          currentEditor.loadValue(d);
+        } else if (d) {
+          formatterResult =  getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
+          applyFormatResultToCellNode(formatterResult, node);
+        } else {
+          node.innerHTML = "";
+        }
+      }
+
+      invalidatePostProcessingResults(row);
+    }
+
+    function getViewportHeight() {
+      return parseFloat($.css($container[0], "height", true)) -
+          parseFloat($.css($container[0], "paddingTop", true)) -
+          parseFloat($.css($container[0], "paddingBottom", true)) -
+          parseFloat($.css($headerScroller[0], "height")) - getVBoxDelta($headerScroller) -
+          (options.showTopPanel ? options.topPanelHeight + getVBoxDelta($topPanelScroller) : 0) -
+          (options.showHeaderRow ? options.headerRowHeight + getVBoxDelta($headerRowScroller) : 0) -
+          (options.createFooterRow && options.showFooterRow ? options.footerRowHeight + getVBoxDelta($footerRowScroller) : 0);
+    }
+
+    function resizeCanvas() {
+      if (!initialized) { return; }
+      if (options.autoHeight) {
+        viewportH = options.rowHeight * getDataLengthIncludingAddNew();
+      } else {
+        viewportH = getViewportHeight();
+      }
+
+      numVisibleRows = Math.ceil(viewportH / options.rowHeight);
+      viewportW = parseFloat($.css($container[0], "width", true));
+      if (!options.autoHeight) {
+        $viewport.height(viewportH);
+      }
+
+      if (options.forceFitColumns) {
+        autosizeColumns();
+      }
+
+      updateRowCount();
+      handleScroll();
+      // Since the width has changed, force the render() to reevaluate virtually rendered cells.
+      lastRenderedScrollLeft = -1;
+      render();
+    }
+
+    function updatePagingStatusFromView( pagingInfo ) {
+        pagingActive = (pagingInfo.pageSize !== 0);
+        pagingIsLastPage = (pagingInfo.pageNum == pagingInfo.totalPages - 1);
+    }
+
+    function updateRowCount() {
+      if (!initialized) { return; }
+
+      var dataLength = getDataLength();
+      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
+      var numberOfRows = dataLengthIncludingAddNew +
+          (options.leaveSpaceForNewRows ? numVisibleRows - 1 : 0);
+
+      var oldViewportHasVScroll = viewportHasVScroll;
+      // with autoHeight, we do not need to accommodate the vertical scroll bar
+      viewportHasVScroll = !options.autoHeight && (numberOfRows * options.rowHeight > viewportH);
+      viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
+
+      makeActiveCellNormal();
+
+      // remove the rows that are now outside of the data range
+      // this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
+      var r1 = dataLength - 1;
+      for (var i in rowsCache) {
+        if (i > r1) {
+          removeRowFromCache(i);
+        }
+      }
+      if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
+
+      if (activeCellNode && activeRow > r1) {
+        resetActiveCell();
+      }
+
+      var oldH = h;
+      th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
+      if (th < maxSupportedCssHeight) {
+        // just one page
+        h = ph = th;
+        n = 1;
+        cj = 0;
+      } else {
+        // break into pages
+        h = maxSupportedCssHeight;
+        ph = h / 100;
+        n = Math.floor(th / ph);
+        cj = (th - h) / (n - 1);
+      }
+
+      if (h !== oldH) {
+        $canvas.css("height", h);
+        scrollTop = $viewport[0].scrollTop;
+      }
+
+      var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
+
+      if (th == 0 || scrollTop == 0) {
+        page = offset = 0;
+      } else if (oldScrollTopInRange) {
+        // maintain virtual position
+        scrollTo(scrollTop + offset);
+      } else {
+        // scroll to bottom
+        scrollTo(th - viewportH);
+      }
+
+      if (h != oldH && options.autoHeight) {
+        resizeCanvas();
+      }
+
+      if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
+        autosizeColumns();
+      }
+      updateCanvasWidth(false);
+    }
+
+    function getVisibleRange(viewportTop, viewportLeft) {
+      if (viewportTop == null) {
+        viewportTop = scrollTop;
+      }
+      if (viewportLeft == null) {
+        viewportLeft = scrollLeft;
+      }
+
+      return {
+        top: getRowFromPosition(viewportTop),
+        bottom: getRowFromPosition(viewportTop + viewportH) + 1,
+        leftPx: viewportLeft,
+        rightPx: viewportLeft + viewportW
+      };
+    }
+
+    function getRenderedRange(viewportTop, viewportLeft) {
+      var range = getVisibleRange(viewportTop, viewportLeft);
+      var buffer = Math.round(viewportH / options.rowHeight);
+      var minBuffer = 3;
+
+      if (vScrollDir == -1) {
+        range.top -= buffer;
+        range.bottom += minBuffer;
+      } else if (vScrollDir == 1) {
+        range.top -= minBuffer;
+        range.bottom += buffer;
+      } else {
+        range.top -= minBuffer;
+        range.bottom += minBuffer;
+      }
+
+      range.top = Math.max(0, range.top);
+      range.bottom = Math.min(getDataLengthIncludingAddNew() - 1, range.bottom);
+
+      range.leftPx -= viewportW;
+      range.rightPx += viewportW;
+
+      range.leftPx = Math.max(0, range.leftPx);
+      range.rightPx = Math.min(canvasWidth, range.rightPx);
+
+      return range;
+    }
+
+    function ensureCellNodesInRowsCache(row) {
+      var cacheEntry = rowsCache[row];
+      if (cacheEntry) {
+        if (cacheEntry.cellRenderQueue.length) {
+          var lastChild = cacheEntry.rowNode.lastChild;
+          while (cacheEntry.cellRenderQueue.length) {
+            var columnIdx = cacheEntry.cellRenderQueue.pop();
+            cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
+            lastChild = lastChild.previousSibling;
+          }
+        }
+      }
+    }
+
+    function cleanUpCells(range, row) {
+      var totalCellsRemoved = 0;
+      var cacheEntry = rowsCache[row];
+
+      // Remove cells outside the range.
+      var cellsToRemove = [];
+      for (var i in cacheEntry.cellNodesByColumnIdx) {
+        // I really hate it when people mess with Array.prototype.
+        if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
+          continue;
+        }
+
+        // This is a string, so it needs to be cast back to a number.
+        i = i | 0;
+
+        var colspan = cacheEntry.cellColSpans[i];
+        if (columnPosLeft[i] > range.rightPx ||
+          columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
+          if (!(row == activeRow && i == activeCell)) {
+            cellsToRemove.push(i);
+          }
+        }
+      }
+
+      var cellToRemove, node;
+      postProcessgroupId++;
+      while ((cellToRemove = cellsToRemove.pop()) != null) {
+        node = cacheEntry.cellNodesByColumnIdx[cellToRemove];
+        if (options.enableAsyncPostRenderCleanup && postProcessedRows[row] && postProcessedRows[row][cellToRemove]) {
+          queuePostProcessedCellForCleanup(node, cellToRemove, row);
+        } else {
+          cacheEntry.rowNode.removeChild(node);
+        }
+
+        delete cacheEntry.cellColSpans[cellToRemove];
+        delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
+        if (postProcessedRows[row]) {
+          delete postProcessedRows[row][cellToRemove];
+        }
+        totalCellsRemoved++;
+      }
+    }
+
+    function cleanUpAndRenderCells(range) {
+      var cacheEntry;
+      var stringArray = [];
+      var processedRows = [];
+      var cellsAdded;
+      var totalCellsAdded = 0;
+      var colspan;
+
+      for (var row = range.top, btm = range.bottom; row <= btm; row++) {
+        cacheEntry = rowsCache[row];
+        if (!cacheEntry) {
+          continue;
+        }
+
+        // cellRenderQueue populated in renderRows() needs to be cleared first
+        ensureCellNodesInRowsCache(row);
+
+        cleanUpCells(range, row);
+
+        // Render missing cells.
+        cellsAdded = 0;
+
+        var metadata = data.getItemMetadata && data.getItemMetadata(row);
+        metadata = metadata && metadata.columns;
+
+        var d = getDataItem(row);
+
+        // TODO:  shorten this loop (index? heuristics? binary search?)
+        for (var i = 0, ii = columns.length; i < ii; i++) {
+          // Cells to the right are outside the range.
+          if (columnPosLeft[i] > range.rightPx) {
+            break;
+          }
+
+          // Already rendered.
+          if ((colspan = cacheEntry.cellColSpans[i]) != null) {
+            i += (colspan > 1 ? colspan - 1 : 0);
+            continue;
+          }
+
+          colspan = 1;
+          if (metadata) {
+            var columnData = metadata[columns[i].id] || metadata[i];
+            colspan = (columnData && columnData.colspan) || 1;
+            if (colspan === "*") {
+              colspan = ii - i;
+            }
+          }
+
+          if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
+            appendCellHtml(stringArray, row, i, colspan, d);
+            cellsAdded++;
+          }
+
+          i += (colspan > 1 ? colspan - 1 : 0);
+        }
+
+        if (cellsAdded) {
+          totalCellsAdded += cellsAdded;
+          processedRows.push(row);
+        }
+      }
+
+      if (!stringArray.length) {
+        return;
+      }
+
+      var x = document.createElement("div");
+      x.innerHTML = stringArray.join("");
+
+      var processedRow;
+      var node;
+      while ((processedRow = processedRows.pop()) != null) {
+        cacheEntry = rowsCache[processedRow];
+        var columnIdx;
+        while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
+          node = x.lastChild;
+          cacheEntry.rowNode.appendChild(node);
+          cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
+        }
+      }
+    }
+
+    function renderRows(range) {
+      var parentNode = $canvas[0],
+          stringArray = [],
+          rows = [],
+          needToReselectCell = false,
+          dataLength = getDataLength();
+
+      for (var i = range.top, ii = range.bottom; i <= ii; i++) {
+        if (rowsCache[i]) {
+          continue;
+        }
+        renderedRows++;
+        rows.push(i);
+
+        // Create an entry right away so that appendRowHtml() can
+        // start populatating it.
+        rowsCache[i] = {
+          "rowNode": null,
+
+          // ColSpans of rendered cells (by column idx).
+          // Can also be used for checking whether a cell has been rendered.
+          "cellColSpans": [],
+
+          // Cell nodes (by column idx).  Lazy-populated by ensureCellNodesInRowsCache().
+          "cellNodesByColumnIdx": [],
+
+          // Column indices of cell nodes that have been rendered, but not yet indexed in
+          // cellNodesByColumnIdx.  These are in the same order as cell nodes added at the
+          // end of the row.
+          "cellRenderQueue": []
+        };
+
+        appendRowHtml(stringArray, i, range, dataLength);
+        if (activeCellNode && activeRow === i) {
+          needToReselectCell = true;
+        }
+        counter_rows_rendered++;
+      }
+
+      if (!rows.length) { return; }
+
+      var x = document.createElement("div");
+      x.innerHTML = stringArray.join("");
+
+      for (var i = 0, ii = rows.length; i < ii; i++) {
+        rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
+      }
+
+      if (needToReselectCell) {
+        activeCellNode = getCellNode(activeRow, activeCell);
+      }
+    }
+
+    function startPostProcessing() {
+      if (!options.enableAsyncPostRender) {
+        return;
+      }
+      clearTimeout(h_postrender);
+      h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
+    }
+
+    function startPostProcessingCleanup() {
+      if (!options.enableAsyncPostRenderCleanup) {
+        return;
+      }
+      clearTimeout(h_postrenderCleanup);
+      h_postrenderCleanup = setTimeout(asyncPostProcessCleanupRows, options.asyncPostRenderCleanupDelay);
+    }
+
+    function invalidatePostProcessingResults(row) {
+      // change status of columns to be re-rendered
+      for (var columnIdx in postProcessedRows[row]) {
+        if (postProcessedRows[row].hasOwnProperty(columnIdx)) {
+          postProcessedRows[row][columnIdx] = 'C';
+        }
+      }
+      postProcessFromRow = Math.min(postProcessFromRow, row);
+      postProcessToRow = Math.max(postProcessToRow, row);
+      startPostProcessing();
+    }
+
+    function updateRowPositions() {
+      for (var row in rowsCache) {
+        rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
+      }
+    }
+
+    function render() {
+      if (!initialized) { return; }
+      var visible = getVisibleRange();
+      var rendered = getRenderedRange();
+
+      // remove rows no longer in the viewport
+      cleanupRows(rendered);
+
+      // add new rows & missing cells in existing rows
+      if (lastRenderedScrollLeft != scrollLeft) {
+        cleanUpAndRenderCells(rendered);
+      }
+
+      // render missing rows
+      renderRows(rendered);
+
+      postProcessFromRow = visible.top;
+      postProcessToRow = Math.min(getDataLengthIncludingAddNew() - 1, visible.bottom);
+      startPostProcessing();
+
+      lastRenderedScrollTop = scrollTop;
+      lastRenderedScrollLeft = scrollLeft;
+      h_render = null;
+    }
+
+    function handleHeaderRowScroll() {
+      var scrollLeft = $headerRowScroller[0].scrollLeft;
+      if (scrollLeft != $viewport[0].scrollLeft) {
+        $viewport[0].scrollLeft = scrollLeft;
+      }
+    }
+
+    function handleFooterRowScroll() {
+      var scrollLeft = $footerRowScroller[0].scrollLeft;
+      if (scrollLeft != $viewport[0].scrollLeft) {
+        $viewport[0].scrollLeft = scrollLeft;
+      }
+    }
+
+    function handleScroll() {
+      scrollTop = $viewport[0].scrollTop;
+      scrollLeft = $viewport[0].scrollLeft;
+      var vScrollDist = Math.abs(scrollTop - prevScrollTop);
+      var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
+
+      if (hScrollDist) {
+        prevScrollLeft = scrollLeft;
+        $headerScroller[0].scrollLeft = scrollLeft;
+        $topPanelScroller[0].scrollLeft = scrollLeft;
+        $headerRowScroller[0].scrollLeft = scrollLeft;
+        if (options.createFooterRow) {
+          $footerRowScroller[0].scrollLeft = scrollLeft;
+        }
+      }
+
+      if (vScrollDist) {
+        vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
+        prevScrollTop = scrollTop;
+
+        // switch virtual pages if needed
+        if (vScrollDist < viewportH) {
+          scrollTo(scrollTop + offset);
+        } else {
+          var oldOffset = offset;
+          if (h == viewportH) {
+            page = 0;
+          } else {
+            page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
+          }
+          offset = Math.round(page * cj);
+          if (oldOffset != offset) {
+            invalidateAllRows();
+          }
+        }
+      }
+
+      if (hScrollDist || vScrollDist) {
+        if (h_render) {
+          clearTimeout(h_render);
+        }
+
+        if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
+            Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
+          if (options.forceSyncScrolling || (
+              Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
+              Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW)) {
+            render();
+          } else {
+            h_render = setTimeout(render, 50);
+          }
+
+          trigger(self.onViewportChanged, {grid: self});
+        }
+      }
+
+      trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop, grid: self});
+    }
+
+    function asyncPostProcessRows() {
+      var dataLength = getDataLength();
+      while (postProcessFromRow <= postProcessToRow) {
+        var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
+        var cacheEntry = rowsCache[row];
+        if (!cacheEntry || row >= dataLength) {
+          continue;
+        }
+
+        if (!postProcessedRows[row]) {
+          postProcessedRows[row] = {};
+        }
+
+        ensureCellNodesInRowsCache(row);
+        for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
+          if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
+            continue;
+          }
+
+          columnIdx = columnIdx | 0;
+
+          var m = columns[columnIdx];
+          var processedStatus = postProcessedRows[row][columnIdx]; // C=cleanup and re-render, R=rendered
+          if (m.asyncPostRender && processedStatus !== 'R') {
+            var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
+            if (node) {
+              m.asyncPostRender(node, row, getDataItem(row), m, (processedStatus === 'C'));
+            }
+            postProcessedRows[row][columnIdx] = 'R';
+          }
+        }
+
+        h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
+        return;
+      }
+    }
+
+    function asyncPostProcessCleanupRows() {
+      if (postProcessedCleanupQueue.length > 0) {
+        var groupId = postProcessedCleanupQueue[0].groupId;
+
+        // loop through all queue members with this groupID
+        while (postProcessedCleanupQueue.length > 0 && postProcessedCleanupQueue[0].groupId == groupId) {
+          var entry = postProcessedCleanupQueue.shift();
+          if (entry.actionType == 'R') {
+            $(entry.node).remove();
+          }
+          if (entry.actionType == 'C') {
+            var column = columns[entry.columnIdx];
+            if (column.asyncPostRenderCleanup && entry.node) {
+              // cleanup must also remove element
+              column.asyncPostRenderCleanup(entry.node, entry.rowIdx, column);
+            }
+          }
+        }
+
+        // call this function again after the specified delay
+        h_postrenderCleanup = setTimeout(asyncPostProcessCleanupRows, options.asyncPostRenderCleanupDelay);
+      }
+    }
+
+    function updateCellCssStylesOnRenderedRows(addedHash, removedHash) {
+      var node, columnId, addedRowHash, removedRowHash;
+      for (var row in rowsCache) {
+        removedRowHash = removedHash && removedHash[row];
+        addedRowHash = addedHash && addedHash[row];
+
+        if (removedRowHash) {
+          for (columnId in removedRowHash) {
+            if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
+              node = getCellNode(row, getColumnIndex(columnId));
+              if (node) {
+                $(node).removeClass(removedRowHash[columnId]);
+              }
+            }
+          }
+        }
+
+        if (addedRowHash) {
+          for (columnId in addedRowHash) {
+            if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
+              node = getCellNode(row, getColumnIndex(columnId));
+              if (node) {
+                $(node).addClass(addedRowHash[columnId]);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    function addCellCssStyles(key, hash) {
+      if (cellCssClasses[key]) {
+        throw new Error("addCellCssStyles: cell CSS hash with key '" + key + "' already exists.");
+      }
+
+      cellCssClasses[key] = hash;
+      updateCellCssStylesOnRenderedRows(hash, null);
+
+      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash, "grid": self });
+    }
+
+    function removeCellCssStyles(key) {
+      if (!cellCssClasses[key]) {
+        return;
+      }
+
+      updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
+      delete cellCssClasses[key];
+
+      trigger(self.onCellCssStylesChanged, { "key": key, "hash": null, "grid": self });
+    }
+
+    function setCellCssStyles(key, hash) {
+      var prevHash = cellCssClasses[key];
+
+      cellCssClasses[key] = hash;
+      updateCellCssStylesOnRenderedRows(hash, prevHash);
+
+      trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash, "grid": self });
+    }
+
+    function getCellCssStyles(key) {
+      return cellCssClasses[key];
+    }
+
+    function flashCell(row, cell, speed) {
+      speed = speed || 100;
+      if (rowsCache[row]) {
+        var $cell = $(getCellNode(row, cell));
+
+        function toggleCellClass(times) {
+          if (!times) {
+            return;
+          }
+          setTimeout(function () {
+                $cell.queue(function () {
+                  $cell.toggleClass(options.cellFlashingCssClass).dequeue();
+                  toggleCellClass(times - 1);
+                });
+              },
+              speed);
+        }
+
+        toggleCellClass(4);
+      }
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Interactivity
+
+    function handleMouseWheel(e) {
+      var rowNode = $(e.target).closest(".slick-row")[0];
+      if (rowNode != rowNodeFromLastMouseWheelEvent) {
+        if (zombieRowNodeFromLastMouseWheelEvent && zombieRowNodeFromLastMouseWheelEvent != rowNode) {
+          if (options.enableAsyncPostRenderCleanup && zombieRowPostProcessedFromLastMouseWheelEvent) {
+            queuePostProcessedRowForCleanup(zombieRowCacheFromLastMouseWheelEvent,
+              zombieRowPostProcessedFromLastMouseWheelEvent);
+          } else {
+            $canvas[0].removeChild(zombieRowNodeFromLastMouseWheelEvent);
+          }
+          zombieRowNodeFromLastMouseWheelEvent = null;
+          zombieRowCacheFromLastMouseWheelEvent = null;
+          zombieRowPostProcessedFromLastMouseWheelEvent = null;
+
+          if (options.enableAsyncPostRenderCleanup) { startPostProcessingCleanup(); }
+        }
+        rowNodeFromLastMouseWheelEvent = rowNode;
+      }
+    }
+
+    function handleDragInit(e, dd) {
+      var cell = getCellFromEvent(e);
+      if (!cell || !cellExists(cell.row, cell.cell)) {
+        return false;
+      }
+
+      var retval = trigger(self.onDragInit, dd, e);
+      if (e.isImmediatePropagationStopped()) {
+        return retval;
+      }
+
+      // if nobody claims to be handling drag'n'drop by stopping immediate propagation,
+      // cancel out of it
+      return false;
+    }
+
+    function handleDragStart(e, dd) {
+      var cell = getCellFromEvent(e);
+      if (!cell || !cellExists(cell.row, cell.cell)) {
+        return false;
+      }
+
+      var retval = trigger(self.onDragStart, dd, e);
+      if (e.isImmediatePropagationStopped()) {
+        return retval;
+      }
+
+      return false;
+    }
+
+    function handleDrag(e, dd) {
+      return trigger(self.onDrag, dd, e);
+    }
+
+    function handleDragEnd(e, dd) {
+      trigger(self.onDragEnd, dd, e);
+    }
+
+    function handleKeyDown(e) {
+      trigger(self.onKeyDown, {row: activeRow, cell: activeCell, grid: self}, e);
+      var handled = e.isImmediatePropagationStopped();
+      var keyCode = Slick.keyCode;
+
+      if (!handled) {
+        if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
+          // editor may specify an array of keys to bubble
+          if (options.editable && currentEditor && currentEditor.keyCaptureList) {
+            if (currentEditor.keyCaptureList.indexOf( e.which ) > -1) {
+                return;
+            }
+          }
+          if (e.which == keyCode.ESCAPE) {
+            if (!getEditorLock().isActive()) {
+              return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
+            }
+            cancelEditAndSetFocus();
+          } else if (e.which == keyCode.PAGE_DOWN) {
+            navigatePageDown();
+            handled = true;
+          } else if (e.which == keyCode.PAGE_UP) {
+            navigatePageUp();
+            handled = true;
+          } else if (e.which == keyCode.LEFT) {
+            handled = navigateLeft();
+          } else if (e.which == keyCode.RIGHT) {
+            handled = navigateRight();
+          } else if (e.which == keyCode.UP) {
+            handled = navigateUp();
+          } else if (e.which == keyCode.DOWN) {
+            handled = navigateDown();
+          } else if (e.which == keyCode.TAB) {
+            handled = navigateNext();
+          } else if (e.which == keyCode.ENTER) {
+            if (options.editable) {
+              if (currentEditor) {
+                // adding new row
+                if (activeRow === getDataLength()) {
+                  navigateDown();
+                } else {
+                  commitEditAndSetFocus();
+                }
+              } else {
+                if (getEditorLock().commitCurrentEdit()) {
+                  makeActiveCellEditable();
+                }
+              }
+            }
+            handled = true;
+          }
+        } else if (e.which == keyCode.TAB && e.shiftKey && !e.ctrlKey && !e.altKey) {
+          handled = navigatePrev();
+        }
+      }
+
+      if (handled) {
+        // the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
+        e.stopPropagation();
+        e.preventDefault();
+        try {
+          e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
+        }
+        // ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl"
+        // (hitting control key only, nothing else), "Shift" (maybe others)
+        catch (error) {
+        }
+      }
+    }
+
+    function handleClick(e) {
+      if (!currentEditor) {
+        // if this click resulted in some cell child node getting focus,
+        // don't steal it back - keyboard events will still bubble up
+        // IE9+ seems to default DIVs to tabIndex=0 instead of -1, so check for cell clicks directly.
+        if (e.target != document.activeElement || $(e.target).hasClass("slick-cell")) {
+          setFocus();
+        }
+      }
+
+      var cell = getCellFromEvent(e);
+      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
+        return;
+      }
+
+      trigger(self.onClick, {row: cell.row, cell: cell.cell, grid: self}, e);
+      if (e.isImmediatePropagationStopped()) {
+        return;
+      }
+      
+      // this optimisation causes trouble - MLeibman #329
+      //if ((activeCell != cell.cell || activeRow != cell.row) && canCellBeActive(cell.row, cell.cell)) {
+      if (canCellBeActive(cell.row, cell.cell)) {
+        if (!getEditorLock().isActive() || getEditorLock().commitCurrentEdit()) {
+          scrollRowIntoView(cell.row, false);
+
+          var preClickModeOn = (e.target && e.target.className === Slick.preClickClassName);
+          setActiveCellInternal(getCellNode(cell.row, cell.cell), null, preClickModeOn);
+        }
+      }
+    }
+
+    function handleContextMenu(e) {
+      var $cell = $(e.target).closest(".slick-cell", $canvas);
+      if ($cell.length === 0) {
+        return;
+      }
+
+      // are we editing this cell?
+      if (activeCellNode === $cell[0] && currentEditor !== null) {
+        return;
+      }
+
+      trigger(self.onContextMenu, {grid: self}, e);
+    }
+
+    function handleDblClick(e) {
+      var cell = getCellFromEvent(e);
+      if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
+        return;
+      }
+
+      trigger(self.onDblClick, {row: cell.row, cell: cell.cell, grid: self}, e);
+      if (e.isImmediatePropagationStopped()) {
+        return;
+      }
+
+      if (options.editable) {
+        gotoCell(cell.row, cell.cell, true);
+      }
+    }
+
+    function handleHeaderMouseEnter(e) {
+      trigger(self.onHeaderMouseEnter, {
+        "column": $(this).data("column"),
+        "grid": self
+      }, e);
+    }
+
+    function handleHeaderMouseLeave(e) {
+      trigger(self.onHeaderMouseLeave, {
+        "column": $(this).data("column"),
+        "grid": self
+      }, e);
+    }
+
+    function handleHeaderContextMenu(e) {
+      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
+      var column = $header && $header.data("column");
+      trigger(self.onHeaderContextMenu, {column: column, grid: self}, e);
+    }
+
+    function handleHeaderClick(e) {
+      var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
+      var column = $header && $header.data("column");
+      if (column) {
+        trigger(self.onHeaderClick, {column: column, grid: self}, e);
+      }
+    }
+
+    function handleMouseEnter(e) {
+      trigger(self.onMouseEnter, {grid: self}, e);
+    }
+
+    function handleMouseLeave(e) {
+      trigger(self.onMouseLeave, {grid: self}, e);
+    }
+
+    function cellExists(row, cell) {
+      return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
+    }
+
+    function getCellFromPoint(x, y) {
+      var row = getRowFromPosition(y);
+      var cell = 0;
+
+      var w = 0;
+      for (var i = 0; i < columns.length && w < x; i++) {
+        w += columns[i].width;
+        cell++;
+      }
+
+      if (cell < 0) {
+        cell = 0;
+      }
+
+      return {row: row, cell: cell - 1};
+    }
+
+    function getCellFromNode(cellNode) {
+      // read column number from .l<columnNumber> CSS class
+      var cls = /l\d+/.exec(cellNode.className);
+      if (!cls) {
+        throw new Error("getCellFromNode: cannot get cell - " + cellNode.className);
+      }
+      return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
+    }
+
+    function getRowFromNode(rowNode) {
+      for (var row in rowsCache) {
+        if (rowsCache[row].rowNode === rowNode) {
+          return row | 0;
+        }
+      }
+
+      return null;
+    }
+
+    function getCellFromEvent(e) {
+      var $cell = $(e.target).closest(".slick-cell", $canvas);
+      if (!$cell.length) {
+        return null;
+      }
+
+      var row = getRowFromNode($cell[0].parentNode);
+      var cell = getCellFromNode($cell[0]);
+
+      if (row == null || cell == null) {
+        return null;
+      } else {
+        return {
+          "row": row,
+          "cell": cell
+        };
+      }
+    }
+
+    function getCellNodeBox(row, cell) {
+      if (!cellExists(row, cell)) {
+        return null;
+      }
+
+      var y1 = getRowTop(row);
+      var y2 = y1 + options.rowHeight - 1;
+      var x1 = 0;
+      for (var i = 0; i < cell; i++) {
+        x1 += columns[i].width;
+      }
+      var x2 = x1 + columns[cell].width;
+
+      return {
+        top: y1,
+        left: x1,
+        bottom: y2,
+        right: x2
+      };
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Cell switching
+
+    function resetActiveCell() {
+      setActiveCellInternal(null, false);
+    }
+
+    function setFocus() {
+      if (tabbingDirection == -1) {
+        $focusSink[0].focus();
+      } else {
+        $focusSink2[0].focus();
+      }
+    }
+
+    function scrollCellIntoView(row, cell, doPaging) {
+      scrollRowIntoView(row, doPaging);
+
+      var colspan = getColspan(row, cell);
+      internalScrollColumnIntoView(columnPosLeft[cell], columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)]);
+    }
+
+    function internalScrollColumnIntoView(left, right) {
+      var scrollRight = scrollLeft + viewportW;
+
+      if (left < scrollLeft) {
+        $viewport.scrollLeft(left);
+        handleScroll();
+        render();
+      } else if (right > scrollRight) {
+        $viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
+        handleScroll();
+        render();
+      }
+    }
+
+    function scrollColumnIntoView(cell) {
+      internalScrollColumnIntoView(columnPosLeft[cell], columnPosRight[cell]);
+    }
+
+    function setActiveCellInternal(newCell, opt_editMode, preClickModeOn, suppressActiveCellChangedEvent) {
+      if (activeCellNode !== null) {
+        makeActiveCellNormal();
+        $(activeCellNode).removeClass("active");
+        if (rowsCache[activeRow]) {
+          $(rowsCache[activeRow].rowNode).removeClass("active");
+        }
+      }
+
+      var activeCellChanged = (activeCellNode !== newCell);
+      activeCellNode = newCell;
+
+      if (activeCellNode != null) {
+        activeRow = getRowFromNode(activeCellNode.parentNode);
+        activeCell = activePosX = getCellFromNode(activeCellNode);
+
+        if (opt_editMode == null) {
+          opt_editMode = (activeRow == getDataLength()) || options.autoEdit;
+        }
+
+        if (options.showCellSelection) {
+        $(activeCellNode).addClass("active");
+        $(rowsCache[activeRow].rowNode).addClass("active");
+        }
+
+        if (options.editable && opt_editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
+          clearTimeout(h_editorLoader);
+
+          if (options.asyncEditorLoading) {
+            h_editorLoader = setTimeout(function () {
+              makeActiveCellEditable(undefined, preClickModeOn);
+            }, options.asyncEditorLoadDelay);
+          } else {
+            makeActiveCellEditable(undefined, preClickModeOn);
+          }
+        }
+      } else {
+        activeRow = activeCell = null;
+      }
+    
+      // this optimisation causes trouble - MLeibman #329
+      //if (activeCellChanged) {
+      if (!suppressActiveCellChangedEvent) { trigger(self.onActiveCellChanged, getActiveCell()); }
+      //}
+    }
+
+    function clearTextSelection() {
+      if (document.selection && document.selection.empty) {
+        try {
+          //IE fails here if selected element is not in dom
+          document.selection.empty();
+        } catch (e) { }
+      } else if (window.getSelection) {
+        var sel = window.getSelection();
+        if (sel && sel.removeAllRanges) {
+          sel.removeAllRanges();
+        }
+      }
+    }
+
+    function isCellPotentiallyEditable(row, cell) {
+      var dataLength = getDataLength();
+      // is the data for this row loaded?
+      if (row < dataLength && !getDataItem(row)) {
+        return false;
+      }
+
+      // are we in the Add New row?  can we create new from this cell?
+      if (columns[cell].cannotTriggerInsert && row >= dataLength) {
+        return false;
+      }
+
+      // does this cell have an editor?
+      if (!getEditor(row, cell)) {
+        return false;
+      }
+
+      return true;
+    }
+
+    function makeActiveCellNormal() {
+      if (!currentEditor) {
+        return;
+      }
+      trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor, grid: self});
+      currentEditor.destroy();
+      currentEditor = null;
+
+      if (activeCellNode) {
+        var d = getDataItem(activeRow);
+        $(activeCellNode).removeClass("editable invalid");
+        if (d) {
+          var column = columns[activeCell];
+          var formatter = getFormatter(activeRow, column);
+          var formatterResult =  formatter(activeRow, activeCell, getDataItemValueForColumn(d, column), column, d, self);
+          applyFormatResultToCellNode(formatterResult, activeCellNode);
+          invalidatePostProcessingResults(activeRow);
+        }
+      }
+      
+      // if there previously was text selected on a page (such as selected text in the edit cell just removed),
+      // IE can't set focus to anything else correctly
+      if (navigator.userAgent.toLowerCase().match(/msie/)) {
+        clearTextSelection();
+      }
+
+      getEditorLock().deactivate(editController);
+    }
+
+    function makeActiveCellEditable(editor, preClickModeOn) {
+      if (!activeCellNode) {
+        return;
+      }
+      if (!options.editable) {
+        throw new Error("Grid : makeActiveCellEditable : should never get called when options.editable is false");
+      }
+
+      // cancel pending async call if there is one
+      clearTimeout(h_editorLoader);
+
+      if (!isCellPotentiallyEditable(activeRow, activeCell)) {
+        return;
+      }
+
+      var columnDef = columns[activeCell];
+      var item = getDataItem(activeRow);
+
+      if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef, grid: self}) === false) {
+        setFocus();
+        return;
+      }
+
+      getEditorLock().activate(editController);
+      $(activeCellNode).addClass("editable");
+
+      var useEditor = editor || getEditor(activeRow, activeCell);
+
+      // don't clear the cell if a custom editor is passed through
+      if (!editor && !useEditor.suppressClearOnEdit) {
+        activeCellNode.innerHTML = "";
+      }
+
+      currentEditor = new useEditor({
+        grid: self,
+        gridPosition: absBox($container[0]),
+        position: absBox(activeCellNode),
+        container: activeCellNode,
+        column: columnDef,
+        item: item || {},
+        commitChanges: commitEditAndSetFocus,
+        cancelChanges: cancelEditAndSetFocus
+      });
+
+      if (item) {
+        currentEditor.loadValue(item);
+        if (preClickModeOn && currentEditor.preClick) {
+          currentEditor.preClick();
+        }
+      }
+
+      serializedEditorValue = currentEditor.serializeValue();
+
+      if (currentEditor.position) {
+        handleActiveCellPositionChange();
+      }
+    }
+
+    function commitEditAndSetFocus() {
+      // if the commit fails, it would do so due to a validation error
+      // if so, do not steal the focus from the editor
+      if (getEditorLock().commitCurrentEdit()) {
+        setFocus();
+        if (options.autoEdit) {
+          navigateDown();
+        }
+      }
+    }
+
+    function cancelEditAndSetFocus() {
+      if (getEditorLock().cancelCurrentEdit()) {
+        setFocus();
+      }
+    }
+
+    function absBox(elem) {
+      var box = {
+        top: elem.offsetTop,
+        left: elem.offsetLeft,
+        bottom: 0,
+        right: 0,
+        width: $(elem).outerWidth(),
+        height: $(elem).outerHeight(),
+        visible: true};
+      box.bottom = box.top + box.height;
+      box.right = box.left + box.width;
+
+      // walk up the tree
+      var offsetParent = elem.offsetParent;
+      while ((elem = elem.parentNode) != document.body) {
+        if (elem == null) break;
+
+        if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible") {
+          box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
+        }
+
+        if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible") {
+          box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
+        }
+
+        box.left -= elem.scrollLeft;
+        box.top -= elem.scrollTop;
+
+        if (elem === offsetParent) {
+          box.left += elem.offsetLeft;
+          box.top += elem.offsetTop;
+          offsetParent = elem.offsetParent;
+        }
+
+        box.bottom = box.top + box.height;
+        box.right = box.left + box.width;
+      }
+
+      return box;
+    }
+
+    function getActiveCellPosition() {
+      return absBox(activeCellNode);
+    }
+
+    function getGridPosition() {
+      return absBox($container[0])
+    }
+
+    function handleActiveCellPositionChange() {
+      if (!activeCellNode) {
+        return;
+      }
+
+      trigger(self.onActiveCellPositionChanged, {grid: self});
+
+      if (currentEditor) {
+        var cellBox = getActiveCellPosition();
+        if (currentEditor.show && currentEditor.hide) {
+          if (!cellBox.visible) {
+            currentEditor.hide();
+          } else {
+            currentEditor.show();
+          }
+        }
+
+        if (currentEditor.position) {
+          currentEditor.position(cellBox);
+        }
+      }
+    }
+
+    function getCellEditor() {
+      return currentEditor;
+    }
+
+    function getActiveCell() {
+      if (!activeCellNode) {
+        return null;
+      } else {
+        return {row: activeRow, cell: activeCell, grid: self};
+      }
+    }
+
+    function getActiveCellNode() {
+      return activeCellNode;
+    }
+
+    function scrollRowIntoView(row, doPaging) {
+      var rowAtTop = row * options.rowHeight;
+      var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
+
+      // need to page down?
+      if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
+        scrollTo(doPaging ? rowAtTop : rowAtBottom);
+        render();
+      }
+      // or page up?
+      else if (row * options.rowHeight < scrollTop + offset) {
+        scrollTo(doPaging ? rowAtBottom : rowAtTop);
+        render();
+      }
+    }
+
+    function scrollRowToTop(row) {
+      scrollTo(row * options.rowHeight);
+      render();
+    }
+
+    function scrollPage(dir) {
+      var deltaRows = dir * numVisibleRows;
+      scrollTo((getRowFromPosition(scrollTop) + deltaRows) * options.rowHeight);
+      render();
+
+      if (options.enableCellNavigation && activeRow != null) {
+        var row = activeRow + deltaRows;
+        var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
+        if (row >= dataLengthIncludingAddNew) {
+          row = dataLengthIncludingAddNew - 1;
+        }
+        if (row < 0) {
+          row = 0;
+        }
+
+        var cell = 0, prevCell = null;
+        var prevActivePosX = activePosX;
+        while (cell <= activePosX) {
+          if (canCellBeActive(row, cell)) {
+            prevCell = cell;
+          }
+          cell += getColspan(row, cell);
+        }
+
+        if (prevCell !== null) {
+          setActiveCellInternal(getCellNode(row, prevCell));
+          activePosX = prevActivePosX;
+        } else {
+          resetActiveCell();
+        }
+      }
+    }
+
+    function navigatePageDown() {
+      scrollPage(1);
+    }
+
+    function navigatePageUp() {
+      scrollPage(-1);
+    }
+
+    function getColspan(row, cell) {
+      var metadata = data.getItemMetadata && data.getItemMetadata(row);
+      if (!metadata || !metadata.columns) {
+        return 1;
+      }
+
+      var columnData = metadata.columns[columns[cell].id] || metadata.columns[cell];
+      var colspan = (columnData && columnData.colspan);
+      if (colspan === "*") {
+        colspan = columns.length - cell;
+      } else {
+        colspan = colspan || 1;
+      }
+
+      return colspan;
+    }
+
+    function findFirstFocusableCell(row) {
+      var cell = 0;
+      while (cell < columns.length) {
+        if (canCellBeActive(row, cell)) {
+          return cell;
+        }
+        cell += getColspan(row, cell);
+      }
+      return null;
+    }
+
+    function findLastFocusableCell(row) {
+      var cell = 0;
+      var lastFocusableCell = null;
+      while (cell < columns.length) {
+        if (canCellBeActive(row, cell)) {
+          lastFocusableCell = cell;
+        }
+        cell += getColspan(row, cell);
+      }
+      return lastFocusableCell;
+    }
+
+    function gotoRight(row, cell, posX) {
+      if (cell >= columns.length) {
+        return null;
+      }
+
+      do {
+        cell += getColspan(row, cell);
+      }
+      while (cell < columns.length && !canCellBeActive(row, cell));
+
+      if (cell < columns.length) {
+        return {
+          "row": row,
+          "cell": cell,
+          "posX": cell
+        };
+      }
+      return null;
+    }
+
+    function gotoLeft(row, cell, posX) {
+      if (cell <= 0) {
+        return null;
+      }
+
+      var firstFocusableCell = findFirstFocusableCell(row);
+      if (firstFocusableCell === null || firstFocusableCell >= cell) {
+        return null;
+      }
+
+      var prev = {
+        "row": row,
+        "cell": firstFocusableCell,
+        "posX": firstFocusableCell
+      };
+      var pos;
+      while (true) {
+        pos = gotoRight(prev.row, prev.cell, prev.posX);
+        if (!pos) {
+          return null;
+        }
+        if (pos.cell >= cell) {
+          return prev;
+        }
+        prev = pos;
+      }
+    }
+
+    function gotoDown(row, cell, posX) {
+      var prevCell;
+      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
+      while (true) {
+        if (++row >= dataLengthIncludingAddNew) {
+          return null;
+        }
+
+        prevCell = cell = 0;
+        while (cell <= posX) {
+          prevCell = cell;
+          cell += getColspan(row, cell);
+        }
+
+        if (canCellBeActive(row, prevCell)) {
+          return {
+            "row": row,
+            "cell": prevCell,
+            "posX": posX
+          };
+        }
+      }
+    }
+
+    function gotoUp(row, cell, posX) {
+      var prevCell;
+      while (true) {
+        if (--row < 0) {
+          return null;
+        }
+
+        prevCell = cell = 0;
+        while (cell <= posX) {
+          prevCell = cell;
+          cell += getColspan(row, cell);
+        }
+
+        if (canCellBeActive(row, prevCell)) {
+          return {
+            "row": row,
+            "cell": prevCell,
+            "posX": posX
+          };
+        }
+      }
+    }
+
+    function gotoNext(row, cell, posX) {
+      if (row == null && cell == null) {
+        row = cell = posX = 0;
+        if (canCellBeActive(row, cell)) {
+          return {
+            "row": row,
+            "cell": cell,
+            "posX": cell
+          };
+        }
+      }
+
+      var pos = gotoRight(row, cell, posX);
+      if (pos) {
+        return pos;
+      }
+
+      var firstFocusableCell = null;
+      var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
+
+      // if at last row, cycle through columns rather than get stuck in the last one
+      if (row === dataLengthIncludingAddNew - 1) { row--; }
+
+      while (++row < dataLengthIncludingAddNew) {
+        firstFocusableCell = findFirstFocusableCell(row);
+        if (firstFocusableCell !== null) {
+          return {
+            "row": row,
+            "cell": firstFocusableCell,
+            "posX": firstFocusableCell
+          };
+        }
+      }
+      return null;
+    }
+
+    function gotoPrev(row, cell, posX) {
+      if (row == null && cell == null) {
+        row = getDataLengthIncludingAddNew() - 1;
+        cell = posX = columns.length - 1;
+        if (canCellBeActive(row, cell)) {
+          return {
+            "row": row,
+            "cell": cell,
+            "posX": cell
+          };
+        }
+      }
+
+      var pos;
+      var lastSelectableCell;
+      while (!pos) {
+        pos = gotoLeft(row, cell, posX);
+        if (pos) {
+          break;
+        }
+        if (--row < 0) {
+          return null;
+        }
+
+        cell = 0;
+        lastSelectableCell = findLastFocusableCell(row);
+        if (lastSelectableCell !== null) {
+          pos = {
+            "row": row,
+            "cell": lastSelectableCell,
+            "posX": lastSelectableCell
+          };
+        }
+      }
+      return pos;
+    }
+
+    function navigateRight() {
+      return navigate("right");
+    }
+
+    function navigateLeft() {
+      return navigate("left");
+    }
+
+    function navigateDown() {
+      return navigate("down");
+    }
+
+    function navigateUp() {
+      return navigate("up");
+    }
+
+    function navigateNext() {
+      return navigate("next");
+    }
+
+    function navigatePrev() {
+      return navigate("prev");
+    }
+
+    /**
+     * @param {string} dir Navigation direction.
+     * @return {boolean} Whether navigation resulted in a change of active cell.
+     */
+    function navigate(dir) {
+      if (!options.enableCellNavigation) {
+        return false;
+      }
+
+      if (!activeCellNode && dir != "prev" && dir != "next") {
+        return false;
+      }
+
+      if (!getEditorLock().commitCurrentEdit()) {
+        return true;
+      }
+      setFocus();
+
+      var tabbingDirections = {
+        "up": -1,
+        "down": 1,
+        "left": -1,
+        "right": 1,
+        "prev": -1,
+        "next": 1
+      };
+      tabbingDirection = tabbingDirections[dir];
+
+      var stepFunctions = {
+        "up": gotoUp,
+        "down": gotoDown,
+        "left": gotoLeft,
+        "right": gotoRight,
+        "prev": gotoPrev,
+        "next": gotoNext
+      };
+      var stepFn = stepFunctions[dir];
+      var pos = stepFn(activeRow, activeCell, activePosX);
+      if (pos) {
+        var isAddNewRow = (pos.row == getDataLength());
+        scrollCellIntoView(pos.row, pos.cell, !isAddNewRow);
+        setActiveCellInternal(getCellNode(pos.row, pos.cell));
+        activePosX = pos.posX;
+        return true;
+      } else {
+        setActiveCellInternal(getCellNode(activeRow, activeCell));
+        return false;
+      }
+    }
+
+    function getCellNode(row, cell) {
+      if (rowsCache[row]) {
+        ensureCellNodesInRowsCache(row);
+        return rowsCache[row].cellNodesByColumnIdx[cell];
+      }
+      return null;
+    }
+
+    function setActiveCell(row, cell, opt_editMode, preClickModeOn, suppressActiveCellChangedEvent) {
+      if (!initialized) { return; }
+      if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
+        return;
+      }
+
+      if (!options.enableCellNavigation) {
+        return;
+      }
+
+      scrollCellIntoView(row, cell, false);
+      setActiveCellInternal(getCellNode(row, cell), opt_editMode, preClickModeOn, suppressActiveCellChangedEvent);
+    }
+
+    function canCellBeActive(row, cell) {
+      if (!options.enableCellNavigation || row >= getDataLengthIncludingAddNew() ||
+          row < 0 || cell >= columns.length || cell < 0) {
+        return false;
+      }
+
+      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
+      if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
+        return rowMetadata.focusable;
+      }
+
+      var columnMetadata = rowMetadata && rowMetadata.columns;
+      if (columnMetadata && columnMetadata[columns[cell].id] && typeof columnMetadata[columns[cell].id].focusable === "boolean") {
+        return columnMetadata[columns[cell].id].focusable;
+      }
+      if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
+        return columnMetadata[cell].focusable;
+      }
+
+      return columns[cell].focusable;
+    }
+
+    function canCellBeSelected(row, cell) {
+      if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
+        return false;
+      }
+
+      var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
+      if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
+        return rowMetadata.selectable;
+      }
+
+      var columnMetadata = rowMetadata && rowMetadata.columns && (rowMetadata.columns[columns[cell].id] || rowMetadata.columns[cell]);
+      if (columnMetadata && typeof columnMetadata.selectable === "boolean") {
+        return columnMetadata.selectable;
+      }
+
+      return columns[cell].selectable;
+    }
+
+    function gotoCell(row, cell, forceEdit) {
+      if (!initialized) { return; }
+      if (!canCellBeActive(row, cell)) {
+        return;
+      }
+
+      if (!getEditorLock().commitCurrentEdit()) {
+        return;
+      }
+
+      scrollCellIntoView(row, cell, false);
+
+      var newCell = getCellNode(row, cell);
+
+      // if selecting the 'add new' row, start editing right away
+      setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
+
+      // if no editor was created, set the focus back on the grid
+      if (!currentEditor) {
+        setFocus();
+      }
+    }
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // IEditor implementation for the editor lock
+
+    function commitCurrentEdit() {
+      var item = getDataItem(activeRow);
+      var column = columns[activeCell];
+
+      if (currentEditor) {
+        if (currentEditor.isValueChanged()) {
+          var validationResults = currentEditor.validate();
+
+          if (validationResults.valid) {
+            if (activeRow < getDataLength()) {
+              var editCommand = {
+                row: activeRow,
+                cell: activeCell,
+                editor: currentEditor,
+                serializedValue: currentEditor.serializeValue(),
+                prevSerializedValue: serializedEditorValue,
+                execute: function () {
+                  this.editor.applyValue(item, this.serializedValue);
+                  updateRow(this.row);
+                  trigger(self.onCellChange, {
+                    row: activeRow,
+                    cell: activeCell,
+                    item: item,
+                    grid: self
+                  });
+                },
+                undo: function () {
+                  this.editor.applyValue(item, this.prevSerializedValue);
+                  updateRow(this.row);
+                  trigger(self.onCellChange, {
+                    row: activeRow,
+                    cell: activeCell,
+                    item: item,
+                    grid: self
+                  });
+                }
+              };
+
+              if (options.editCommandHandler) {
+                makeActiveCellNormal();
+                options.editCommandHandler(item, column, editCommand);
+              } else {
+                editCommand.execute();
+                makeActiveCellNormal();
+              }
+
+            } else {
+              var newItem = {};
+              currentEditor.applyValue(newItem, currentEditor.serializeValue());
+              makeActiveCellNormal();
+              trigger(self.onAddNewRow, {item: newItem, column: column, grid: self});
+            }
+
+            // check whether the lock has been re-acquired by event handlers
+            return !getEditorLock().isActive();
+          } else {
+            // Re-add the CSS class to trigger transitions, if any.
+            $(activeCellNode).removeClass("invalid");
+            $(activeCellNode).width();  // force layout
+            $(activeCellNode).addClass("invalid");
+
+            trigger(self.onValidationError, {
+              editor: currentEditor,
+              cellNode: activeCellNode,
+              validationResults: validationResults,
+              row: activeRow,
+              cell: activeCell,
+              column: column,
+              grid: self
+            });
+
+            currentEditor.focus();
+            return false;
+          }
+        }
+
+        makeActiveCellNormal();
+      }
+      return true;
+    }
+
+    function cancelCurrentEdit() {
+      makeActiveCellNormal();
+      return true;
+    }
+
+    function rowsToRanges(rows) {
+      var ranges = [];
+      var lastCell = columns.length - 1;
+      for (var i = 0; i < rows.length; i++) {
+        ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
+      }
+      return ranges;
+    }
+
+    function getSelectedRows() {
+      if (!selectionModel) {
+        throw new Error("Selection model is not set");
+      }
+      return selectedRows;
+    }
+
+    function setSelectedRows(rows) {
+      if (!selectionModel) {
+        throw new Error("Selection model is not set");
+      }
+      selectionModel.setSelectedRanges(rowsToRanges(rows));
+    }
+
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Debug
+
+    this.debug = function () {
+      var s = "";
+
+      s += ("\n" + "counter_rows_rendered:  " + counter_rows_rendered);
+      s += ("\n" + "counter_rows_removed:  " + counter_rows_removed);
+      s += ("\n" + "renderedRows:  " + renderedRows);
+      s += ("\n" + "numVisibleRows:  " + numVisibleRows);
+      s += ("\n" + "maxSupportedCssHeight:  " + maxSupportedCssHeight);
+      s += ("\n" + "n(umber of pages):  " + n);
+      s += ("\n" + "(current) page:  " + page);
+      s += ("\n" + "page height (ph):  " + ph);
+      s += ("\n" + "vScrollDir:  " + vScrollDir);
+
+      alert(s);
+    };
+
+    // a debug helper to be able to access private members
+    this.eval = function (expr) {
+      return eval(expr);
+    };
+
+    //////////////////////////////////////////////////////////////////////////////////////////////
+    // Public API
+
+    $.extend(this, {
+      "slickGridVersion": "2.3.4",
+
+      // Events
+      "onScroll": new Slick.Event(),
+      "onSort": new Slick.Event(),
+      "onHeaderMouseEnter": new Slick.Event(),
+      "onHeaderMouseLeave": new Slick.Event(),
+      "onHeaderContextMenu": new Slick.Event(),
+      "onHeaderClick": new Slick.Event(),
+      "onHeaderCellRendered": new Slick.Event(),
+      "onBeforeHeaderCellDestroy": new Slick.Event(),
+      "onHeaderRowCellRendered": new Slick.Event(),
+      "onFooterRowCellRendered": new Slick.Event(),
+      "onBeforeHeaderRowCellDestroy": new Slick.Event(),
+      "onBeforeFooterRowCellDestroy": new Slick.Event(),
+      "onMouseEnter": new Slick.Event(),
+      "onMouseLeave": new Slick.Event(),
+      "onClick": new Slick.Event(),
+      "onDblClick": new Slick.Event(),
+      "onContextMenu": new Slick.Event(),
+      "onKeyDown": new Slick.Event(),
+      "onAddNewRow": new Slick.Event(),
+      "onBeforeAppendCell": new Slick.Event(),
+      "onValidationError": new Slick.Event(),
+      "onViewportChanged": new Slick.Event(),
+      "onColumnsReordered": new Slick.Event(),
+      "onColumnsResized": new Slick.Event(),
+      "onCellChange": new Slick.Event(),
+      "onBeforeEditCell": new Slick.Event(),
+      "onBeforeCellEditorDestroy": new Slick.Event(),
+      "onBeforeDestroy": new Slick.Event(),
+      "onActiveCellChanged": new Slick.Event(),
+      "onActiveCellPositionChanged": new Slick.Event(),
+      "onDragInit": new Slick.Event(),
+      "onDragStart": new Slick.Event(),
+      "onDrag": new Slick.Event(),
+      "onDragEnd": new Slick.Event(),
+      "onSelectedRowsChanged": new Slick.Event(),
+      "onCellCssStylesChanged": new Slick.Event(),
+
+      // Methods
+      "registerPlugin": registerPlugin,
+      "unregisterPlugin": unregisterPlugin,
+      "getColumns": getColumns,
+      "setColumns": setColumns,
+      "getColumnIndex": getColumnIndex,
+      "updateColumnHeader": updateColumnHeader,
+      "setSortColumn": setSortColumn,
+      "setSortColumns": setSortColumns,
+      "getSortColumns": getSortColumns,
+      "autosizeColumns": autosizeColumns,
+      "getOptions": getOptions,
+      "setOptions": setOptions,
+      "getData": getData,
+      "getDataLength": getDataLength,
+      "getDataItem": getDataItem,
+      "setData": setData,
+      "getSelectionModel": getSelectionModel,
+      "setSelectionModel": setSelectionModel,
+      "getSelectedRows": getSelectedRows,
+      "setSelectedRows": setSelectedRows,
+      "getContainerNode": getContainerNode,
+      "updatePagingStatusFromView": updatePagingStatusFromView,
+
+      "render": render,
+      "invalidate": invalidate,
+      "invalidateRow": invalidateRow,
+      "invalidateRows": invalidateRows,
+      "invalidateAllRows": invalidateAllRows,
+      "updateCell": updateCell,
+      "updateRow": updateRow,
+      "getViewport": getVisibleRange,
+      "getRenderedRange": getRenderedRange,
+      "resizeCanvas": resizeCanvas,
+      "updateRowCount": updateRowCount,
+      "scrollRowIntoView": scrollRowIntoView,
+      "scrollRowToTop": scrollRowToTop,
+      "scrollCellIntoView": scrollCellIntoView,
+      "getCanvasNode": getCanvasNode,
+      "focus": setFocus,
+
+      "getCellFromPoint": getCellFromPoint,
+      "getCellFromEvent": getCellFromEvent,
+      "getActiveCell": getActiveCell,
+      "setActiveCell": setActiveCell,
+      "getActiveCellNode": getActiveCellNode,
+      "getActiveCellPosition": getActiveCellPosition,
+      "resetActiveCell": resetActiveCell,
+      "editActiveCell": makeActiveCellEditable,
+      "getCellEditor": getCellEditor,
+      "getCellNode": getCellNode,
+      "getCellNodeBox": getCellNodeBox,
+      "canCellBeSelected": canCellBeSelected,
+      "canCellBeActive": canCellBeActive,
+      "navigatePrev": navigatePrev,
+      "navigateNext": navigateNext,
+      "navigateUp": navigateUp,
+      "navigateDown": navigateDown,
+      "navigateLeft": navigateLeft,
+      "navigateRight": navigateRight,
+      "navigatePageUp": navigatePageUp,
+      "navigatePageDown": navigatePageDown,
+      "gotoCell": gotoCell,
+      "getTopPanel": getTopPanel,
+      "setTopPanelVisibility": setTopPanelVisibility,
+      "setHeaderRowVisibility": setHeaderRowVisibility,
+      "getHeaderRow": getHeaderRow,
+      "getHeaderRowColumn": getHeaderRowColumn,
+      "setFooterRowVisibility": setFooterRowVisibility,
+      "getFooterRow": getFooterRow,
+      "getFooterRowColumn": getFooterRowColumn,
+      "getGridPosition": getGridPosition,
+      "flashCell": flashCell,
+      "addCellCssStyles": addCellCssStyles,
+      "setCellCssStyles": setCellCssStyles,
+      "removeCellCssStyles": removeCellCssStyles,
+      "getCellCssStyles": getCellCssStyles,
+
+      "init": finishInitialization,
+      "destroy": destroy,
+
+      // IEditor implementation
+      "getEditorLock": getEditorLock,
+      "getEditController": getEditController
+    });
+
+    init();
+  }
+}(jQuery));
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.groupitemmetadataprovider.js b/web/pgadmin/static/vendor/slickgrid/slick.groupitemmetadataprovider.js
new file mode 100644
index 00000000..de05c721
--- /dev/null
+++ b/web/pgadmin/static/vendor/slickgrid/slick.groupitemmetadataprovider.js
@@ -0,0 +1,158 @@
+(function ($) {
+  $.extend(true, window, {
+    Slick: {
+      Data: {
+        GroupItemMetadataProvider: GroupItemMetadataProvider
+      }
+    }
+  });
+
+
+  /***
+   * Provides item metadata for group (Slick.Group) and totals (Slick.Totals) rows produced by the DataView.
+   * This metadata overrides the default behavior and formatting of those rows so that they appear and function
+   * correctly when processed by the grid.
+   *
+   * This class also acts as a grid plugin providing event handlers to expand & collapse groups.
+   * If "grid.registerPlugin(...)" is not called, expand & collapse will not work.
+   *
+   * @class GroupItemMetadataProvider
+   * @module Data
+   * @namespace Slick.Data
+   * @constructor
+   * @param options
+   */
+  function GroupItemMetadataProvider(options) {
+    var _grid;
+    var _defaults = {
+      groupCssClass: "slick-group",
+      groupTitleCssClass: "slick-group-title",
+      totalsCssClass: "slick-group-totals",
+      groupFocusable: true,
+      totalsFocusable: false,
+      toggleCssClass: "slick-group-toggle",
+      toggleExpandedCssClass: "expanded",
+      toggleCollapsedCssClass: "collapsed",
+      enableExpandCollapse: true,
+      groupFormatter: defaultGroupCellFormatter,
+      totalsFormatter: defaultTotalsCellFormatter
+    };
+
+    options = $.extend(true, {}, _defaults, options);
+
+
+    function defaultGroupCellFormatter(row, cell, value, columnDef, item) {
+      if (!options.enableExpandCollapse) {
+        return item.title;
+      }
+
+      var indentation = item.level * 15 + "px";
+
+      return "<span class='" + options.toggleCssClass + " " +
+          (item.collapsed ? options.toggleCollapsedCssClass : options.toggleExpandedCssClass) +
+          "' style='margin-left:" + indentation +"'>" +
+          "</span>" +
+          "<span class='" + options.groupTitleCssClass + "' level='" + item.level + "'>" +
+            item.title +
+          "</span>";
+    }
+
+    function defaultTotalsCellFormatter(row, cell, value, columnDef, item) {
+      return (columnDef.groupTotalsFormatter && columnDef.groupTotalsFormatter(item, columnDef)) || "";
+    }
+
+
+    function init(grid) {
+      _grid = grid;
+      _grid.onClick.subscribe(handleGridClick);
+      _grid.onKeyDown.subscribe(handleGridKeyDown);
+
+    }
+
+    function destroy() {
+      if (_grid) {
+        _grid.onClick.unsubscribe(handleGridClick);
+        _grid.onKeyDown.unsubscribe(handleGridKeyDown);
+      }
+    }
+
+    function handleGridClick(e, args) {
+      var item = this.getDataItem(args.row);
+      if (item && item instanceof Slick.Group && $(e.target).hasClass(options.toggleCssClass)) {
+        var range = _grid.getRenderedRange();
+        this.getData().setRefreshHints({
+          ignoreDiffsBefore: range.top,
+          ignoreDiffsAfter: range.bottom + 1
+        });
+
+        if (item.collapsed) {
+          this.getData().expandGroup(item.groupingKey);
+        } else {
+          this.getData().collapseGroup(item.groupingKey);
+        }
+
+        e.stopImmediatePropagation();
+        e.preventDefault();
+      }
+    }
+
+    // TODO:  add -/+ handling
+    function handleGridKeyDown(e, args) {
+      if (options.enableExpandCollapse && (e.which == $.ui.keyCode.SPACE)) {
+        var activeCell = this.getActiveCell();
+        if (activeCell) {
+          var item = this.getDataItem(activeCell.row);
+          if (item && item instanceof Slick.Group) {
+            var range = _grid.getRenderedRange();
+            this.getData().setRefreshHints({
+              ignoreDiffsBefore: range.top,
+              ignoreDiffsAfter: range.bottom + 1
+            });
+
+            if (item.collapsed) {
+              this.getData().expandGroup(item.groupingKey);
+            } else {
+              this.getData().collapseGroup(item.groupingKey);
+            }
+
+            e.stopImmediatePropagation();
+            e.preventDefault();
+          }
+        }
+      }
+    }
+
+    function getGroupRowMetadata(item) {
+      return {
+        selectable: false,
+        focusable: options.groupFocusable,
+        cssClasses: options.groupCssClass,
+        columns: {
+          0: {
+            colspan: "*",
+            formatter: options.groupFormatter,
+            editor: null
+          }
+        }
+      };
+    }
+
+    function getTotalsRowMetadata(item) {
+      return {
+        selectable: false,
+        focusable: options.totalsFocusable,
+        cssClasses: options.totalsCssClass,
+        formatter: options.totalsFormatter,
+        editor: null
+      };
+    }
+
+
+    return {
+      "init": init,
+      "destroy": destroy,
+      "getGroupRowMetadata": getGroupRowMetadata,
+      "getTotalsRowMetadata": getTotalsRowMetadata
+    };
+  }
+})(jQuery);
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.remotemodel-yahoo.js b/web/pgadmin/static/vendor/slickgrid/slick.remotemodel-yahoo.js
new file mode 100644
index 00000000..fb11d85a
--- /dev/null
+++ b/web/pgadmin/static/vendor/slickgrid/slick.remotemodel-yahoo.js
@@ -0,0 +1,206 @@
+(function ($) {
+  /***
+   * A sample AJAX data store implementation.
+   * Right now, it's hooked up to load Hackernews stories, but can
+   * easily be extended to support any JSONP-compatible backend that accepts paging parameters.
+   */
+  function RemoteModel() {
+    // private
+    var PAGESIZE = 10;
+    var data = {length: 0};
+    var h_request = null;
+    var req = null; // ajax request
+
+    // events
+    var onDataLoading = new Slick.Event();
+    var onDataLoaded = new Slick.Event();
+
+
+    function init() {
+    }
+
+
+    function isDataLoaded(from, to) {
+      for (var i = from; i <= to; i++) {
+        if (data[i] == undefined || data[i] == null) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+
+    function clear() {
+      for (var key in data) {
+        delete data[key];
+      }
+      data.length = 0;
+    }
+
+    function ensureData(from, to) {
+      if (req) {
+        req.abort();
+        for (var i = req.fromPage; i <= req.toPage; i++) {
+          data[i * PAGESIZE] = undefined;
+        }
+      }
+
+      if (from < 0) {
+        from = 0;
+      }
+
+      if (data.length > 0) {
+        to = Math.min(to, data.length - 1);
+      }
+
+      var fromPage = Math.floor(from / PAGESIZE);
+      var toPage = Math.floor(to / PAGESIZE);
+
+      while (data[fromPage * PAGESIZE] !== undefined && fromPage < toPage)
+        fromPage++;
+
+      while (data[toPage * PAGESIZE] !== undefined && fromPage < toPage)
+        toPage--;
+
+      if (fromPage > toPage || ((fromPage == toPage) && data[fromPage * PAGESIZE] !== undefined)) {
+        // TODO:  look-ahead
+        onDataLoaded.notify({from: from, to: to});
+        return;
+      }
+
+      var recStart = (fromPage * PAGESIZE);
+      var recCount = (((toPage - fromPage) * PAGESIZE) + PAGESIZE);
+
+      var url = "https://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20rss"
+        + "(" + recStart + "%2C" + recCount + ")"
+        + "%20where%20url%3D%22http%3A%2F%2Frss.news.yahoo.com%2Frss%2Ftopstories%22"
+        + "&format=json";
+
+      if (h_request != null) {
+        clearTimeout(h_request);
+      }
+
+      h_request = setTimeout(function () {
+        for (var i = fromPage; i <= toPage; i++)
+          data[i * PAGESIZE] = null; // null indicates a 'requested but not available yet'
+
+        onDataLoading.notify({from: from, to: to});
+
+        req = $.jsonp({
+          url: url,
+          callbackParameter: "callback",
+          cache: true,
+          success: function (json, textStatus, xOptions) {
+            onSuccess(json, recStart)
+          },
+          error: function () {
+            onError(fromPage, toPage)
+          }
+        });
+
+        req.fromPage = fromPage;
+        req.toPage = toPage;
+      }, 50);
+    }
+
+
+    function onError(fromPage, toPage) {
+      alert("error loading pages " + fromPage + " to " + toPage);
+    }
+
+// SAMPLE DATA
+//    {
+//      "query": {
+//        "count": 40,
+//        "created": "2015-03-03T00:34:00Z",
+//        "lang": "en-US",
+//        "results": {
+//          "item": [
+//            {
+//              "title": "Netanyahu assails Iran deal, touts US-Israel ties",
+//              "description": "<p><a href=\"http://news.yahoo.com/netanyahu-us-officials-face-off-iran-133539021--politics.html\"><img src=\"http://l2.yimg.com/bt/api/res/1.2/4eoBxbJStrbGAKbmBYOJfg--/YXBwaWQ9eW5ld3M7Zmk9ZmlsbDtoPTg2O3E9NzU7dz0xMzA-/http://media.zenfs.com/en_us/News/ap_webfeeds/2f3a20c2d46d9f096f0f6a706700d430.jpg\" width=\"130\" height=\"86\" alt=\"Israeli Prime Minister Benjamin Netanyahu addresses the 2015 American Israel Public Affairs Committee (AIPAC) Policy Conference in Washington, Monday, March 2, 2015. (AP Photo/Cliff Owen)\" align=\"left\" title=\"Israeli Prime Minister Benjamin Netanyahu addresses the 2015 American Israel Public Affairs Committee (AIPAC) Policy Conference in Washington, Monday, March 2, 2015. (AP Photo/Cliff Owen)\" border=\"0\" /></a>WASHINGTON (AP) — Seeking to lower tensions, Benjamin Netanyahu and U.S. officials cast their dispute over Iran as a family squabble on Monday, even as the Israeli leader claimed President Barack Obama did not — and could not — fully understand his nation&#039;s vital security concerns.</p><br clear=\"all\"/>",
+//              "link": "http://news.yahoo.com/netanyahu-us-officials-face-off-iran-133539021--politics.html",
+//              "pubDate": "Mon, 02 Mar 2015 19:17:36 -0500",
+//              "source": {
+//                "url": "http://www.ap.org/",
+//                "content": "Associated Press"
+//              },
+//              "guid": {
+//                "isPermaLink": "false",
+//                "content": "netanyahu-us-officials-face-off-iran-133539021--politics"
+//              },
+//              "content": {
+//                "height": "86",
+//                "type": "image/jpeg",
+//                "url": "http://l2.yimg.com/bt/api/res/1.2/4eoBxbJStrbGAKbmBYOJfg--/YXBwaWQ9eW5ld3M7Zmk9ZmlsbDtoPTg2O3E9NzU7dz0xMzA-/http://media.zenfs.com/en_us/News/ap_webfeeds/2f3a20c2d46d9f096f0f6a706700d430.jpg",
+//                "width": "130"
+//              },
+//              "text": {
+//                "type": "html",
+//                "content": "<p><a href=\"http://news.yahoo.com/netanyahu-us-officials-face-off-iran-133539021--politics.html\"><img src=\"http://l2.yimg.com/bt/api/res/1.2/4eoBxbJStrbGAKbmBYOJfg--/YXBwaWQ9eW5ld3M7Zmk9ZmlsbDtoPTg2O3E9NzU7dz0xMzA-/http://media.zenfs.com/en_us/News/ap_webfeeds/2f3a20c2d46d9f096f0f6a706700d430.jpg\" width=\"130\" height=\"86\" alt=\"Israeli Prime Minister Benjamin Netanyahu addresses the 2015 American Israel Public Affairs Committee (AIPAC) Policy Conference in Washington, Monday, March 2, 2015. (AP Photo/Cliff Owen)\" align=\"left\" title=\"Israeli Prime Minister Benjamin Netanyahu addresses the 2015 American Israel Public Affairs Committee (AIPAC) Policy Conference in Washington, Monday, March 2, 2015. (AP Photo/Cliff Owen)\" border=\"0\" /></a>WASHINGTON (AP) — Seeking to lower tensions, Benjamin Netanyahu and U.S. officials cast their dispute over Iran as a family squabble on Monday, even as the Israeli leader claimed President Barack Obama did not — and could not — fully understand his nation&#039;s vital security concerns.</p><br clear=\"all\"/>"
+//              },
+//              "credit": {
+//                "role": "publishing company"
+//              }
+//            },
+//            {... },
+//            {... },
+//          ]
+//        }
+//      }
+//    }
+
+    function onSuccess(json, recStart) {
+      var recEnd = recStart;
+      if (json.query.count > 0) {
+        var results = json.query.results.item;
+        recEnd = recStart + results.length;
+        data.length = 100;// Math.min(parseInt(results.length),1000); // limitation of the API
+
+        for (var i = 0; i < results.length; i++) {
+          var item = results[i];
+
+          item.pubDate = new Date(item.pubDate);
+
+          data[recStart + i] = { index: recStart + i };
+          data[recStart + i].pubDate = item.pubDate;
+          data[recStart + i].title = item.title;
+          data[recStart + i].url = item.link;
+          data[recStart + i].text = item.description;
+        }
+      }
+      req = null;
+
+      onDataLoaded.notify({from: recStart, to: recEnd});
+    }
+
+
+    function reloadData(from, to) {
+      for (var i = from; i <= to; i++)
+        delete data[i];
+
+      ensureData(from, to);
+    }
+
+    init();
+
+    return {
+      // properties
+      "data": data,
+
+      // methods
+      "clear": clear,
+      "isDataLoaded": isDataLoaded,
+      "ensureData": ensureData,
+      "reloadData": reloadData,
+
+      // events
+      "onDataLoading": onDataLoading,
+      "onDataLoaded": onDataLoaded
+    };
+  }
+
+  // Slick.Data.RemoteModel
+  $.extend(true, window, { Slick: { Data: { RemoteModel: RemoteModel }}});
+})(jQuery);
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.remotemodel.js b/web/pgadmin/static/vendor/slickgrid/slick.remotemodel.js
new file mode 100644
index 00000000..f81ca3f4
--- /dev/null
+++ b/web/pgadmin/static/vendor/slickgrid/slick.remotemodel.js
@@ -0,0 +1,169 @@
+(function ($) {
+  /***
+   * A sample AJAX data store implementation.
+   * Right now, it's hooked up to load search results from Octopart, but can
+   * easily be extended to support any JSONP-compatible backend that accepts paging parameters.
+   */
+  function RemoteModel() {
+    // private
+    var PAGESIZE = 50;
+    var data = {length: 0};
+    var searchstr = "";
+    var sortcol = null;
+    var sortdir = 1;
+    var h_request = null;
+    var req = null; // ajax request
+
+    // events
+    var onDataLoading = new Slick.Event();
+    var onDataLoaded = new Slick.Event();
+
+
+    function init() {
+    }
+
+
+    function isDataLoaded(from, to) {
+      for (var i = from; i <= to; i++) {
+        if (data[i] == undefined || data[i] == null) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+
+    function clear() {
+      for (var key in data) {
+        delete data[key];
+      }
+      data.length = 0;
+    }
+
+
+    function ensureData(from, to) {
+      if (req) {
+        req.abort();
+        for (var i = req.fromPage; i <= req.toPage; i++)
+          data[i * PAGESIZE] = undefined;
+      }
+
+      if (from < 0) {
+        from = 0;
+      }
+
+      if (data.length > 0) {
+        to = Math.min(to, data.length - 1);
+      }
+
+      var fromPage = Math.floor(from / PAGESIZE);
+      var toPage = Math.floor(to / PAGESIZE);
+
+      while (data[fromPage * PAGESIZE] !== undefined && fromPage < toPage)
+        fromPage++;
+
+      while (data[toPage * PAGESIZE] !== undefined && fromPage < toPage)
+        toPage--;
+
+      if (fromPage > toPage || ((fromPage == toPage) && data[fromPage * PAGESIZE] !== undefined)) {
+        // TODO:  look-ahead
+        onDataLoaded.notify({from: from, to: to});
+        return;
+      }
+
+      var url = "http://octopart.com/api/v3/parts/search?apikey=68b25f31&include[]=short_description&show[]=uid&show[]=manufacturer&show[]=mpn&show[]=brand&show[]=octopart_url&show[]=short_description&q=" + searchstr + "&start=" + (fromPage * PAGESIZE) + "&limit=" + (((toPage - fromPage) * PAGESIZE) + PAGESIZE);
+
+      if (sortcol != null) {
+        url += ("&sortby=" + sortcol + ((sortdir > 0) ? "+asc" : "+desc"));
+      }
+
+      if (h_request != null) {
+        clearTimeout(h_request);
+      }
+
+      h_request = setTimeout(function () {
+        for (var i = fromPage; i <= toPage; i++)
+          data[i * PAGESIZE] = null; // null indicates a 'requested but not available yet'
+
+        onDataLoading.notify({from: from, to: to});
+
+        req = $.jsonp({
+          url: url,
+          callbackParameter: "callback",
+          cache: true,
+          success: onSuccess,
+          error: function () {
+            onError(fromPage, toPage)
+          }
+        });
+        req.fromPage = fromPage;
+        req.toPage = toPage;
+      }, 50);
+    }
+
+
+    function onError(fromPage, toPage) {
+      alert("error loading pages " + fromPage + " to " + toPage);
+    }
+
+    function onSuccess(resp) {
+      var from = resp.request.start, to = from + resp.results.length;
+      data.length = Math.min(parseInt(resp.hits),1000); // limitation of the API
+
+      for (var i = 0; i < resp.results.length; i++) {
+        var item = resp.results[i].item;
+
+        data[from + i] = item;
+        data[from + i].index = from + i;
+      }
+
+      req = null;
+
+      onDataLoaded.notify({from: from, to: to});
+    }
+
+
+    function reloadData(from, to) {
+      for (var i = from; i <= to; i++)
+        delete data[i];
+
+      ensureData(from, to);
+    }
+
+
+    function setSort(column, dir) {
+      sortcol = column;
+      sortdir = dir;
+      clear();
+    }
+
+    function setSearch(str) {
+      searchstr = str;
+      clear();
+    }
+
+
+    init();
+
+    return {
+      // properties
+      "data": data,
+
+      // methods
+      "clear": clear,
+      "isDataLoaded": isDataLoaded,
+      "ensureData": ensureData,
+      "reloadData": reloadData,
+      "setSort": setSort,
+      "setSearch": setSearch,
+
+      // events
+      "onDataLoading": onDataLoading,
+      "onDataLoaded": onDataLoaded
+    };
+  }
+
+  // Slick.Data.RemoteModel
+  $.extend(true, window, { Slick: { Data: { RemoteModel: RemoteModel }}});
+})(jQuery);
\ No newline at end of file
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 54e3688d..f3fcbedf 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -279,7 +279,7 @@ li {
   background-color: white;
 }
 
-.sc.cell-move-handle {
+.slick-cell.cell-move-handle {
   font-weight: bold;
   text-align: right;
   border-right: solid gray;
@@ -291,15 +291,15 @@ li {
   background: #b6b9bd;
 }
 
-.sr.selected .cell-move-handle {
+.slick-row.selected .cell-move-handle {
   background: #D5DC8D;
 }
 
-.sr .cell-actions {
+.slick-row .cell-actions {
   text-align: left;
 }
 
-.sr.complete {
+.slick-row.complete {
   background-color: #DFD;
   color: #555;
 }
@@ -332,7 +332,7 @@ li {
   font-size: 10px;
 }
 
-.sr.selected .cell-selection {
+.slick-row.selected .cell-selection {
   background-color: transparent; /* show default selected row background */
 }
 
@@ -359,6 +359,10 @@ li {
   display: table-cell;
 }
 
+.column-description {
+  display: table-cell;
+}
+
 .long_text_editor {
   margin-left: 5px;
   font-size: 12px !important;
@@ -401,10 +405,17 @@ input.editor-checkbox:focus {
 }
 
 /* Override selected row color */
-.sc.selected {
+.slick-cell.selected {
     background-color: #DEE8F1 !important;
 }
 
+/* Remove active cell border */
+.sc.active {
+  border: 1px solid transparent;
+  border-right: 1px dotted silver;
+  border-bottom-color: silver;
+}
+
 /* To highlight all newly inserted rows */
 .grid-canvas .new_row {
   background: #dff0d7;
@@ -431,8 +442,8 @@ input.editor-checkbox:focus {
 }
 
 /* color the first column */
-.sr .sc:first-child {
-    background-color: #2c76b4;
+.slick-row .slick-cell.l0.r0 {
+  background-color: #2c76b4;
 }
 
 #datagrid div.slick-header.ui-state-default {
@@ -453,6 +464,11 @@ input.editor-checkbox:focus {
   -ms-box-sizing: content-box;
 }
 
-.sr.ui-widget-content {
+.slick-row.ui-widget-content {
   border-top: 1px solid silver;
+}
+
+.select-all-icon {
+  margin-left: 12px;
+  vertical-align: bottom;
 }
\ No newline at end of file
diff --git a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
index d8ae73ee..1bf919f9 100644
--- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
@@ -2,8 +2,12 @@ define(
   [
     'jquery', 'underscore', 'underscore.string', 'alertify', 'pgadmin',
     'backbone', 'backgrid', 'codemirror', 'pgadmin.misc.explain',
-    'sources/selection/grid_selector', 'sources/selection/clipboard',
+    'sources/selection/grid_selector',
+    'sources/selection/active_cell_capture',
+    'sources/selection/clipboard',
     'sources/selection/copy_data',
+    'sources/selection/range_selection_helper',
+    'sources/slickgrid/event_handlers/handle_query_output_keyboard_event',
 
     'slickgrid', 'bootstrap', 'pgadmin.browser', 'wcdocker',
     'codemirror/mode/sql/sql', 'codemirror/addon/selection/mark-selection',
@@ -27,7 +31,8 @@ define(
     'slickgrid/slick.grid'
   ],
   function(
-    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector, clipboard, copyData
+    $, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector,
+    ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent
   ) {
     /* Return back, this has been called more than once */
     if (pgAdmin.SqlEditor)
@@ -645,7 +650,8 @@ define(
 
         var grid = new Slick.Grid($data_grid, collection, grid_columns, grid_options);
         grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) );
-        grid.setSelectionModel(new Slick.RowSelectionModel({selectActiveRow: false}));
+        grid.registerPlugin(new ActiveCellCapture());
+        grid.setSelectionModel(new Slick.CellSelectionModel());
         grid.registerPlugin(gridSelector);
 
         var editor_data = {
@@ -707,16 +713,24 @@ define(
                 $("#btn-copy-row").prop('disabled', true);
               }
 
-              // If any row(s) selected ?
-              if(selected_rows_list.length) {
-                if(this.editor.handler.can_edit)
+              if(this.selection.getSelectedRanges().length > 0) {
+                // Enable copy rows button
+                $("#btn-copy-row").prop('disabled', false);
+              } else {
+                // Disable copy rows button
+                $("#btn-copy-row").prop('disabled', true);
+              }
+
+              // If any range(s) selected ?
+              if(grid.getSelectionModel().getSelectedRanges().length > 0) {
+                if (this.editor.handler.can_edit) {
                   // Enable delete rows and copy rows button
                   $("#btn-delete-row").prop('disabled', false);
-                  $("#btn-copy-row").prop('disabled', false);
-                  // Collect primary key data from collection as needed for stage row
-                  _.each(selected_rows_list, function(row_index) {
-                    var row_data = collection[row_index],
-                      pkey_data = _.pick(row_data, primary_key_list);
+                }
+                // Collect primary key data from collection as needed for stage row
+                _.each(selected_rows_list, function (row_index) {
+                  var row_data = collection[row_index],
+                    pkey_data = _.pick(row_data, primary_key_list);
 
                     // Store Primary key data for selected rows
                     if (!_.isUndefined(row_data) && !_.isUndefined(pkey_data)) {
@@ -725,10 +739,6 @@ define(
                     }
                   });
               } else {
-                //clear staged rows
-                clear_staged_rows();
-              }
-              if (!Object.keys(rows_for_stage).length) {
                 clear_staged_rows();
               }
 
@@ -771,41 +781,7 @@ define(
             }
         });
 
-        // Listener function for COPY/PASTE operation on grid
-        grid.onKeyDown.subscribe(function (e, args) {
-          var c = e.keyCode,
-            ctrlDown = e.ctrlKey||e.metaKey; // Mac support
-
-          //    (ctrlDown && c==67) return false // c
-          //    (ctrlDown && c==86) return false // v
-          //    (ctrlDown && c==88) return false // x
-
-
-          if (!ctrlDown && !(c==67 || c==86 || c==88)) {
-            return;  // Not a copy paste opration
-          }
-
-          var grid = args.grid, column_info, column_values, value,
-            cell = args.cell, row = args.row;
-
-          // Copy operation (Only when if there is no row selected)
-          // When user press `Ctrl + c` on selected cell
-          if(ctrlDown && c==67) {
-            // May be single cell is selected
-            column_info = grid.getColumns()[cell]
-            // Fetch current row data from grid
-            column_values = grid.getDataItem(row, cell)
-            //  Get the value from cell
-            value = column_values[column_info.pos] || '';
-            // Copy this value to Clipboard
-            if(value)
-              clipboard.copyTextToClipboard(value);
-            // Go to cell again
-            grid.gotoCell(row, cell, false);
-          }
-
-        });
-
+        grid.onKeyDown.subscribe(handleQueryOutputKeyboardEvent);
 
         // Listener function which will be called when user updates existing rows
         grid.onCellChange.subscribe(function (e, args) {
diff --git a/web/pgadmin/utils/tests/test_versioned_template_loader.py b/web/pgadmin/utils/tests/test_versioned_template_loader.py
index 4ba56033..a0d97899 100644
--- a/web/pgadmin/utils/tests/test_versioned_template_loader.py
+++ b/web/pgadmin/utils/tests/test_versioned_template_loader.py
@@ -19,16 +19,27 @@ from pgadmin.utils.route import BaseTestGenerator
 
 class TestVersionedTemplateLoader(BaseTestGenerator):
     scenarios = [
-        ("Test versioned template loader", dict())
+        ("Render a template when called", dict(scenario=1)),
+        ("Render a version 9.1 template when it is present", dict(scenario=2)),
+        ("Render a version 9.2 template when request for a higher version", dict(scenario=3)),
+        ("Render default version when version 9.0 was requested and only 9.1 and 9.2 are present", dict(scenario=4)),
+        ("Raise error when version is smaller than available templates", dict(scenario=5))
     ]
 
     def setUp(self):
         self.loader = VersionedTemplateLoader(FakeApp())
 
     def runTest(self):
-        self.test_get_source_returns_a_template()
-        self.test_get_source_when_the_version_is_9_1_returns_9_1_template()
-        self.test_get_source_when_the_version_is_9_3_and_there_are_templates_for_9_2_and_9_1_returns_9_2_template()
+        if self.scenario == 1:
+            self.test_get_source_returns_a_template()
+        if self.scenario == 2:
+            self.test_get_source_when_the_version_is_9_1_returns_9_1_template()
+        if self.scenario == 3:
+            self.test_get_source_when_the_version_is_9_3_and_there_are_templates_for_9_2_and_9_1_returns_9_2_template()
+        if self.scenario == 4:
+            self.test_get_source_when_version_is_9_0_and_there_are_templates_for_9_1_and_9_2_returns_default_template()
+        if self.scenario == 5:
+            self.test_raise_not_found_exception_when_postgres_version_less_than_all_available_sql_templates()
 
     def test_get_source_returns_a_template(self):
         expected_content = "Some SQL" \
@@ -57,10 +68,12 @@ class TestVersionedTemplateLoader(BaseTestGenerator):
         self.assertEqual("Some 9.2 SQL", str(content).replace("\r",""))
         self.assertIn(sql_path, filename)
 
-    def test_get_source_when_the_version_is_9_0_and_there_are_templates_for_9_1_and_9_2_returns_default_template(self):
+    def test_get_source_when_version_is_9_0_and_there_are_templates_for_9_1_and_9_2_returns_default_template(self):
         # For cross platform we join the SQL path (This solves the slashes issue)
         sql_path = os.path.join("some_feature", "sql", "default", "some_action_with_default.sql")
-        content, filename, up_to_dateness = self.loader.get_source(None, "some_feature/sql/#90000#/some_action_with_default.sql")
+        content, filename, up_to_dateness = self.loader.get_source(
+            None,
+            "some_feature/sql/#90000#/some_action_with_default.sql")
 
         self.assertEqual("Some default SQL", str(content).replace("\r",""))
         self.assertIn(sql_path, filename)
diff --git a/web/regression/javascript/selection/active_cell_capture_spec.js b/web/regression/javascript/selection/active_cell_capture_spec.js
new file mode 100644
index 00000000..aaf08344
--- /dev/null
+++ b/web/regression/javascript/selection/active_cell_capture_spec.js
@@ -0,0 +1,231 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+define([
+  'sources/selection/active_cell_capture',
+  'sources/selection/range_selection_helper',
+], function (ActiveCellCapture, RangeSelectionHelper) {
+  describe('ActiveCellCapture', function () {
+    var grid, activeCellPlugin, getSelectedRangesSpy, setSelectedRangesSpy;
+    var onHeaderClickFunction, onClickFunction;
+    beforeEach(function () {
+      getSelectedRangesSpy = jasmine.createSpy('getSelectedRangesSpy');
+      setSelectedRangesSpy = jasmine.createSpy('setSelectedRangesSpy');
+      grid = {
+        getSelectionModel: function () {
+          return {
+            getSelectedRanges: getSelectedRangesSpy,
+            setSelectedRanges: setSelectedRangesSpy,
+          }
+        },
+
+        getColumns: function () {
+          return [
+            {id: 'row-header-column'},
+            {id: 'column-1'},
+            {id: 'column-2'}
+          ]
+        },
+
+        onDragEnd: jasmine.createSpyObj('onDragEnd', ['subscribe']),
+        onActiveCellChanged: jasmine.createSpyObj('onActiveCellChanged', ['subscribe']),
+        onHeaderClick: jasmine.createSpy('onHeaderClick'),
+        onClick: jasmine.createSpy('onClick'),
+        onKeyDown: jasmine.createSpyObj('onKeyDown', ['subscribe']),
+
+        getDataLength: function () { return 10 },
+
+        setActiveCell: jasmine.createSpy('setActiveCell'),
+        getActiveCell: jasmine.createSpy('getActiveCell'),
+        resetActiveCell: jasmine.createSpy('resetActiveCell'),
+      };
+      activeCellPlugin = new ActiveCellCapture();
+
+      grid.onHeaderClick.subscribe = jasmine.createSpy('subscribe').and.callFake(function (callback) {
+        onHeaderClickFunction = callback.bind(activeCellPlugin);
+      });
+
+      grid.onClick.subscribe = jasmine.createSpy('subscribe').and.callFake(function (callback) {
+        onClickFunction = callback.bind(activeCellPlugin);
+      });
+
+      activeCellPlugin.init(grid);
+    });
+
+    describe('onHeaderClickHandler', function () {
+      describe('when no ranges are selected', function () {
+        beforeEach(function () {
+          getSelectedRangesSpy.and.returnValue([]);
+          onHeaderClickFunction({}, {column: {pos: 1}});
+          grid.getActiveCell.and.returnValue(null);
+        });
+
+        it('should set the active cell', function () {
+          expect(grid.setActiveCell).toHaveBeenCalledWith(0, 2);
+        });
+
+        it('should not change the selected ranges', function () {
+          expect(setSelectedRangesSpy).not.toHaveBeenCalled();
+        });
+      });
+
+      describe('when only one column is already selected', function () {
+        beforeEach(function () {
+          getSelectedRangesSpy.and.returnValue([RangeSelectionHelper.rangeForColumn(grid, 2)]);
+          grid.getActiveCell.and.returnValue({cell: 2, row: 0});
+        });
+
+        describe('when a different column is clicked', function () {
+          beforeEach(function () {
+            onHeaderClickFunction({}, {column: {pos: 4}})
+          });
+
+          it('should set the active cell to the newly clicked columns top cell', function () {
+            expect(grid.setActiveCell).toHaveBeenCalledWith(0, 5);
+          });
+        });
+
+        describe('when the same column is clicked', function () {
+          beforeEach(function () {
+            onHeaderClickFunction({}, {column: {pos: 1}})
+          });
+
+          it('should reset the active cell', function () {
+            expect(grid.resetActiveCell).toHaveBeenCalled();
+          });
+        });
+      });
+
+      describe('when three non-consecutive columns are selected', function () {
+        beforeEach(function () {
+          getSelectedRangesSpy.and.returnValue([
+            RangeSelectionHelper.rangeForColumn(grid, 10),
+            RangeSelectionHelper.rangeForColumn(grid, 6),
+            RangeSelectionHelper.rangeForColumn(grid, 22),
+          ]);
+          grid.getActiveCell.and.returnValue({cell: 22, row: 0});
+        });
+
+        describe('when the third column is clicked (thereby deselecting it)', function () {
+          beforeEach(function () {
+            onHeaderClickFunction({}, {column: {pos: 21}})
+          });
+
+          it('should set the active cell to the second column', function () {
+            expect(grid.setActiveCell).toHaveBeenCalledWith(0, 6);
+          });
+        });
+
+        describe('when the second column is clicked (thereby deselecting it)', function () {
+          beforeEach(function () {
+            onHeaderClickFunction({}, {column: {pos: 5}})
+          });
+
+          it('should not set the active cell', function () {
+            expect(grid.setActiveCell).not.toHaveBeenCalled();
+          });
+        });
+      });
+    });
+
+    describe('onClick', function () {
+      describe('when the target is a random cell in the grid', function () {
+        it('should not change the active cell', function () {
+          onClickFunction({}, {row: 1, cell: 2});
+          grid.getActiveCell.and.returnValue(null);
+
+          expect(grid.setActiveCell).not.toHaveBeenCalled();
+        });
+      });
+
+      describe('when the target is the row header', function () {
+
+        describe('when no rows are selected', function () {
+          beforeEach(function () {
+            getSelectedRangesSpy.and.returnValue([]);
+            onClickFunction({}, {row: 1, cell: 0});
+            grid.getActiveCell.and.returnValue(null);
+          });
+
+          it('changes the active cell', function () {
+            expect(grid.setActiveCell).toHaveBeenCalledWith(1, 1);
+          });
+
+          it('should not change the selected ranges', function () {
+            expect(setSelectedRangesSpy).not.toHaveBeenCalled();
+          });
+        });
+
+        describe('when there is one cell selected', function () {
+          beforeEach(function () {
+            grid.getActiveCell.and.returnValue({row: 4, cell: 5});
+            getSelectedRangesSpy.and.returnValue([
+              new Slick.Range(4, 5)
+            ]);
+          });
+
+          it('sets the active cell', function () {
+            onClickFunction({}, {row: 4, cell: 0});
+
+            expect(grid.setActiveCell).toHaveBeenCalledWith(4, 1);
+          });
+        });
+
+        describe('when there is one row selected', function () {
+          beforeEach(function () {
+            grid.getActiveCell.and.returnValue({row: 3, cell: 1});
+            getSelectedRangesSpy.and.returnValue([
+              RangeSelectionHelper.rangeForRow(grid, 3)
+            ]);
+          });
+
+          it('resets the selected row', function () {
+            onClickFunction({}, {row: 3, cell: 0});
+
+            expect(grid.resetActiveCell).toHaveBeenCalled();
+          });
+
+          describe('when we select a different row', function () {
+            it('should change the active cell', function () {
+              onClickFunction({}, {row: 10, cell: 0});
+
+              expect(grid.setActiveCell).toHaveBeenCalledWith(10, 1);
+            });
+          });
+        });
+
+        describe('when there are 2 rows selected', function () {
+          beforeEach(function () {
+            grid.getActiveCell.and.returnValue({row: 3, cell: 1});
+            getSelectedRangesSpy.and.returnValue([
+              RangeSelectionHelper.rangeForRow(grid, 5),
+              RangeSelectionHelper.rangeForRow(grid, 3)
+            ]);
+          });
+
+          describe('when the last selected row is clicked again', function () {
+            it('should change the active cell to the first selected row', function () {
+              onClickFunction({}, {row: 3, cell: 0});
+
+              expect(grid.setActiveCell).toHaveBeenCalledWith(5, 1);
+            });
+          });
+
+          describe('when the first selected row is clicked again', function () {
+            it('should not change the active cell', function () {
+              onClickFunction({}, {row: 5, cell: 0});
+
+              expect(grid.setActiveCell).not.toHaveBeenCalled();
+            });
+          });
+        });
+      });
+    });
+  });
+});
diff --git a/web/regression/javascript/selection/column_selector_spec.js b/web/regression/javascript/selection/column_selector_spec.js
index 947f4852..4526b602 100644
--- a/web/regression/javascript/selection/column_selector_spec.js
+++ b/web/regression/javascript/selection/column_selector_spec.js
@@ -1,32 +1,63 @@
 define(
   ["jquery",
     "underscore",
-    "slickgrid/slick.grid",
     "sources/selection/column_selector",
-    "slickgrid/slick.rowselectionmodel",
-    "slickgrid"
+    "sources/selection/active_cell_capture",
+    "sources/selection/grid_selector",
+
+    "slickgrid",
+    'slickgrid/slick.cellrangedecorator',
+    'slickgrid/slick.cellrangeselector',
+    "slickgrid/slick.cellselectionmodel",
+    "slickgrid/slick.grid",
   ],
-  function ($, _, SlickGrid, ColumnSelector, RowSelectionModel, Slick) {
+  function ($, _, ColumnSelector, ActiveCellCapture, GridSelector) {
+    var KEY_RIGHT = 39;
+    var KEY_LEFT = 37;
+    var KEY_UP = 38;
+    var KEY_DOWN = 40;
+
+    var Slick = window.Slick;
+    var SlickGrid = Slick.Grid;
+
     describe("ColumnSelector", function () {
       var container, data, columns, options;
       beforeEach(function () {
         container = $("<div></div>");
         container.height(9999);
+        container.width(9999);
 
-        data = [{'some-column-name': 'first value', 'second column': 'second value'}];
+        data = [{
+          'some-column-name': 'first value',
+          'second column': 'second value',
+          'third column': 'nonselectable value'
+        }];
 
         columns = [
           {
+            id: 'row-header-column',
+            name: 'row header column name',
+            selectable: false,
+          },
+          {
             id: '1',
             name: 'some-column-name',
+            pos: 0
           },
           {
             id: '2',
             name: 'second column',
+            pos: 1
+          },
+          {
+            id: 'third-column-id',
+            name: 'third column',
+            pos: 2
           },
           {
             name: 'some-non-selectable-column',
-            selectable: false
+            selectable: false,
+            pos: 3
           }
         ]
       });
@@ -41,22 +72,22 @@ define(
 
           setupGrid(columns);
 
-          expect(container.find('.slick-header-columns input').length).toBe(2)
+          expect(container.find('.slick-header-columns input').length).toBe(3)
         });
       });
 
       it("renders a checkbox in the column header", function () {
         setupGrid(columns);
 
-        expect(container.find('.slick-header-columns input').length).toBe(2)
+        expect(container.find('.slick-header-columns input').length).toBe(3)
       });
 
       it("displays the name of the column", function () {
         setupGrid(columns);
 
-        expect($(container.find('.slick-header-columns .slick-column-name')[0]).text())
-          .toContain('some-column-name');
         expect($(container.find('.slick-header-columns .slick-column-name')[1]).text())
+          .toContain('some-column-name');
+        expect($(container.find('.slick-header-columns .slick-column-name')[2]).text())
           .toContain('second column');
       });
 
@@ -64,22 +95,28 @@ define(
         var columnSelector = new ColumnSelector();
         var selectableColumns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
 
-        expect(selectableColumns[0].id).toBe('1');
+        expect(selectableColumns[1].id).toBe('1');
       });
 
-      describe("selecting columns", function () {
-        var grid, rowSelectionModel;
+      describe("with ActiveCellCapture, CellSelectionModel, and GridSelector: selecting columns", function () {
+        var grid, cellSelectionModel;
         beforeEach(function () {
           var columnSelector = new ColumnSelector();
           columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
           data = [];
           for (var i = 0; i < 10; i++) {
-            data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
+            data.push({
+              'some-column-name': 'some-value-' + i,
+              'second column': 'second value ' + i,
+              'third column': 'third value ' + i,
+              'fourth column': 'fourth value ' + i,
+            });
           }
-          grid = new SlickGrid(container, data, columns, options);
+          grid = new SlickGrid(container, data, columns);
 
-          rowSelectionModel = new RowSelectionModel();
-          grid.setSelectionModel(rowSelectionModel);
+          grid.registerPlugin(new ActiveCellCapture());
+          cellSelectionModel = new Slick.CellSelectionModel();
+          grid.setSelectionModel(cellSelectionModel);
 
           grid.registerPlugin(columnSelector);
           grid.invalidate();
@@ -92,144 +129,214 @@ define(
 
         describe("when the user clicks a column header", function () {
           it("selects the column", function () {
-            container.find('.slick-header-column')[0].click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            container.find('.slick-header-column:contains(some-column-name)').click();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
             expectOnlyTheFirstColumnToBeSelected(selectedRanges);
           });
+
+          xit("toggles a selected class to the header cell", function () {
+            container.find('.slick-header-column:contains(second column)').click();
+            expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+              .toBe(true);
+
+            container.find('.slick-header-column:contains(second column)').click();
+            expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+              .toBe(false);
+          });
         });
 
-        describe("when the user clicks additional column headers", function () {
+        describe("when the user clicks an additional column header", function () {
           beforeEach(function () {
-            container.find('.slick-header-column')[1].click();
+            container.find('.slick-header-column:contains(some-column-name)').click();
           });
 
           it("selects additional columns", function () {
-            container.find('.slick-header-column')[0].click();
+            container.find('.slick-header-column:contains(second column)').click();
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
-            var column1 = selectedRanges[0];
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
 
-            expect(selectedRanges.length).toEqual(2);
+            expect(selectedRanges.length).toBe(2);
+            var column1 = selectedRanges[0];
             expect(column1.fromCell).toBe(1);
             expect(column1.toCell).toBe(1);
 
             var column2 = selectedRanges[1];
+            expect(column2.fromCell).toBe(2);
+            expect(column2.toCell).toBe(2);
+          });
+
+          describe("and presses shift + right-arrow", function () {
+            beforeEach(function () {
+              container.find('.slick-header-column:contains(second column)').click();
+            });
 
-            expect(column2.fromCell).toBe(0);
-            expect(column2.toCell).toBe(0);
+            it("keeps the last column selected", function () {
+              pressShiftArrow(KEY_RIGHT);
+
+              expect(cellSelectionModel.getSelectedRanges().length).toBe(1);
+            });
+
+            it("grows the selection to the right", function () {
+              pressShiftArrow(KEY_RIGHT);
+
+              var selectedRange = cellSelectionModel.getSelectedRanges()[0];
+              expect(selectedRange.fromCell).toBe(2);
+              expect(selectedRange.toCell).toBe(3);
+              expect(selectedRange.fromRow).toBe(0);
+              expect(selectedRange.toRow).toBe(9);
+            });
+          });
+
+          describe("when the user deselects the last selected column header", function () {
+            beforeEach(function () {
+              container.find('.slick-header-column:contains(second column)').click();
+              container.find('.slick-header-column:contains(second column)').click();
+            });
+
+            describe("and presses shift + right-arrow", function () {
+              it("first and second columns are selected", function () {
+                pressShiftArrow(KEY_RIGHT);
+
+                var selectedRanges = cellSelectionModel.getSelectedRanges();
+
+                expect(selectedRanges.length).toBe(1);
+                expect(selectedRanges[0].fromCell).toBe(1);
+                expect(selectedRanges[0].toCell).toBe(2);
+                expect(selectedRanges[0].fromRow).toBe(0);
+                expect(selectedRanges[0].toRow).toBe(9);
+              });
+            });
           });
         });
 
-        describe("when the user clicks a column header checkbox", function () {
+        describe("when the user clicks a column header description", function () {
           it("selects the column", function () {
-            container.find('.slick-header-columns input')[0].click();
+            container.find('.slick-header-columns span.column-description:contains(some-column-name)').click();
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
             expectOnlyTheFirstColumnToBeSelected(selectedRanges);
           });
 
-          it("checks the checkbox", function () {
-            container.find('.slick-header-column')[1].click();
-            expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeTruthy();
+          xit("toggles a selected class to the header cell", function () {
+            container.find('.slick-header-column span.column-description:contains(second column)').click();
+            expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+              .toBe(true);
+
+            container.find('.slick-header-column span.column-description:contains(second column)').click();
+            expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+              .toBe(false);
+
           });
-        });
 
         describe("when a row is selected", function () {
           beforeEach(function () {
             var selectedRanges = [new Slick.Range(0, 0, 0, 1)];
-            rowSelectionModel.setSelectedRanges(selectedRanges);
+            cellSelectionModel.setSelectedRanges(selectedRanges);
           });
 
-          it("deselects the row", function () {
-            container.find('.slick-header-column')[1].click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            it("deselects the row", function () {
+              container.find('.slick-header-column')[1].click();
+              var selectedRanges = cellSelectionModel.getSelectedRanges();
 
-            expect(selectedRanges.length).toBe(1);
+              expect(selectedRanges.length).toBe(1);
 
-            var column = selectedRanges[0];
-
-            expect(column.fromCell).toBe(1);
-            expect(column.toCell).toBe(1);
-            expect(column.fromRow).toBe(0);
-            expect(column.toRow).toBe(9);
-          })
-        });
+              var column = selectedRanges[0];
 
-        describe("clicking a second time", function () {
-          beforeEach(function () {
-            container.find('.slick-header-column')[1].click();
+              expect(column.fromCell).toBe(1);
+              expect(column.toCell).toBe(1);
+              expect(column.fromRow).toBe(0);
+              expect(column.toRow).toBe(9);
+            })
           });
 
-          it("unchecks checkbox", function () {
-            container.find('.slick-header-column')[1].click();
-            expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy();
-          });
+          describe("clicking a second time", function () {
+            beforeEach(function () {
+              container.find('.slick-header-column')[1].click();
+            });
 
-          it("deselects the column", function () {
-            container.find('.slick-header-column')[1].click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            it("unchecks checkbox", function () {
+              container.find('.slick-header-column')[1].click();
+              expect($(container.find('.slick-header-columns input')[1]).is(':checked')).toBeFalsy();
+            });
 
-            expect(selectedRanges.length).toEqual(0);
-          })
-        });
+            it("deselects the column", function () {
+              container.find('.slick-header-column')[1].click();
+              var selectedRanges = cellSelectionModel.getSelectedRanges();
 
-        describe("when the column is not selectable", function () {
-          it("does not select the column", function () {
-            $(container.find('.slick-header-column:contains(some-non-selectable-column)')).click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
-
-            expect(selectedRanges.length).toEqual(0);
+              expect(selectedRanges.length).toEqual(0);
+            })
           });
-        });
 
-        describe("when the column is deselected through setSelectedRanges", function () {
-          beforeEach(function () {
-            container.find('.slick-header-column')[1].click();
+          describe("when the column is not selectable", function () {
+            it("does not select the column", function () {
+              $(container.find('.slick-header-column:contains(some-non-selectable-column)')).click();
+              var selectedRanges = cellSelectionModel.getSelectedRanges();
+
+              expect(selectedRanges.length).toEqual(0);
+            });
           });
 
-          it("unchecks the checkbox", function () {
-            rowSelectionModel.setSelectedRanges([]);
+          describe("when the column is deselected through setSelectedRanges", function () {
+            beforeEach(function () {
+              container.find('.slick-header-column')[1].click();
+            });
 
-            expect($(container.find('.slick-header-columns input')[1])
-              .is(':checked')).toBeFalsy();
-          });
-        });
+            xit("removes selected class from header", function () {
+              cellSelectionModel.setSelectedRanges([]);
 
-        describe("when a non-column range was already selected", function () {
-          beforeEach(function () {
-            var selectedRanges = [new Slick.Range(0, 0, 1, 0)];
-            rowSelectionModel.setSelectedRanges(selectedRanges);
+              expect($(container.find('.slick-header-column')[1]).hasClass('selected'))
+                .toBe(false);
+
+            });
           });
 
-          it("deselects the non-column range", function () {
-            container.find('.slick-header-column')[0].click();
+          describe("when a non-column range was already selected", function () {
+            beforeEach(function () {
+              var selectedRanges = [new Slick.Range(0, 0, 1, 0)];
+              cellSelectionModel.setSelectedRanges(selectedRanges);
+            });
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
-            expectOnlyTheFirstColumnToBeSelected(selectedRanges);
-          })
+            it("deselects the non-column range", function () {
+              container.find('.slick-header-column:contains(some-column-name)').click();
+
+              var selectedRanges = cellSelectionModel.getSelectedRanges();
+              expectOnlyTheFirstColumnToBeSelected(selectedRanges);
+            })
+          });
         });
-      });
 
-      var setupGrid = function (columns) {
+      });
+      function setupGrid(columns) {
         var columnSelector = new ColumnSelector();
         columns = columnSelector.getColumnDefinitionsWithCheckboxes(columns);
         var grid = new SlickGrid(container, data, columns, options);
 
-        var rowSelectionModel = new RowSelectionModel();
-        grid.setSelectionModel(rowSelectionModel);
+        var cellSelectionModel = new Slick.CellSelectionModel();
+        grid.setSelectionModel(cellSelectionModel);
 
         grid.registerPlugin(columnSelector);
         grid.invalidate();
-      };
+      }
 
       function expectOnlyTheFirstColumnToBeSelected(selectedRanges) {
         var row = selectedRanges[0];
 
         expect(selectedRanges.length).toEqual(1);
-        expect(row.fromCell).toBe(0);
-        expect(row.toCell).toBe(0);
+        expect(row.fromCell).toBe(1);
+        expect(row.toCell).toBe(1);
         expect(row.fromRow).toBe(0);
         expect(row.toRow).toBe(9);
       }
+
+      function pressShiftArrow(keyCode) {
+        var pressEvent = new $.Event("keydown");
+        pressEvent.shiftKey = true;
+        pressEvent.ctrlKey = false;
+        pressEvent.altKey = false;
+        pressEvent.which = keyCode;
+
+        $(container.find('.grid-canvas')).trigger(pressEvent);
+      }
+
     });
   });
diff --git a/web/regression/javascript/selection/copy_data_spec.js b/web/regression/javascript/selection/copy_data_spec.js
index affad66e..718a830b 100644
--- a/web/regression/javascript/selection/copy_data_spec.js
+++ b/web/regression/javascript/selection/copy_data_spec.js
@@ -1,3 +1,12 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
 define(
   ["jquery",
     "slickgrid/slick.grid",
diff --git a/web/regression/javascript/selection/grid_selector_spec.js b/web/regression/javascript/selection/grid_selector_spec.js
index a74a66f9..1c2f7898 100644
--- a/web/regression/javascript/selection/grid_selector_spec.js
+++ b/web/regression/javascript/selection/grid_selector_spec.js
@@ -13,10 +13,10 @@ define(["jquery",
         container.height(9999);
         columns = [{
           id: '1',
-          name: 'some-column-name',
+          name: 'some-column-name'
         }, {
           id: '2',
-          name: 'second column',
+          name: 'second column'
         }];
 
         gridSelector = new GridSelector();
diff --git a/web/regression/javascript/selection/range_boundary_navigator_spec.js b/web/regression/javascript/selection/range_boundary_navigator_spec.js
index 8376d0a7..b1ca45eb 100644
--- a/web/regression/javascript/selection/range_boundary_navigator_spec.js
+++ b/web/regression/javascript/selection/range_boundary_navigator_spec.js
@@ -61,7 +61,6 @@ define(['sources/selection/range_boundary_navigator'], function (rangeBoundaryNa
     });
   });
 
-
   describe("#mapDimensionBoundaryUnion", function () {
     it("returns a list of the results of the callback", function () {
       var rangeBounds = [[0, 1], [3, 3]];
@@ -132,20 +131,27 @@ define(['sources/selection/range_boundary_navigator'], function (rangeBoundaryNa
     });
 
     it("returns csv for the provided ranges", function () {
-
       var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, ranges);
 
       expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'");
     });
 
+    describe("when no cells are selected", function () {
+      it("should return an empty string", function () {
+        var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, []);
+
+        expect(csvResult).toEqual('');
+      });
+    });
+
     describe("when there is an extra column with checkboxes", function () {
+
       beforeEach(function () {
         columnDefinitions = [{name: 'not-a-data-column'}, {name: 'id', pos: 0}, {name: 'animal', pos: 1}, {
           name: 'size',
           pos: 2
         }];
         ranges = [new Slick.Range(0, 0, 0, 3), new Slick.Range(3, 0, 3, 3)];
-
       });
 
       it("returns csv for the columns with data", function () {
@@ -153,6 +159,13 @@ define(['sources/selection/range_boundary_navigator'], function (rangeBoundaryNa
 
         expect(csvResult).toEqual("1,'leopard','12'\n4,'tiger','10'");
       });
+      describe("when no cells are selected", function () {
+        it("should return an empty string", function () {
+          var csvResult = rangeBoundaryNavigator.rangesToCsv(data, columnDefinitions, []);
+
+          expect(csvResult).toEqual('');
+        });
+      });
     });
   });
 });
\ No newline at end of file
diff --git a/web/regression/javascript/selection/row_selector_spec.js b/web/regression/javascript/selection/row_selector_spec.js
index 10697e6a..5907c449 100644
--- a/web/regression/javascript/selection/row_selector_spec.js
+++ b/web/regression/javascript/selection/row_selector_spec.js
@@ -2,26 +2,33 @@ define(
   ["jquery",
     "underscore",
     "slickgrid/slick.grid",
+    "sources/selection/active_cell_capture",
     "sources/selection/row_selector",
-    "slickgrid/slick.rowselectionmodel",
+
     "slickgrid",
+    'slickgrid/slick.cellrangedecorator',
+    'slickgrid/slick.cellrangeselector',
+    "slickgrid/slick.cellselectionmodel",
   ],
-  function ($, _, SlickGrid, RowSelector, RowSelectionModel, Slick) {
+  function ($, _, SlickGrid, ActiveCellCapture, RowSelector, Slick) {
     describe("RowSelector", function () {
-      var container, data, columnDefinitions, grid, rowSelectionModel;
+      var container, data, columnDefinitions, grid, cellSelectionModel;
 
       beforeEach(function () {
         container = $("<div></div>");
         container.height(9999);
+        container.width(9999);
 
         columnDefinitions = [{
           id: '1',
           name: 'some-column-name',
-          selectable: true
+          selectable: true,
+          pos: 0
         }, {
           id: '2',
           name: 'second column',
-          selectable: true
+          selectable: true,
+          pos: 1
         }];
 
         var rowSelector = new RowSelector();
@@ -32,8 +39,10 @@ define(
         columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes(columnDefinitions);
         grid = new SlickGrid(container, data, columnDefinitions);
 
-        rowSelectionModel = new RowSelectionModel();
-        grid.setSelectionModel(rowSelectionModel);
+        grid.registerPlugin(new ActiveCellCapture());
+        cellSelectionModel = new Slick.CellSelectionModel();
+        grid.setSelectionModel(cellSelectionModel);
+
         grid.registerPlugin(rowSelector);
         grid.invalidate();
 
@@ -53,9 +62,9 @@ define(
         expect(leftmostColumn.selectable).toBe(false);
       });
 
-      it("renders a checkbox the leftmost column", function () {
-        expect(container.find('.sr').length).toBe(11);
-        expect(container.find('.sr .sc:first-child input[type="checkbox"]').length).toBe(10);
+      it("renders a span on the leftmost column", function () {
+        expect(container.find('.slick-row').length).toBe(10);
+        expect(container.find('.slick-row .slick-cell:first-child input[type="checkbox"]').length).toBe(10);
       });
 
       it("preserves the other attributes of column definitions", function () {
@@ -64,44 +73,44 @@ define(
       });
 
       describe("selecting rows", function () {
-        describe("when the user clicks a row header checkbox", function () {
+        describe("when the user clicks a row header span", function () {
           it("selects the row", function () {
-            container.find('.sr .sc:first-child input[type="checkbox"]')[0].click();
+            container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[0].click();
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
             expectOnlyTheFirstRowToBeSelected(selectedRanges);
           });
 
           it("checks the checkbox", function () {
-            container.find('.sr .sc:first-child input[type="checkbox"]')[5].click();
+            container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[5].click();
 
-            expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[5])
+            expect($(container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[5])
               .is(':checked')).toBeTruthy();
           });
         });
 
         describe("when the user clicks a row header", function () {
           it("selects the row", function () {
-            container.find('.sr .sc:first-child')[0].click();
+            container.find('.slick-row .slick-cell:first-child')[0].click();
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
             expectOnlyTheFirstRowToBeSelected(selectedRanges);
           });
 
           it("checks the checkbox", function () {
-            container.find('.sr .sc:first-child')[7].click();
+            container.find('.slick-row .slick-cell:first-child')[7].click();
 
-            expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[7])
+            expect($(container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[7])
               .is(':checked')).toBeTruthy();
           });
         });
 
         describe("when the user clicks multiple row headers", function () {
           it("selects another row", function () {
-            container.find('.sr .sc:first-child')[4].click();
-            container.find('.sr .sc:first-child')[0].click();
+            container.find('.slick-row .slick-cell:first-child')[4].click();
+            container.find('.slick-row .slick-cell:first-child')[0].click();
 
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
             expect(selectedRanges.length).toEqual(2);
 
             var row1 = selectedRanges[0];
@@ -117,12 +126,12 @@ define(
         describe("when a column was already selected", function () {
           beforeEach(function () {
             var selectedRanges = [new Slick.Range(0, 0, 0, 1)];
-            rowSelectionModel.setSelectedRanges(selectedRanges);
+            cellSelectionModel.setSelectedRanges(selectedRanges);
           });
 
           it("deselects the column", function () {
-            container.find('.sr .sc:first-child')[0].click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            container.find('.slick-row .slick-cell:first-child')[0].click();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
 
             expectOnlyTheFirstRowToBeSelected(selectedRanges);
           })
@@ -130,31 +139,31 @@ define(
 
         describe("when the row is deselected through setSelectedRanges", function () {
           beforeEach(function () {
-            container.find('.sr .sc:first-child')[4].click();
+            container.find('.slick-row .slick-cell:first-child')[4].click();
           });
 
           it("should uncheck the checkbox", function () {
-            rowSelectionModel.setSelectedRanges([]);
+            cellSelectionModel.setSelectedRanges([]);
 
-            expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[4])
+            expect($(container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[4])
               .is(':checked')).toBeFalsy();
           });
         });
 
         describe("click a second time", function () {
           beforeEach(function () {
-            container.find('.sr .sc:first-child')[1].click();
+            container.find('.slick-row .slick-cell:first-child')[1].click();
           });
 
           it("unchecks checkbox", function () {
-            container.find('.sr .sc:first-child')[1].click();
-            expect($(container.find('.sr .sc:first-child input[type="checkbox"]')[1])
+            container.find('.slick-row .slick-cell:first-child')[1].click();
+            expect($(container.find('.slick-row .slick-cell:first-child input[type="checkbox"]')[1])
               .is(':checked')).toBeFalsy();
           });
 
           it("unselects the row", function () {
-            container.find('.sr .sc:first-child')[1].click();
-            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            container.find('.slick-row .slick-cell:first-child')[1].click();
+            var selectedRanges = cellSelectionModel.getSelectedRanges();
 
             expect(selectedRanges.length).toEqual(0);
           })
diff --git a/web/regression/javascript/slickgrid/cell_selector_spec.js b/web/regression/javascript/slickgrid/cell_selector_spec.js
new file mode 100644
index 00000000..f1178541
--- /dev/null
+++ b/web/regression/javascript/slickgrid/cell_selector_spec.js
@@ -0,0 +1,77 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+define(["jquery",
+    "slickgrid/slick.grid",
+    "slickgrid/slick.rowselectionmodel",
+    "sources/slickgrid/cell_selector",
+    "sources/selection/range_selection_helper"
+  ],
+  function ($, SlickGrid, RowSelectionModel, CellSelector, RangeSelectionHelper) {
+    describe("CellSelector", function () {
+      var container, columns, cellSelector, data, rowSelectionModel, grid;
+      beforeEach(function () {
+        container = $("<div></div>");
+        container.height(9999);
+        container.width(9999);
+        columns = [{
+          name: 'some-column-name',
+        }, {
+          name: 'second column',
+        }];
+
+        cellSelector = new CellSelector();
+
+        data = [];
+        for (var i = 0; i < 10; i++) {
+          data.push({'some-column-name': 'some-value-' + i, 'second column': 'second value ' + i});
+        }
+        grid = new SlickGrid(container, data, columns);
+
+        rowSelectionModel = new RowSelectionModel();
+        grid.setSelectionModel(rowSelectionModel);
+
+        grid.registerPlugin(cellSelector);
+        grid.invalidate();
+
+        $("body").append(container);
+      });
+
+      afterEach(function () {
+        $("body").find(container).remove();
+      });
+
+      describe("when the user clicks or tabs to a cell", function () {
+        it("sets the selected range to that cell", function () {
+          var row = 1, column = 0;
+          $(container.find(".slick-row .slick-cell.l" + column)[row]).click();
+
+          var selectedRanges = rowSelectionModel.getSelectedRanges();
+          expect(selectedRanges.length).toBe(1);
+          expect(selectedRanges[0].fromCell).toBe(0);
+          expect(selectedRanges[0].toCell).toBe(0);
+          expect(selectedRanges[0].fromRow).toBe(1);
+          expect(selectedRanges[0].toRow).toBe(1);
+        });
+
+        it("deselects previously selected ranges", function () {
+          var row2Range = RangeSelectionHelper.rangeForRow(grid, 2);
+          var ranges = RangeSelectionHelper.addRange(rowSelectionModel.getSelectedRanges(),
+            row2Range);
+          rowSelectionModel.setSelectedRanges(ranges);
+
+          var row = 4, column = 1;
+          $(container.find(".slick-row .slick-cell.l" + column)[row]).click();
+
+          expect(RangeSelectionHelper.isRangeSelected(rowSelectionModel.getSelectedRanges(), row2Range))
+            .toBe(false);
+        });
+      });
+    });
+  });
\ No newline at end of file
diff --git a/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js b/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js
new file mode 100644
index 00000000..b61e5aff
--- /dev/null
+++ b/web/regression/javascript/slickgrid/event_handlers/handle_query_output_keyboard_event_spec.js
@@ -0,0 +1,145 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2017, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+define([
+    'sources/slickgrid/event_handlers/handle_query_output_keyboard_event',
+    'sources/selection/clipboard',
+    'sources/selection/range_selection_helper',
+    'slickgrid',
+    'slickgrid/slick.cellrangedecorator',
+    'slickgrid/slick.cellrangeselector',
+    'slickgrid/slick.cellselectionmodel',
+  ],
+function (handleQueryOutputKeyboardEvent, clipboard, RangeSelectionHelper) {
+  var Slick = window.Slick;
+
+  describe('#handleQueryOutputKeyboardEvent', function () {
+    var event, view, grid, slickEvent;
+    beforeEach(function () {
+      event = {
+        shiftKey: false,
+        ctrlKey: false,
+        metaKey: false,
+        which: -1,
+        keyCode: -1,
+        preventDefault: jasmine.createSpy('preventDefault')
+      };
+
+      var data = [['', '0,0-cell-content', '0,1-cell-content'],
+        ['', '1,0-cell-content', '1,1-cell-content'],
+        ['', '2,0-cell-content', '2,1-cell-content']];
+      var columnDefinitions = [{name: 'checkboxColumn'}, {pos: 1, name: 'firstColumn'}, {
+        pos: 2,
+        name: 'secondColumn'
+      }];
+      grid = new Slick.Grid($('<div></div>'), data, columnDefinitions);
+      grid.setSelectionModel(new Slick.CellSelectionModel());
+
+      slickEvent = {
+        grid: grid
+      };
+
+      view = {};
+      spyOn(clipboard, 'copyTextToClipboard');
+    });
+
+    describe("when a range is selected", function () {
+      beforeEach(function () {
+        grid.getSelectionModel().setSelectedRanges([
+          RangeSelectionHelper.rangeForRow(grid, 0),
+          RangeSelectionHelper.rangeForRow(grid, 2),
+        ]);
+      });
+
+      describe("pressing Command + C", function () {
+        beforeEach(function () {
+          event.metaKey = true;
+          event.keyCode = 67;
+        });
+
+        it("copies the cell content to the clipboard", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(clipboard.copyTextToClipboard).toHaveBeenCalledWith("'0,0-cell-content','0,1-cell-content'\n'2,0-cell-content','2,1-cell-content'");
+        });
+      });
+
+      describe("pressing Ctrl + C", function () {
+        beforeEach(function () {
+          event.ctrlKey = true;
+          event.keyCode = 67;
+        });
+
+        it("copies the cell content to the clipboard", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(clipboard.copyTextToClipboard).toHaveBeenCalledWith("'0,0-cell-content','0,1-cell-content'\n'2,0-cell-content','2,1-cell-content'");
+        });
+      });
+
+      describe("pressing Command + A", function () {
+        beforeEach(function () {
+          event.metaKey = true;
+          event.keyCode = 65;
+        });
+
+        it("selects the entire grid to ranges", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(RangeSelectionHelper.isEntireGridSelected(grid)).toBeTruthy();
+          expect(grid.getSelectionModel().getSelectedRanges().length).toBe(1);
+        });
+      });
+
+      describe("pressing Ctrl + A", function () {
+        beforeEach(function () {
+          event.ctrlKey = true;
+          event.keyCode = 65;
+        });
+
+        it("selects the entire grid to ranges", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(RangeSelectionHelper.isEntireGridSelected(grid)).toBeTruthy();
+          expect(grid.getSelectionModel().getSelectedRanges().length).toBe(1);
+        });
+      });
+    });
+
+    describe("when no ranges are selected", function () {
+      describe("pressing Command + A", function () {
+        beforeEach(function () {
+          event.metaKey = true;
+          event.keyCode = 65;
+        });
+
+        it("selects the entire grid to ranges", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(RangeSelectionHelper.isEntireGridSelected(grid)).toBeTruthy();
+          expect(grid.getSelectionModel().getSelectedRanges().length).toBe(1);
+        });
+      });
+
+      describe("pressing Ctrl + A", function () {
+        beforeEach(function () {
+          event.ctrlKey = true;
+          event.keyCode = 65;
+        });
+
+        it("selects the entire grid to ranges", function () {
+          handleQueryOutputKeyboardEvent(event, slickEvent);
+
+          expect(RangeSelectionHelper.isEntireGridSelected(grid)).toBeTruthy();
+          expect(grid.getSelectionModel().getSelectedRanges().length).toBe(1);
+        });
+      });
+    });
+  });
+});
\ No newline at end of file
diff --git a/web/regression/javascript/test-main.js b/web/regression/javascript/test-main.js
index fe28a5f8..b2db316d 100644
--- a/web/regression/javascript/test-main.js
+++ b/web/regression/javascript/test-main.js
@@ -36,6 +36,9 @@ require.config({
     'slickgrid': sourcesDir + 'vendor/slickgrid/slick.core',
     'slickgrid/slick.grid': sourcesDir + 'vendor/slickgrid/slick.grid',
     'slickgrid/slick.rowselectionmodel': sourcesDir + 'vendor/slickgrid/plugins/slick.rowselectionmodel',
+    'slickgrid/slick.cellselectionmodel': sourcesDir + 'vendor/slickgrid/plugins/slick.cellselectionmodel',
+    'slickgrid/slick.cellrangedecorator': sourcesDir + 'vendor/slickgrid/plugins/slick.cellrangedecorator',
+    'slickgrid/slick.cellrangeselector': sourcesDir + 'vendor/slickgrid/plugins/slick.cellrangeselector',
     'translations': '/base/regression/javascript/fake_translations',
     'sources': sourcesDir + 'js'
   },
diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py
index 2b7c6954..a661ecfe 100644
--- a/web/regression/python_test_utils/test_utils.py
+++ b/web/regression/python_test_utils/test_utils.py
@@ -159,12 +159,14 @@ def create_table(server, db_name, table_name):
         connection.set_isolation_level(0)
         pg_cursor = connection.cursor()
         pg_cursor.execute(
-            '''CREATE TABLE "%s" (some_column VARCHAR, value NUMERIC)''' %
+            '''CREATE TABLE "%s" (some_column VARCHAR, value NUMERIC, details VARCHAR)''' %
             table_name)
         pg_cursor.execute(
-            '''INSERT INTO "%s" VALUES ('Some-Name', 6)''' % table_name)
+            '''INSERT INTO "%s" VALUES ('Some-Name', 6, 'some info')''' % table_name)
         pg_cursor.execute(
-            '''INSERT INTO "%s" VALUES ('Some-Other-Name', 22)''' % table_name)
+            '''INSERT INTO "%s" VALUES ('Some-Other-Name', 22, 'some other info')''' % table_name)
+        pg_cursor.execute(
+            '''INSERT INTO "%s" VALUES ('Yet-Another-Name', 14, 'cool info')''' % table_name)
 
         connection.set_isolation_level(old_isolation_level)
         connection.commit()
-- 
2.12.0

