From 70f5dc3c0eaab8f9bffa73fa4ad25fcb6d5d50df Mon Sep 17 00:00:00 2001
From: Matt Kleiman and Tira Odhner <pair+mkleiman+aodhner@pivotal.io>
Date: Thu, 30 Mar 2017 10:12:52 -0400
Subject: [PATCH 05/11] Write a new row selection plugin to replace the
 problematic checkboxselectcolumn plugin

- do not swallow events
- the entire cell is clickable
- the select-all checkbox will be a separate plugin
---
 web/pgadmin/static/js/selection/row_selector.js    | 115 ++++++++++++++
 .../javascript/selection/row_selector_spec.js      | 174 +++++++++++++++++++++
 2 files changed, 289 insertions(+)
 create mode 100644 web/pgadmin/static/js/selection/row_selector.js
 create mode 100644 web/regression/javascript/selection/row_selector_spec.js

diff --git a/web/pgadmin/static/js/selection/row_selector.js b/web/pgadmin/static/js/selection/row_selector.js
new file mode 100644
index 00000000..29a37588
--- /dev/null
+++ b/web/pgadmin/static/js/selection/row_selector.js
@@ -0,0 +1,115 @@
+define(['jquery', 'slickgrid'], function ($) {
+  var RowSelector = function (columnDefinitions) {
+    var Slick = window.Slick;
+
+    var gridEventBus = new Slick.EventHandler();
+
+    var init = function (grid) {
+      grid.getSelectionModel()
+        .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid))
+      gridEventBus
+        .subscribe(grid.onClick, handleClick.bind(null, grid))
+    };
+
+    function handleClick(grid, event, args) {
+      if (grid.getColumns()[args.cell].id === 'row-header-column') {
+        if (event.target.type != "checkbox") {
+          var checkbox = $(event.target).find('input[type="checkbox"]');
+          toggleCheckbox($(checkbox));
+        }
+        updateRanges(grid, args.row);
+      }
+    }
+
+    function handleSelectedRangesChanged(grid, event, ranges) {
+      $('[data-cell-type="row-header-checkbox"]:checked')
+        .each(function (index, checkbox) {
+          var $checkbox = $(checkbox);
+          var row = parseInt($checkbox.data('row'));
+          var isStillSelected = isRangeSelected(ranges, rangeForRow(grid, row));
+          if (!isStillSelected) {
+            toggleCheckbox($checkbox);
+          }
+        });
+    }
+
+    function rangeForRow(grid, rowId) {
+      return new Slick.Range(rowId, 1, rowId, grid.getColumns().length - 1);
+    }
+
+    function updateRanges(grid, rowId) {
+      var selectionModel = grid.getSelectionModel();
+      var ranges = selectionModel.getSelectedRanges();
+
+      var rowRange = rangeForRow(grid, rowId);
+
+      var newRanges;
+      if (isRangeSelected(ranges, rowRange)) {
+        newRanges = removeRange(ranges, rowRange);
+      } else {
+        if (allRangesAreRows(ranges, grid)) {
+          newRanges = addRange(ranges, rowRange);
+        } else {
+          newRanges = [rowRange];
+        }
+      }
+      selectionModel.setSelectedRanges(newRanges);
+    }
+
+    var isRangeSelected = function (selectedRanges, range) {
+      return _.any(selectedRanges, function (selectedRange) {
+        return isSameRange(selectedRange, range)
+      })
+    };
+
+    var isSameRange = function (range, otherRange) {
+      return range.fromCell == otherRange.fromCell && range.toCell == otherRange.toCell &&
+        range.fromRow == otherRange.fromRow && range.toRow == otherRange.toRow;
+    };
+
+    var removeRange = function (selectedRanges, range) {
+      return _.filter(selectedRanges, function (selectedRange) {
+        return !(isSameRange(selectedRange, range))
+      })
+    };
+    var addRange = function (ranges, range) {
+      ranges.push(range);
+      return ranges;
+    };
+
+    var allRangesAreRows = function (ranges, grid) {
+      return _.every(ranges, function (range) {
+        return range.fromRow == range.toRow &&
+          range.fromCell == 1 && range.toCell == grid.getColumns().length - 1
+      })
+    };
+
+    var toggleCheckbox = function (checkbox) {
+      if (checkbox.prop("checked")) {
+        checkbox.prop("checked", false)
+      } else {
+        checkbox.prop("checked", true)
+      }
+    };
+
+    var getColumnDefinitionsWithCheckboxes = function () {
+      columnDefinitions.unshift({
+        id: 'row-header-column',
+        selectable: false,
+        focusable: false,
+        formatter: function (rowIndex) {
+          return '<input type="checkbox" ' +
+            'data-row="' + rowIndex + '" ' +
+            'data-cell-type="row-header-checkbox"/>'
+        }
+      });
+      return columnDefinitions;
+    };
+
+    $.extend(this, {
+      "init": init,
+      "getColumnDefinitionsWithCheckboxes": getColumnDefinitionsWithCheckboxes
+    });
+  };
+  return RowSelector;
+});
diff --git a/web/regression/javascript/selection/row_selector_spec.js b/web/regression/javascript/selection/row_selector_spec.js
new file mode 100644
index 00000000..30f155c9
--- /dev/null
+++ b/web/regression/javascript/selection/row_selector_spec.js
@@ -0,0 +1,174 @@
+define(
+  ["jquery",
+    "underscore",
+    "slickgrid/slick.grid",
+    "sources/selection/row_selector",
+    "slickgrid/slick.rowselectionmodel",
+    "slickgrid",
+  ],
+  function ($, _, SlickGrid, RowSelector, RowSelectionModel, Slick) {
+    describe("RowSelector", function () {
+      var container, data, columnDefinitions, grid, rowSelectionModel;
+
+      beforeEach(function () {
+        container = $("<div></div>");
+        container.height(9999);
+
+        columnDefinitions = [{
+          id: '1',
+          name: 'some-column-name',
+          selectable: true
+        }, {
+          id: '2',
+          name: 'second column',
+          selectable: true
+        }];
+
+        var rowSelector = new RowSelector(columnDefinitions);
+        data = [];
+        for (var i = 0; i < 10; i++) {
+          data.push(['some-value-' + i, 'second value ' + i]);
+        }
+        columnDefinitions = rowSelector.getColumnDefinitionsWithCheckboxes();
+        grid = new SlickGrid(container, data, columnDefinitions);
+
+        rowSelectionModel = new RowSelectionModel();
+        grid.setSelectionModel(rowSelectionModel);
+        grid.registerPlugin(rowSelector);
+        grid.invalidate();
+
+        $("body").append(container);
+      });
+
+      afterEach(function () {
+        $("body").find(container).remove();
+      });
+
+      it("renders an additional column on the left", function () {
+        expect(columnDefinitions.length).toBe(3);
+
+        var leftmostColumn = columnDefinitions[0];
+        expect(leftmostColumn.id).toBe('row-header-column');
+        expect(leftmostColumn.name).toBe('');
+        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("preserves the other attributes of column definitions", function () {
+        expect(columnDefinitions[1].id).toBe('1');
+        expect(columnDefinitions[1].selectable).toBe(true);
+      });
+
+      describe("selecting rows", function () {
+        describe("when the user clicks a row header checkbox", function () {
+          it("selects the row", function () {
+            container.find('.sr .sc:first-child input[type="checkbox"]')[0].click();
+
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            expectOnlyTheFirstRowToBeSelected(selectedRanges);
+          });
+
+          it("checks the checkbox", function () {
+            container.find('.sr .sc:first-child input[type="checkbox"]')[5].click();
+
+            expect($(container.find('.sr .sc: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();
+
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            expectOnlyTheFirstRowToBeSelected(selectedRanges);
+          });
+
+          it("checks the checkbox", function () {
+            container.find('.sr .sc:first-child')[7].click();
+
+            expect($(container.find('.sr .sc: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();
+
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+            expect(selectedRanges.length).toEqual(2);
+
+            var row1 = selectedRanges[0];
+            expect(row1.fromRow).toBe(4);
+            expect(row1.toRow).toBe(4);
+
+            var row2 = selectedRanges[1];
+            expect(row2.fromRow).toBe(0);
+            expect(row2.toRow).toBe(0);
+          });
+        });
+
+        describe("when a column was already selected", function () {
+          beforeEach(function () {
+            var selectedRanges = [new Slick.Range(0, 0, 0, 1)];
+            rowSelectionModel.setSelectedRanges(selectedRanges);
+          });
+
+          it("deselects the column", function () {
+            container.find('.sr .sc:first-child')[0].click();
+            var selectedRanges = rowSelectionModel.getSelectedRanges();
+
+            expectOnlyTheFirstRowToBeSelected(selectedRanges);
+          })
+        });
+
+        describe("when the row is deselected through setSelectedRanges", function () {
+          beforeEach(function () {
+            container.find('.sr .sc:first-child')[4].click();
+          });
+
+          it("should uncheck the checkbox", function () {
+            rowSelectionModel.setSelectedRanges([]);
+
+            expect($(container.find('.sr .sc: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();
+          });
+
+          it("unchecks checkbox", function () {
+            container.find('.sr .sc:first-child')[1].click();
+            expect($(container.find('.sr .sc: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();
+
+            expect(selectedRanges.length).toEqual(0);
+          })
+        });
+      });
+    });
+
+    function expectOnlyTheFirstRowToBeSelected(selectedRanges) {
+      var row = selectedRanges[0];
+
+      expect(selectedRanges.length).toEqual(1);
+      expect(row.fromCell).toBe(1);
+      expect(row.toCell).toBe(2);
+      expect(row.fromRow).toBe(0);
+      expect(row.toRow).toBe(0);
+    }
+  });
\ No newline at end of file
-- 
2.12.0

