From 899a16a85789a96433c889b2881a64d7035d90ea Mon Sep 17 00:00:00 2001 From: Matt Kleiman and Tira Odhner 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 '' + } + }); + 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 = $("
"); + 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